@codemcp/ade 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/issues.jsonl +14 -0
- package/.beads/last-touched +1 -1
- package/.vibe/beads-state-ade-main-iazal7.json +29 -0
- package/.vibe/development-plan-extensibility.md +169 -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 +166 -32
- 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/core/package.json +3 -2
- package/packages/core/src/catalog/facets/process.ts +10 -1
- 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/registry.ts +3 -2
- package/packages/core/src/resolver.spec.ts +29 -0
- package/packages/core/src/types.ts +71 -0
- package/packages/core/src/writers/mcp-server.spec.ts +62 -0
- package/packages/core/src/writers/mcp-server.ts +25 -0
- package/packages/core/src/writers/workflows.spec.ts +22 -0
- package/packages/core/src/writers/workflows.ts +5 -2
- 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/packages/harnesses/src/writers/copilot.spec.ts +2 -6
- package/packages/harnesses/src/writers/copilot.ts +2 -9
- package/packages/harnesses/src/writers/kiro.spec.ts +32 -0
- package/packages/harnesses/src/writers/kiro.ts +22 -5
- package/packages/harnesses/src/writers/opencode.spec.ts +66 -0
- package/packages/harnesses/src/writers/opencode.ts +30 -3
- package/pnpm-workspace.yaml +2 -0
- /package/docs/{adrs → adr}/0001-tui-framework-selection.md +0 -0
|
@@ -11805,14 +11805,34 @@ var instructionWriter = {
|
|
|
11805
11805
|
var workflowsWriter = {
|
|
11806
11806
|
id: "workflows",
|
|
11807
11807
|
async write(config) {
|
|
11808
|
-
const { package: pkg, ref, env: env2 } = config;
|
|
11808
|
+
const { package: pkg, ref, env: env2, allowedTools } = config;
|
|
11809
11809
|
return {
|
|
11810
11810
|
mcp_servers: [
|
|
11811
11811
|
{
|
|
11812
11812
|
ref: ref ?? pkg,
|
|
11813
11813
|
command: "npx",
|
|
11814
11814
|
args: [pkg],
|
|
11815
|
-
env: env2 ?? {}
|
|
11815
|
+
env: env2 ?? {},
|
|
11816
|
+
...allowedTools !== void 0 ? { allowedTools } : {}
|
|
11817
|
+
}
|
|
11818
|
+
]
|
|
11819
|
+
};
|
|
11820
|
+
}
|
|
11821
|
+
};
|
|
11822
|
+
|
|
11823
|
+
// ../core/dist/writers/mcp-server.js
|
|
11824
|
+
var mcpServerWriter = {
|
|
11825
|
+
id: "mcp-server",
|
|
11826
|
+
async write(config) {
|
|
11827
|
+
const { ref, command: command2, args: args2, env: env2, allowedTools } = config;
|
|
11828
|
+
return {
|
|
11829
|
+
mcp_servers: [
|
|
11830
|
+
{
|
|
11831
|
+
ref,
|
|
11832
|
+
command: command2,
|
|
11833
|
+
args: args2,
|
|
11834
|
+
env: env2 ?? {},
|
|
11835
|
+
...allowedTools !== void 0 ? { allowedTools } : {}
|
|
11816
11836
|
}
|
|
11817
11837
|
]
|
|
11818
11838
|
};
|
|
@@ -11882,12 +11902,13 @@ function createDefaultRegistry() {
|
|
|
11882
11902
|
const registry2 = createRegistry();
|
|
11883
11903
|
registerProvisionWriter(registry2, instructionWriter);
|
|
11884
11904
|
registerProvisionWriter(registry2, workflowsWriter);
|
|
11905
|
+
registerProvisionWriter(registry2, mcpServerWriter);
|
|
11885
11906
|
registerProvisionWriter(registry2, skillsWriter);
|
|
11886
11907
|
registerProvisionWriter(registry2, knowledgeWriter);
|
|
11887
11908
|
registerProvisionWriter(registry2, gitHooksWriter);
|
|
11888
11909
|
registerProvisionWriter(registry2, setupNoteWriter);
|
|
11889
11910
|
registerProvisionWriter(registry2, permissionPolicyWriter);
|
|
11890
|
-
for (const id of ["
|
|
11911
|
+
for (const id of ["installable"]) {
|
|
11891
11912
|
registerProvisionWriter(registry2, {
|
|
11892
11913
|
id,
|
|
11893
11914
|
write: async () => ({})
|
|
@@ -11912,7 +11933,16 @@ var processFacet = {
|
|
|
11912
11933
|
writer: "workflows",
|
|
11913
11934
|
config: {
|
|
11914
11935
|
package: "@codemcp/workflows-server@latest",
|
|
11915
|
-
ref: "workflows"
|
|
11936
|
+
ref: "workflows",
|
|
11937
|
+
env: {
|
|
11938
|
+
VIBE_WORKFLOW_DOMAINS: "skilled"
|
|
11939
|
+
},
|
|
11940
|
+
allowedTools: [
|
|
11941
|
+
"whats_next",
|
|
11942
|
+
"conduct_review",
|
|
11943
|
+
"list_workflows",
|
|
11944
|
+
"get_tool_info"
|
|
11945
|
+
]
|
|
11916
11946
|
}
|
|
11917
11947
|
},
|
|
11918
11948
|
{
|
|
@@ -12763,6 +12793,22 @@ function getVisibleOptions(facet, choices, catalog) {
|
|
|
12763
12793
|
return option.available(deps);
|
|
12764
12794
|
});
|
|
12765
12795
|
}
|
|
12796
|
+
function mergeExtensions(catalog, extensions) {
|
|
12797
|
+
let facets = catalog.facets.map((f) => ({
|
|
12798
|
+
...f,
|
|
12799
|
+
options: [...f.options]
|
|
12800
|
+
}));
|
|
12801
|
+
for (const [facetId, newOptions] of Object.entries(extensions.facetContributions ?? {})) {
|
|
12802
|
+
const facet = facets.find((f) => f.id === facetId);
|
|
12803
|
+
if (facet) {
|
|
12804
|
+
facet.options = [...facet.options, ...newOptions];
|
|
12805
|
+
}
|
|
12806
|
+
}
|
|
12807
|
+
if (extensions.facets && extensions.facets.length > 0) {
|
|
12808
|
+
facets = [...facets, ...extensions.facets];
|
|
12809
|
+
}
|
|
12810
|
+
return { facets };
|
|
12811
|
+
}
|
|
12766
12812
|
|
|
12767
12813
|
// ../core/dist/resolver.js
|
|
12768
12814
|
async function resolve(userConfig, catalog, registry2) {
|
|
@@ -12918,6 +12964,23 @@ function collectDocsets(choices, catalog) {
|
|
|
12918
12964
|
return Array.from(seen.values());
|
|
12919
12965
|
}
|
|
12920
12966
|
|
|
12967
|
+
// ../core/dist/types.js
|
|
12968
|
+
import { z as z3 } from "zod";
|
|
12969
|
+
var OptionSchema = z3.custom((val) => typeof val === "object" && val !== null && typeof val.id === "string" && typeof val.label === "string" && typeof val.description === "string" && Array.isArray(val.recipe), { message: "Option must have id, label, description and recipe fields" });
|
|
12970
|
+
var FacetSchema = z3.custom((val) => typeof val === "object" && val !== null && typeof val.id === "string" && typeof val.label === "string" && typeof val.description === "string" && typeof val.required === "boolean" && Array.isArray(val.options), { message: "Facet must have id, label, description, required and options" });
|
|
12971
|
+
var HarnessWriterSchema = z3.custom((val) => typeof val === "object" && val !== null && typeof val.id === "string" && typeof val.label === "string" && typeof val.description === "string" && typeof val.install === "function", { message: "HarnessWriter must have id, label, description and install()" });
|
|
12972
|
+
var ProvisionWriterDefSchema = z3.custom((val) => typeof val === "object" && val !== null && typeof val.id === "string" && typeof val.write === "function", { message: "ProvisionWriterDef must have id and write()" });
|
|
12973
|
+
var AdeExtensionsSchema = z3.object({
|
|
12974
|
+
/** Add new options to existing facets, keyed by facet id */
|
|
12975
|
+
facetContributions: z3.record(z3.string(), z3.array(OptionSchema)).optional(),
|
|
12976
|
+
/** Add entirely new facets */
|
|
12977
|
+
facets: z3.array(FacetSchema).optional(),
|
|
12978
|
+
/** Add new provision writers */
|
|
12979
|
+
provisionWriters: z3.array(ProvisionWriterDefSchema).optional(),
|
|
12980
|
+
/** Add new harness writers */
|
|
12981
|
+
harnessWriters: z3.array(HarnessWriterSchema).optional()
|
|
12982
|
+
});
|
|
12983
|
+
|
|
12921
12984
|
// ../harnesses/dist/skills-installer.js
|
|
12922
12985
|
import { join as join8 } from "path";
|
|
12923
12986
|
|
|
@@ -18050,7 +18113,7 @@ var V2 = "[";
|
|
|
18050
18113
|
var nD = "]";
|
|
18051
18114
|
var G2 = "m";
|
|
18052
18115
|
var _2 = `${nD}8;;`;
|
|
18053
|
-
var
|
|
18116
|
+
var z4 = (e2) => `${d.values().next().value}${V2}${e2}${G2}`;
|
|
18054
18117
|
var K2 = (e2) => `${d.values().next().value}${_2}${e2}${y3}`;
|
|
18055
18118
|
var aD = (e2) => e2.split(" ").map((u2) => p(u2));
|
|
18056
18119
|
var k2 = (e2, u2, t2) => {
|
|
@@ -18111,8 +18174,8 @@ var lD = (e2, u2, t2 = {}) => {
|
|
|
18111
18174
|
}
|
|
18112
18175
|
const o2 = ED.codes.get(Number(s));
|
|
18113
18176
|
n[E + 1] === `
|
|
18114
|
-
` ? (i && (F2 += K2("")), s && o2 && (F2 +=
|
|
18115
|
-
` && (s && o2 && (F2 +=
|
|
18177
|
+
` ? (i && (F2 += K2("")), s && o2 && (F2 += z4(o2))) : a === `
|
|
18178
|
+
` && (s && o2 && (F2 += z4(s)), i && (F2 += K2(i)));
|
|
18116
18179
|
}
|
|
18117
18180
|
return F2;
|
|
18118
18181
|
};
|
|
@@ -22295,13 +22358,7 @@ function getBuiltInTools(profile) {
|
|
|
22295
22358
|
}
|
|
22296
22359
|
}
|
|
22297
22360
|
function getForwardedMcpTools(servers) {
|
|
22298
|
-
return servers.
|
|
22299
|
-
const allowedTools = server.allowedTools ?? ["*"];
|
|
22300
|
-
if (allowedTools.includes("*")) {
|
|
22301
|
-
return [`${server.ref}/*`];
|
|
22302
|
-
}
|
|
22303
|
-
return allowedTools.map((tool) => `${server.ref}/${tool}`);
|
|
22304
|
-
});
|
|
22361
|
+
return servers.map((server) => `${server.ref}/*`);
|
|
22305
22362
|
}
|
|
22306
22363
|
function renderCopilotAgentMcpServers(servers) {
|
|
22307
22364
|
if (servers.length === 0) {
|
|
@@ -22313,7 +22370,7 @@ function renderCopilotAgentMcpServers(servers) {
|
|
|
22313
22370
|
lines.push(" type: stdio");
|
|
22314
22371
|
lines.push(` command: ${JSON.stringify(server.command)}`);
|
|
22315
22372
|
lines.push(` args: ${JSON.stringify(server.args)}`);
|
|
22316
|
-
lines.push(` tools: ${JSON.stringify(
|
|
22373
|
+
lines.push(` tools: ${JSON.stringify(["*"])}`);
|
|
22317
22374
|
if (Object.keys(server.env).length > 0) {
|
|
22318
22375
|
lines.push(" env:");
|
|
22319
22376
|
for (const [key, value] of Object.entries(server.env)) {
|
|
@@ -22460,20 +22517,21 @@ var kiroWriter = {
|
|
|
22460
22517
|
})
|
|
22461
22518
|
});
|
|
22462
22519
|
const tools = getKiroTools(getAutonomyProfile(config), config.mcp_servers);
|
|
22520
|
+
const allowedTools = getKiroAllowedTools(getAutonomyProfile(config), config.mcp_servers);
|
|
22463
22521
|
await writeJson(join17(projectRoot, ".kiro", "agents", "ade.json"), {
|
|
22464
22522
|
name: "ade",
|
|
22465
22523
|
description: "ADE \u2014 Agentic Development Environment agent with project conventions and tools.",
|
|
22466
22524
|
prompt: config.instructions.join("\n\n") || "ADE \u2014 Agentic Development Environment agent.",
|
|
22467
22525
|
mcpServers: getKiroAgentMcpServers(config.mcp_servers),
|
|
22468
22526
|
tools,
|
|
22469
|
-
allowedTools
|
|
22527
|
+
allowedTools,
|
|
22470
22528
|
useLegacyMcpJson: true
|
|
22471
22529
|
});
|
|
22472
22530
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
22473
22531
|
}
|
|
22474
22532
|
};
|
|
22475
22533
|
function getKiroTools(profile, servers) {
|
|
22476
|
-
const mcpTools =
|
|
22534
|
+
const mcpTools = servers.map((server) => `@${server.ref}/*`);
|
|
22477
22535
|
switch (profile) {
|
|
22478
22536
|
case "rigid":
|
|
22479
22537
|
return ["read", "shell", "spec", ...mcpTools];
|
|
@@ -22485,14 +22543,24 @@ function getKiroTools(profile, servers) {
|
|
|
22485
22543
|
return ["read", "write", "shell", "spec", ...mcpTools];
|
|
22486
22544
|
}
|
|
22487
22545
|
}
|
|
22488
|
-
function
|
|
22489
|
-
|
|
22546
|
+
function getKiroAllowedTools(profile, servers) {
|
|
22547
|
+
const mcpAllowedTools = servers.flatMap((server) => {
|
|
22490
22548
|
const allowedTools = server.allowedTools ?? ["*"];
|
|
22491
22549
|
if (allowedTools.includes("*")) {
|
|
22492
22550
|
return [`@${server.ref}/*`];
|
|
22493
22551
|
}
|
|
22494
22552
|
return allowedTools.map((tool) => `@${server.ref}/${tool}`);
|
|
22495
22553
|
});
|
|
22554
|
+
switch (profile) {
|
|
22555
|
+
case "rigid":
|
|
22556
|
+
return ["read", "shell", "spec", ...mcpAllowedTools];
|
|
22557
|
+
case "sensible-defaults":
|
|
22558
|
+
return ["read", "write", "shell", "spec", ...mcpAllowedTools];
|
|
22559
|
+
case "max-autonomy":
|
|
22560
|
+
return ["read", "write", "shell(*)", "spec", ...mcpAllowedTools];
|
|
22561
|
+
default:
|
|
22562
|
+
return ["read", "write", "shell", "spec", ...mcpAllowedTools];
|
|
22563
|
+
}
|
|
22496
22564
|
}
|
|
22497
22565
|
function getKiroAgentMcpServers(servers) {
|
|
22498
22566
|
return Object.fromEntries(servers.map((server) => [
|
|
@@ -22630,6 +22698,19 @@ var MAX_AUTONOMY_RULES = {
|
|
|
22630
22698
|
codesearch: "ask",
|
|
22631
22699
|
doom_loop: "deny"
|
|
22632
22700
|
};
|
|
22701
|
+
function getMcpPermissions(servers) {
|
|
22702
|
+
const entries = servers.flatMap((server) => {
|
|
22703
|
+
const allowedTools = server.allowedTools ?? ["*"];
|
|
22704
|
+
if (allowedTools.includes("*")) {
|
|
22705
|
+
return [[`${server.ref}*`, "allow"]];
|
|
22706
|
+
}
|
|
22707
|
+
return [
|
|
22708
|
+
[`${server.ref}*`, "ask"],
|
|
22709
|
+
...allowedTools.map((tool) => [`${server.ref}_${tool}`, "allow"])
|
|
22710
|
+
];
|
|
22711
|
+
});
|
|
22712
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
22713
|
+
}
|
|
22633
22714
|
function getPermissionRules(profile) {
|
|
22634
22715
|
switch (profile) {
|
|
22635
22716
|
case "rigid":
|
|
@@ -22658,9 +22739,11 @@ var opencodeWriter = {
|
|
|
22658
22739
|
defaults: { $schema: "https://opencode.ai/config.json" }
|
|
22659
22740
|
});
|
|
22660
22741
|
const permission = getPermissionRules(getAutonomyProfile(config));
|
|
22742
|
+
const mcpPermissions = getMcpPermissions(config.mcp_servers);
|
|
22743
|
+
const mergedPermission = permission || mcpPermissions ? { ...mcpPermissions ?? {}, ...permission ?? {} } : void 0;
|
|
22661
22744
|
await writeAgentMd(config, {
|
|
22662
22745
|
path: join18(projectRoot, ".opencode", "agents", "ade.md"),
|
|
22663
|
-
extraFrontmatter:
|
|
22746
|
+
extraFrontmatter: mergedPermission ? renderYamlMapping("permission", mergedPermission) : void 0,
|
|
22664
22747
|
fallbackBody: "ADE \u2014 Agentic Development Environment agent with project conventions and tools."
|
|
22665
22748
|
});
|
|
22666
22749
|
await writeGitHooks(config.git_hooks, projectRoot);
|
|
@@ -22697,9 +22780,12 @@ function getHarnessWriter(id) {
|
|
|
22697
22780
|
function getHarnessIds() {
|
|
22698
22781
|
return allHarnessWriters.map((w2) => w2.id);
|
|
22699
22782
|
}
|
|
22783
|
+
function buildHarnessWriters(extensions) {
|
|
22784
|
+
return [...allHarnessWriters, ...extensions.harnessWriters ?? []];
|
|
22785
|
+
}
|
|
22700
22786
|
|
|
22701
22787
|
// src/commands/setup.ts
|
|
22702
|
-
async function runSetup(projectRoot, catalog) {
|
|
22788
|
+
async function runSetup(projectRoot, catalog, harnessWriters = allHarnessWriters) {
|
|
22703
22789
|
let lineIndex = 0;
|
|
22704
22790
|
const LOGO_LINES = [
|
|
22705
22791
|
"\n",
|
|
@@ -22793,13 +22879,13 @@ async function runSetup(projectRoot, catalog) {
|
|
|
22793
22879
|
}
|
|
22794
22880
|
}
|
|
22795
22881
|
const existingHarnesses = existingConfig?.harnesses;
|
|
22796
|
-
const harnessOptions =
|
|
22882
|
+
const harnessOptions = harnessWriters.map((w2) => ({
|
|
22797
22883
|
value: w2.id,
|
|
22798
22884
|
label: w2.label,
|
|
22799
22885
|
hint: w2.description
|
|
22800
22886
|
}));
|
|
22801
22887
|
const validExistingHarnesses = existingHarnesses?.filter(
|
|
22802
|
-
(h3) =>
|
|
22888
|
+
(h3) => harnessWriters.some((w2) => w2.id === h3)
|
|
22803
22889
|
);
|
|
22804
22890
|
const selectedHarnesses = await Lt2({
|
|
22805
22891
|
message: "Which coding agents should receive config?\nADE generates config files for each agent you select.\n",
|
|
@@ -22829,7 +22915,7 @@ async function runSetup(projectRoot, catalog) {
|
|
|
22829
22915
|
};
|
|
22830
22916
|
await writeLockFile(projectRoot, lockFile);
|
|
22831
22917
|
for (const harnessId of harnesses) {
|
|
22832
|
-
const writer = getHarnessWriter(harnessId);
|
|
22918
|
+
const writer = harnessWriters.find((w2) => w2.id === harnessId) ?? getHarnessWriter(harnessId);
|
|
22833
22919
|
if (writer) {
|
|
22834
22920
|
await writer.install(logicalConfig, projectRoot);
|
|
22835
22921
|
}
|
|
@@ -22917,24 +23003,25 @@ function promptMultiSelect(facet, existingChoices) {
|
|
|
22917
23003
|
}
|
|
22918
23004
|
|
|
22919
23005
|
// src/commands/install.ts
|
|
22920
|
-
async function runInstall(projectRoot, harnessIds) {
|
|
23006
|
+
async function runInstall(projectRoot, harnessIds, harnessWriters = allHarnessWriters) {
|
|
22921
23007
|
Wt2("ade install");
|
|
22922
23008
|
const lockFile = await readLockFile(projectRoot);
|
|
22923
23009
|
if (!lockFile) {
|
|
22924
23010
|
throw new Error("config.lock.yaml not found. Run `ade setup` first.");
|
|
22925
23011
|
}
|
|
22926
23012
|
const ids = harnessIds ?? lockFile.harnesses ?? ["universal"];
|
|
22927
|
-
const validIds = getHarnessIds();
|
|
23013
|
+
const validIds = [...getHarnessIds(), ...harnessWriters.map((w2) => w2.id)];
|
|
23014
|
+
const uniqueValidIds = [...new Set(validIds)];
|
|
22928
23015
|
for (const id of ids) {
|
|
22929
|
-
if (!
|
|
23016
|
+
if (!uniqueValidIds.includes(id)) {
|
|
22930
23017
|
throw new Error(
|
|
22931
|
-
`Unknown harness "${id}". Available: ${
|
|
23018
|
+
`Unknown harness "${id}". Available: ${uniqueValidIds.join(", ")}`
|
|
22932
23019
|
);
|
|
22933
23020
|
}
|
|
22934
23021
|
}
|
|
22935
23022
|
const logicalConfig = lockFile.logical_config;
|
|
22936
23023
|
for (const id of ids) {
|
|
22937
|
-
const writer = getHarnessWriter(id);
|
|
23024
|
+
const writer = harnessWriters.find((w2) => w2.id === id) ?? getHarnessWriter(id);
|
|
22938
23025
|
if (writer) {
|
|
22939
23026
|
await writer.install(logicalConfig, projectRoot);
|
|
22940
23027
|
}
|
|
@@ -22973,15 +23060,62 @@ To use the latest defaults, remove .ade/skills/ and re-run install.`
|
|
|
22973
23060
|
Gt("Install complete!");
|
|
22974
23061
|
}
|
|
22975
23062
|
|
|
23063
|
+
// src/extensions.ts
|
|
23064
|
+
import { access as access3 } from "fs/promises";
|
|
23065
|
+
import { join as join19 } from "path";
|
|
23066
|
+
import { pathToFileURL } from "url";
|
|
23067
|
+
var SEARCH_ORDER = [
|
|
23068
|
+
"ade.extensions.ts",
|
|
23069
|
+
"ade.extensions.mjs",
|
|
23070
|
+
"ade.extensions.js"
|
|
23071
|
+
];
|
|
23072
|
+
async function loadExtensions(projectRoot) {
|
|
23073
|
+
for (const filename of SEARCH_ORDER) {
|
|
23074
|
+
const filePath = join19(projectRoot, filename);
|
|
23075
|
+
if (!await fileExists(filePath)) continue;
|
|
23076
|
+
const mod = await loadModule(filePath, filename);
|
|
23077
|
+
const raw = mod?.default ?? mod;
|
|
23078
|
+
const result = AdeExtensionsSchema.safeParse(raw);
|
|
23079
|
+
if (!result.success) {
|
|
23080
|
+
throw new Error(
|
|
23081
|
+
`Invalid ade.extensions file at ${filePath}:
|
|
23082
|
+
${result.error.message}`
|
|
23083
|
+
);
|
|
23084
|
+
}
|
|
23085
|
+
return result.data;
|
|
23086
|
+
}
|
|
23087
|
+
return {};
|
|
23088
|
+
}
|
|
23089
|
+
async function fileExists(filePath) {
|
|
23090
|
+
try {
|
|
23091
|
+
await access3(filePath);
|
|
23092
|
+
return true;
|
|
23093
|
+
} catch {
|
|
23094
|
+
return false;
|
|
23095
|
+
}
|
|
23096
|
+
}
|
|
23097
|
+
async function loadModule(filePath, filename) {
|
|
23098
|
+
if (filename.endsWith(".ts")) {
|
|
23099
|
+
const { createJiti } = await import("jiti");
|
|
23100
|
+
const jiti = createJiti(import.meta.url);
|
|
23101
|
+
return jiti.import(filePath);
|
|
23102
|
+
}
|
|
23103
|
+
return import(pathToFileURL(filePath).href);
|
|
23104
|
+
}
|
|
23105
|
+
|
|
22976
23106
|
// src/index.ts
|
|
22977
23107
|
var args = process.argv.slice(2);
|
|
22978
23108
|
var command = args[0];
|
|
22979
23109
|
if (command === "setup") {
|
|
22980
23110
|
const projectRoot = args[1] ?? process.cwd();
|
|
22981
|
-
const
|
|
22982
|
-
|
|
23111
|
+
const extensions = await loadExtensions(projectRoot);
|
|
23112
|
+
const catalog = mergeExtensions(getDefaultCatalog(), extensions);
|
|
23113
|
+
const harnessWriters = buildHarnessWriters(extensions);
|
|
23114
|
+
await runSetup(projectRoot, catalog, harnessWriters);
|
|
22983
23115
|
} else if (command === "install") {
|
|
22984
23116
|
const projectRoot = args[1] ?? process.cwd();
|
|
23117
|
+
const extensions = await loadExtensions(projectRoot);
|
|
23118
|
+
const harnessWriters = buildHarnessWriters(extensions);
|
|
22985
23119
|
let harnessIds;
|
|
22986
23120
|
if (args.includes("--harness")) {
|
|
22987
23121
|
const val = args[args.indexOf("--harness") + 1];
|
|
@@ -22989,7 +23123,7 @@ if (command === "setup") {
|
|
|
22989
23123
|
harnessIds = val.split(",").map((s) => s.trim());
|
|
22990
23124
|
}
|
|
22991
23125
|
}
|
|
22992
|
-
await runInstall(projectRoot, harnessIds);
|
|
23126
|
+
await runInstall(projectRoot, harnessIds, harnessWriters);
|
|
22993
23127
|
} else if (command === "--version" || command === "-v") {
|
|
22994
23128
|
console.log(version);
|
|
22995
23129
|
} else {
|
|
@@ -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.0"
|
|
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
|
}
|