@sweny-ai/core 0.1.77 → 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 (119) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +16 -0
  4. package/dist/claude.js +98 -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/loader.d.ts +40 -0
  13. package/dist/loader.js +73 -0
  14. package/dist/mcp-catalog.d.ts +47 -0
  15. package/dist/mcp-catalog.js +304 -0
  16. package/dist/mcp.d.ts +6 -9
  17. package/dist/mcp.js +24 -314
  18. package/dist/schema.d.ts +183 -151
  19. package/dist/schema.js +49 -7
  20. package/dist/skills/custom-loader.d.ts +40 -0
  21. package/dist/skills/custom-loader.js +107 -32
  22. package/dist/skills/index.d.ts +13 -1
  23. package/dist/skills/index.js +12 -3
  24. package/dist/studio.js +4 -1
  25. package/dist/templates.d.ts +29 -8
  26. package/dist/templates.js +30 -20
  27. package/dist/types.d.ts +12 -0
  28. package/dist/verify.js +42 -4
  29. package/package.json +8 -4
  30. package/dist/__tests__/cascade.test.d.ts +0 -11
  31. package/dist/__tests__/cascade.test.js +0 -452
  32. package/dist/__tests__/claude.test.d.ts +0 -1
  33. package/dist/__tests__/claude.test.js +0 -427
  34. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  35. package/dist/__tests__/custom-loader.test.js +0 -116
  36. package/dist/__tests__/executor.test.d.ts +0 -1
  37. package/dist/__tests__/executor.test.js +0 -1635
  38. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  39. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  40. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  41. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  42. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  43. package/dist/__tests__/integration/github.integration.test.js +0 -37
  44. package/dist/__tests__/integration/harness.d.ts +0 -24
  45. package/dist/__tests__/integration/harness.js +0 -34
  46. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  48. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  50. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  52. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  53. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  54. package/dist/__tests__/publish.test.d.ts +0 -1
  55. package/dist/__tests__/publish.test.js +0 -180
  56. package/dist/__tests__/requires.test.d.ts +0 -1
  57. package/dist/__tests__/requires.test.js +0 -82
  58. package/dist/__tests__/retry.test.d.ts +0 -1
  59. package/dist/__tests__/retry.test.js +0 -213
  60. package/dist/__tests__/schema.test.d.ts +0 -1
  61. package/dist/__tests__/schema.test.js +0 -600
  62. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  63. package/dist/__tests__/skill-config-integration.test.js +0 -204
  64. package/dist/__tests__/skills-index.test.d.ts +0 -1
  65. package/dist/__tests__/skills-index.test.js +0 -119
  66. package/dist/__tests__/skills.test.d.ts +0 -1
  67. package/dist/__tests__/skills.test.js +0 -565
  68. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  69. package/dist/__tests__/spec-conformance.test.js +0 -1084
  70. package/dist/__tests__/studio.test.d.ts +0 -1
  71. package/dist/__tests__/studio.test.js +0 -403
  72. package/dist/__tests__/templates.test.d.ts +0 -1
  73. package/dist/__tests__/templates.test.js +0 -49
  74. package/dist/__tests__/testing.test.d.ts +0 -1
  75. package/dist/__tests__/testing.test.js +0 -242
  76. package/dist/__tests__/verify.test.d.ts +0 -1
  77. package/dist/__tests__/verify.test.js +0 -517
  78. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  79. package/dist/__tests__/version-consistency.test.js +0 -312
  80. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  81. package/dist/__tests__/workflow-builder.test.js +0 -110
  82. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  83. package/dist/__tests__/workflow-yaml.test.js +0 -201
  84. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  85. package/dist/cli/__tests__/diagram.test.js +0 -225
  86. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  87. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  88. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  89. package/dist/cli/__tests__/upgrade.test.js +0 -317
  90. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  91. package/dist/cli/__tests__/version-check.test.js +0 -209
  92. package/dist/cli/config.test.d.ts +0 -1
  93. package/dist/cli/config.test.js +0 -200
  94. package/dist/cli/credentials.test.d.ts +0 -1
  95. package/dist/cli/credentials.test.js +0 -172
  96. package/dist/cli/e2e.test.d.ts +0 -1
  97. package/dist/cli/e2e.test.js +0 -690
  98. package/dist/cli/marketplace.test.d.ts +0 -1
  99. package/dist/cli/marketplace.test.js +0 -355
  100. package/dist/cli/new.test.d.ts +0 -1
  101. package/dist/cli/new.test.js +0 -1043
  102. package/dist/cli/output.test.d.ts +0 -1
  103. package/dist/cli/output.test.js +0 -180
  104. package/dist/cli/publish.test.d.ts +0 -1
  105. package/dist/cli/publish.test.js +0 -610
  106. package/dist/cli/renderer.test.d.ts +0 -1
  107. package/dist/cli/renderer.test.js +0 -302
  108. package/dist/executor.test.d.ts +0 -1
  109. package/dist/executor.test.js +0 -618
  110. package/dist/mcp.test.d.ts +0 -1
  111. package/dist/mcp.test.js +0 -735
  112. package/dist/mermaid.test.d.ts +0 -1
  113. package/dist/mermaid.test.js +0 -256
  114. package/dist/sources.test.d.ts +0 -1
  115. package/dist/sources.test.js +0 -265
  116. package/dist/sources.validation.test.d.ts +0 -12
  117. package/dist/sources.validation.test.js +0 -380
  118. package/dist/workflow-builder.test.d.ts +0 -1
  119. package/dist/workflow-builder.test.js +0 -123
@@ -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 {};