@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.
Files changed (40) hide show
  1. package/dist/__tests__/publish.test.js +146 -0
  2. package/dist/__tests__/schema.test.js +156 -1
  3. package/dist/__tests__/skills.test.js +135 -4
  4. package/dist/__tests__/templates.test.d.ts +1 -0
  5. package/dist/__tests__/templates.test.js +49 -0
  6. package/dist/__tests__/version-consistency.test.js +7 -7
  7. package/dist/cli/config-file.d.ts +1 -1
  8. package/dist/cli/config-file.js +1 -1
  9. package/dist/cli/credentials.d.ts +1 -1
  10. package/dist/cli/credentials.js +1 -1
  11. package/dist/cli/credentials.test.js +1 -1
  12. package/dist/cli/e2e.d.ts +5 -1
  13. package/dist/cli/e2e.js +7 -3
  14. package/dist/cli/e2e.test.js +46 -1
  15. package/dist/cli/main.js +21 -7
  16. package/dist/cli/new.d.ts +101 -0
  17. package/dist/cli/new.js +670 -0
  18. package/dist/cli/new.test.d.ts +1 -0
  19. package/dist/cli/{init.test.js → new.test.js} +216 -78
  20. package/dist/cli/publish.d.ts +34 -0
  21. package/dist/cli/publish.js +368 -0
  22. package/dist/cli/templates.d.ts +13 -0
  23. package/dist/cli/templates.js +181 -0
  24. package/dist/executor.js +63 -20
  25. package/dist/executor.test.js +233 -0
  26. package/dist/index.d.ts +5 -5
  27. package/dist/index.js +3 -3
  28. package/dist/mcp.d.ts +2 -0
  29. package/dist/mcp.js +8 -0
  30. package/dist/mcp.test.js +67 -0
  31. package/dist/schema.d.ts +428 -18
  32. package/dist/schema.js +71 -4
  33. package/dist/skills/custom-loader.d.ts +12 -12
  34. package/dist/skills/custom-loader.js +105 -50
  35. package/dist/types.d.ts +18 -1
  36. package/dist/workflows/browser.js +3 -3
  37. package/package.json +10 -2
  38. package/dist/cli/init.d.ts +0 -53
  39. package/dist/cli/init.js +0 -521
  40. /package/dist/{cli/init.test.d.ts → __tests__/publish.test.d.ts} +0 -0
package/dist/cli/init.js DELETED
@@ -1,521 +0,0 @@
1
- /**
2
- * sweny init — Interactive setup wizard
3
- *
4
- * Pure functions for file generation + thin @clack/prompts interactive layer.
5
- * Tests cover the pure functions; the interactive wizard is thin glue.
6
- */
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
- import * as p from "@clack/prompts";
10
- import chalk from "chalk";
11
- // ── Credential table ───────────────────────────────────────────────────
12
- export const PROVIDER_CREDENTIALS = {
13
- github: [
14
- {
15
- key: "GITHUB_TOKEN",
16
- url: "https://github.com/settings/tokens",
17
- hint: "repo + issues scopes",
18
- },
19
- ],
20
- gitlab: [
21
- {
22
- key: "GITLAB_TOKEN",
23
- hint: "api scope",
24
- url: "https://gitlab.com/-/profile/personal_access_tokens",
25
- },
26
- { key: "GITLAB_URL", hint: "e.g. https://gitlab.com", default: "https://gitlab.com" },
27
- ],
28
- datadog: [
29
- {
30
- key: "DD_API_KEY",
31
- url: "https://app.datadoghq.com/organization-settings",
32
- hint: "Organization Settings > API Keys",
33
- },
34
- { key: "DD_APP_KEY", hint: "Organization Settings > Application Keys" },
35
- { key: "DD_SITE", hint: "datadoghq.com, datadoghq.eu, etc.", default: "datadoghq.com" },
36
- ],
37
- sentry: [
38
- {
39
- key: "SENTRY_AUTH_TOKEN",
40
- url: "https://sentry.io/settings/auth-tokens/",
41
- },
42
- { key: "SENTRY_ORG", hint: "sentry.io/organizations/slug/" },
43
- ],
44
- betterstack: [
45
- {
46
- key: "BETTERSTACK_API_TOKEN",
47
- url: "https://betterstack.com/docs/logs/api",
48
- },
49
- ],
50
- newrelic: [{ key: "NR_API_KEY", url: "https://one.newrelic.com/api-keys" }],
51
- cloudwatch: [
52
- { key: "AWS_ACCESS_KEY_ID", hint: "IAM user access key" },
53
- { key: "AWS_SECRET_ACCESS_KEY", hint: "IAM user secret key" },
54
- { key: "AWS_REGION", hint: "e.g. us-east-1", default: "us-east-1" },
55
- ],
56
- "github-issues": [],
57
- linear: [
58
- {
59
- key: "LINEAR_API_KEY",
60
- url: "https://linear.app/settings/api",
61
- },
62
- {
63
- key: "LINEAR_TEAM_ID",
64
- hint: "Settings > Teams > copy ID from URL",
65
- },
66
- ],
67
- jira: [
68
- { key: "JIRA_BASE_URL", hint: "e.g. https://your-org.atlassian.net" },
69
- { key: "JIRA_EMAIL", hint: "your Atlassian account email" },
70
- {
71
- key: "JIRA_API_TOKEN",
72
- url: "https://id.atlassian.com/manage-profile/security/api-tokens",
73
- },
74
- ],
75
- console: [],
76
- slack: [{ key: "SLACK_BOT_TOKEN", url: "https://api.slack.com/apps" }],
77
- discord: [{ key: "DISCORD_WEBHOOK_URL", hint: "Server Settings > Integrations > Webhooks" }],
78
- teams: [{ key: "TEAMS_WEBHOOK_URL", hint: "Channel > Connectors > Incoming Webhook" }],
79
- webhook: [{ key: "NOTIFICATION_WEBHOOK_URL", hint: "Your webhook endpoint URL" }],
80
- };
81
- const ALWAYS_CREDENTIALS = [
82
- {
83
- key: "ANTHROPIC_API_KEY",
84
- url: "https://console.anthropic.com/settings/api-keys",
85
- hint: "Claude API key",
86
- },
87
- ];
88
- // ── Functions ──────────────────────────────────────────────────────────
89
- /**
90
- * Gather all unique credentials required for the selected providers.
91
- * Always includes ANTHROPIC_API_KEY. Deduplicates by key name.
92
- */
93
- export function collectCredentials(selections) {
94
- const seen = new Map();
95
- // Always-present credentials first
96
- for (const cred of ALWAYS_CREDENTIALS) {
97
- seen.set(cred.key, cred);
98
- }
99
- const providers = [
100
- selections.sourceControl,
101
- selections.observability,
102
- selections.issueTracker,
103
- selections.notification,
104
- ].filter((p) => p !== null);
105
- for (const provider of providers) {
106
- const creds = PROVIDER_CREDENTIALS[provider];
107
- if (!creds)
108
- continue;
109
- for (const cred of creds) {
110
- if (!seen.has(cred.key)) {
111
- seen.set(cred.key, cred);
112
- }
113
- }
114
- }
115
- return Array.from(seen.values());
116
- }
117
- /**
118
- * Detect the git remote origin from `.git/config`.
119
- * Returns provider + normalised remote (e.g. "github.com/owner/repo"), or null.
120
- */
121
- export function detectGitRemote(cwd) {
122
- const configPath = path.join(cwd, ".git", "config");
123
- let content;
124
- try {
125
- content = fs.readFileSync(configPath, "utf-8");
126
- }
127
- catch {
128
- return null;
129
- }
130
- // Find [remote "origin"] section and extract url
131
- const remoteMatch = content.match(/\[remote\s+"origin"\]\s*\n(?:\s+[^\[]*?\n)*?\s*url\s*=\s*(.+)/);
132
- if (!remoteMatch)
133
- return null;
134
- const url = remoteMatch[1].trim();
135
- // SSH: git@github.com:owner/repo.git
136
- const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
137
- if (sshMatch) {
138
- const host = sshMatch[1];
139
- const repoPath = sshMatch[2];
140
- if (host === "github.com") {
141
- return { provider: "github", remote: `github.com/${repoPath}` };
142
- }
143
- if (host === "gitlab.com") {
144
- return { provider: "gitlab", remote: `gitlab.com/${repoPath}` };
145
- }
146
- return null;
147
- }
148
- // SSH scheme: ssh://git@github.com/owner/repo.git
149
- const sshSchemeMatch = url.match(/^ssh:\/\/git@([^/]+)\/(.+?)(?:\.git)?$/);
150
- if (sshSchemeMatch) {
151
- const host = sshSchemeMatch[1];
152
- const repoPath = sshSchemeMatch[2];
153
- if (host === "github.com") {
154
- return { provider: "github", remote: `github.com/${repoPath}` };
155
- }
156
- if (host === "gitlab.com") {
157
- return { provider: "gitlab", remote: `gitlab.com/${repoPath}` };
158
- }
159
- return null;
160
- }
161
- // HTTPS: https://github.com/owner/repo.git
162
- const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/);
163
- if (httpsMatch) {
164
- const host = httpsMatch[1];
165
- const repoPath = httpsMatch[2];
166
- if (host === "github.com") {
167
- return { provider: "github", remote: `github.com/${repoPath}` };
168
- }
169
- if (host === "gitlab.com") {
170
- return { provider: "gitlab", remote: `gitlab.com/${repoPath}` };
171
- }
172
- return null;
173
- }
174
- return null;
175
- }
176
- /**
177
- * Generate .sweny.yml content from selections.
178
- */
179
- export function buildSwenyYml(selections) {
180
- const lines = [];
181
- lines.push("# .sweny.yml — SWEny project configuration");
182
- lines.push("# Secrets (API keys, tokens) go in .env (gitignored).");
183
- lines.push("# Docs: https://docs.sweny.ai/cli");
184
- lines.push("");
185
- lines.push(`source-control-provider: ${selections.sourceControl}`);
186
- if (selections.observability !== null) {
187
- lines.push(`observability-provider: ${selections.observability}`);
188
- }
189
- lines.push(`issue-tracker-provider: ${selections.issueTracker}`);
190
- // Omit notification-provider when "console" (it's the default)
191
- if (selections.notification !== "console") {
192
- lines.push(`notification-provider: ${selections.notification}`);
193
- }
194
- lines.push("");
195
- return lines.join("\n");
196
- }
197
- /**
198
- * Generate .env template with comments for each credential.
199
- */
200
- export function buildEnvTemplate(credentials) {
201
- const lines = [];
202
- lines.push("# .env — SWEny credentials (DO NOT COMMIT)");
203
- lines.push("# Fill in each value, then run: sweny check");
204
- lines.push("");
205
- for (const cred of credentials) {
206
- if (cred.url) {
207
- lines.push(`# ${cred.url}`);
208
- }
209
- if (cred.hint) {
210
- lines.push(`# ${cred.hint}`);
211
- }
212
- if (cred.default !== undefined) {
213
- lines.push(`${cred.key}=${cred.default}`);
214
- }
215
- else {
216
- lines.push(`${cred.key}=`);
217
- }
218
- lines.push("");
219
- }
220
- return lines.join("\n");
221
- }
222
- /**
223
- * Generate a GitHub Actions workflow YAML for SWEny.
224
- * Only includes credentials WITHOUT defaults as secrets.
225
- */
226
- export function buildActionWorkflow(credentials, cronExpression) {
227
- const secretCreds = credentials.filter((c) => c.default === undefined);
228
- const lines = [];
229
- lines.push("name: SWEny Triage");
230
- lines.push("");
231
- lines.push("on:");
232
- lines.push(" schedule:");
233
- lines.push(` - cron: "${cronExpression}"`);
234
- lines.push(" workflow_dispatch:");
235
- lines.push("");
236
- lines.push("jobs:");
237
- lines.push(" triage:");
238
- lines.push(" runs-on: ubuntu-latest");
239
- lines.push(" steps:");
240
- lines.push(" - uses: actions/checkout@v4");
241
- lines.push(" - uses: swenyai/sweny@v5");
242
- lines.push(" with:");
243
- lines.push(" workflow: triage");
244
- if (secretCreds.length > 0) {
245
- lines.push(" env:");
246
- for (const cred of secretCreds) {
247
- lines.push(` ${cred.key}: \${{ secrets.${cred.key} }}`);
248
- }
249
- }
250
- lines.push("");
251
- return lines.join("\n");
252
- }
253
- // ── Interactive wizard ────────────────────────────────────────────────
254
- function cancel() {
255
- p.cancel("Setup cancelled.");
256
- process.exit(0);
257
- }
258
- /**
259
- * Interactive setup wizard — prompts the user through provider selection,
260
- * then generates `.sweny.yml`, `.env`, and an optional GitHub Action workflow.
261
- */
262
- export async function runInit() {
263
- const cwd = process.cwd();
264
- // ── Intro ───────────────────────────────────────────────────────────
265
- p.intro("Let's set up SWEny");
266
- const gitInfo = detectGitRemote(cwd);
267
- if (gitInfo) {
268
- p.log.info(`Detected git remote: ${chalk.cyan(gitInfo.remote)}`);
269
- }
270
- // ── Existing file check ─────────────────────────────────────────────
271
- const configPath = path.join(cwd, ".sweny.yml");
272
- if (fs.existsSync(configPath)) {
273
- const overwrite = await p.confirm({
274
- message: ".sweny.yml already exists. Overwrite?",
275
- initialValue: false,
276
- });
277
- if (p.isCancel(overwrite))
278
- cancel();
279
- if (!overwrite) {
280
- p.cancel("Setup cancelled — existing config preserved.");
281
- process.exit(0);
282
- }
283
- }
284
- // ── Screen 1: Source control ────────────────────────────────────────
285
- const sourceControl = await p.select({
286
- message: "Source control provider",
287
- initialValue: gitInfo?.provider ?? "github",
288
- options: [
289
- {
290
- value: "github",
291
- label: "GitHub" + (gitInfo?.provider === "github" ? " (detected)" : ""),
292
- },
293
- {
294
- value: "gitlab",
295
- label: "GitLab" + (gitInfo?.provider === "gitlab" ? " (detected)" : ""),
296
- },
297
- ],
298
- });
299
- if (p.isCancel(sourceControl))
300
- cancel();
301
- // ── Screen 2: Observability ─────────────────────────────────────────
302
- const obsRaw = await p.select({
303
- message: "Observability provider",
304
- options: [
305
- { value: "datadog", label: "Datadog" },
306
- { value: "sentry", label: "Sentry" },
307
- { value: "betterstack", label: "BetterStack" },
308
- { value: "newrelic", label: "New Relic" },
309
- { value: "cloudwatch", label: "CloudWatch" },
310
- { value: "__other", label: "Other" },
311
- { value: "__none", label: "None" },
312
- ],
313
- });
314
- if (p.isCancel(obsRaw))
315
- cancel();
316
- let observability;
317
- if (obsRaw === "__none") {
318
- observability = null;
319
- }
320
- else if (obsRaw === "__other") {
321
- const custom = await p.text({
322
- message: "Enter your observability provider name",
323
- validate: (v) => {
324
- if (!v || v.trim().length === 0)
325
- return "Provider name is required";
326
- if (!/^[a-z0-9-]+$/.test(v.trim()))
327
- return "Provider name must be lowercase letters, numbers, and hyphens only";
328
- return undefined;
329
- },
330
- });
331
- if (p.isCancel(custom))
332
- cancel();
333
- observability = custom.trim().toLowerCase();
334
- }
335
- else {
336
- observability = obsRaw;
337
- }
338
- // ── Screen 3: Issue tracker ─────────────────────────────────────────
339
- const issueTracker = await p.select({
340
- message: "Issue tracker",
341
- initialValue: sourceControl === "github" ? "github-issues" : undefined,
342
- options: [
343
- { value: "github-issues", label: "GitHub Issues" },
344
- { value: "linear", label: "Linear" },
345
- { value: "jira", label: "Jira" },
346
- ],
347
- });
348
- if (p.isCancel(issueTracker))
349
- cancel();
350
- // ── Screen 4: Notification ──────────────────────────────────────────
351
- const notification = await p.select({
352
- message: "Notification channel",
353
- initialValue: "console",
354
- options: [
355
- { value: "console", label: "Console (default)" },
356
- { value: "slack", label: "Slack" },
357
- { value: "discord", label: "Discord" },
358
- { value: "teams", label: "Microsoft Teams" },
359
- { value: "webhook", label: "Webhook" },
360
- ],
361
- });
362
- if (p.isCancel(notification))
363
- cancel();
364
- // ── Screen 5: GitHub Action ─────────────────────────────────────────
365
- const wantAction = await p.confirm({
366
- message: "Set up a GitHub Action workflow?",
367
- initialValue: true,
368
- });
369
- if (p.isCancel(wantAction))
370
- cancel();
371
- let cronExpression = null;
372
- if (wantAction) {
373
- const schedule = await p.select({
374
- message: "How often should the triage run?",
375
- options: [
376
- { value: "0 9 * * *", label: "Daily (9 AM UTC)" },
377
- { value: "0 9 * * 1", label: "Weekly (Monday 9 AM UTC)" },
378
- { value: "__custom", label: "Custom cron expression" },
379
- ],
380
- });
381
- if (p.isCancel(schedule))
382
- cancel();
383
- if (schedule === "__custom") {
384
- const customCron = await p.text({
385
- message: "Enter cron expression (e.g. 0 8 * * 1-5)",
386
- validate: (v) => {
387
- if (!v || v.trim().length === 0)
388
- return "Cron expression is required";
389
- if (!/^[\d*,\/-]+(\s+[\d*,\/-]+){4}$/.test(v.trim()))
390
- return "Must be a valid 5-field cron expression";
391
- return undefined;
392
- },
393
- });
394
- if (p.isCancel(customCron))
395
- cancel();
396
- cronExpression = customCron.trim();
397
- }
398
- else {
399
- cronExpression = schedule;
400
- }
401
- }
402
- // ── Assemble selections ─────────────────────────────────────────────
403
- const selections = {
404
- sourceControl: sourceControl,
405
- observability,
406
- issueTracker: issueTracker,
407
- notification: notification,
408
- githubAction: !!wantAction,
409
- cronExpression,
410
- };
411
- const credentials = collectCredentials(selections);
412
- // ── Screen 6: Summary ───────────────────────────────────────────────
413
- const files = [".sweny.yml", ".env"];
414
- if (selections.githubAction)
415
- files.push(".github/workflows/sweny.yml");
416
- p.log.message([
417
- chalk.bold("Files to create:"),
418
- ...files.map((f) => ` ${chalk.cyan(f)}`),
419
- "",
420
- chalk.bold("Providers:"),
421
- ` Source control: ${selections.sourceControl}`,
422
- ` Observability: ${selections.observability ?? "none"}`,
423
- ` Issue tracker: ${selections.issueTracker}`,
424
- ` Notification: ${selections.notification}`,
425
- selections.githubAction ? ` GitHub Action: ${selections.cronExpression}` : " GitHub Action: no",
426
- ].join("\n"));
427
- const confirmed = await p.confirm({
428
- message: "Create these files?",
429
- initialValue: true,
430
- });
431
- if (p.isCancel(confirmed))
432
- cancel();
433
- if (!confirmed) {
434
- p.cancel("Setup cancelled.");
435
- process.exit(0);
436
- }
437
- // ── Screen 7: Write files ──────────────────────────────────────────
438
- // 1. Write .sweny.yml
439
- fs.writeFileSync(configPath, buildSwenyYml(selections), "utf-8");
440
- p.log.success("Created .sweny.yml");
441
- // 2. Write .env
442
- const envPath = path.join(cwd, ".env");
443
- if (fs.existsSync(envPath)) {
444
- const existing = fs.readFileSync(envPath, "utf-8");
445
- // Parse defined keys (ignore comment lines)
446
- const definedKeys = new Set(existing
447
- .split("\n")
448
- .filter((l) => !l.trimStart().startsWith("#") && l.includes("="))
449
- .map((l) => l.split("=")[0].trim()));
450
- const newKeys = [];
451
- for (const cred of credentials) {
452
- if (!definedKeys.has(cred.key)) {
453
- newKeys.push(cred);
454
- }
455
- }
456
- if (newKeys.length > 0) {
457
- const appendBlock = "\n" + buildEnvTemplate(newKeys);
458
- fs.appendFileSync(envPath, appendBlock, "utf-8");
459
- p.log.success(`Appended ${newKeys.length} new key(s) to .env`);
460
- }
461
- else {
462
- p.log.info(".env already contains all required keys — skipped");
463
- }
464
- }
465
- else {
466
- fs.writeFileSync(envPath, buildEnvTemplate(credentials), "utf-8");
467
- p.log.success("Created .env");
468
- }
469
- // 3. GitHub Action workflow
470
- if (selections.githubAction && selections.cronExpression) {
471
- const workflowDir = path.join(cwd, ".github", "workflows");
472
- const workflowPath = path.join(workflowDir, "sweny.yml");
473
- if (fs.existsSync(workflowPath)) {
474
- const overwriteWf = await p.confirm({
475
- message: ".github/workflows/sweny.yml already exists. Overwrite?",
476
- initialValue: false,
477
- });
478
- if (p.isCancel(overwriteWf))
479
- cancel();
480
- if (!overwriteWf) {
481
- p.log.info("Skipped GitHub Action workflow");
482
- }
483
- else {
484
- fs.mkdirSync(workflowDir, { recursive: true });
485
- fs.writeFileSync(workflowPath, buildActionWorkflow(credentials, selections.cronExpression), "utf-8");
486
- p.log.success("Created .github/workflows/sweny.yml");
487
- }
488
- }
489
- else {
490
- fs.mkdirSync(workflowDir, { recursive: true });
491
- fs.writeFileSync(workflowPath, buildActionWorkflow(credentials, selections.cronExpression), "utf-8");
492
- p.log.success("Created .github/workflows/sweny.yml");
493
- }
494
- }
495
- // 4. .gitignore check
496
- const gitignorePath = path.join(cwd, ".gitignore");
497
- if (fs.existsSync(gitignorePath)) {
498
- const gitignore = fs.readFileSync(gitignorePath, "utf-8");
499
- const envIgnored = gitignore.split("\n").some((line) => line.trim() === ".env");
500
- if (!envIgnored) {
501
- p.log.warn(chalk.yellow(".env is not in .gitignore — add it to avoid committing secrets"));
502
- }
503
- }
504
- else {
505
- p.log.warn(chalk.yellow("No .gitignore found — make sure .env is not committed"));
506
- }
507
- // 5. Next steps
508
- const docUrls = credentials.filter((c) => c.url).map((c) => ` ${c.key}: ${c.url}`);
509
- p.note([
510
- "1. Fill in your API keys in .env:",
511
- ...docUrls,
512
- "",
513
- "2. Verify connectivity:",
514
- " sweny check",
515
- "",
516
- "3. Run a dry-run triage:",
517
- " sweny triage --dry-run",
518
- ].join("\n"), "Next steps");
519
- // 6. Done
520
- p.outro("You're all set!");
521
- }