@sweny-ai/core 0.1.51 → 0.1.53
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/__tests__/publish.test.js +146 -0
- package/dist/__tests__/schema.test.js +156 -1
- package/dist/__tests__/skills.test.js +135 -4
- package/dist/__tests__/templates.test.d.ts +1 -0
- package/dist/__tests__/templates.test.js +49 -0
- package/dist/__tests__/version-consistency.test.js +7 -7
- package/dist/cli/config-file.d.ts +1 -1
- package/dist/cli/config-file.js +1 -1
- package/dist/cli/credentials.d.ts +1 -1
- package/dist/cli/credentials.js +1 -1
- package/dist/cli/credentials.test.js +1 -1
- package/dist/cli/e2e.d.ts +5 -1
- package/dist/cli/e2e.js +7 -3
- package/dist/cli/e2e.test.js +46 -1
- package/dist/cli/main.js +21 -7
- package/dist/cli/new.d.ts +101 -0
- package/dist/cli/new.js +670 -0
- package/dist/cli/new.test.d.ts +1 -0
- package/dist/cli/{init.test.js → new.test.js} +216 -78
- package/dist/cli/publish.d.ts +34 -0
- package/dist/cli/publish.js +368 -0
- package/dist/cli/templates.d.ts +13 -0
- package/dist/cli/templates.js +181 -0
- package/dist/executor.js +63 -20
- package/dist/executor.test.js +233 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +8 -0
- package/dist/mcp.test.js +67 -0
- package/dist/schema.d.ts +428 -18
- package/dist/schema.js +71 -4
- package/dist/skills/custom-loader.d.ts +12 -12
- package/dist/skills/custom-loader.js +105 -50
- package/dist/types.d.ts +18 -1
- package/dist/workflows/browser.js +3 -3
- package/package.json +10 -2
- package/dist/cli/init.d.ts +0 -53
- package/dist/cli/init.js +0 -521
- /package/dist/{cli/init.test.d.ts → __tests__/publish.test.d.ts} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { validateWorkflowFile, validateSkillDir } from "../cli/publish.js";
|
|
6
|
+
describe("validateWorkflowFile", () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-publish-test-"));
|
|
10
|
+
});
|
|
11
|
+
afterAll(() => {
|
|
12
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it("validates a correct workflow file", () => {
|
|
15
|
+
const file = path.join(tmpDir, "good.yml");
|
|
16
|
+
fs.writeFileSync(file, `id: test-workflow
|
|
17
|
+
name: Test
|
|
18
|
+
description: A test workflow
|
|
19
|
+
entry: start
|
|
20
|
+
nodes:
|
|
21
|
+
start:
|
|
22
|
+
name: Start
|
|
23
|
+
instruction: Do something.
|
|
24
|
+
skills: [github]
|
|
25
|
+
edges: []
|
|
26
|
+
`);
|
|
27
|
+
const result = validateWorkflowFile(file);
|
|
28
|
+
expect(result.valid).toBe(true);
|
|
29
|
+
expect(result.id).toBe("test-workflow");
|
|
30
|
+
expect(result.name).toBe("Test");
|
|
31
|
+
expect(result.nodeCount).toBe(1);
|
|
32
|
+
expect(result.edgeCount).toBe(0);
|
|
33
|
+
expect(result.errors).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("rejects a non-existent file", () => {
|
|
36
|
+
const result = validateWorkflowFile(path.join(tmpDir, "nope.yml"));
|
|
37
|
+
expect(result.valid).toBe(false);
|
|
38
|
+
expect(result.errors[0]).toBe("File not found");
|
|
39
|
+
});
|
|
40
|
+
it("rejects invalid YAML", () => {
|
|
41
|
+
const file = path.join(tmpDir, "bad.yml");
|
|
42
|
+
fs.writeFileSync(file, "{{invalid yaml");
|
|
43
|
+
const result = validateWorkflowFile(file);
|
|
44
|
+
expect(result.valid).toBe(false);
|
|
45
|
+
expect(result.errors[0]).toMatch(/Invalid YAML/);
|
|
46
|
+
});
|
|
47
|
+
it("rejects a workflow missing required fields", () => {
|
|
48
|
+
const file = path.join(tmpDir, "incomplete.yml");
|
|
49
|
+
fs.writeFileSync(file, "id: test\nname: Test\n");
|
|
50
|
+
const result = validateWorkflowFile(file);
|
|
51
|
+
expect(result.valid).toBe(false);
|
|
52
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("validateSkillDir", () => {
|
|
56
|
+
let tmpDir;
|
|
57
|
+
beforeAll(() => {
|
|
58
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-skill-test-"));
|
|
59
|
+
});
|
|
60
|
+
afterAll(() => {
|
|
61
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
it("validates a correct instruction skill", () => {
|
|
64
|
+
const skillDir = path.join(tmpDir, "my-skill");
|
|
65
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
66
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
67
|
+
name: my-skill
|
|
68
|
+
description: A test skill
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
When reviewing code, check for security issues first.
|
|
72
|
+
`);
|
|
73
|
+
const result = validateSkillDir(skillDir);
|
|
74
|
+
expect(result.valid).toBe(true);
|
|
75
|
+
expect(result.id).toBe("my-skill");
|
|
76
|
+
expect(result.hasInstruction).toBe(true);
|
|
77
|
+
expect(result.hasMcp).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it("validates a correct MCP skill", () => {
|
|
80
|
+
const skillDir = path.join(tmpDir, "mcp-skill");
|
|
81
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
82
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
83
|
+
name: mcp-skill
|
|
84
|
+
description: A test MCP skill
|
|
85
|
+
mcp:
|
|
86
|
+
command: npx
|
|
87
|
+
args: ["-y", "@company/server"]
|
|
88
|
+
---
|
|
89
|
+
`);
|
|
90
|
+
const result = validateSkillDir(skillDir);
|
|
91
|
+
expect(result.valid).toBe(true);
|
|
92
|
+
expect(result.hasMcp).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it("rejects a directory without SKILL.md", () => {
|
|
95
|
+
const emptyDir = path.join(tmpDir, "empty");
|
|
96
|
+
fs.mkdirSync(emptyDir, { recursive: true });
|
|
97
|
+
const result = validateSkillDir(emptyDir);
|
|
98
|
+
expect(result.valid).toBe(false);
|
|
99
|
+
expect(result.errors[0]).toBe("SKILL.md not found in directory");
|
|
100
|
+
});
|
|
101
|
+
it("rejects a skill with no frontmatter", () => {
|
|
102
|
+
const skillDir = path.join(tmpDir, "no-fm");
|
|
103
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), "Just some text");
|
|
105
|
+
const result = validateSkillDir(skillDir);
|
|
106
|
+
expect(result.valid).toBe(false);
|
|
107
|
+
expect(result.errors[0]).toBe("No YAML frontmatter found");
|
|
108
|
+
});
|
|
109
|
+
it("rejects an invalid skill ID", () => {
|
|
110
|
+
const skillDir = path.join(tmpDir, "Bad-Name");
|
|
111
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
112
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
113
|
+
name: Bad-Name
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
Some instruction.
|
|
117
|
+
`);
|
|
118
|
+
const result = validateSkillDir(skillDir);
|
|
119
|
+
expect(result.valid).toBe(false);
|
|
120
|
+
expect(result.errors[0]).toMatch(/Invalid skill ID/);
|
|
121
|
+
});
|
|
122
|
+
it("rejects a skill with mismatched directory name", () => {
|
|
123
|
+
const skillDir = path.join(tmpDir, "wrong-dir");
|
|
124
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
125
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
126
|
+
name: correct-name
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
Some instruction.
|
|
130
|
+
`);
|
|
131
|
+
const result = validateSkillDir(skillDir);
|
|
132
|
+
expect(result.valid).toBe(false);
|
|
133
|
+
expect(result.errors).toContain(`Directory name "wrong-dir" doesn't match skill name "correct-name"`);
|
|
134
|
+
});
|
|
135
|
+
it("rejects a skill with no instruction and no MCP", () => {
|
|
136
|
+
const skillDir = path.join(tmpDir, "empty-skill");
|
|
137
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
139
|
+
name: empty-skill
|
|
140
|
+
---
|
|
141
|
+
`);
|
|
142
|
+
const result = validateSkillDir(skillDir);
|
|
143
|
+
expect(result.valid).toBe(false);
|
|
144
|
+
expect(result.errors).toContain("Skill must have an instruction body or an mcp config");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { workflowZ, nodeZ, edgeZ, skillZ, toolZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "../schema.js";
|
|
2
|
+
import { workflowZ, nodeZ, edgeZ, skillZ, toolZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "../schema.js";
|
|
3
3
|
import { triageWorkflow, implementWorkflow } from "../workflows/index.js";
|
|
4
4
|
// ─── Test fixtures ───────────────────────────────────────────────
|
|
5
5
|
const validWorkflow = {
|
|
@@ -100,6 +100,128 @@ describe("Zod schemas", () => {
|
|
|
100
100
|
expect(result.tools).toHaveLength(1);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
|
+
describe("mcpServerConfigZ", () => {
|
|
104
|
+
it("accepts stdio config", () => {
|
|
105
|
+
const result = mcpServerConfigZ.parse({ command: "npx", args: ["-y", "@company/tool-server"] });
|
|
106
|
+
expect(result.command).toBe("npx");
|
|
107
|
+
});
|
|
108
|
+
it("accepts http config", () => {
|
|
109
|
+
const result = mcpServerConfigZ.parse({
|
|
110
|
+
url: "https://mcp.example.com",
|
|
111
|
+
headers: { Authorization: "Bearer token" },
|
|
112
|
+
});
|
|
113
|
+
expect(result.url).toBe("https://mcp.example.com");
|
|
114
|
+
});
|
|
115
|
+
it("accepts explicit type", () => {
|
|
116
|
+
const result = mcpServerConfigZ.parse({ type: "stdio", command: "node", args: ["server.js"] });
|
|
117
|
+
expect(result.type).toBe("stdio");
|
|
118
|
+
});
|
|
119
|
+
it("accepts env field", () => {
|
|
120
|
+
const result = mcpServerConfigZ.parse({
|
|
121
|
+
command: "npx",
|
|
122
|
+
args: ["-y", "server"],
|
|
123
|
+
env: { API_KEY: "Company API key" },
|
|
124
|
+
});
|
|
125
|
+
expect(result.env).toEqual({ API_KEY: "Company API key" });
|
|
126
|
+
});
|
|
127
|
+
it("rejects config with neither command nor url", () => {
|
|
128
|
+
expect(() => mcpServerConfigZ.parse({ args: ["foo"] })).toThrow("MCP server must have either command (stdio) or url (HTTP)");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("skillDefinitionZ", () => {
|
|
132
|
+
it("accepts instruction-only skill", () => {
|
|
133
|
+
const result = skillDefinitionZ.parse({
|
|
134
|
+
name: "Code Standards",
|
|
135
|
+
instruction: "Follow our coding conventions...",
|
|
136
|
+
});
|
|
137
|
+
expect(result.instruction).toBe("Follow our coding conventions...");
|
|
138
|
+
});
|
|
139
|
+
it("accepts mcp-only skill", () => {
|
|
140
|
+
const result = skillDefinitionZ.parse({ mcp: { command: "npx", args: ["-y", "server"] } });
|
|
141
|
+
expect(result.mcp?.command).toBe("npx");
|
|
142
|
+
});
|
|
143
|
+
it("accepts both instruction and mcp", () => {
|
|
144
|
+
const result = skillDefinitionZ.parse({
|
|
145
|
+
instruction: "Use this server for...",
|
|
146
|
+
mcp: { url: "https://mcp.example.com" },
|
|
147
|
+
});
|
|
148
|
+
expect(result.instruction).toBeDefined();
|
|
149
|
+
expect(result.mcp).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
it("rejects skill with neither instruction nor mcp", () => {
|
|
152
|
+
expect(() => skillDefinitionZ.parse({ name: "Empty" })).toThrow("Inline skill must provide instruction, mcp, or both");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe("skillZ with new fields", () => {
|
|
156
|
+
it("accepts skill with instruction and no tools", () => {
|
|
157
|
+
const result = skillZ.parse({
|
|
158
|
+
id: "code-standards",
|
|
159
|
+
name: "Code Standards",
|
|
160
|
+
description: "Team coding conventions",
|
|
161
|
+
category: "general",
|
|
162
|
+
instruction: "Follow camelCase naming...",
|
|
163
|
+
});
|
|
164
|
+
expect(result.tools).toEqual([]);
|
|
165
|
+
expect(result.instruction).toBe("Follow camelCase naming...");
|
|
166
|
+
});
|
|
167
|
+
it("accepts skill with mcp and no tools", () => {
|
|
168
|
+
const result = skillZ.parse({
|
|
169
|
+
id: "our-crm",
|
|
170
|
+
name: "Our CRM",
|
|
171
|
+
description: "CRM integration",
|
|
172
|
+
category: "general",
|
|
173
|
+
mcp: { url: "https://crm.example.com/mcp" },
|
|
174
|
+
});
|
|
175
|
+
expect(result.tools).toEqual([]);
|
|
176
|
+
expect(result.mcp?.url).toBe("https://crm.example.com/mcp");
|
|
177
|
+
});
|
|
178
|
+
it("rejects skill with no tools, no instruction, no mcp", () => {
|
|
179
|
+
expect(() => skillZ.parse({ id: "empty", name: "Empty", description: "Nothing", category: "general" })).toThrow("Skill must provide at least one of: tools, instruction, or mcp");
|
|
180
|
+
});
|
|
181
|
+
it("defaults tools to empty array", () => {
|
|
182
|
+
const result = skillZ.parse({
|
|
183
|
+
id: "x",
|
|
184
|
+
name: "X",
|
|
185
|
+
description: "X",
|
|
186
|
+
category: "general",
|
|
187
|
+
instruction: "do stuff",
|
|
188
|
+
});
|
|
189
|
+
expect(result.tools).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
it("defaults config to empty object", () => {
|
|
192
|
+
const result = skillZ.parse({
|
|
193
|
+
id: "x",
|
|
194
|
+
name: "X",
|
|
195
|
+
description: "X",
|
|
196
|
+
category: "general",
|
|
197
|
+
instruction: "do stuff",
|
|
198
|
+
});
|
|
199
|
+
expect(result.config).toEqual({});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe("workflowZ with skills", () => {
|
|
203
|
+
it("parses workflow with inline skills", () => {
|
|
204
|
+
const result = workflowZ.parse({
|
|
205
|
+
id: "test",
|
|
206
|
+
name: "Test",
|
|
207
|
+
entry: "a",
|
|
208
|
+
nodes: { a: { name: "A", instruction: "Do A", skills: ["my-skill"] } },
|
|
209
|
+
edges: [],
|
|
210
|
+
skills: { "my-skill": { name: "My Skill", instruction: "Custom guidance here" } },
|
|
211
|
+
});
|
|
212
|
+
expect(result.skills?.["my-skill"]?.instruction).toBe("Custom guidance here");
|
|
213
|
+
});
|
|
214
|
+
it("defaults skills to empty object", () => {
|
|
215
|
+
const result = workflowZ.parse({
|
|
216
|
+
id: "x",
|
|
217
|
+
name: "X",
|
|
218
|
+
entry: "a",
|
|
219
|
+
nodes: { a: { name: "A", instruction: "Do A" } },
|
|
220
|
+
edges: [],
|
|
221
|
+
});
|
|
222
|
+
expect(result.skills).toEqual({});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
103
225
|
describe("workflowZ", () => {
|
|
104
226
|
it("parses a valid workflow", () => {
|
|
105
227
|
const raw = JSON.parse(JSON.stringify(validWorkflow));
|
|
@@ -251,6 +373,39 @@ describe("validateWorkflow", () => {
|
|
|
251
373
|
const errors = validateWorkflow(empty);
|
|
252
374
|
expect(errors).toContainEqual(expect.objectContaining({ code: "MISSING_ENTRY" }));
|
|
253
375
|
});
|
|
376
|
+
it("recognizes inline workflow skills as known", () => {
|
|
377
|
+
const wf = {
|
|
378
|
+
...validWorkflow,
|
|
379
|
+
nodes: { ...validWorkflow.nodes, a: { ...validWorkflow.nodes.a, skills: ["custom-rubric"] } },
|
|
380
|
+
skills: { "custom-rubric": { instruction: "Score things 1-5" } },
|
|
381
|
+
};
|
|
382
|
+
const errors = validateWorkflow(wf, new Set(["github"]));
|
|
383
|
+
expect(errors.filter((e) => e.code === "UNKNOWN_SKILL")).toEqual([]);
|
|
384
|
+
});
|
|
385
|
+
it("rejects inline skills with neither instruction nor mcp", () => {
|
|
386
|
+
const wf = {
|
|
387
|
+
...validWorkflow,
|
|
388
|
+
skills: { "bad-skill": { name: "Bad" } },
|
|
389
|
+
};
|
|
390
|
+
const errors = validateWorkflow(wf);
|
|
391
|
+
expect(errors).toContainEqual(expect.objectContaining({ code: "INVALID_INLINE_SKILL" }));
|
|
392
|
+
});
|
|
393
|
+
it("accepts inline skills with instruction only", () => {
|
|
394
|
+
const wf = {
|
|
395
|
+
...validWorkflow,
|
|
396
|
+
skills: { "ok-skill": { instruction: "Do something" } },
|
|
397
|
+
};
|
|
398
|
+
const errors = validateWorkflow(wf);
|
|
399
|
+
expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
it("accepts inline skills with mcp only", () => {
|
|
402
|
+
const wf = {
|
|
403
|
+
...validWorkflow,
|
|
404
|
+
skills: { "mcp-skill": { mcp: { command: "npx", args: ["-y", "server"] } } },
|
|
405
|
+
};
|
|
406
|
+
const errors = validateWorkflow(wf);
|
|
407
|
+
expect(errors.filter((e) => e.code === "INVALID_INLINE_SKILL")).toEqual([]);
|
|
408
|
+
});
|
|
254
409
|
});
|
|
255
410
|
// ─── JSON Schema shape tests ─────────────────────────────────────
|
|
256
411
|
describe("workflowJsonSchema", () => {
|
|
@@ -9,7 +9,7 @@ import { sentry } from "../skills/sentry.js";
|
|
|
9
9
|
import { datadog } from "../skills/datadog.js";
|
|
10
10
|
import { notification } from "../skills/notification.js";
|
|
11
11
|
import { createSkillMap } from "../skills/index.js";
|
|
12
|
-
import { loadCustomSkills, configuredSkills } from "../skills/custom-loader.js";
|
|
12
|
+
import { loadCustomSkills, discoverSkills, configuredSkills } from "../skills/custom-loader.js";
|
|
13
13
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
14
14
|
const mockCtx = (config = {}) => ({
|
|
15
15
|
config,
|
|
@@ -399,13 +399,15 @@ describe("configuredSkills with custom skills", () => {
|
|
|
399
399
|
expect(ids).toContain("notification");
|
|
400
400
|
expect(ids).toContain("slack");
|
|
401
401
|
});
|
|
402
|
-
it("
|
|
403
|
-
writeSkill("notification", `---\nname: notification\ndescription: Custom override
|
|
402
|
+
it("custom skills override built-in ID but merge tools", () => {
|
|
403
|
+
writeSkill("notification", `---\nname: notification\ndescription: Custom override\n---\nCustom notification guidance.`);
|
|
404
404
|
const skills = configuredSkills({}, tmpDir);
|
|
405
405
|
const notif = skills.filter((s) => s.id === "notification");
|
|
406
406
|
expect(notif).toHaveLength(1);
|
|
407
|
-
//
|
|
407
|
+
// Built-in tools are preserved
|
|
408
408
|
expect(notif[0].tools.length).toBeGreaterThan(0);
|
|
409
|
+
// Custom instruction is added
|
|
410
|
+
expect(notif[0].instruction).toBe("Custom notification guidance.");
|
|
409
411
|
});
|
|
410
412
|
it("custom skills are usable in createSkillMap", () => {
|
|
411
413
|
writeSkill("extract-colors", `---\nname: extract-colors\ndescription: Route color extraction\n---\nBody`);
|
|
@@ -415,3 +417,132 @@ describe("configuredSkills with custom skills", () => {
|
|
|
415
417
|
expect(map.get("extract-colors").tools).toEqual([]);
|
|
416
418
|
});
|
|
417
419
|
});
|
|
420
|
+
// ─── Multi-directory discovery tests ─────────────────────────────
|
|
421
|
+
describe("discoverSkills (multi-directory)", () => {
|
|
422
|
+
let tmpDir;
|
|
423
|
+
beforeEach(() => {
|
|
424
|
+
tmpDir = mkdtempSync(join(tmpdir(), "sweny-discover-test-"));
|
|
425
|
+
});
|
|
426
|
+
afterEach(() => {
|
|
427
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
428
|
+
});
|
|
429
|
+
function writeSkillAt(base, dirName, content) {
|
|
430
|
+
const dir = join(tmpDir, base, "skills", dirName);
|
|
431
|
+
mkdirSync(dir, { recursive: true });
|
|
432
|
+
writeFileSync(join(dir, "SKILL.md"), content);
|
|
433
|
+
}
|
|
434
|
+
it("discovers skills from .sweny/skills/", () => {
|
|
435
|
+
writeSkillAt(".sweny", "my-skill", `---\nname: my-skill\ndescription: A skill\n---\nDo things well.`);
|
|
436
|
+
const skills = discoverSkills(tmpDir);
|
|
437
|
+
expect(skills).toHaveLength(1);
|
|
438
|
+
expect(skills[0].id).toBe("my-skill");
|
|
439
|
+
});
|
|
440
|
+
it("discovers skills from .claude/skills/", () => {
|
|
441
|
+
writeSkillAt(".claude", "claude-skill", `---\nname: claude-skill\ndescription: Claude\n---\nInstructions.`);
|
|
442
|
+
const skills = discoverSkills(tmpDir);
|
|
443
|
+
expect(skills).toHaveLength(1);
|
|
444
|
+
expect(skills[0].id).toBe("claude-skill");
|
|
445
|
+
});
|
|
446
|
+
it("discovers skills from .agents/skills/", () => {
|
|
447
|
+
writeSkillAt(".agents", "agent-skill", `---\nname: agent-skill\ndescription: Agent\n---\nBody.`);
|
|
448
|
+
const skills = discoverSkills(tmpDir);
|
|
449
|
+
expect(skills).toHaveLength(1);
|
|
450
|
+
});
|
|
451
|
+
it("discovers skills from .gemini/skills/", () => {
|
|
452
|
+
writeSkillAt(".gemini", "gemini-skill", `---\nname: gemini-skill\ndescription: Gemini\n---\nBody.`);
|
|
453
|
+
const skills = discoverSkills(tmpDir);
|
|
454
|
+
expect(skills).toHaveLength(1);
|
|
455
|
+
});
|
|
456
|
+
it("higher-priority directory wins on ID collision", () => {
|
|
457
|
+
writeSkillAt(".sweny", "shared", `---\nname: shared\ndescription: From sweny\n---\nSweny instructions.`);
|
|
458
|
+
writeSkillAt(".claude", "shared", `---\nname: shared\ndescription: From claude\n---\nClaude instructions.`);
|
|
459
|
+
const skills = discoverSkills(tmpDir);
|
|
460
|
+
const shared = skills.find((s) => s.id === "shared");
|
|
461
|
+
expect(shared.description).toBe("From sweny");
|
|
462
|
+
expect(shared.instruction).toBe("Sweny instructions.");
|
|
463
|
+
});
|
|
464
|
+
it("captures markdown body as instruction", () => {
|
|
465
|
+
writeSkillAt(".sweny", "code-standards", `---
|
|
466
|
+
name: code-standards
|
|
467
|
+
description: Coding conventions
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
When writing TypeScript:
|
|
471
|
+
- Use camelCase for variables
|
|
472
|
+
- Use PascalCase for types`);
|
|
473
|
+
const skills = discoverSkills(tmpDir);
|
|
474
|
+
expect(skills[0].instruction).toContain("Use camelCase for variables");
|
|
475
|
+
expect(skills[0].instruction).toContain("Use PascalCase for types");
|
|
476
|
+
});
|
|
477
|
+
it("parses mcp from frontmatter", () => {
|
|
478
|
+
writeSkillAt(".sweny", "our-crm", `---
|
|
479
|
+
name: our-crm
|
|
480
|
+
description: CRM integration
|
|
481
|
+
mcp:
|
|
482
|
+
command: npx
|
|
483
|
+
args:
|
|
484
|
+
- -y
|
|
485
|
+
- "@company/crm-server"
|
|
486
|
+
env:
|
|
487
|
+
API_KEY: CRM API key
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
Use the CRM tools to look up customer data.`);
|
|
491
|
+
const skills = discoverSkills(tmpDir);
|
|
492
|
+
expect(skills[0].mcp).toEqual({
|
|
493
|
+
type: "stdio",
|
|
494
|
+
command: "npx",
|
|
495
|
+
args: ["-y", "@company/crm-server"],
|
|
496
|
+
env: { API_KEY: "CRM API key" },
|
|
497
|
+
});
|
|
498
|
+
expect(skills[0].instruction).toContain("Use the CRM tools");
|
|
499
|
+
});
|
|
500
|
+
it("parses mcp with url (HTTP transport)", () => {
|
|
501
|
+
writeSkillAt(".sweny", "http-tool", `---
|
|
502
|
+
name: http-tool
|
|
503
|
+
description: HTTP MCP
|
|
504
|
+
mcp:
|
|
505
|
+
url: https://mcp.example.com
|
|
506
|
+
headers:
|
|
507
|
+
Authorization: Bearer token
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
Instructions.`);
|
|
511
|
+
const skills = discoverSkills(tmpDir);
|
|
512
|
+
expect(skills[0].mcp?.url).toBe("https://mcp.example.com");
|
|
513
|
+
});
|
|
514
|
+
it("returns empty array when no skill directories exist", () => {
|
|
515
|
+
const skills = discoverSkills(tmpDir);
|
|
516
|
+
expect(skills).toEqual([]);
|
|
517
|
+
});
|
|
518
|
+
it("merges skills from all directories", () => {
|
|
519
|
+
writeSkillAt(".sweny", "skill-a", `---\nname: skill-a\ndescription: A\n---\nA.`);
|
|
520
|
+
writeSkillAt(".claude", "skill-b", `---\nname: skill-b\ndescription: B\n---\nB.`);
|
|
521
|
+
writeSkillAt(".agents", "skill-c", `---\nname: skill-c\ndescription: C\n---\nC.`);
|
|
522
|
+
const skills = discoverSkills(tmpDir);
|
|
523
|
+
expect(skills).toHaveLength(3);
|
|
524
|
+
const ids = skills.map((s) => s.id).sort();
|
|
525
|
+
expect(ids).toEqual(["skill-a", "skill-b", "skill-c"]);
|
|
526
|
+
});
|
|
527
|
+
it("rejects skill IDs with invalid characters", () => {
|
|
528
|
+
writeSkillAt(".sweny", "valid-skill", `---\nname: ../../etc/passwd\ndescription: malicious\n---\nBody`);
|
|
529
|
+
const skills = discoverSkills(tmpDir);
|
|
530
|
+
expect(skills).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
it("rejects skill IDs with uppercase", () => {
|
|
533
|
+
writeSkillAt(".sweny", "MySkill", `---\nname: MySkill\ndescription: bad\n---\nBody`);
|
|
534
|
+
const skills = discoverSkills(tmpDir);
|
|
535
|
+
expect(skills).toHaveLength(0);
|
|
536
|
+
});
|
|
537
|
+
it("rejects skill IDs with consecutive hyphens", () => {
|
|
538
|
+
writeSkillAt(".sweny", "bad--name", `---\nname: bad--name\ndescription: bad\n---\nBody`);
|
|
539
|
+
const skills = discoverSkills(tmpDir);
|
|
540
|
+
expect(skills).toHaveLength(0);
|
|
541
|
+
});
|
|
542
|
+
it("rejects skill IDs longer than 64 chars", () => {
|
|
543
|
+
const longId = "a".repeat(65);
|
|
544
|
+
writeSkillAt(".sweny", longId, `---\nname: ${longId}\ndescription: long\n---\nBody`);
|
|
545
|
+
const skills = discoverSkills(tmpDir);
|
|
546
|
+
expect(skills).toHaveLength(0);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { WORKFLOW_TEMPLATES } from "../cli/templates.js";
|
|
4
|
+
import { parseWorkflow, validateWorkflow } from "../schema.js";
|
|
5
|
+
describe("WORKFLOW_TEMPLATES", () => {
|
|
6
|
+
it("has at least 4 templates", () => {
|
|
7
|
+
expect(WORKFLOW_TEMPLATES.length).toBeGreaterThanOrEqual(4);
|
|
8
|
+
});
|
|
9
|
+
it("each template has unique id, name, and description", () => {
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
for (const t of WORKFLOW_TEMPLATES) {
|
|
12
|
+
expect(t.id).toBeTruthy();
|
|
13
|
+
expect(t.name).toBeTruthy();
|
|
14
|
+
expect(t.description).toBeTruthy();
|
|
15
|
+
expect(ids.has(t.id)).toBe(false);
|
|
16
|
+
ids.add(t.id);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
for (const template of WORKFLOW_TEMPLATES) {
|
|
20
|
+
describe(`template: ${template.id}`, () => {
|
|
21
|
+
it("parses as valid YAML", () => {
|
|
22
|
+
const parsed = parse(template.yaml);
|
|
23
|
+
expect(parsed).toBeDefined();
|
|
24
|
+
expect(parsed.id).toBe(template.id);
|
|
25
|
+
});
|
|
26
|
+
it("passes parseWorkflow validation", () => {
|
|
27
|
+
const parsed = parse(template.yaml);
|
|
28
|
+
const workflow = parseWorkflow(parsed);
|
|
29
|
+
expect(workflow.id).toBe(template.id);
|
|
30
|
+
expect(Object.keys(workflow.nodes).length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
it("passes validateWorkflow with no errors", () => {
|
|
33
|
+
const parsed = parse(template.yaml);
|
|
34
|
+
const workflow = parseWorkflow(parsed);
|
|
35
|
+
const errors = validateWorkflow(workflow);
|
|
36
|
+
expect(errors).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
it("uses only the github skill", () => {
|
|
39
|
+
const parsed = parse(template.yaml);
|
|
40
|
+
const workflow = parseWorkflow(parsed);
|
|
41
|
+
for (const node of Object.values(workflow.nodes)) {
|
|
42
|
+
for (const skill of node.skills) {
|
|
43
|
+
expect(skill).toBe("github");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
@@ -9,7 +9,7 @@ import { execSync } from "node:child_process";
|
|
|
9
9
|
* These tests validate that:
|
|
10
10
|
* 1. No stale v4 action references remain in source or docs
|
|
11
11
|
* 2. The README uses dark-mode-safe logo rendering
|
|
12
|
-
* 3. The CLI
|
|
12
|
+
* 3. The CLI new wizard generates the correct action version
|
|
13
13
|
* 4. Docs code blocks don't mix the generic action with triage-only inputs
|
|
14
14
|
*/
|
|
15
15
|
const ROOT = resolve(import.meta.dirname, "..", "..", "..", "..");
|
|
@@ -69,10 +69,10 @@ describe("action version consistency", () => {
|
|
|
69
69
|
const claude = readFileSync(resolve(ROOT, "CLAUDE.md"), "utf-8");
|
|
70
70
|
expect(claude).toMatch(new RegExp(`@sweny-ai/core.*${CURRENT_ACTION_VERSION}`));
|
|
71
71
|
});
|
|
72
|
-
it("CLI
|
|
73
|
-
const
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
72
|
+
it("CLI new wizard generates workflows with the current action version", () => {
|
|
73
|
+
const newSrc = readFileSync(resolve(ROOT, "packages/core/src/cli/new.ts"), "utf-8");
|
|
74
|
+
expect(newSrc).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
|
|
75
|
+
expect(newSrc).not.toContain(`swenyai/sweny@${PREV_VERSION}`);
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
// ─── 2. README quality ──────────────────────────────────────────────
|
|
@@ -96,8 +96,8 @@ describe("README", () => {
|
|
|
96
96
|
it("does not use the old Recipes heading", () => {
|
|
97
97
|
expect(readme).not.toMatch(/^## Recipes$/m);
|
|
98
98
|
});
|
|
99
|
-
it("has
|
|
100
|
-
expect(readme).toMatch(/^## Actions$/m);
|
|
99
|
+
it("has a GitHub Actions section with the multi-repo table", () => {
|
|
100
|
+
expect(readme).toMatch(/^## GitHub Actions$/m);
|
|
101
101
|
expect(readme).toContain("swenyai/triage@v1");
|
|
102
102
|
expect(readme).toContain("swenyai/e2e@v1");
|
|
103
103
|
expect(readme).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
|
|
@@ -11,5 +11,5 @@ export type FileConfig = Record<string, string | string[]>;
|
|
|
11
11
|
* Returns empty object if no config file is found.
|
|
12
12
|
*/
|
|
13
13
|
export declare function loadConfigFile(cwd?: string): FileConfig;
|
|
14
|
-
/** Starter config written by `sweny
|
|
14
|
+
/** Starter config written by `sweny new`. */
|
|
15
15
|
export declare const STARTER_CONFIG = "# .sweny.yml \u2014 SWEny project configuration\n# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).\n#\n# Every key matches a CLI flag: \"time-range: 4h\" is the same as \"--time-range 4h\".\n# CLI flags override this file; env vars override this file; this file overrides defaults.\n\n# \u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# observability-provider: datadog # datadog | sentry | cloudwatch | splunk | elastic | newrelic | loki | prometheus | pagerduty | heroku | opsgenie | vercel | supabase | netlify | fly | render | file\n# issue-tracker-provider: github-issues # github-issues | linear | jira\n# source-control-provider: github # github | gitlab\n# coding-agent-provider: claude # claude | codex | gemini\n# notification-provider: console # console | slack | teams | discord | email | webhook\n\n# \u2500\u2500 Investigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# time-range: 24h\n# severity-focus: errors\n# service-filter: \"*\"\n# investigation-depth: standard # quick | standard | thorough\n\n# \u2500\u2500 PR / branch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# base-branch: main\n# pr-labels: agent,triage,needs-review\n\n# \u2500\u2500 Paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# service-map-path: .github/service-map.yml\n# log-file: ./logs/errors.json # required when observability-provider is \"file\"\n\n# \u2500\u2500 Cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# cache-dir: .sweny/cache\n# cache-ttl: 86400\n\n# \u2500\u2500 MCP servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Extend the coding agent with additional tools via MCP.\n# Value is a JSON object \u2014 each key is a server name you choose.\n# See docs/mcp-servers.md for a full catalog with copy-paste configs.\n#\n# Example: GitHub MCP server (query PRs, issues, CI run logs)\n# mcp-servers-json: '{\"github\":{\"type\":\"stdio\",\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-github@latest\"],\"env\":{\"GITHUB_PERSONAL_ACCESS_TOKEN\":\"ghp_...\"}}}'\n\n# \u2500\u2500 Local-only quick start \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Uncomment to run without any external services (just an LLM API key):\n# observability-provider: file\n# log-file: ./sample-errors.json\n# issue-tracker-provider: file\n# source-control-provider: file\n# notification-provider: file\n# output-dir: .sweny/output\n\n# \u2500\u2500 Credentials (.env) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Copy the relevant block into your .env file and fill in the values.\n#\n# Claude (coding agent) \u2014 https://console.anthropic.com/settings/api-keys\n# ANTHROPIC_API_KEY=sk-ant-...\n#\n# GitHub (source control + issue tracker)\n# GITHUB_TOKEN=ghp_... # https://github.com/settings/tokens (repo + issues scopes)\n#\n# Datadog (observability) \u2014 https://app.datadoghq.com/organization-settings\n# DD_API_KEY=... # Organization Settings > API Keys\n# DD_APP_KEY=... # Organization Settings > Application Keys\n# DD_SITE=datadoghq.com # or datadoghq.eu, us3.datadoghq.com, etc.\n#\n# Sentry (observability) \u2014 https://sentry.io/settings/auth-tokens/\n# SENTRY_AUTH_TOKEN=sntrys_...\n# SENTRY_ORG=your-org-slug # from sentry.io/organizations/<slug>/\n# SENTRY_PROJECT=your-project # Project Settings > General > Project Slug\n#\n# Linear (issue tracker) \u2014 https://linear.app/settings/api\n# LINEAR_API_KEY=lin_api_...\n# LINEAR_TEAM_ID=... # Settings > Workspace > Teams > [team] > copy ID from URL\n# LINEAR_BUG_LABEL_ID=... # Settings > Labels > [label] > copy ID from URL\n#\n# Jira (issue tracker) \u2014 https://your-org.atlassian.net\n# JIRA_BASE_URL=https://your-org.atlassian.net\n# JIRA_EMAIL=you@company.com # your Atlassian account email\n# JIRA_API_TOKEN=... # https://id.atlassian.com/manage-profile/security/api-tokens\n#\n# Vercel (observability) \u2014 https://vercel.com/account/tokens\n# VERCEL_TOKEN=...\n# VERCEL_PROJECT_ID=prj_... # Project Settings > General > Project ID\n# VERCEL_TEAM_ID=team_... # optional, for team-owned projects\n#\n# Supabase (observability) \u2014 https://supabase.com/dashboard/account/tokens\n# SUPABASE_MANAGEMENT_KEY=...\n# SUPABASE_PROJECT_REF=... # Project Settings > General > Reference ID\n#\n# Netlify (observability) \u2014 https://app.netlify.com/user/applications#personal-access-tokens\n# NETLIFY_TOKEN=...\n# NETLIFY_SITE_ID=... # Site Settings > General > Site ID\n#\n# Fly.io (observability) \u2014 https://fly.io/user/personal_access_tokens\n# FLY_TOKEN=...\n# FLY_APP_NAME=... # the name of your Fly.io application\n#\n# Render (observability) \u2014 https://dashboard.render.com/u/settings\n# RENDER_API_KEY=...\n# RENDER_SERVICE_ID=srv-... # from your service's Settings page\n#\n# Prometheus (observability) \u2014 self-hosted or Grafana Cloud\n# PROMETHEUS_URL=http://prometheus.internal:9090\n# PROMETHEUS_TOKEN=... # optional, for secured instances\n#\n# PagerDuty (observability) \u2014 https://your-account.pagerduty.com/api_keys\n# PAGERDUTY_API_KEY=...\n#\n# Honeycomb (observability) \u2014 https://docs.honeycomb.io/api/\n# HONEYCOMB_API_KEY=...\n# HONEYCOMB_DATASET=... # dataset name (e.g. production)\n#\n# Heroku (observability) \u2014 https://devcenter.heroku.com/articles/platform-api-reference\n# HEROKU_API_KEY=... # https://dashboard.heroku.com/account\n# HEROKU_APP_NAME=... # the name of your Heroku application\n#\n# OpsGenie (observability) \u2014 https://support.atlassian.com/opsgenie/docs/api-key-management/\n# OPSGENIE_API_KEY=...\n# OPSGENIE_REGION=us # or eu for EU-hosted accounts\n#\n# Slack (notifications) \u2014 https://api.slack.com/apps\n# NOTIFICATION_WEBHOOK_URL=https://hooks.slack.com/services/...\n# # or use a bot token: SLACK_BOT_TOKEN=xoxb-...\n";
|
package/dist/cli/config-file.js
CHANGED
|
@@ -86,7 +86,7 @@ function findConfigFile(startDir) {
|
|
|
86
86
|
dir = parent;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
/** Starter config written by `sweny
|
|
89
|
+
/** Starter config written by `sweny new`. */
|
|
90
90
|
export const STARTER_CONFIG = `# .sweny.yml — SWEny project configuration
|
|
91
91
|
# Commit this file. Secrets (API keys, tokens) go in .env (gitignored).
|
|
92
92
|
#
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* and buildSkillMcpServers.
|
|
4
4
|
*
|
|
5
5
|
* Several env vars have two accepted spellings — the user-facing one
|
|
6
|
-
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in
|
|
6
|
+
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in new.ts,
|
|
7
7
|
* config.ts, and docs, and the canonical one used by the MCP wiring layer
|
|
8
8
|
* (`NEW_RELIC_API_KEY`, `JIRA_URL`, `SENTRY_URL`, …). We accept either
|
|
9
9
|
* spelling and write the value under the canonical key the wiring functions
|
package/dist/cli/credentials.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* and buildSkillMcpServers.
|
|
4
4
|
*
|
|
5
5
|
* Several env vars have two accepted spellings — the user-facing one
|
|
6
|
-
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in
|
|
6
|
+
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in new.ts,
|
|
7
7
|
* config.ts, and docs, and the canonical one used by the MCP wiring layer
|
|
8
8
|
* (`NEW_RELIC_API_KEY`, `JIRA_URL`, `SENTRY_URL`, …). We accept either
|
|
9
9
|
* spelling and write the value under the canonical key the wiring functions
|
|
@@ -6,7 +6,7 @@ import { buildSkillMcpServers } from "../mcp.js";
|
|
|
6
6
|
// ─────────────────────────────────────────────────────────────────────
|
|
7
7
|
//
|
|
8
8
|
// These tests catch env-var name drift between:
|
|
9
|
-
// - the user-facing env var name (what docs /
|
|
9
|
+
// - the user-facing env var name (what docs / new.ts / config.ts read)
|
|
10
10
|
// - the canonical key the MCP wiring functions look up
|
|
11
11
|
//
|
|
12
12
|
// Unit tests in mcp.test.ts inject the credentials map directly. That
|
package/dist/cli/e2e.d.ts
CHANGED
|
@@ -66,11 +66,15 @@ export declare function buildFlowWorkflow(flow: FlowConfig, baseUrl: string, cle
|
|
|
66
66
|
* Build .env template content for e2e testing.
|
|
67
67
|
*/
|
|
68
68
|
export declare function buildE2eEnvTemplate(selections: E2eSelections): string;
|
|
69
|
+
export interface E2eInitOptions {
|
|
70
|
+
/** Skip the `p.intro()` call — used when invoked from inside another wizard. */
|
|
71
|
+
skipIntro?: boolean;
|
|
72
|
+
}
|
|
69
73
|
/**
|
|
70
74
|
* Interactive E2E setup wizard — prompts through flow selection,
|
|
71
75
|
* then generates .sweny/e2e/*.yml and .env additions.
|
|
72
76
|
*/
|
|
73
|
-
export declare function runE2eInit(): Promise<void>;
|
|
77
|
+
export declare function runE2eInit(options?: E2eInitOptions): Promise<void>;
|
|
74
78
|
export interface E2eRunOptions {
|
|
75
79
|
file?: string;
|
|
76
80
|
timeout?: number;
|