@codemcp/ade 0.5.0 → 0.6.1
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 +24 -0
- package/.beads/last-touched +1 -1
- package/.vibe/beads-state-ade-fix-zod-7eypxn.json +34 -0
- package/.vibe/beads-state-ade-main-iazal7.json +29 -0
- package/.vibe/beads-state-ade-partially-skilled-ywlqhb.json +24 -0
- package/.vibe/development-plan-extensibility.md +169 -0
- package/.vibe/development-plan-fix-zod.md +72 -0
- package/.vibe/development-plan-partially-skilled.md +44 -0
- package/ade.extensions.mjs +66 -0
- package/docs/adr/0002-extension-file-type-safety.md +97 -0
- package/docs/guide/extensions.md +187 -0
- package/package.json +3 -2
- package/packages/cli/dist/index.js +33333 -12021
- package/packages/cli/package.json +4 -2
- package/packages/cli/src/commands/extensions.integration.spec.ts +122 -0
- package/packages/cli/src/commands/install.spec.ts +21 -1
- package/packages/cli/src/commands/install.ts +10 -5
- package/packages/cli/src/commands/setup.ts +8 -4
- package/packages/cli/src/extensions.spec.ts +128 -0
- package/packages/cli/src/extensions.ts +71 -0
- package/packages/cli/src/index.ts +10 -5
- package/packages/cli/tsup.config.ts +7 -1
- package/packages/core/package.json +3 -2
- package/packages/core/src/catalog/facets/process.ts +174 -0
- package/packages/core/src/catalog/index.ts +38 -1
- package/packages/core/src/extensions.spec.ts +169 -0
- package/packages/core/src/index.ts +3 -1
- package/packages/core/src/types.ts +71 -0
- package/packages/harnesses/package.json +1 -1
- package/packages/harnesses/src/index.spec.ts +48 -1
- package/packages/harnesses/src/index.ts +10 -0
- package/pnpm-workspace.yaml +2 -0
- /package/docs/{adrs → adr}/0001-tui-framework-selection.md +0 -0
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"@clack/prompts": "^1.1.0",
|
|
29
29
|
"@codemcp/ade-core": "workspace:*",
|
|
30
30
|
"@codemcp/ade-harnesses": "workspace:*",
|
|
31
|
-
"
|
|
31
|
+
"jiti": "2.6.1",
|
|
32
|
+
"yaml": "^2.8.2",
|
|
33
|
+
"zod": "catalog:"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@codemcp/knowledge": "2.1.0",
|
|
@@ -39,5 +41,5 @@
|
|
|
39
41
|
"typescript": "catalog:",
|
|
40
42
|
"vitest": "catalog:"
|
|
41
43
|
},
|
|
42
|
-
"version": "0.
|
|
44
|
+
"version": "0.6.1"
|
|
43
45
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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 (catalog, registry, resolver, config I/O, writers) is real
|
|
7
|
+
vi.mock("@clack/prompts", () => ({
|
|
8
|
+
intro: vi.fn(),
|
|
9
|
+
outro: vi.fn(),
|
|
10
|
+
note: vi.fn(),
|
|
11
|
+
select: vi.fn(),
|
|
12
|
+
multiselect: vi.fn(),
|
|
13
|
+
confirm: vi.fn().mockResolvedValue(false), // decline skill install prompt
|
|
14
|
+
isCancel: vi.fn().mockReturnValue(false),
|
|
15
|
+
cancel: vi.fn(),
|
|
16
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() },
|
|
17
|
+
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import * as clack from "@clack/prompts";
|
|
21
|
+
import { runSetup } from "./setup.js";
|
|
22
|
+
import {
|
|
23
|
+
readLockFile,
|
|
24
|
+
getDefaultCatalog,
|
|
25
|
+
mergeExtensions
|
|
26
|
+
} from "@codemcp/ade-core";
|
|
27
|
+
import type { AdeExtensions } from "@codemcp/ade-core";
|
|
28
|
+
|
|
29
|
+
describe("extension e2e — option contributes skills and knowledge to setup output", () => {
|
|
30
|
+
let dir: string;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
vi.mocked(clack.confirm).mockResolvedValue(false); // don't install skills
|
|
35
|
+
dir = await mkdtemp(join(tmpdir(), "ade-ext-e2e-"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await rm(dir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it(
|
|
43
|
+
"extension-contributed architecture option writes inline skill and knowledge source",
|
|
44
|
+
{ timeout: 60_000 },
|
|
45
|
+
async () => {
|
|
46
|
+
// Build an extension with a SAP option that has an inline skill + knowledge
|
|
47
|
+
const extensions: AdeExtensions = {
|
|
48
|
+
facetContributions: {
|
|
49
|
+
architecture: [
|
|
50
|
+
{
|
|
51
|
+
id: "sap-abap",
|
|
52
|
+
label: "SAP BTP / ABAP",
|
|
53
|
+
description: "SAP BTP ABAP Cloud development",
|
|
54
|
+
recipe: [
|
|
55
|
+
{
|
|
56
|
+
writer: "skills",
|
|
57
|
+
config: {
|
|
58
|
+
skills: [
|
|
59
|
+
{
|
|
60
|
+
name: "sap-abap-code",
|
|
61
|
+
description: "SAP ABAP coding guidelines",
|
|
62
|
+
body: "# SAP ABAP Code\nUse ABAP Cloud APIs only."
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
writer: "knowledge",
|
|
69
|
+
config: {
|
|
70
|
+
name: "sap-abap-docs",
|
|
71
|
+
origin: "https://help.sap.com/docs/abap-cloud",
|
|
72
|
+
description: "SAP ABAP Cloud documentation"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const catalog = mergeExtensions(getDefaultCatalog(), extensions);
|
|
82
|
+
|
|
83
|
+
// Facet order from sortFacets: process → architecture → practices → backpressure → autonomy
|
|
84
|
+
vi.mocked(clack.select)
|
|
85
|
+
.mockResolvedValueOnce("native-agents-md") // process
|
|
86
|
+
.mockResolvedValueOnce("sap-abap"); // architecture — the extended option
|
|
87
|
+
vi.mocked(clack.multiselect)
|
|
88
|
+
.mockResolvedValueOnce([]) // practices: none
|
|
89
|
+
// backpressure: sap-abap has no matching options so skipped
|
|
90
|
+
.mockResolvedValueOnce([]); // harnesses
|
|
91
|
+
|
|
92
|
+
await runSetup(dir, catalog);
|
|
93
|
+
|
|
94
|
+
// ── Skill should be staged to .ade/skills/sap-abap-code/SKILL.md ────
|
|
95
|
+
const skillMd = await readFile(
|
|
96
|
+
join(dir, ".ade", "skills", "sap-abap-code", "SKILL.md"),
|
|
97
|
+
"utf-8"
|
|
98
|
+
);
|
|
99
|
+
expect(skillMd).toContain("name: sap-abap-code");
|
|
100
|
+
expect(skillMd).toContain("SAP ABAP Code");
|
|
101
|
+
expect(skillMd).toContain("ABAP Cloud APIs only");
|
|
102
|
+
|
|
103
|
+
// ── Knowledge source should appear in the lock file ──────────────────
|
|
104
|
+
const lock = await readLockFile(dir);
|
|
105
|
+
expect(lock).not.toBeNull();
|
|
106
|
+
const knowledgeSources = lock!.logical_config.knowledge_sources;
|
|
107
|
+
expect(knowledgeSources).toHaveLength(1);
|
|
108
|
+
expect(knowledgeSources[0].name).toBe("sap-abap-docs");
|
|
109
|
+
expect(knowledgeSources[0].origin).toBe(
|
|
110
|
+
"https://help.sap.com/docs/abap-cloud"
|
|
111
|
+
);
|
|
112
|
+
expect(knowledgeSources[0].description).toBe(
|
|
113
|
+
"SAP ABAP Cloud documentation"
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ── config.yaml should record the extension option as the choice ──────
|
|
117
|
+
const { readUserConfig } = await import("@codemcp/ade-core");
|
|
118
|
+
const config = await readUserConfig(dir);
|
|
119
|
+
expect(config!.choices.architecture).toBe("sap-abap");
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
});
|
|
@@ -27,9 +27,29 @@ vi.mock("@codemcp/ade-core", async (importOriginal) => {
|
|
|
27
27
|
};
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const mockInstall = vi.fn().mockResolvedValue(undefined);
|
|
30
|
+
const mockInstall = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
|
|
31
31
|
|
|
32
32
|
vi.mock("@codemcp/ade-harnesses", () => ({
|
|
33
|
+
allHarnessWriters: [
|
|
34
|
+
{
|
|
35
|
+
id: "universal",
|
|
36
|
+
label: "Universal",
|
|
37
|
+
description: "Universal",
|
|
38
|
+
install: mockInstall
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "claude-code",
|
|
42
|
+
label: "Claude Code",
|
|
43
|
+
description: "Claude Code",
|
|
44
|
+
install: mockInstall
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "cursor",
|
|
48
|
+
label: "Cursor",
|
|
49
|
+
description: "Cursor",
|
|
50
|
+
install: mockInstall
|
|
51
|
+
}
|
|
52
|
+
],
|
|
33
53
|
getHarnessWriter: vi.fn().mockImplementation((id: string) => {
|
|
34
54
|
if (id === "universal" || id === "claude-code" || id === "cursor") {
|
|
35
55
|
return { id, install: mockInstall };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as clack from "@clack/prompts";
|
|
2
2
|
import { readLockFile } from "@codemcp/ade-core";
|
|
3
3
|
import {
|
|
4
|
+
type HarnessWriter,
|
|
5
|
+
allHarnessWriters,
|
|
4
6
|
getHarnessWriter,
|
|
5
7
|
getHarnessIds,
|
|
6
8
|
installSkills,
|
|
@@ -9,7 +11,8 @@ import {
|
|
|
9
11
|
|
|
10
12
|
export async function runInstall(
|
|
11
13
|
projectRoot: string,
|
|
12
|
-
harnessIds?: string[]
|
|
14
|
+
harnessIds?: string[],
|
|
15
|
+
harnessWriters: HarnessWriter[] = allHarnessWriters
|
|
13
16
|
): Promise<void> {
|
|
14
17
|
clack.intro("ade install");
|
|
15
18
|
|
|
@@ -24,11 +27,12 @@ export async function runInstall(
|
|
|
24
27
|
// 3. default: universal
|
|
25
28
|
const ids = harnessIds ?? lockFile.harnesses ?? ["universal"];
|
|
26
29
|
|
|
27
|
-
const validIds = getHarnessIds();
|
|
30
|
+
const validIds = [...getHarnessIds(), ...harnessWriters.map((w) => w.id)];
|
|
31
|
+
const uniqueValidIds = [...new Set(validIds)];
|
|
28
32
|
for (const id of ids) {
|
|
29
|
-
if (!
|
|
33
|
+
if (!uniqueValidIds.includes(id)) {
|
|
30
34
|
throw new Error(
|
|
31
|
-
`Unknown harness "${id}". Available: ${
|
|
35
|
+
`Unknown harness "${id}". Available: ${uniqueValidIds.join(", ")}`
|
|
32
36
|
);
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -36,7 +40,8 @@ export async function runInstall(
|
|
|
36
40
|
const logicalConfig = lockFile.logical_config;
|
|
37
41
|
|
|
38
42
|
for (const id of ids) {
|
|
39
|
-
const writer =
|
|
43
|
+
const writer =
|
|
44
|
+
harnessWriters.find((w) => w.id === id) ?? getHarnessWriter(id);
|
|
40
45
|
if (writer) {
|
|
41
46
|
await writer.install(logicalConfig, projectRoot);
|
|
42
47
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getVisibleOptions
|
|
17
17
|
} from "@codemcp/ade-core";
|
|
18
18
|
import {
|
|
19
|
+
type HarnessWriter,
|
|
19
20
|
allHarnessWriters,
|
|
20
21
|
getHarnessWriter,
|
|
21
22
|
installSkills,
|
|
@@ -24,7 +25,8 @@ import {
|
|
|
24
25
|
|
|
25
26
|
export async function runSetup(
|
|
26
27
|
projectRoot: string,
|
|
27
|
-
catalog: Catalog
|
|
28
|
+
catalog: Catalog,
|
|
29
|
+
harnessWriters: HarnessWriter[] = allHarnessWriters
|
|
28
30
|
): Promise<void> {
|
|
29
31
|
let lineIndex = 0;
|
|
30
32
|
const LOGO_LINES = [
|
|
@@ -138,14 +140,14 @@ export async function runSetup(
|
|
|
138
140
|
|
|
139
141
|
// Harness selection — multi-select from all available harnesses
|
|
140
142
|
const existingHarnesses = existingConfig?.harnesses;
|
|
141
|
-
const harnessOptions =
|
|
143
|
+
const harnessOptions = harnessWriters.map((w) => ({
|
|
142
144
|
value: w.id,
|
|
143
145
|
label: w.label,
|
|
144
146
|
hint: w.description
|
|
145
147
|
}));
|
|
146
148
|
|
|
147
149
|
const validExistingHarnesses = existingHarnesses?.filter((h) =>
|
|
148
|
-
|
|
150
|
+
harnessWriters.some((w) => w.id === h)
|
|
149
151
|
);
|
|
150
152
|
|
|
151
153
|
const selectedHarnesses = await clack.multiselect({
|
|
@@ -188,7 +190,9 @@ export async function runSetup(
|
|
|
188
190
|
|
|
189
191
|
// Install to all selected harnesses
|
|
190
192
|
for (const harnessId of harnesses) {
|
|
191
|
-
const writer =
|
|
193
|
+
const writer =
|
|
194
|
+
harnessWriters.find((w) => w.id === harnessId) ??
|
|
195
|
+
getHarnessWriter(harnessId);
|
|
192
196
|
if (writer) {
|
|
193
197
|
await writer.install(logicalConfig, projectRoot);
|
|
194
198
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { loadExtensions } from "./extensions.js";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
describe("loadExtensions", () => {
|
|
8
|
+
it("returns an empty object when no extensions file exists", async () => {
|
|
9
|
+
const dir = await mkdtemp(join(tmpdir(), "ade-ext-test-"));
|
|
10
|
+
try {
|
|
11
|
+
const result = await loadExtensions(dir);
|
|
12
|
+
expect(result).toEqual({});
|
|
13
|
+
} finally {
|
|
14
|
+
await rm(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("loads and validates a valid .mjs extensions file", async () => {
|
|
19
|
+
const dir = await mkdtemp(join(tmpdir(), "ade-ext-test-"));
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(
|
|
22
|
+
join(dir, "ade.extensions.mjs"),
|
|
23
|
+
`export default {
|
|
24
|
+
facetContributions: {
|
|
25
|
+
architecture: [
|
|
26
|
+
{
|
|
27
|
+
id: "sap",
|
|
28
|
+
label: "SAP BTP / ABAP",
|
|
29
|
+
description: "SAP BTP ABAP development",
|
|
30
|
+
recipe: [{ writer: "skills", config: { skills: [] } }]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
};`
|
|
35
|
+
);
|
|
36
|
+
const result = await loadExtensions(dir);
|
|
37
|
+
expect(result.facetContributions?.architecture).toHaveLength(1);
|
|
38
|
+
expect(result.facetContributions?.architecture?.[0].id).toBe("sap");
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("throws a descriptive error when the extensions file exports an invalid shape", async () => {
|
|
45
|
+
const dir = await mkdtemp(join(tmpdir(), "ade-ext-test-"));
|
|
46
|
+
try {
|
|
47
|
+
await writeFile(
|
|
48
|
+
join(dir, "ade.extensions.mjs"),
|
|
49
|
+
`export default { facetContributions: "not-an-object" };`
|
|
50
|
+
);
|
|
51
|
+
await expect(loadExtensions(dir)).rejects.toThrow(/invalid/i);
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("loads a .js file when only .js exists", async () => {
|
|
58
|
+
const dir = await mkdtemp(join(tmpdir(), "ade-ext-test-"));
|
|
59
|
+
try {
|
|
60
|
+
await writeFile(
|
|
61
|
+
join(dir, "ade.extensions.js"),
|
|
62
|
+
`export default {
|
|
63
|
+
facets: [
|
|
64
|
+
{
|
|
65
|
+
id: "js-only-facet",
|
|
66
|
+
label: "JS Only",
|
|
67
|
+
description: "From .js fallback",
|
|
68
|
+
required: false,
|
|
69
|
+
options: []
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
};`
|
|
73
|
+
);
|
|
74
|
+
const result = await loadExtensions(dir);
|
|
75
|
+
expect(result.facets).toHaveLength(1);
|
|
76
|
+
expect(result.facets?.[0].id).toBe("js-only-facet");
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(dir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("prefers .mjs over .js when both exist", async () => {
|
|
83
|
+
const dir = await mkdtemp(join(tmpdir(), "ade-ext-test-"));
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(
|
|
86
|
+
join(dir, "ade.extensions.mjs"),
|
|
87
|
+
`export default { facets: [{ id: "from-mjs", label: "MJS", description: "MJS wins", required: false, options: [] }] };`
|
|
88
|
+
);
|
|
89
|
+
await writeFile(
|
|
90
|
+
join(dir, "ade.extensions.js"),
|
|
91
|
+
`export default { facets: [{ id: "from-js", label: "JS", description: "JS loses", required: false, options: [] }] };`
|
|
92
|
+
);
|
|
93
|
+
const result = await loadExtensions(dir);
|
|
94
|
+
expect(result.facets?.[0].id).toBe("from-mjs");
|
|
95
|
+
} finally {
|
|
96
|
+
await rm(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("loads from an absolute path — simulating npx run from a different cwd", async () => {
|
|
101
|
+
// This is the published-package scenario:
|
|
102
|
+
// The CLI binary lives in ~/.npm/_npx/... but projectRoot is the user's cwd.
|
|
103
|
+
// loadExtensions(projectRoot) must look in projectRoot, not in the CLI package dir.
|
|
104
|
+
const userProjectDir = await mkdtemp(join(tmpdir(), "ade-user-project-"));
|
|
105
|
+
const cliPackageDir = await mkdtemp(join(tmpdir(), "ade-cli-package-"));
|
|
106
|
+
try {
|
|
107
|
+
// Simulate: user project has ade.extensions.mjs
|
|
108
|
+
await writeFile(
|
|
109
|
+
join(userProjectDir, "ade.extensions.mjs"),
|
|
110
|
+
`export default {
|
|
111
|
+
facets: [{ id: "user-project-facet", label: "User", description: "From user project", required: false, options: [] }]
|
|
112
|
+
};`
|
|
113
|
+
);
|
|
114
|
+
// CLI package dir has no extension file (it shouldn't be used)
|
|
115
|
+
|
|
116
|
+
// loadExtensions is called with the user's project dir as projectRoot
|
|
117
|
+
const result = await loadExtensions(userProjectDir);
|
|
118
|
+
expect(result.facets?.[0].id).toBe("user-project-facet");
|
|
119
|
+
|
|
120
|
+
// CLI package dir produces empty extensions — it is never consulted
|
|
121
|
+
const cliResult = await loadExtensions(cliPackageDir);
|
|
122
|
+
expect(cliResult).toEqual({});
|
|
123
|
+
} finally {
|
|
124
|
+
await rm(userProjectDir, { recursive: true });
|
|
125
|
+
await rm(cliPackageDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { AdeExtensionsSchema, type AdeExtensions } from "@codemcp/ade-core";
|
|
5
|
+
|
|
6
|
+
const SEARCH_ORDER = [
|
|
7
|
+
"ade.extensions.ts",
|
|
8
|
+
"ade.extensions.mjs",
|
|
9
|
+
"ade.extensions.js"
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loads and validates the project's `ade.extensions` file (if any).
|
|
14
|
+
*
|
|
15
|
+
* Search order: ade.extensions.ts → ade.extensions.mjs → ade.extensions.js
|
|
16
|
+
*
|
|
17
|
+
* - `.ts` files are loaded via `jiti` for TypeScript support.
|
|
18
|
+
* - `.mjs` / `.js` files are loaded via native dynamic `import()`.
|
|
19
|
+
* - Returns `{}` when no extensions file exists.
|
|
20
|
+
* - Throws with a descriptive message when the file exports an invalid shape.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadExtensions(
|
|
23
|
+
projectRoot: string
|
|
24
|
+
): Promise<AdeExtensions> {
|
|
25
|
+
for (const filename of SEARCH_ORDER) {
|
|
26
|
+
const filePath = join(projectRoot, filename);
|
|
27
|
+
|
|
28
|
+
if (!(await fileExists(filePath))) continue;
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line no-await-in-loop
|
|
31
|
+
const mod = await loadModule(filePath, filename);
|
|
32
|
+
const raw = mod?.default ?? mod;
|
|
33
|
+
|
|
34
|
+
const result = AdeExtensionsSchema.safeParse(raw);
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid ade.extensions file at ${filePath}:\n${result.error.message}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result.data;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
await access(filePath);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadModule(
|
|
57
|
+
filePath: string,
|
|
58
|
+
filename: string
|
|
59
|
+
): Promise<Record<string, unknown>> {
|
|
60
|
+
if (filename.endsWith(".ts")) {
|
|
61
|
+
// Use jiti for TypeScript support
|
|
62
|
+
const { createJiti } = await import("jiti");
|
|
63
|
+
const jiti = createJiti(import.meta.url);
|
|
64
|
+
return jiti.import(filePath) as Promise<Record<string, unknown>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Native ESM for .mjs / .js
|
|
68
|
+
return import(pathToFileURL(filePath).href) as Promise<
|
|
69
|
+
Record<string, unknown>
|
|
70
|
+
>;
|
|
71
|
+
}
|
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
import { version } from "./version.js";
|
|
4
4
|
import { runSetup } from "./commands/setup.js";
|
|
5
5
|
import { runInstall } from "./commands/install.js";
|
|
6
|
-
import { getDefaultCatalog } from "@codemcp/ade-core";
|
|
7
|
-
import { getHarnessIds } from "@codemcp/ade-harnesses";
|
|
6
|
+
import { getDefaultCatalog, mergeExtensions } from "@codemcp/ade-core";
|
|
7
|
+
import { getHarnessIds, buildHarnessWriters } from "@codemcp/ade-harnesses";
|
|
8
|
+
import { loadExtensions } from "./extensions.js";
|
|
8
9
|
|
|
9
10
|
const args = process.argv.slice(2);
|
|
10
11
|
const command = args[0];
|
|
11
12
|
|
|
12
13
|
if (command === "setup") {
|
|
13
14
|
const projectRoot = args[1] ?? process.cwd();
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const extensions = await loadExtensions(projectRoot);
|
|
16
|
+
const catalog = mergeExtensions(getDefaultCatalog(), extensions);
|
|
17
|
+
const harnessWriters = buildHarnessWriters(extensions);
|
|
18
|
+
await runSetup(projectRoot, catalog, harnessWriters);
|
|
16
19
|
} else if (command === "install") {
|
|
17
20
|
const projectRoot = args[1] ?? process.cwd();
|
|
21
|
+
const extensions = await loadExtensions(projectRoot);
|
|
22
|
+
const harnessWriters = buildHarnessWriters(extensions);
|
|
18
23
|
|
|
19
24
|
let harnessIds: string[] | undefined;
|
|
20
25
|
|
|
@@ -26,7 +31,7 @@ if (command === "setup") {
|
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
await runInstall(projectRoot, harnessIds);
|
|
34
|
+
await runInstall(projectRoot, harnessIds, harnessWriters);
|
|
30
35
|
} else if (command === "--version" || command === "-v") {
|
|
31
36
|
console.log(version);
|
|
32
37
|
} else {
|
|
@@ -7,7 +7,13 @@ export default defineConfig({
|
|
|
7
7
|
tsconfig: "tsconfig.build.json",
|
|
8
8
|
target: "node22",
|
|
9
9
|
clean: true,
|
|
10
|
-
noExternal: [
|
|
10
|
+
noExternal: [
|
|
11
|
+
"@clack/prompts",
|
|
12
|
+
"@codemcp/ade-core",
|
|
13
|
+
"@codemcp/ade-harnesses",
|
|
14
|
+
"yaml",
|
|
15
|
+
"zod"
|
|
16
|
+
],
|
|
11
17
|
esbuildOptions(options) {
|
|
12
18
|
options.banner = {
|
|
13
19
|
js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"yaml": "^2.8.2"
|
|
31
|
+
"yaml": "^2.8.2",
|
|
32
|
+
"zod": "catalog:"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"oxlint": "catalog:",
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
"typescript": "catalog:",
|
|
39
40
|
"vitest": "catalog:"
|
|
40
41
|
},
|
|
41
|
-
"version": "0.
|
|
42
|
+
"version": "0.6.1"
|
|
42
43
|
}
|