@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,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sweny publish — Interactive CLI for publishing workflows and skills to the marketplace.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - Workflow YAML files → marketplace community submission
|
|
6
|
+
* - Skill directories (SKILL.md) → marketplace skill submission
|
|
7
|
+
* - Three output modes: GitHub PR (via `gh`), copy to clipboard, save locally
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
import { parseWorkflow, validateWorkflow } from "../schema.js";
|
|
16
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
17
|
+
const VALID_CATEGORIES = ["triage", "security", "devops", "code-review", "testing", "content", "ops"];
|
|
18
|
+
const VALID_SKILL_ID = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
19
|
+
// ── Validation helpers ─────────────────────────────────────────────────
|
|
20
|
+
export function validateWorkflowFile(filePath) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
if (!fs.existsSync(filePath)) {
|
|
23
|
+
return { valid: false, errors: ["File not found"] };
|
|
24
|
+
}
|
|
25
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = parseYaml(raw);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return { valid: false, errors: [`Invalid YAML: ${e instanceof Error ? e.message : e}`] };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const workflow = parseWorkflow(parsed);
|
|
35
|
+
const schemaErrors = validateWorkflow(workflow);
|
|
36
|
+
if (schemaErrors.length > 0) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
id: workflow.id,
|
|
40
|
+
name: workflow.name,
|
|
41
|
+
errors: schemaErrors.map((e) => e.message),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
valid: true,
|
|
46
|
+
id: workflow.id,
|
|
47
|
+
name: workflow.name,
|
|
48
|
+
nodeCount: Object.keys(workflow.nodes).length,
|
|
49
|
+
edgeCount: workflow.edges.length,
|
|
50
|
+
errors: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return { valid: false, errors: [`Schema error: ${e instanceof Error ? e.message : e}`] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function validateSkillDir(dirPath) {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const skillMdPath = path.join(dirPath, "SKILL.md");
|
|
60
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
61
|
+
return { valid: false, hasInstruction: false, hasMcp: false, errors: ["SKILL.md not found in directory"] };
|
|
62
|
+
}
|
|
63
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
64
|
+
// Parse frontmatter
|
|
65
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
66
|
+
if (!fmMatch) {
|
|
67
|
+
return { valid: false, hasInstruction: false, hasMcp: false, errors: ["No YAML frontmatter found"] };
|
|
68
|
+
}
|
|
69
|
+
let fm;
|
|
70
|
+
try {
|
|
71
|
+
fm = parseYaml(fmMatch[1]);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
valid: false,
|
|
76
|
+
hasInstruction: false,
|
|
77
|
+
hasMcp: false,
|
|
78
|
+
errors: [`Invalid frontmatter YAML: ${e instanceof Error ? e.message : e}`],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const id = String(fm.name ?? "");
|
|
82
|
+
if (!id) {
|
|
83
|
+
errors.push("Missing 'name' field in frontmatter");
|
|
84
|
+
}
|
|
85
|
+
else if (!VALID_SKILL_ID.test(id) || id.includes("--") || id.length > 64) {
|
|
86
|
+
errors.push(`Invalid skill ID: "${id}" — must be lowercase, hyphens, no consecutive hyphens, max 64 chars`);
|
|
87
|
+
}
|
|
88
|
+
// Check directory name matches skill name
|
|
89
|
+
const dirName = path.basename(dirPath);
|
|
90
|
+
if (id && dirName !== id) {
|
|
91
|
+
errors.push(`Directory name "${dirName}" doesn't match skill name "${id}"`);
|
|
92
|
+
}
|
|
93
|
+
const body = content.slice(fmMatch[0].length).trim();
|
|
94
|
+
const hasInstruction = body.length > 0;
|
|
95
|
+
const hasMcp = fm.mcp != null;
|
|
96
|
+
if (!hasInstruction && !hasMcp) {
|
|
97
|
+
errors.push("Skill must have an instruction body or an mcp config");
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
valid: errors.length === 0,
|
|
101
|
+
id,
|
|
102
|
+
name: id,
|
|
103
|
+
hasInstruction,
|
|
104
|
+
hasMcp,
|
|
105
|
+
errors,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ── GitHub PR helpers ──────────────────────────────────────────────────
|
|
109
|
+
function ghAvailable() {
|
|
110
|
+
try {
|
|
111
|
+
execSync("gh --version", { stdio: "pipe" });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function ghAuthenticated() {
|
|
119
|
+
try {
|
|
120
|
+
execSync("gh auth status", { stdio: "pipe" });
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ── Cancel helper ──────────────────────────────────────────────────────
|
|
128
|
+
function cancel() {
|
|
129
|
+
p.cancel("Publish cancelled.");
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
// ── Main flow ──────────────────────────────────────────────────────────
|
|
133
|
+
export async function runPublish() {
|
|
134
|
+
p.intro("Publish to the SWEny Marketplace");
|
|
135
|
+
// Step 1: What to publish
|
|
136
|
+
const contentType = await p.select({
|
|
137
|
+
message: "What would you like to publish?",
|
|
138
|
+
options: [
|
|
139
|
+
{ value: "workflow", label: "Workflow", hint: ".yml file" },
|
|
140
|
+
{ value: "skill", label: "Skill", hint: "SKILL.md directory" },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
if (p.isCancel(contentType))
|
|
144
|
+
cancel();
|
|
145
|
+
if (contentType === "workflow") {
|
|
146
|
+
return await publishWorkflow();
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
return await publishSkill();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function publishWorkflow() {
|
|
153
|
+
// Step 2: Path to workflow
|
|
154
|
+
const filePath = await p.text({
|
|
155
|
+
message: "Path to workflow YAML file",
|
|
156
|
+
placeholder: ".sweny/workflows/my-workflow.yml",
|
|
157
|
+
validate: (v) => {
|
|
158
|
+
if (!v || v.trim().length === 0)
|
|
159
|
+
return "Path is required";
|
|
160
|
+
if (!fs.existsSync(v.trim()))
|
|
161
|
+
return "File not found";
|
|
162
|
+
return undefined;
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
if (p.isCancel(filePath))
|
|
166
|
+
cancel();
|
|
167
|
+
const absPath = path.resolve(filePath.trim());
|
|
168
|
+
const validation = validateWorkflowFile(absPath);
|
|
169
|
+
if (!validation.valid) {
|
|
170
|
+
p.log.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
p.log.success(`Validated: ${chalk.cyan(validation.name)} — ${validation.nodeCount} nodes, ${validation.edgeCount} edges`);
|
|
174
|
+
// Step 3: Metadata
|
|
175
|
+
const raw = parseYaml(fs.readFileSync(absPath, "utf-8"));
|
|
176
|
+
let author = raw.author;
|
|
177
|
+
if (!author) {
|
|
178
|
+
const authorInput = await p.text({
|
|
179
|
+
message: "Author name",
|
|
180
|
+
placeholder: "your-username",
|
|
181
|
+
validate: (v) => (!v || v.trim().length === 0 ? "Author is required" : undefined),
|
|
182
|
+
});
|
|
183
|
+
if (p.isCancel(authorInput))
|
|
184
|
+
cancel();
|
|
185
|
+
author = authorInput.trim();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
p.log.info(`Author: ${chalk.cyan(author)}`);
|
|
189
|
+
}
|
|
190
|
+
let category = raw.category;
|
|
191
|
+
if (!category || !VALID_CATEGORIES.includes(category)) {
|
|
192
|
+
const catInput = await p.select({
|
|
193
|
+
message: "Category",
|
|
194
|
+
options: VALID_CATEGORIES.map((c) => ({ value: c, label: c })),
|
|
195
|
+
});
|
|
196
|
+
if (p.isCancel(catInput))
|
|
197
|
+
cancel();
|
|
198
|
+
category = catInput;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
p.log.info(`Category: ${chalk.cyan(category)}`);
|
|
202
|
+
}
|
|
203
|
+
let tags = raw.tags;
|
|
204
|
+
if (!tags || !Array.isArray(tags) || tags.length === 0) {
|
|
205
|
+
const tagsInput = await p.text({
|
|
206
|
+
message: "Tags (comma-separated)",
|
|
207
|
+
placeholder: "automation, testing, ci",
|
|
208
|
+
});
|
|
209
|
+
if (p.isCancel(tagsInput))
|
|
210
|
+
cancel();
|
|
211
|
+
tags = tagsInput
|
|
212
|
+
.split(",")
|
|
213
|
+
.map((t) => t.trim())
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
p.log.info(`Tags: ${chalk.cyan(tags.join(", "))}`);
|
|
218
|
+
}
|
|
219
|
+
// Step 4: Output mode
|
|
220
|
+
return await selectOutputMode("workflow", validation.id, absPath, {
|
|
221
|
+
author,
|
|
222
|
+
category,
|
|
223
|
+
tags,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function publishSkill() {
|
|
227
|
+
// Step 2: Path to skill directory
|
|
228
|
+
const dirPath = await p.text({
|
|
229
|
+
message: "Path to skill directory (containing SKILL.md)",
|
|
230
|
+
placeholder: ".sweny/skills/my-skill/",
|
|
231
|
+
validate: (v) => {
|
|
232
|
+
if (!v || v.trim().length === 0)
|
|
233
|
+
return "Path is required";
|
|
234
|
+
const resolved = path.resolve(v.trim());
|
|
235
|
+
if (!fs.existsSync(resolved))
|
|
236
|
+
return "Directory not found";
|
|
237
|
+
if (!fs.statSync(resolved).isDirectory())
|
|
238
|
+
return "Path must be a directory";
|
|
239
|
+
if (!fs.existsSync(path.join(resolved, "SKILL.md")))
|
|
240
|
+
return "SKILL.md not found in directory";
|
|
241
|
+
return undefined;
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
if (p.isCancel(dirPath))
|
|
245
|
+
cancel();
|
|
246
|
+
const absDir = path.resolve(dirPath.trim());
|
|
247
|
+
const validation = validateSkillDir(absDir);
|
|
248
|
+
if (!validation.valid) {
|
|
249
|
+
p.log.error(`Validation failed:\n${validation.errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const badges = [];
|
|
253
|
+
if (validation.hasInstruction)
|
|
254
|
+
badges.push("instruction");
|
|
255
|
+
if (validation.hasMcp)
|
|
256
|
+
badges.push("MCP");
|
|
257
|
+
p.log.success(`Validated: ${chalk.cyan(validation.id)} [${badges.join(" + ")}]`);
|
|
258
|
+
return await selectOutputMode("skill", validation.id, absDir, {});
|
|
259
|
+
}
|
|
260
|
+
async function selectOutputMode(type, id, sourcePath, meta) {
|
|
261
|
+
const hasGh = ghAvailable();
|
|
262
|
+
const options = [
|
|
263
|
+
...(hasGh
|
|
264
|
+
? [{ value: "pr", label: "Open GitHub PR", hint: "recommended — forks marketplace repo and opens a PR" }]
|
|
265
|
+
: []),
|
|
266
|
+
{ value: "save", label: "Save submission file", hint: "saves to ./sweny-publish/" },
|
|
267
|
+
];
|
|
268
|
+
const mode = await p.select({
|
|
269
|
+
message: "How would you like to publish?",
|
|
270
|
+
options,
|
|
271
|
+
});
|
|
272
|
+
if (p.isCancel(mode))
|
|
273
|
+
cancel();
|
|
274
|
+
if (mode === "pr") {
|
|
275
|
+
return await openGitHubPR(type, id, sourcePath, meta);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
return await saveLocally(type, id, sourcePath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function openGitHubPR(type, id, sourcePath, meta) {
|
|
282
|
+
if (!ghAuthenticated()) {
|
|
283
|
+
p.log.error("Not authenticated with GitHub. Run: gh auth login");
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const s = p.spinner();
|
|
287
|
+
s.start("Forking marketplace repository...");
|
|
288
|
+
try {
|
|
289
|
+
// Fork the marketplace repo (idempotent — if already forked, just continues)
|
|
290
|
+
execSync("gh repo fork swenyai/marketplace --clone=false 2>&1", { stdio: "pipe" });
|
|
291
|
+
// Get the fork name
|
|
292
|
+
const forkInfo = execSync("gh api user", { stdio: "pipe", encoding: "utf-8" });
|
|
293
|
+
const username = JSON.parse(forkInfo).login;
|
|
294
|
+
const forkRepo = `${username}/marketplace`;
|
|
295
|
+
s.message("Creating branch...");
|
|
296
|
+
const branchName = `publish/${type}/${id}`;
|
|
297
|
+
// Clone to temp, create branch, add file, push, create PR
|
|
298
|
+
const tmpDir = fs.mkdtempSync(path.join(process.env.TMPDIR ?? "/tmp", "sweny-publish-"));
|
|
299
|
+
execSync(`gh repo clone ${forkRepo} ${tmpDir} -- --depth 1`, { stdio: "pipe" });
|
|
300
|
+
execSync(`git -C ${tmpDir} checkout -b ${branchName}`, { stdio: "pipe" });
|
|
301
|
+
if (type === "workflow") {
|
|
302
|
+
const dest = path.join(tmpDir, "workflows", "community", `${id}.yml`);
|
|
303
|
+
fs.copyFileSync(sourcePath, dest);
|
|
304
|
+
execSync(`git -C ${tmpDir} add workflows/community/${id}.yml`, { stdio: "pipe" });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
const destDir = path.join(tmpDir, "skills", "community", id);
|
|
308
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
309
|
+
// Copy entire skill directory
|
|
310
|
+
const files = fs.readdirSync(sourcePath);
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
fs.copyFileSync(path.join(sourcePath, file), path.join(destDir, file));
|
|
313
|
+
}
|
|
314
|
+
execSync(`git -C ${tmpDir} add skills/community/${id}`, { stdio: "pipe" });
|
|
315
|
+
}
|
|
316
|
+
s.message("Pushing...");
|
|
317
|
+
const commitMsg = type === "workflow" ? `feat: add ${id} workflow` : `feat: add ${id} skill`;
|
|
318
|
+
execSync(`git -C ${tmpDir} commit -m "${commitMsg}"`, { stdio: "pipe" });
|
|
319
|
+
execSync(`git -C ${tmpDir} push origin ${branchName}`, { stdio: "pipe" });
|
|
320
|
+
s.message("Creating pull request...");
|
|
321
|
+
const prBody = type === "workflow"
|
|
322
|
+
? `## New Workflow: ${id}\n\n**Author:** ${meta.author}\n**Category:** ${meta.category}\n**Tags:** ${meta.tags?.join(", ")}\n\nSubmitted via \`sweny publish\`.`
|
|
323
|
+
: `## New Skill: ${id}\n\nSubmitted via \`sweny publish\`.`;
|
|
324
|
+
const prResult = execSync(`gh pr create --repo swenyai/marketplace --head ${username}:${branchName} --title "feat: add ${type} ${id}" --body "${prBody.replace(/"/g, '\\"')}"`, { stdio: "pipe", encoding: "utf-8" });
|
|
325
|
+
const prUrl = prResult.trim();
|
|
326
|
+
// Cleanup temp dir
|
|
327
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
328
|
+
s.stop(`PR created: ${chalk.cyan(prUrl)}`);
|
|
329
|
+
return { type, id, name: id, prUrl };
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
s.stop("Failed to create PR");
|
|
333
|
+
p.log.error(`GitHub error: ${e instanceof Error ? e.message : e}`);
|
|
334
|
+
p.log.info("Falling back to save locally...");
|
|
335
|
+
return await saveLocally(type, id, sourcePath);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function saveLocally(type, id, sourcePath) {
|
|
339
|
+
const outputDir = path.resolve("sweny-publish");
|
|
340
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
341
|
+
if (type === "workflow") {
|
|
342
|
+
const dest = path.join(outputDir, `${id}.yml`);
|
|
343
|
+
fs.copyFileSync(sourcePath, dest);
|
|
344
|
+
p.log.success(`Saved to ${chalk.cyan(dest)}`);
|
|
345
|
+
p.log.info(`Submit manually: copy to workflows/community/ in a fork of swenyai/marketplace`);
|
|
346
|
+
return { type, id, name: id, outputPath: dest };
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
const destDir = path.join(outputDir, id);
|
|
350
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
351
|
+
const files = fs.readdirSync(sourcePath);
|
|
352
|
+
for (const file of files) {
|
|
353
|
+
fs.copyFileSync(path.join(sourcePath, file), path.join(destDir, file));
|
|
354
|
+
}
|
|
355
|
+
p.log.success(`Saved to ${chalk.cyan(destDir)}`);
|
|
356
|
+
p.log.info(`Submit manually: copy to skills/community/ in a fork of swenyai/marketplace`);
|
|
357
|
+
return { type, id, name: id, outputPath: destDir };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// ── Command registration ───────────────────────────────────────────────
|
|
361
|
+
export function registerPublishCommand(program) {
|
|
362
|
+
program
|
|
363
|
+
.command("publish")
|
|
364
|
+
.description("Publish a workflow or skill to the SWEny Marketplace")
|
|
365
|
+
.action(async () => {
|
|
366
|
+
await runPublish();
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter workflow templates offered during `sweny new`.
|
|
3
|
+
*
|
|
4
|
+
* Each template is a valid SWEny workflow YAML. Templates are self-contained
|
|
5
|
+
* strings — no filesystem reads needed, works from npm install.
|
|
6
|
+
*/
|
|
7
|
+
export interface WorkflowTemplate {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
yaml: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const WORKFLOW_TEMPLATES: WorkflowTemplate[];
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter workflow templates offered during `sweny new`.
|
|
3
|
+
*
|
|
4
|
+
* Each template is a valid SWEny workflow YAML. Templates are self-contained
|
|
5
|
+
* strings — no filesystem reads needed, works from npm install.
|
|
6
|
+
*/
|
|
7
|
+
export const WORKFLOW_TEMPLATES = [
|
|
8
|
+
{
|
|
9
|
+
id: "pr-review",
|
|
10
|
+
name: "PR Review Bot",
|
|
11
|
+
description: "Automated code review on pull requests",
|
|
12
|
+
yaml: `id: pr-review
|
|
13
|
+
name: PR Review Bot
|
|
14
|
+
description: Review pull requests for code quality, security issues, and best practices.
|
|
15
|
+
entry: fetch-diff
|
|
16
|
+
|
|
17
|
+
nodes:
|
|
18
|
+
fetch-diff:
|
|
19
|
+
name: Fetch PR Changes
|
|
20
|
+
instruction: |
|
|
21
|
+
Fetch the pull request diff. Identify which files changed,
|
|
22
|
+
what was added, modified, or removed. Summarize the scope
|
|
23
|
+
of the change.
|
|
24
|
+
skills: [github]
|
|
25
|
+
|
|
26
|
+
review-code:
|
|
27
|
+
name: Review Code
|
|
28
|
+
instruction: |
|
|
29
|
+
Review the code changes for:
|
|
30
|
+
- Security vulnerabilities (injection, auth issues, secrets)
|
|
31
|
+
- Logic errors and edge cases
|
|
32
|
+
- Code style and readability
|
|
33
|
+
- Test coverage gaps
|
|
34
|
+
|
|
35
|
+
Be specific — reference file names and line numbers.
|
|
36
|
+
Categorize findings as critical, important, or minor.
|
|
37
|
+
skills: [github]
|
|
38
|
+
|
|
39
|
+
post-review:
|
|
40
|
+
name: Post Review Comment
|
|
41
|
+
instruction: |
|
|
42
|
+
Post a structured review comment on the pull request.
|
|
43
|
+
Lead with a summary, then list findings by severity.
|
|
44
|
+
If no issues found, approve with a brief note.
|
|
45
|
+
skills: [github]
|
|
46
|
+
|
|
47
|
+
edges:
|
|
48
|
+
- from: fetch-diff
|
|
49
|
+
to: review-code
|
|
50
|
+
- from: review-code
|
|
51
|
+
to: post-review
|
|
52
|
+
`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "issue-triage",
|
|
56
|
+
name: "Issue Triage",
|
|
57
|
+
description: "Classify, prioritize, and label incoming issues",
|
|
58
|
+
yaml: `id: issue-triage
|
|
59
|
+
name: Issue Triage
|
|
60
|
+
description: Automatically classify, prioritize, and label new issues.
|
|
61
|
+
entry: classify
|
|
62
|
+
|
|
63
|
+
nodes:
|
|
64
|
+
classify:
|
|
65
|
+
name: Classify Issue
|
|
66
|
+
instruction: |
|
|
67
|
+
Read the issue title and body. Classify it as one of:
|
|
68
|
+
- bug: Something is broken
|
|
69
|
+
- feature: A new capability request
|
|
70
|
+
- question: Asking for help or clarification
|
|
71
|
+
- chore: Maintenance, refactoring, docs
|
|
72
|
+
|
|
73
|
+
Also assess priority:
|
|
74
|
+
- P0: Production is down or data loss
|
|
75
|
+
- P1: Major feature broken, workaround exists
|
|
76
|
+
- P2: Minor issue, low impact
|
|
77
|
+
- P3: Nice to have, no urgency
|
|
78
|
+
skills: [github]
|
|
79
|
+
|
|
80
|
+
label-and-assign:
|
|
81
|
+
name: Apply Labels
|
|
82
|
+
instruction: |
|
|
83
|
+
Based on the classification, apply the appropriate labels
|
|
84
|
+
to the issue (e.g. "bug", "P1", "needs-triage").
|
|
85
|
+
Add a comment explaining the classification rationale.
|
|
86
|
+
skills: [github]
|
|
87
|
+
|
|
88
|
+
edges:
|
|
89
|
+
- from: classify
|
|
90
|
+
to: label-and-assign
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "security-scan",
|
|
95
|
+
name: "Security Audit",
|
|
96
|
+
description: "Scan code and dependencies for security issues",
|
|
97
|
+
yaml: `id: security-scan
|
|
98
|
+
name: Security Audit
|
|
99
|
+
description: Scan repository code and dependencies for security vulnerabilities.
|
|
100
|
+
entry: scan-code
|
|
101
|
+
|
|
102
|
+
nodes:
|
|
103
|
+
scan-code:
|
|
104
|
+
name: Scan Code for Secrets
|
|
105
|
+
instruction: |
|
|
106
|
+
Search the repository for exposed secrets, API keys,
|
|
107
|
+
passwords, and tokens in code, config files, and
|
|
108
|
+
environment templates. Check for common patterns:
|
|
109
|
+
hardcoded credentials, leaked keys, insecure defaults.
|
|
110
|
+
skills: [github]
|
|
111
|
+
|
|
112
|
+
scan-deps:
|
|
113
|
+
name: Scan Dependencies
|
|
114
|
+
instruction: |
|
|
115
|
+
Review dependency manifests (package.json, requirements.txt,
|
|
116
|
+
go.mod, etc.) for known vulnerable versions. Check for
|
|
117
|
+
outdated packages with security advisories.
|
|
118
|
+
skills: [github]
|
|
119
|
+
|
|
120
|
+
compile-report:
|
|
121
|
+
name: Compile Security Report
|
|
122
|
+
instruction: |
|
|
123
|
+
Compile findings from the code and dependency scans into
|
|
124
|
+
a structured security report. Categorize by severity
|
|
125
|
+
(critical, high, medium, low). Include remediation steps
|
|
126
|
+
for each finding.
|
|
127
|
+
skills: [github]
|
|
128
|
+
|
|
129
|
+
edges:
|
|
130
|
+
- from: scan-code
|
|
131
|
+
to: scan-deps
|
|
132
|
+
- from: scan-deps
|
|
133
|
+
to: compile-report
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "release-notes",
|
|
138
|
+
name: "Release Notes",
|
|
139
|
+
description: "Generate release notes from commits and PRs",
|
|
140
|
+
yaml: `id: release-notes
|
|
141
|
+
name: Release Notes Generator
|
|
142
|
+
description: Generate structured release notes from recent commits and merged PRs.
|
|
143
|
+
entry: gather
|
|
144
|
+
|
|
145
|
+
nodes:
|
|
146
|
+
gather:
|
|
147
|
+
name: Gather Changes
|
|
148
|
+
instruction: |
|
|
149
|
+
List all commits and merged pull requests since the last
|
|
150
|
+
release tag. For each, extract: title, author, PR number,
|
|
151
|
+
and a brief description of what changed.
|
|
152
|
+
skills: [github]
|
|
153
|
+
|
|
154
|
+
categorize:
|
|
155
|
+
name: Categorize Changes
|
|
156
|
+
instruction: |
|
|
157
|
+
Group the changes into categories:
|
|
158
|
+
- Features: New capabilities
|
|
159
|
+
- Fixes: Bug fixes
|
|
160
|
+
- Improvements: Performance, refactoring, DX
|
|
161
|
+
- Breaking Changes: Anything that requires migration
|
|
162
|
+
|
|
163
|
+
Flag any breaking changes prominently.
|
|
164
|
+
skills: [github]
|
|
165
|
+
|
|
166
|
+
write-notes:
|
|
167
|
+
name: Write Release Notes
|
|
168
|
+
instruction: |
|
|
169
|
+
Write polished release notes in markdown. Lead with
|
|
170
|
+
highlights, then list changes by category. Credit
|
|
171
|
+
contributors. Keep it concise but informative.
|
|
172
|
+
skills: [github]
|
|
173
|
+
|
|
174
|
+
edges:
|
|
175
|
+
- from: gather
|
|
176
|
+
to: categorize
|
|
177
|
+
- from: categorize
|
|
178
|
+
to: write-notes
|
|
179
|
+
`,
|
|
180
|
+
},
|
|
181
|
+
];
|