@sweny-ai/core 0.1.76 → 0.1.78

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.
Files changed (123) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +20 -0
  4. package/dist/claude.js +139 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/executor.js +104 -43
  13. package/dist/loader.d.ts +40 -0
  14. package/dist/loader.js +73 -0
  15. package/dist/mcp-catalog.d.ts +47 -0
  16. package/dist/mcp-catalog.js +304 -0
  17. package/dist/mcp.d.ts +6 -9
  18. package/dist/mcp.js +24 -314
  19. package/dist/requires.d.ts +7 -0
  20. package/dist/requires.js +34 -0
  21. package/dist/retry.d.ts +23 -0
  22. package/dist/retry.js +72 -0
  23. package/dist/schema.d.ts +667 -80
  24. package/dist/schema.js +144 -5
  25. package/dist/skills/custom-loader.d.ts +40 -0
  26. package/dist/skills/custom-loader.js +107 -32
  27. package/dist/skills/index.d.ts +13 -1
  28. package/dist/skills/index.js +12 -3
  29. package/dist/studio.js +4 -1
  30. package/dist/templates.d.ts +29 -8
  31. package/dist/templates.js +30 -20
  32. package/dist/testing.d.ts +7 -0
  33. package/dist/testing.js +7 -0
  34. package/dist/types.d.ts +76 -0
  35. package/dist/verify.d.ts +1 -0
  36. package/dist/verify.js +43 -5
  37. package/package.json +8 -4
  38. package/dist/__tests__/cascade.test.d.ts +0 -11
  39. package/dist/__tests__/cascade.test.js +0 -449
  40. package/dist/__tests__/claude.test.d.ts +0 -1
  41. package/dist/__tests__/claude.test.js +0 -427
  42. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  43. package/dist/__tests__/custom-loader.test.js +0 -116
  44. package/dist/__tests__/executor.test.d.ts +0 -1
  45. package/dist/__tests__/executor.test.js +0 -736
  46. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  48. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  50. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/github.integration.test.js +0 -37
  52. package/dist/__tests__/integration/harness.d.ts +0 -24
  53. package/dist/__tests__/integration/harness.js +0 -34
  54. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  55. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  56. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  57. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  58. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  59. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  60. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  61. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  62. package/dist/__tests__/publish.test.d.ts +0 -1
  63. package/dist/__tests__/publish.test.js +0 -180
  64. package/dist/__tests__/schema.test.d.ts +0 -1
  65. package/dist/__tests__/schema.test.js +0 -526
  66. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  67. package/dist/__tests__/skill-config-integration.test.js +0 -204
  68. package/dist/__tests__/skills-index.test.d.ts +0 -1
  69. package/dist/__tests__/skills-index.test.js +0 -119
  70. package/dist/__tests__/skills.test.d.ts +0 -1
  71. package/dist/__tests__/skills.test.js +0 -565
  72. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  73. package/dist/__tests__/spec-conformance.test.js +0 -1074
  74. package/dist/__tests__/studio.test.d.ts +0 -1
  75. package/dist/__tests__/studio.test.js +0 -403
  76. package/dist/__tests__/templates.test.d.ts +0 -1
  77. package/dist/__tests__/templates.test.js +0 -49
  78. package/dist/__tests__/testing.test.d.ts +0 -1
  79. package/dist/__tests__/testing.test.js +0 -224
  80. package/dist/__tests__/verify.test.d.ts +0 -1
  81. package/dist/__tests__/verify.test.js +0 -517
  82. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  83. package/dist/__tests__/version-consistency.test.js +0 -312
  84. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  85. package/dist/__tests__/workflow-builder.test.js +0 -110
  86. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  87. package/dist/__tests__/workflow-yaml.test.js +0 -201
  88. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  89. package/dist/cli/__tests__/diagram.test.js +0 -225
  90. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  91. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  92. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  93. package/dist/cli/__tests__/upgrade.test.js +0 -317
  94. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  95. package/dist/cli/__tests__/version-check.test.js +0 -209
  96. package/dist/cli/config.test.d.ts +0 -1
  97. package/dist/cli/config.test.js +0 -200
  98. package/dist/cli/credentials.test.d.ts +0 -1
  99. package/dist/cli/credentials.test.js +0 -172
  100. package/dist/cli/e2e.test.d.ts +0 -1
  101. package/dist/cli/e2e.test.js +0 -690
  102. package/dist/cli/marketplace.test.d.ts +0 -1
  103. package/dist/cli/marketplace.test.js +0 -355
  104. package/dist/cli/new.test.d.ts +0 -1
  105. package/dist/cli/new.test.js +0 -1043
  106. package/dist/cli/output.test.d.ts +0 -1
  107. package/dist/cli/output.test.js +0 -180
  108. package/dist/cli/publish.test.d.ts +0 -1
  109. package/dist/cli/publish.test.js +0 -610
  110. package/dist/cli/renderer.test.d.ts +0 -1
  111. package/dist/cli/renderer.test.js +0 -302
  112. package/dist/executor.test.d.ts +0 -1
  113. package/dist/executor.test.js +0 -603
  114. package/dist/mcp.test.d.ts +0 -1
  115. package/dist/mcp.test.js +0 -735
  116. package/dist/mermaid.test.d.ts +0 -1
  117. package/dist/mermaid.test.js +0 -256
  118. package/dist/sources.test.d.ts +0 -1
  119. package/dist/sources.test.js +0 -265
  120. package/dist/sources.validation.test.d.ts +0 -12
  121. package/dist/sources.validation.test.js +0 -380
  122. package/dist/workflow-builder.test.d.ts +0 -1
  123. package/dist/workflow-builder.test.js +0 -117
@@ -1,610 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- // ── Mocks ─────────────────────────────────────────────────────────
5
- const mockExecFileSync = vi.fn();
6
- vi.mock("node:child_process", () => ({
7
- execFileSync: mockExecFileSync,
8
- }));
9
- vi.mock("../schema.js", () => ({
10
- parseWorkflow: vi.fn(),
11
- validateWorkflow: vi.fn(),
12
- }));
13
- const mockSelect = vi.fn();
14
- const mockText = vi.fn();
15
- const mockSpinner = vi.fn();
16
- const mockIsCancel = vi.fn((_value) => false);
17
- const mockIntro = vi.fn();
18
- const mockCancel = vi.fn();
19
- const mockLog = { error: vi.fn(), success: vi.fn(), info: vi.fn() };
20
- vi.mock("@clack/prompts", () => ({
21
- select: (...args) => mockSelect(...args),
22
- text: (...args) => mockText(...args),
23
- spinner: () => {
24
- const s = { start: vi.fn(), message: vi.fn(), stop: vi.fn() };
25
- mockSpinner(s);
26
- return s;
27
- },
28
- isCancel: (value) => mockIsCancel(value),
29
- intro: (...args) => mockIntro(...args),
30
- cancel: (...args) => mockCancel(...args),
31
- log: mockLog,
32
- }));
33
- vi.mock("chalk", () => ({
34
- default: { cyan: (s) => s },
35
- }));
36
- const { validateWorkflowFile, validateSkillDir, runPublish } = await import("./publish.js");
37
- const { parseWorkflow, validateWorkflow } = await import("../schema.js");
38
- // ── Helpers ───────────────────────────────────────────────────────
39
- let tmpDirs = [];
40
- function makeTmpDir() {
41
- const dir = fs.mkdtempSync(path.join(process.env.TMPDIR ?? "/tmp", "publish-test-"));
42
- tmpDirs.push(dir);
43
- return dir;
44
- }
45
- function makeTmpFile(name, content) {
46
- const dir = makeTmpDir();
47
- const filePath = path.join(dir, name);
48
- fs.writeFileSync(filePath, content);
49
- return filePath;
50
- }
51
- /** Minimal mock node that satisfies the Node type. */
52
- const mockNode = (name = "n") => ({ name, instruction: "test", skills: [] });
53
- // ── Setup / Teardown ──────────────────────────────────────────────
54
- beforeEach(() => {
55
- vi.clearAllMocks();
56
- mockIsCancel.mockReturnValue(false);
57
- tmpDirs = [];
58
- });
59
- afterEach(() => {
60
- for (const dir of tmpDirs) {
61
- fs.rmSync(dir, { recursive: true, force: true });
62
- }
63
- });
64
- // ── validateWorkflowFile ──────────────────────────────────────────
65
- describe("validateWorkflowFile", () => {
66
- it("returns error for missing file", () => {
67
- const result = validateWorkflowFile("/nonexistent/file.yml");
68
- expect(result.valid).toBe(false);
69
- expect(result.errors).toContain("File not found");
70
- });
71
- it("returns error for invalid YAML", () => {
72
- const filePath = makeTmpFile("bad.yml", "{{{{invalid yaml");
73
- const result = validateWorkflowFile(filePath);
74
- expect(result.valid).toBe(false);
75
- expect(result.errors[0]).toMatch(/Invalid YAML/);
76
- });
77
- it("returns schema validation errors with id and name", () => {
78
- const filePath = makeTmpFile("test.yml", "id: test\nname: Test\n");
79
- vi.mocked(parseWorkflow).mockReturnValue({
80
- id: "test",
81
- name: "Test",
82
- description: "",
83
- nodes: { start: mockNode("start") },
84
- edges: [],
85
- entry: "start",
86
- skills: {},
87
- });
88
- vi.mocked(validateWorkflow).mockReturnValue([
89
- { code: "MISSING_ENTRY", message: 'Entry node "start" does not exist' },
90
- ]);
91
- const result = validateWorkflowFile(filePath);
92
- expect(result.valid).toBe(false);
93
- expect(result.id).toBe("test");
94
- expect(result.name).toBe("Test");
95
- expect(result.errors).toContain('Entry node "start" does not exist');
96
- });
97
- it("returns valid result with node and edge counts", () => {
98
- const filePath = makeTmpFile("good.yml", "id: test\nname: Good\n");
99
- vi.mocked(parseWorkflow).mockReturnValue({
100
- id: "test",
101
- name: "Good",
102
- description: "",
103
- nodes: { a: mockNode("a"), b: mockNode("b"), c: mockNode("c") },
104
- edges: [
105
- { from: "a", to: "b" },
106
- { from: "b", to: "c" },
107
- ],
108
- entry: "a",
109
- skills: {},
110
- });
111
- vi.mocked(validateWorkflow).mockReturnValue([]);
112
- const result = validateWorkflowFile(filePath);
113
- expect(result.valid).toBe(true);
114
- expect(result.nodeCount).toBe(3);
115
- expect(result.edgeCount).toBe(2);
116
- expect(result.errors).toEqual([]);
117
- });
118
- it("catches parseWorkflow throw as schema error", () => {
119
- const filePath = makeTmpFile("throw.yml", "id: test\n");
120
- vi.mocked(parseWorkflow).mockImplementation(() => {
121
- throw new Error("Missing required field: entry");
122
- });
123
- const result = validateWorkflowFile(filePath);
124
- expect(result.valid).toBe(false);
125
- expect(result.errors[0]).toMatch(/Schema error.*Missing required field/);
126
- });
127
- });
128
- // ── validateSkillDir ──────────────────────────────────────────────
129
- describe("validateSkillDir", () => {
130
- it("returns error when SKILL.md is missing", () => {
131
- const dir = makeTmpDir();
132
- const result = validateSkillDir(dir);
133
- expect(result.valid).toBe(false);
134
- expect(result.errors).toContain("SKILL.md not found in directory");
135
- });
136
- it("returns error when no frontmatter found", () => {
137
- const dir = makeTmpDir();
138
- fs.writeFileSync(path.join(dir, "SKILL.md"), "Just text, no frontmatter");
139
- const result = validateSkillDir(dir);
140
- expect(result.valid).toBe(false);
141
- expect(result.errors).toContain("No YAML frontmatter found");
142
- });
143
- it("returns error for invalid frontmatter YAML", () => {
144
- const dir = makeTmpDir();
145
- fs.writeFileSync(path.join(dir, "SKILL.md"), "---\n{{{{bad\n---\n\nBody");
146
- const result = validateSkillDir(dir);
147
- expect(result.valid).toBe(false);
148
- expect(result.errors[0]).toMatch(/Invalid frontmatter YAML/);
149
- });
150
- it("returns error when name is missing from frontmatter", () => {
151
- const dir = makeTmpDir();
152
- fs.writeFileSync(path.join(dir, "SKILL.md"), "---\ndescription: test\n---\n\nBody");
153
- const result = validateSkillDir(dir);
154
- expect(result.valid).toBe(false);
155
- expect(result.errors).toContain("Missing 'name' field in frontmatter");
156
- });
157
- it("rejects uppercase in skill ID", () => {
158
- const dir = makeTmpDir();
159
- const skillDir = path.join(dir, "BadName");
160
- fs.mkdirSync(skillDir);
161
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: BadName\n---\n\nBody");
162
- const result = validateSkillDir(skillDir);
163
- expect(result.valid).toBe(false);
164
- expect(result.errors[0]).toMatch(/Invalid skill ID/);
165
- });
166
- it("rejects consecutive hyphens in skill ID", () => {
167
- const dir = makeTmpDir();
168
- const skillDir = path.join(dir, "my--skill");
169
- fs.mkdirSync(skillDir);
170
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: my--skill\n---\n\nBody");
171
- const result = validateSkillDir(skillDir);
172
- expect(result.valid).toBe(false);
173
- expect(result.errors[0]).toMatch(/Invalid skill ID/);
174
- });
175
- it("rejects skill ID over 64 characters", () => {
176
- const dir = makeTmpDir();
177
- const longId = "a".repeat(65);
178
- const skillDir = path.join(dir, longId);
179
- fs.mkdirSync(skillDir);
180
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---\nname: ${longId}\n---\n\nBody`);
181
- const result = validateSkillDir(skillDir);
182
- expect(result.valid).toBe(false);
183
- expect(result.errors[0]).toMatch(/Invalid skill ID/);
184
- });
185
- it("accepts exactly 64-character skill ID", () => {
186
- const dir = makeTmpDir();
187
- const id64 = "a".repeat(64);
188
- const skillDir = path.join(dir, id64);
189
- fs.mkdirSync(skillDir);
190
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---\nname: ${id64}\n---\n\nBody`);
191
- const result = validateSkillDir(skillDir);
192
- expect(result.valid).toBe(true);
193
- });
194
- it("rejects skill ID starting with hyphen", () => {
195
- const dir = makeTmpDir();
196
- const skillDir = path.join(dir, "-bad");
197
- fs.mkdirSync(skillDir);
198
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: -bad\n---\n\nBody");
199
- const result = validateSkillDir(skillDir);
200
- expect(result.valid).toBe(false);
201
- });
202
- it("rejects skill ID ending with hyphen", () => {
203
- const dir = makeTmpDir();
204
- const skillDir = path.join(dir, "bad-");
205
- fs.mkdirSync(skillDir);
206
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: bad-\n---\n\nBody");
207
- const result = validateSkillDir(skillDir);
208
- expect(result.valid).toBe(false);
209
- });
210
- it("reports when directory name doesn't match skill name", () => {
211
- const dir = makeTmpDir();
212
- const skillDir = path.join(dir, "wrong-name");
213
- fs.mkdirSync(skillDir);
214
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: real-name\n---\n\nBody");
215
- const result = validateSkillDir(skillDir);
216
- expect(result.valid).toBe(false);
217
- expect(result.errors).toContain('Directory name "wrong-name" doesn\'t match skill name "real-name"');
218
- });
219
- it("rejects skill with neither instruction body nor mcp", () => {
220
- const dir = makeTmpDir();
221
- const skillDir = path.join(dir, "empty");
222
- fs.mkdirSync(skillDir);
223
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: empty\n---\n");
224
- const result = validateSkillDir(skillDir);
225
- expect(result.valid).toBe(false);
226
- expect(result.errors).toContain("Skill must have an instruction body or an mcp config");
227
- });
228
- it("validates skill with instruction body", () => {
229
- const dir = makeTmpDir();
230
- const skillDir = path.join(dir, "my-skill");
231
- fs.mkdirSync(skillDir);
232
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: my-skill\n---\n\nDo the thing.");
233
- const result = validateSkillDir(skillDir);
234
- expect(result.valid).toBe(true);
235
- expect(result.id).toBe("my-skill");
236
- expect(result.hasInstruction).toBe(true);
237
- expect(result.hasMcp).toBe(false);
238
- });
239
- it("validates skill with mcp config only", () => {
240
- const dir = makeTmpDir();
241
- const skillDir = path.join(dir, "mcp-only");
242
- fs.mkdirSync(skillDir);
243
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), '---\nname: mcp-only\nmcp:\n command: "node"\n---\n');
244
- const result = validateSkillDir(skillDir);
245
- expect(result.valid).toBe(true);
246
- expect(result.hasInstruction).toBe(false);
247
- expect(result.hasMcp).toBe(true);
248
- });
249
- it("validates skill with both instruction and mcp", () => {
250
- const dir = makeTmpDir();
251
- const skillDir = path.join(dir, "both");
252
- fs.mkdirSync(skillDir);
253
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), '---\nname: both\nmcp:\n command: "node"\n---\n\nAlso instructions.');
254
- const result = validateSkillDir(skillDir);
255
- expect(result.valid).toBe(true);
256
- expect(result.hasInstruction).toBe(true);
257
- expect(result.hasMcp).toBe(true);
258
- });
259
- it("accepts single-character skill ID", () => {
260
- const dir = makeTmpDir();
261
- const skillDir = path.join(dir, "a");
262
- fs.mkdirSync(skillDir);
263
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: a\n---\n\nBody.");
264
- const result = validateSkillDir(skillDir);
265
- expect(result.valid).toBe(true);
266
- });
267
- it("accepts skill ID with numbers", () => {
268
- const dir = makeTmpDir();
269
- const skillDir = path.join(dir, "tool2");
270
- fs.mkdirSync(skillDir);
271
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: tool2\n---\n\nBody.");
272
- const result = validateSkillDir(skillDir);
273
- expect(result.valid).toBe(true);
274
- });
275
- });
276
- // ── runPublish ────────────────────────────────────────────────────
277
- describe("runPublish", () => {
278
- it("shows intro message", async () => {
279
- mockSelect.mockResolvedValue("workflow");
280
- mockText.mockResolvedValue("/nonexistent.yml");
281
- await runPublish();
282
- expect(mockIntro).toHaveBeenCalledWith("Publish to the SWEny Marketplace");
283
- });
284
- it("returns null when user cancels content type selection", async () => {
285
- const cancelSymbol = Symbol("cancel");
286
- mockSelect.mockResolvedValue(cancelSymbol);
287
- mockIsCancel.mockImplementation((v) => v === cancelSymbol);
288
- // cancel() calls process.exit(0), which we need to catch
289
- const mockExit = vi.spyOn(process, "exit").mockImplementation(() => {
290
- throw new Error("process.exit");
291
- });
292
- await expect(runPublish()).rejects.toThrow("process.exit");
293
- expect(mockCancel).toHaveBeenCalledWith("Publish cancelled.");
294
- mockExit.mockRestore();
295
- });
296
- });
297
- // ── openGitHubPR (via runPublish integration) ─────────────────────
298
- describe("openGitHubPR flow", () => {
299
- function setupWorkflowPRFlow(workflowPath) {
300
- // Step 1: select "workflow"
301
- mockSelect.mockResolvedValueOnce("workflow");
302
- // Step 2: text input for file path
303
- mockText.mockResolvedValueOnce(workflowPath);
304
- // Step 3: author
305
- mockText.mockResolvedValueOnce("test-author");
306
- // Step 4: category
307
- mockSelect.mockResolvedValueOnce("testing");
308
- // Step 5: tags
309
- mockText.mockResolvedValueOnce("test, ci");
310
- // Step 6: output mode = PR
311
- mockSelect.mockResolvedValueOnce("pr");
312
- // Mock workflow validation
313
- vi.mocked(parseWorkflow).mockReturnValue({
314
- id: "my-wf",
315
- name: "My Workflow",
316
- description: "",
317
- nodes: { a: mockNode("a") },
318
- edges: [],
319
- entry: "a",
320
- skills: {},
321
- });
322
- vi.mocked(validateWorkflow).mockReturnValue([]);
323
- // Mock gh available — clone creates the dir structure the code expects
324
- mockExecFileSync.mockImplementation((cmd, args) => {
325
- if (cmd === "gh" && args[0] === "--version")
326
- return "gh version 2.0.0";
327
- if (cmd === "gh" && args[0] === "auth")
328
- return "";
329
- if (cmd === "gh" && args[0] === "repo" && args[1] === "fork")
330
- return "";
331
- if (cmd === "gh" && args[0] === "api" && args[1] === "user") {
332
- return JSON.stringify({ login: "testuser" });
333
- }
334
- if (cmd === "gh" && args[0] === "repo" && args[1] === "clone") {
335
- // args: ["repo", "clone", forkRepo, tmpDir, "--", "--depth", "1"]
336
- const cloneDir = args[3];
337
- fs.mkdirSync(path.join(cloneDir, "workflows", "community"), { recursive: true });
338
- fs.mkdirSync(path.join(cloneDir, "skills", "community"), { recursive: true });
339
- return "";
340
- }
341
- if (cmd === "git")
342
- return "";
343
- if (cmd === "gh" && args[0] === "pr")
344
- return "https://github.com/swenyai/marketplace/pull/42\n";
345
- return "";
346
- });
347
- }
348
- it("creates PR with correct gh pr create arguments", async () => {
349
- const dir = makeTmpDir();
350
- const wfPath = path.join(dir, "test.yml");
351
- fs.writeFileSync(wfPath, "id: my-wf\nname: My Workflow\n");
352
- setupWorkflowPRFlow(wfPath);
353
- const result = await runPublish();
354
- // Find the gh pr create call
355
- const prCall = mockExecFileSync.mock.calls.find((c) => c[0] === "gh" && c[1]?.[0] === "pr" && c[1]?.[1] === "create");
356
- expect(prCall).toBeDefined();
357
- const args = prCall[1];
358
- expect(args).toContain("--repo");
359
- expect(args).toContain("swenyai/marketplace");
360
- expect(args).toContain("--head");
361
- expect(args).toContain("testuser:publish/workflow/my-wf");
362
- expect(args).toContain("--title");
363
- expect(args).toContain("feat: add workflow my-wf");
364
- expect(result?.prUrl).toBe("https://github.com/swenyai/marketplace/pull/42");
365
- expect(result?.type).toBe("workflow");
366
- expect(result?.id).toBe("my-wf");
367
- });
368
- it("passes commit message as array arg, not shell string", async () => {
369
- const dir = makeTmpDir();
370
- const wfPath = path.join(dir, "test.yml");
371
- fs.writeFileSync(wfPath, "id: my-wf\nname: My Workflow\n");
372
- setupWorkflowPRFlow(wfPath);
373
- await runPublish();
374
- const commitCall = mockExecFileSync.mock.calls.find((c) => c[0] === "git" && c[1]?.includes("commit"));
375
- expect(commitCall).toBeDefined();
376
- const args = commitCall[1];
377
- // Commit message is a single array element, not split by shell
378
- expect(args).toContain("-m");
379
- expect(args).toContain("feat: add my-wf workflow");
380
- });
381
- it("falls back to saveLocally when gh commands fail", async () => {
382
- const dir = makeTmpDir();
383
- const wfPath = path.join(dir, "test.yml");
384
- fs.writeFileSync(wfPath, "id: my-wf\nname: My Workflow\n");
385
- // Step 1: select "workflow"
386
- mockSelect.mockResolvedValueOnce("workflow");
387
- mockText.mockResolvedValueOnce(wfPath);
388
- mockText.mockResolvedValueOnce("author");
389
- mockSelect.mockResolvedValueOnce("testing");
390
- mockText.mockResolvedValueOnce("test");
391
- mockSelect.mockResolvedValueOnce("pr"); // choose PR mode
392
- vi.mocked(parseWorkflow).mockReturnValue({
393
- id: "my-wf",
394
- name: "My Workflow",
395
- description: "",
396
- nodes: { a: mockNode("a") },
397
- edges: [],
398
- entry: "a",
399
- skills: {},
400
- });
401
- vi.mocked(validateWorkflow).mockReturnValue([]);
402
- mockExecFileSync.mockImplementation((cmd, args) => {
403
- if (cmd === "gh" && args[0] === "--version")
404
- return "gh version 2.0.0";
405
- if (cmd === "gh" && args[0] === "auth")
406
- return "";
407
- // Fork fails
408
- if (cmd === "gh" && args[0] === "repo" && args[1] === "fork") {
409
- throw new Error("Network error");
410
- }
411
- return "";
412
- });
413
- const result = await runPublish();
414
- // Should have fallen back to save locally
415
- expect(result?.type).toBe("workflow");
416
- expect(result?.outputPath).toBeDefined();
417
- expect(mockLog.error).toHaveBeenCalled();
418
- expect(mockLog.info).toHaveBeenCalledWith(expect.stringContaining("Falling back"));
419
- });
420
- it("handles special characters in skill ID safely via array args", async () => {
421
- const dir = makeTmpDir();
422
- // Create a skill directory with a name that would be dangerous in shell
423
- const skillDir = path.join(dir, "safe-skill");
424
- fs.mkdirSync(skillDir);
425
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: safe-skill\n---\n\nBody.");
426
- // Select skill → provide path → choose PR mode
427
- mockSelect.mockResolvedValueOnce("skill");
428
- mockText.mockResolvedValueOnce(skillDir);
429
- mockSelect.mockResolvedValueOnce("pr");
430
- mockExecFileSync.mockImplementation((cmd, args) => {
431
- if (cmd === "gh" && args[0] === "--version")
432
- return "gh version 2.0.0";
433
- if (cmd === "gh" && args[0] === "auth")
434
- return "";
435
- if (cmd === "gh" && args[0] === "repo" && args[1] === "fork")
436
- return "";
437
- if (cmd === "gh" && args[0] === "api")
438
- return JSON.stringify({ login: "user" });
439
- if (cmd === "gh" && args[0] === "repo" && args[1] === "clone") {
440
- const cloneDir = args[2];
441
- fs.mkdirSync(path.join(cloneDir, "workflows", "community"), { recursive: true });
442
- fs.mkdirSync(path.join(cloneDir, "skills", "community"), { recursive: true });
443
- return "";
444
- }
445
- if (cmd === "git")
446
- return "";
447
- if (cmd === "gh" && args[0] === "pr")
448
- return "https://github.com/swenyai/marketplace/pull/99\n";
449
- return "";
450
- });
451
- const result = await runPublish();
452
- expect(result?.type).toBe("skill");
453
- // Verify all git/gh commands used array args (not string interpolation)
454
- for (const call of mockExecFileSync.mock.calls) {
455
- expect(typeof call[0]).toBe("string"); // command is a string
456
- expect(Array.isArray(call[1])).toBe(true); // args is an array
457
- }
458
- });
459
- });
460
- // ── saveLocally flow ──────────────────────────────────────────────
461
- describe("saveLocally flow", () => {
462
- afterEach(() => {
463
- // Clean up sweny-publish directory if created
464
- const publishDir = path.resolve("sweny-publish");
465
- if (fs.existsSync(publishDir)) {
466
- fs.rmSync(publishDir, { recursive: true, force: true });
467
- }
468
- });
469
- it("saves workflow file locally when no gh available", async () => {
470
- const dir = makeTmpDir();
471
- const wfPath = path.join(dir, "test.yml");
472
- fs.writeFileSync(wfPath, "id: local-wf\nname: Local\n");
473
- mockSelect.mockResolvedValueOnce("workflow");
474
- mockText.mockResolvedValueOnce(wfPath);
475
- mockText.mockResolvedValueOnce("author");
476
- mockSelect.mockResolvedValueOnce("testing");
477
- mockText.mockResolvedValueOnce("test");
478
- // gh not available → only "save" option
479
- mockSelect.mockResolvedValueOnce("save");
480
- vi.mocked(parseWorkflow).mockReturnValue({
481
- id: "local-wf",
482
- name: "Local",
483
- description: "",
484
- nodes: { a: mockNode("a") },
485
- edges: [],
486
- entry: "a",
487
- skills: {},
488
- });
489
- vi.mocked(validateWorkflow).mockReturnValue([]);
490
- mockExecFileSync.mockImplementation(() => {
491
- throw new Error("not found");
492
- });
493
- const result = await runPublish();
494
- expect(result?.type).toBe("workflow");
495
- expect(result?.outputPath).toMatch(/sweny-publish.*local-wf\.yml/);
496
- expect(fs.existsSync(result.outputPath)).toBe(true);
497
- });
498
- it("saves skill directory locally", async () => {
499
- const dir = makeTmpDir();
500
- const skillDir = path.join(dir, "my-skill");
501
- fs.mkdirSync(skillDir);
502
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), "---\nname: my-skill\n---\n\nBody.");
503
- fs.writeFileSync(path.join(skillDir, "extra.txt"), "extra file");
504
- mockSelect.mockResolvedValueOnce("skill");
505
- mockText.mockResolvedValueOnce(skillDir);
506
- mockSelect.mockResolvedValueOnce("save");
507
- mockExecFileSync.mockImplementation(() => {
508
- throw new Error("not found");
509
- });
510
- const result = await runPublish();
511
- expect(result?.type).toBe("skill");
512
- expect(result?.outputPath).toMatch(/sweny-publish.*my-skill/);
513
- // Verify files were copied
514
- expect(fs.existsSync(path.join(result.outputPath, "SKILL.md"))).toBe(true);
515
- expect(fs.existsSync(path.join(result.outputPath, "extra.txt"))).toBe(true);
516
- });
517
- });
518
- // ── Validation failure paths ──────────────────────────────────────
519
- describe("validation failure paths", () => {
520
- it("returns null when workflow validation fails", async () => {
521
- const dir = makeTmpDir();
522
- const wfPath = path.join(dir, "bad.yml");
523
- fs.writeFileSync(wfPath, "id: bad\n");
524
- mockSelect.mockResolvedValueOnce("workflow");
525
- mockText.mockResolvedValueOnce(wfPath);
526
- vi.mocked(parseWorkflow).mockImplementation(() => {
527
- throw new Error("Invalid");
528
- });
529
- const result = await runPublish();
530
- expect(result).toBeNull();
531
- expect(mockLog.error).toHaveBeenCalled();
532
- });
533
- it("returns null when skill validation fails", async () => {
534
- const dir = makeTmpDir();
535
- // No SKILL.md in directory
536
- mockSelect.mockResolvedValueOnce("skill");
537
- // The text validator will reject this before we get to validateSkillDir
538
- // So let's provide a dir with SKILL.md that has bad frontmatter
539
- fs.writeFileSync(path.join(dir, "SKILL.md"), "no frontmatter");
540
- mockText.mockResolvedValueOnce(dir);
541
- const result = await runPublish();
542
- expect(result).toBeNull();
543
- expect(mockLog.error).toHaveBeenCalled();
544
- });
545
- });
546
- // ── ghAvailable / ghAuthenticated ─────────────────────────────────
547
- describe("gh availability detection", () => {
548
- it("shows PR option when gh is available", async () => {
549
- const dir = makeTmpDir();
550
- const wfPath = path.join(dir, "test.yml");
551
- fs.writeFileSync(wfPath, "id: wf\nname: WF\n");
552
- mockSelect.mockResolvedValueOnce("workflow");
553
- mockText.mockResolvedValueOnce(wfPath);
554
- mockText.mockResolvedValueOnce("author");
555
- mockSelect.mockResolvedValueOnce("testing");
556
- mockText.mockResolvedValueOnce("test");
557
- mockSelect.mockResolvedValueOnce("save"); // choose save
558
- vi.mocked(parseWorkflow).mockReturnValue({
559
- id: "wf",
560
- name: "WF",
561
- description: "",
562
- nodes: { a: mockNode("a") },
563
- edges: [],
564
- entry: "a",
565
- skills: {},
566
- });
567
- vi.mocked(validateWorkflow).mockReturnValue([]);
568
- // gh is available — only --version needs to succeed for ghAvailable()
569
- mockExecFileSync.mockImplementation((cmd, args) => {
570
- if (cmd === "gh" && args[0] === "--version")
571
- return "gh version 2.0.0";
572
- return "";
573
- });
574
- await runPublish();
575
- // Verify the output mode select was called with PR option
576
- const outputModeCall = mockSelect.mock.calls[2]; // 3rd select call
577
- const options = outputModeCall[0].options;
578
- expect(options.some((o) => o.value === "pr")).toBe(true);
579
- });
580
- it("hides PR option when gh is not available", async () => {
581
- const dir = makeTmpDir();
582
- const wfPath = path.join(dir, "test.yml");
583
- fs.writeFileSync(wfPath, "id: wf\nname: WF\n");
584
- mockSelect.mockResolvedValueOnce("workflow");
585
- mockText.mockResolvedValueOnce(wfPath);
586
- mockText.mockResolvedValueOnce("author");
587
- mockSelect.mockResolvedValueOnce("testing");
588
- mockText.mockResolvedValueOnce("test");
589
- mockSelect.mockResolvedValueOnce("save");
590
- vi.mocked(parseWorkflow).mockReturnValue({
591
- id: "wf",
592
- name: "WF",
593
- description: "",
594
- nodes: { a: mockNode("a") },
595
- edges: [],
596
- entry: "a",
597
- skills: {},
598
- });
599
- vi.mocked(validateWorkflow).mockReturnValue([]);
600
- // gh not available
601
- mockExecFileSync.mockImplementation(() => {
602
- throw new Error("not found");
603
- });
604
- await runPublish();
605
- // Verify output mode select has no PR option
606
- const outputModeCall = mockSelect.mock.calls[2];
607
- const options = outputModeCall[0].options;
608
- expect(options.every((o) => o.value !== "pr")).toBe(true);
609
- });
610
- });
@@ -1 +0,0 @@
1
- export {};