@codemcp/ade-core 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/catalog/facets/architecture.d.ts +2 -0
- package/dist/catalog/facets/architecture.js +424 -0
- package/dist/catalog/facets/backpressure.d.ts +2 -0
- package/dist/catalog/facets/backpressure.js +123 -0
- package/dist/catalog/facets/practices.d.ts +2 -0
- package/dist/catalog/facets/practices.js +163 -0
- package/dist/catalog/facets/process.d.ts +2 -0
- package/dist/catalog/facets/process.js +47 -0
- package/dist/catalog/index.d.ts +14 -0
- package/dist/catalog/index.js +71 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +29 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +41 -0
- package/dist/resolver.d.ts +7 -0
- package/dist/resolver.js +142 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.js +2 -0
- package/dist/writers/git-hooks.d.ts +2 -0
- package/dist/writers/git-hooks.js +7 -0
- package/dist/writers/instruction.d.ts +2 -0
- package/dist/writers/instruction.js +6 -0
- package/dist/writers/knowledge.d.ts +2 -0
- package/dist/writers/knowledge.js +9 -0
- package/dist/writers/setup-note.d.ts +2 -0
- package/dist/writers/setup-note.js +7 -0
- package/dist/writers/skills.d.ts +2 -0
- package/dist/writers/skills.js +7 -0
- package/dist/writers/workflows.d.ts +2 -0
- package/dist/writers/workflows.js +16 -0
- package/eslint.config.mjs +40 -0
- package/nodemon.json +7 -0
- package/package.json +34 -0
- package/src/catalog/catalog.spec.ts +531 -0
- package/src/catalog/facets/architecture.ts +438 -0
- package/src/catalog/facets/backpressure.ts +143 -0
- package/src/catalog/facets/practices.ts +173 -0
- package/src/catalog/facets/process.ts +50 -0
- package/src/catalog/index.ts +86 -0
- package/src/config.spec.ts +165 -0
- package/src/config.ts +39 -0
- package/src/index.ts +49 -0
- package/src/registry.spec.ts +144 -0
- package/src/registry.ts +68 -0
- package/src/resolver.spec.ts +581 -0
- package/src/resolver.ts +170 -0
- package/src/types.ts +151 -0
- package/src/writers/git-hooks.ts +9 -0
- package/src/writers/instruction.spec.ts +42 -0
- package/src/writers/instruction.ts +8 -0
- package/src/writers/knowledge.spec.ts +26 -0
- package/src/writers/knowledge.ts +15 -0
- package/src/writers/setup-note.ts +9 -0
- package/src/writers/skills.spec.ts +109 -0
- package/src/writers/skills.ts +9 -0
- package/src/writers/workflows.spec.ts +72 -0
- package/src/writers/workflows.ts +26 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +7 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.vitest.json +7 -0
- package/vitest.config.ts +5 -0
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UserConfig,
|
|
3
|
+
Catalog,
|
|
4
|
+
WriterRegistry,
|
|
5
|
+
LogicalConfig,
|
|
6
|
+
McpServerEntry,
|
|
7
|
+
ResolutionContext,
|
|
8
|
+
DocsetDef
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
import { getFacet, getOption } from "./catalog/index.js";
|
|
11
|
+
import { getProvisionWriter } from "./registry.js";
|
|
12
|
+
|
|
13
|
+
export async function resolve(
|
|
14
|
+
userConfig: UserConfig,
|
|
15
|
+
catalog: Catalog,
|
|
16
|
+
registry: WriterRegistry
|
|
17
|
+
): Promise<LogicalConfig> {
|
|
18
|
+
const result: LogicalConfig = {
|
|
19
|
+
mcp_servers: [],
|
|
20
|
+
instructions: [],
|
|
21
|
+
cli_actions: [],
|
|
22
|
+
knowledge_sources: [],
|
|
23
|
+
skills: [],
|
|
24
|
+
git_hooks: [],
|
|
25
|
+
setup_notes: []
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const context: ResolutionContext = { resolved: {} };
|
|
29
|
+
|
|
30
|
+
for (const [facetId, optionId] of Object.entries(userConfig.choices)) {
|
|
31
|
+
const facet = getFacet(catalog, facetId);
|
|
32
|
+
if (!facet) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const selectedIds = Array.isArray(optionId) ? optionId : [optionId];
|
|
37
|
+
|
|
38
|
+
for (const selectedId of selectedIds) {
|
|
39
|
+
const option = getOption(facet, selectedId);
|
|
40
|
+
if (!option) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Unknown option "${selectedId}" for facet "${facetId}"`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
context.resolved[facetId] = { optionId: selectedId, option };
|
|
47
|
+
|
|
48
|
+
for (const provision of option.recipe) {
|
|
49
|
+
const writer = getProvisionWriter(registry, provision.writer);
|
|
50
|
+
if (!writer) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const partial = await writer.write(provision.config, context);
|
|
54
|
+
if (partial.mcp_servers) {
|
|
55
|
+
result.mcp_servers.push(...partial.mcp_servers);
|
|
56
|
+
}
|
|
57
|
+
if (partial.instructions) {
|
|
58
|
+
result.instructions.push(...partial.instructions);
|
|
59
|
+
}
|
|
60
|
+
if (partial.cli_actions) {
|
|
61
|
+
result.cli_actions.push(...partial.cli_actions);
|
|
62
|
+
}
|
|
63
|
+
if (partial.knowledge_sources) {
|
|
64
|
+
result.knowledge_sources.push(...partial.knowledge_sources);
|
|
65
|
+
}
|
|
66
|
+
if (partial.skills) {
|
|
67
|
+
result.skills.push(...partial.skills);
|
|
68
|
+
}
|
|
69
|
+
if (partial.git_hooks) {
|
|
70
|
+
result.git_hooks.push(...partial.git_hooks);
|
|
71
|
+
}
|
|
72
|
+
if (partial.setup_notes) {
|
|
73
|
+
result.setup_notes.push(...partial.setup_notes);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Collect docsets from all selected options, dedup by id, filter exclusions
|
|
80
|
+
const seenDocsets = new Map<string, DocsetDef>();
|
|
81
|
+
for (const [facetId, optionId] of Object.entries(userConfig.choices)) {
|
|
82
|
+
const facet = getFacet(catalog, facetId);
|
|
83
|
+
if (!facet) continue;
|
|
84
|
+
const selectedIds = Array.isArray(optionId) ? optionId : [optionId];
|
|
85
|
+
for (const selectedId of selectedIds) {
|
|
86
|
+
const option = getOption(facet, selectedId);
|
|
87
|
+
if (!option?.docsets) continue;
|
|
88
|
+
for (const docset of option.docsets) {
|
|
89
|
+
if (!seenDocsets.has(docset.id)) {
|
|
90
|
+
seenDocsets.set(docset.id, docset);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const excludedSet = new Set(userConfig.excluded_docsets ?? []);
|
|
97
|
+
for (const [id, docset] of seenDocsets) {
|
|
98
|
+
if (excludedSet.has(id)) continue;
|
|
99
|
+
result.knowledge_sources.push({
|
|
100
|
+
name: docset.id,
|
|
101
|
+
origin: docset.origin,
|
|
102
|
+
description: docset.description
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add knowledge-server MCP entry if any knowledge_sources were collected
|
|
107
|
+
if (result.knowledge_sources.length > 0) {
|
|
108
|
+
result.mcp_servers.push({
|
|
109
|
+
ref: "knowledge",
|
|
110
|
+
command: "npx",
|
|
111
|
+
args: ["-y", "@codemcp/knowledge-server"],
|
|
112
|
+
env: {}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add skills-server MCP entry if any skills were collected
|
|
117
|
+
if (result.skills.length > 0) {
|
|
118
|
+
result.mcp_servers.push({
|
|
119
|
+
ref: "agentskills",
|
|
120
|
+
command: "npx",
|
|
121
|
+
args: ["-y", "@codemcp/skills-server"],
|
|
122
|
+
env: {}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Merge custom section
|
|
127
|
+
if (userConfig.custom) {
|
|
128
|
+
if (userConfig.custom.instructions) {
|
|
129
|
+
result.instructions.push(...userConfig.custom.instructions);
|
|
130
|
+
}
|
|
131
|
+
if (userConfig.custom.mcp_servers) {
|
|
132
|
+
result.mcp_servers.push(...userConfig.custom.mcp_servers);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Dedup mcp_servers by ref (last wins)
|
|
137
|
+
const serversByRef = new Map<string, McpServerEntry>();
|
|
138
|
+
for (const server of result.mcp_servers) {
|
|
139
|
+
serversByRef.set(server.ref, server);
|
|
140
|
+
}
|
|
141
|
+
result.mcp_servers = Array.from(serversByRef.values());
|
|
142
|
+
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collect all unique docsets implied by the given choices.
|
|
148
|
+
* Used by the TUI to present docsets for confirmation before resolution.
|
|
149
|
+
*/
|
|
150
|
+
export function collectDocsets(
|
|
151
|
+
choices: Record<string, string | string[]>,
|
|
152
|
+
catalog: Catalog
|
|
153
|
+
): DocsetDef[] {
|
|
154
|
+
const seen = new Map<string, DocsetDef>();
|
|
155
|
+
for (const [facetId, optionId] of Object.entries(choices)) {
|
|
156
|
+
const facet = getFacet(catalog, facetId);
|
|
157
|
+
if (!facet) continue;
|
|
158
|
+
const selectedIds = Array.isArray(optionId) ? optionId : [optionId];
|
|
159
|
+
for (const selectedId of selectedIds) {
|
|
160
|
+
const option = getOption(facet, selectedId);
|
|
161
|
+
if (!option?.docsets) continue;
|
|
162
|
+
for (const docset of option.docsets) {
|
|
163
|
+
if (!seen.has(docset.id)) {
|
|
164
|
+
seen.set(docset.id, docset);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Array.from(seen.values());
|
|
170
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// --- Catalog types ---
|
|
2
|
+
|
|
3
|
+
export interface Catalog {
|
|
4
|
+
facets: Facet[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Facet {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
multiSelect?: boolean;
|
|
13
|
+
dependsOn?: string[];
|
|
14
|
+
options: Option[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Option {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
description: string;
|
|
21
|
+
recipe: Provision[];
|
|
22
|
+
docsets?: DocsetDef[];
|
|
23
|
+
available?: (deps: Record<string, Option | undefined>) => boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DocsetDef {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
origin: string;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Provision {
|
|
34
|
+
writer: ProvisionWriter;
|
|
35
|
+
config: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ProvisionWriter =
|
|
39
|
+
| "workflows"
|
|
40
|
+
| "skills"
|
|
41
|
+
| "knowledge"
|
|
42
|
+
| "mcp-server"
|
|
43
|
+
| "instruction"
|
|
44
|
+
| "installable"
|
|
45
|
+
| "git-hooks"
|
|
46
|
+
| "setup-note";
|
|
47
|
+
|
|
48
|
+
// --- LogicalConfig types ---
|
|
49
|
+
|
|
50
|
+
export interface InlineSkill {
|
|
51
|
+
name: string;
|
|
52
|
+
description: string;
|
|
53
|
+
body: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ExternalSkill {
|
|
57
|
+
name: string;
|
|
58
|
+
source: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type SkillDefinition = InlineSkill | ExternalSkill;
|
|
62
|
+
|
|
63
|
+
export interface GitHook {
|
|
64
|
+
phase: "pre-commit" | "pre-push";
|
|
65
|
+
script: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LogicalConfig {
|
|
69
|
+
mcp_servers: McpServerEntry[];
|
|
70
|
+
instructions: string[];
|
|
71
|
+
cli_actions: CliAction[];
|
|
72
|
+
knowledge_sources: KnowledgeSource[];
|
|
73
|
+
skills: SkillDefinition[];
|
|
74
|
+
git_hooks: GitHook[];
|
|
75
|
+
setup_notes: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface McpServerEntry {
|
|
79
|
+
ref: string;
|
|
80
|
+
command: string;
|
|
81
|
+
args: string[];
|
|
82
|
+
env: Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* Tool names the agent is pre-approved to use from this server.
|
|
85
|
+
* Defaults to `["*"]` (all tools) when not specified.
|
|
86
|
+
*/
|
|
87
|
+
allowedTools?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CliAction {
|
|
91
|
+
command: string;
|
|
92
|
+
args: string[];
|
|
93
|
+
phase: "setup" | "install";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface KnowledgeSource {
|
|
97
|
+
name: string;
|
|
98
|
+
origin: string;
|
|
99
|
+
description: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Resolution context ---
|
|
103
|
+
|
|
104
|
+
export interface ResolutionContext {
|
|
105
|
+
resolved: Record<string, ResolvedFacet>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ResolvedFacet {
|
|
109
|
+
optionId: string;
|
|
110
|
+
option: Option;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Config file types ---
|
|
114
|
+
|
|
115
|
+
export interface UserConfig {
|
|
116
|
+
choices: Record<string, string | string[]>;
|
|
117
|
+
excluded_docsets?: string[];
|
|
118
|
+
harnesses?: string[];
|
|
119
|
+
custom?: {
|
|
120
|
+
mcp_servers?: McpServerEntry[];
|
|
121
|
+
instructions?: string[];
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface LockFile {
|
|
126
|
+
version: 1;
|
|
127
|
+
generated_at: string;
|
|
128
|
+
choices: Record<string, string | string[]>;
|
|
129
|
+
harnesses?: string[];
|
|
130
|
+
logical_config: LogicalConfig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Writer contracts (open, any package can implement) ---
|
|
134
|
+
|
|
135
|
+
export interface ProvisionWriterDef {
|
|
136
|
+
id: string;
|
|
137
|
+
write(
|
|
138
|
+
config: Record<string, unknown>,
|
|
139
|
+
context: ResolutionContext
|
|
140
|
+
): Promise<Partial<LogicalConfig>>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface AgentWriterDef {
|
|
144
|
+
id: string;
|
|
145
|
+
install(config: LogicalConfig, projectRoot: string): Promise<void>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface WriterRegistry {
|
|
149
|
+
provisions: Map<string, ProvisionWriterDef>;
|
|
150
|
+
agents: Map<string, AgentWriterDef>;
|
|
151
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { instructionWriter } from "./instruction.js";
|
|
3
|
+
import type { ResolutionContext } from "../types.js";
|
|
4
|
+
|
|
5
|
+
describe("instructionWriter", () => {
|
|
6
|
+
const context: ResolutionContext = { resolved: {} };
|
|
7
|
+
|
|
8
|
+
it("has id 'instruction'", () => {
|
|
9
|
+
expect(instructionWriter.id).toBe("instruction");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns the text wrapped in an instructions array", async () => {
|
|
13
|
+
const result = await instructionWriter.write(
|
|
14
|
+
{ text: "Always use strict mode" },
|
|
15
|
+
context
|
|
16
|
+
);
|
|
17
|
+
expect(result).toEqual({ instructions: ["Always use strict mode"] });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("passes through the exact text without modification", async () => {
|
|
21
|
+
const verbatim = " leading spaces and trailing spaces ";
|
|
22
|
+
const result = await instructionWriter.write({ text: verbatim }, context);
|
|
23
|
+
expect(result).toEqual({ instructions: [verbatim] });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("only returns instructions, not other LogicalConfig keys", async () => {
|
|
27
|
+
const result = await instructionWriter.write(
|
|
28
|
+
{ text: "some instruction" },
|
|
29
|
+
context
|
|
30
|
+
);
|
|
31
|
+
expect(Object.keys(result)).toEqual(["instructions"]);
|
|
32
|
+
expect(result).not.toHaveProperty("mcp_servers");
|
|
33
|
+
expect(result).not.toHaveProperty("cli_actions");
|
|
34
|
+
expect(result).not.toHaveProperty("knowledge_sources");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("handles multi-line text correctly", async () => {
|
|
38
|
+
const multiLine = "Line one\nLine two\nLine three";
|
|
39
|
+
const result = await instructionWriter.write({ text: multiLine }, context);
|
|
40
|
+
expect(result).toEqual({ instructions: [multiLine] });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { knowledgeWriter } from "./knowledge.js";
|
|
3
|
+
|
|
4
|
+
describe("knowledgeWriter", () => {
|
|
5
|
+
it("has id 'knowledge'", () => {
|
|
6
|
+
expect(knowledgeWriter.id).toBe("knowledge");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("produces a knowledge_sources entry from config", async () => {
|
|
10
|
+
const result = await knowledgeWriter.write(
|
|
11
|
+
{
|
|
12
|
+
name: "react-docs",
|
|
13
|
+
origin: "https://github.com/facebook/react.git",
|
|
14
|
+
description: "Official React documentation"
|
|
15
|
+
},
|
|
16
|
+
{ resolved: {} }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(result.knowledge_sources).toHaveLength(1);
|
|
20
|
+
expect(result.knowledge_sources![0]).toEqual({
|
|
21
|
+
name: "react-docs",
|
|
22
|
+
origin: "https://github.com/facebook/react.git",
|
|
23
|
+
description: "Official React documentation"
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProvisionWriterDef } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const knowledgeWriter: ProvisionWriterDef = {
|
|
4
|
+
id: "knowledge",
|
|
5
|
+
async write(config) {
|
|
6
|
+
const { name, origin, description } = config as {
|
|
7
|
+
name: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
knowledge_sources: [{ name, origin, description }]
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { skillsWriter } from "./skills.js";
|
|
3
|
+
|
|
4
|
+
describe("skillsWriter", () => {
|
|
5
|
+
const emptyContext = { resolved: {} };
|
|
6
|
+
|
|
7
|
+
it("returns skills from config", async () => {
|
|
8
|
+
const result = await skillsWriter.write(
|
|
9
|
+
{
|
|
10
|
+
skills: [
|
|
11
|
+
{
|
|
12
|
+
name: "my-skill",
|
|
13
|
+
description: "A test skill",
|
|
14
|
+
body: "Do the thing."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
emptyContext
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.skills).toHaveLength(1);
|
|
22
|
+
expect(result.skills![0]).toEqual({
|
|
23
|
+
name: "my-skill",
|
|
24
|
+
description: "A test skill",
|
|
25
|
+
body: "Do the thing."
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns multiple skills", async () => {
|
|
30
|
+
const result = await skillsWriter.write(
|
|
31
|
+
{
|
|
32
|
+
skills: [
|
|
33
|
+
{ name: "skill-a", description: "First", body: "Body A" },
|
|
34
|
+
{ name: "skill-b", description: "Second", body: "Body B" }
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
emptyContext
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(result.skills).toHaveLength(2);
|
|
41
|
+
expect(result.skills!.map((s) => s.name)).toEqual(["skill-a", "skill-b"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns only the skills key", async () => {
|
|
45
|
+
const result = await skillsWriter.write(
|
|
46
|
+
{
|
|
47
|
+
skills: [{ name: "x", description: "desc", body: "body" }]
|
|
48
|
+
},
|
|
49
|
+
emptyContext
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(Object.keys(result)).toEqual(["skills"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("preserves multi-line body content", async () => {
|
|
56
|
+
const body =
|
|
57
|
+
"# Architecture\n\nUse layered architecture.\n\n## Rules\n- Rule 1\n- Rule 2";
|
|
58
|
+
const result = await skillsWriter.write(
|
|
59
|
+
{
|
|
60
|
+
skills: [{ name: "arch", description: "Architecture", body }]
|
|
61
|
+
},
|
|
62
|
+
emptyContext
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(result.skills![0]).toMatchObject({ body });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns external skills with source reference", async () => {
|
|
69
|
+
const result = await skillsWriter.write(
|
|
70
|
+
{
|
|
71
|
+
skills: [
|
|
72
|
+
{
|
|
73
|
+
name: "playwright-cli",
|
|
74
|
+
source: "microsoft/playwright-cli/skills/playwright-cli"
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
emptyContext
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(result.skills).toHaveLength(1);
|
|
82
|
+
expect(result.skills![0]).toEqual({
|
|
83
|
+
name: "playwright-cli",
|
|
84
|
+
source: "microsoft/playwright-cli/skills/playwright-cli"
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("handles mixed inline and external skills", async () => {
|
|
89
|
+
const result = await skillsWriter.write(
|
|
90
|
+
{
|
|
91
|
+
skills: [
|
|
92
|
+
{ name: "my-skill", description: "Inline", body: "Do stuff." },
|
|
93
|
+
{ name: "ext-skill", source: "org/repo/skills/ext" }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
emptyContext
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(result.skills).toHaveLength(2);
|
|
100
|
+
expect(result.skills![0]).toMatchObject({
|
|
101
|
+
name: "my-skill",
|
|
102
|
+
body: "Do stuff."
|
|
103
|
+
});
|
|
104
|
+
expect(result.skills![1]).toMatchObject({
|
|
105
|
+
name: "ext-skill",
|
|
106
|
+
source: "org/repo/skills/ext"
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { workflowsWriter } from "./workflows.js";
|
|
3
|
+
import type { ResolutionContext } from "../types.js";
|
|
4
|
+
|
|
5
|
+
describe("workflowsWriter", () => {
|
|
6
|
+
const context: ResolutionContext = { resolved: {} };
|
|
7
|
+
|
|
8
|
+
it("has id 'workflows'", () => {
|
|
9
|
+
expect(workflowsWriter.id).toBe("workflows");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns mcp_servers with correct ref, command, and args for a given package", async () => {
|
|
13
|
+
const result = await workflowsWriter.write(
|
|
14
|
+
{ package: "@codemcp/workflows-server" },
|
|
15
|
+
context
|
|
16
|
+
);
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
mcp_servers: [
|
|
19
|
+
{
|
|
20
|
+
ref: "@codemcp/workflows-server",
|
|
21
|
+
command: "npx",
|
|
22
|
+
args: ["@codemcp/workflows-server"],
|
|
23
|
+
env: {}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("uses ref override when provided", async () => {
|
|
30
|
+
const result = await workflowsWriter.write(
|
|
31
|
+
{ package: "@codemcp/workflows-server@latest", ref: "workflows" },
|
|
32
|
+
context
|
|
33
|
+
);
|
|
34
|
+
expect(result.mcp_servers![0].ref).toBe("workflows");
|
|
35
|
+
expect(result.mcp_servers![0].args).toEqual([
|
|
36
|
+
"@codemcp/workflows-server@latest"
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("includes env in the entry when env is specified", async () => {
|
|
41
|
+
const result = await workflowsWriter.write(
|
|
42
|
+
{
|
|
43
|
+
package: "@codemcp/workflows-server",
|
|
44
|
+
env: { API_KEY: "secret", NODE_ENV: "production" }
|
|
45
|
+
},
|
|
46
|
+
context
|
|
47
|
+
);
|
|
48
|
+
expect(result.mcp_servers![0].env).toEqual({
|
|
49
|
+
API_KEY: "secret",
|
|
50
|
+
NODE_ENV: "production"
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("defaults env to an empty object when not specified", async () => {
|
|
55
|
+
const result = await workflowsWriter.write(
|
|
56
|
+
{ package: "@codemcp/workflows-server" },
|
|
57
|
+
context
|
|
58
|
+
);
|
|
59
|
+
expect(result.mcp_servers![0].env).toEqual({});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("only returns mcp_servers, not other LogicalConfig keys", async () => {
|
|
63
|
+
const result = await workflowsWriter.write(
|
|
64
|
+
{ package: "@codemcp/workflows-server" },
|
|
65
|
+
context
|
|
66
|
+
);
|
|
67
|
+
expect(Object.keys(result)).toEqual(["mcp_servers"]);
|
|
68
|
+
expect(result).not.toHaveProperty("instructions");
|
|
69
|
+
expect(result).not.toHaveProperty("cli_actions");
|
|
70
|
+
expect(result).not.toHaveProperty("knowledge_sources");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ProvisionWriterDef } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const workflowsWriter: ProvisionWriterDef = {
|
|
4
|
+
id: "workflows",
|
|
5
|
+
async write(config) {
|
|
6
|
+
const {
|
|
7
|
+
package: pkg,
|
|
8
|
+
ref,
|
|
9
|
+
env
|
|
10
|
+
} = config as {
|
|
11
|
+
package: string;
|
|
12
|
+
ref?: string;
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
mcp_servers: [
|
|
17
|
+
{
|
|
18
|
+
ref: ref ?? pkg,
|
|
19
|
+
command: "npx",
|
|
20
|
+
args: [pkg],
|
|
21
|
+
env: env ?? {}
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|