@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
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import * as os from "node:os";
5
5
  import { parse as parseYaml } from "yaml";
6
- import { collectCredentials, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, PROVIDER_CREDENTIALS, } from "./init.js";
6
+ import { collectCredentials, collectCredentialsForSkills, extractSkillsFromYaml, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, runInit, runNew, PROVIDER_CREDENTIALS, SKILL_CREDENTIALS, } from "./new.js";
7
7
  // ── detectGitRemote ────────────────────────────────────────────────────
8
8
  describe("detectGitRemote", () => {
9
9
  const tmpDirs = [];
@@ -171,50 +171,17 @@ describe("PROVIDER_CREDENTIALS", () => {
171
171
  // ── buildSwenyYml ──────────────────────────────────────────────────────
172
172
  describe("buildSwenyYml", () => {
173
173
  it("includes all selected providers", () => {
174
- const yml = buildSwenyYml({
175
- sourceControl: "github",
176
- observability: "datadog",
177
- issueTracker: "linear",
178
- notification: "slack",
179
- githubAction: true,
180
- cronExpression: "0 8 * * 1",
181
- });
174
+ const yml = buildSwenyYml("github", "datadog", "linear");
182
175
  expect(yml).toContain("source-control-provider: github");
183
176
  expect(yml).toContain("observability-provider: datadog");
184
177
  expect(yml).toContain("issue-tracker-provider: linear");
185
- expect(yml).toContain("notification-provider: slack");
186
178
  });
187
179
  it("omits observability-provider when null", () => {
188
- const yml = buildSwenyYml({
189
- sourceControl: "github",
190
- observability: null,
191
- issueTracker: "github-issues",
192
- notification: "slack",
193
- githubAction: false,
194
- cronExpression: null,
195
- });
180
+ const yml = buildSwenyYml("github", null, "github-issues");
196
181
  expect(yml).not.toContain("observability-provider");
197
182
  });
198
- it("omits notification-provider when console (default)", () => {
199
- const yml = buildSwenyYml({
200
- sourceControl: "github",
201
- observability: "sentry",
202
- issueTracker: "github-issues",
203
- notification: "console",
204
- githubAction: false,
205
- cronExpression: null,
206
- });
207
- expect(yml).not.toContain("notification-provider");
208
- });
209
183
  it("includes header comments", () => {
210
- const yml = buildSwenyYml({
211
- sourceControl: "github",
212
- observability: null,
213
- issueTracker: "github-issues",
214
- notification: "console",
215
- githubAction: false,
216
- cronExpression: null,
217
- });
184
+ const yml = buildSwenyYml("github", null, "github-issues");
218
185
  expect(yml).toContain("# .sweny.yml");
219
186
  });
220
187
  });
@@ -407,49 +374,25 @@ describe("detectGitRemote — edge cases", () => {
407
374
  // ── buildSwenyYml: YAML validity ──────────────────────────────────────
408
375
  describe("buildSwenyYml — YAML validity", () => {
409
376
  it("generates valid YAML that can be parsed", () => {
410
- const yml = buildSwenyYml({
411
- sourceControl: "github",
412
- observability: "datadog",
413
- issueTracker: "linear",
414
- notification: "slack",
415
- githubAction: false,
416
- cronExpression: null,
417
- });
377
+ const yml = buildSwenyYml("github", "datadog", "linear");
418
378
  const parsed = parseYaml(yml);
419
379
  expect(parsed["source-control-provider"]).toBe("github");
420
380
  expect(parsed["observability-provider"]).toBe("datadog");
421
381
  expect(parsed["issue-tracker-provider"]).toBe("linear");
422
- expect(parsed["notification-provider"]).toBe("slack");
423
382
  });
424
383
  it("generates valid YAML with minimal selections", () => {
425
- const yml = buildSwenyYml({
426
- sourceControl: "github",
427
- observability: null,
428
- issueTracker: "github-issues",
429
- notification: "console",
430
- githubAction: false,
431
- cronExpression: null,
432
- });
384
+ const yml = buildSwenyYml("github", null, "github-issues");
433
385
  const parsed = parseYaml(yml);
434
386
  expect(parsed["source-control-provider"]).toBe("github");
435
387
  expect(parsed["issue-tracker-provider"]).toBe("github-issues");
436
388
  expect(parsed["observability-provider"]).toBeUndefined();
437
- expect(parsed["notification-provider"]).toBeUndefined();
438
389
  });
439
390
  it("uses correct key names matching config parser expectations", () => {
440
- const yml = buildSwenyYml({
441
- sourceControl: "gitlab",
442
- observability: "sentry",
443
- issueTracker: "jira",
444
- notification: "teams",
445
- githubAction: false,
446
- cronExpression: null,
447
- });
391
+ const yml = buildSwenyYml("gitlab", "sentry", "jira");
448
392
  // These are the exact keys the config parser reads
449
393
  expect(yml).toContain("source-control-provider:");
450
394
  expect(yml).toContain("observability-provider:");
451
395
  expect(yml).toContain("issue-tracker-provider:");
452
- expect(yml).toContain("notification-provider:");
453
396
  // Must NOT contain the short form
454
397
  expect(yml).not.toMatch(/^source-control:/m);
455
398
  expect(yml).not.toMatch(/^issue-tracker:/m);
@@ -458,23 +401,13 @@ describe("buildSwenyYml — YAML validity", () => {
458
401
  const sourceControls = ["github", "gitlab"];
459
402
  const observabilities = ["datadog", "sentry", "betterstack", "newrelic", "cloudwatch", null];
460
403
  const issueTrackers = ["github-issues", "linear", "jira"];
461
- const notifications = ["console", "slack", "discord", "teams", "webhook"];
462
404
  for (const sc of sourceControls) {
463
405
  for (const obs of observabilities) {
464
406
  for (const it of issueTrackers) {
465
- for (const notif of notifications) {
466
- const yml = buildSwenyYml({
467
- sourceControl: sc,
468
- observability: obs,
469
- issueTracker: it,
470
- notification: notif,
471
- githubAction: false,
472
- cronExpression: null,
473
- });
474
- // Should not throw
475
- const parsed = parseYaml(yml);
476
- expect(parsed["source-control-provider"]).toBe(sc);
477
- }
407
+ const yml = buildSwenyYml(sc, obs, it);
408
+ // Should not throw
409
+ const parsed = parseYaml(yml);
410
+ expect(parsed["source-control-provider"]).toBe(sc);
478
411
  }
479
412
  }
480
413
  }
@@ -662,3 +595,208 @@ describe("collectCredentials — additional cases", () => {
662
595
  }
663
596
  });
664
597
  });
598
+ // ═══════════════════════════════════════════════════════════════════════
599
+ // Workflow-first functions: extractSkillsFromYaml, collectCredentialsForSkills
600
+ // ═══════════════════════════════════════════════════════════════════════
601
+ describe("extractSkillsFromYaml", () => {
602
+ it("extracts skills from a single node", () => {
603
+ const yaml = `
604
+ nodes:
605
+ review:
606
+ name: Code Review
607
+ instruction: Review the PR
608
+ skills: [github]
609
+ `;
610
+ expect(extractSkillsFromYaml(yaml)).toEqual(["github"]);
611
+ });
612
+ it("extracts and deduplicates skills from multiple nodes", () => {
613
+ const yaml = `
614
+ nodes:
615
+ review:
616
+ name: Code Review
617
+ skills: [github, linear]
618
+ scan:
619
+ name: Security Scan
620
+ skills: [github, sentry]
621
+ `;
622
+ const skills = extractSkillsFromYaml(yaml);
623
+ expect(skills).toContain("github");
624
+ expect(skills).toContain("linear");
625
+ expect(skills).toContain("sentry");
626
+ expect(skills.filter((s) => s === "github")).toHaveLength(1);
627
+ });
628
+ it("returns empty array when no skills present", () => {
629
+ const yaml = `
630
+ nodes:
631
+ review:
632
+ name: Code Review
633
+ instruction: Review the PR
634
+ `;
635
+ expect(extractSkillsFromYaml(yaml)).toEqual([]);
636
+ });
637
+ it("handles skills with spaces around commas", () => {
638
+ const yaml = `
639
+ nodes:
640
+ review:
641
+ skills: [github , linear , sentry]
642
+ `;
643
+ const skills = extractSkillsFromYaml(yaml);
644
+ expect(skills).toEqual(["github", "linear", "sentry"]);
645
+ });
646
+ it("handles single-skill arrays", () => {
647
+ const yaml = `
648
+ nodes:
649
+ review:
650
+ skills: [datadog]
651
+ `;
652
+ expect(extractSkillsFromYaml(yaml)).toEqual(["datadog"]);
653
+ });
654
+ });
655
+ describe("collectCredentialsForSkills", () => {
656
+ it("always includes ANTHROPIC_API_KEY even with no skills", () => {
657
+ const creds = collectCredentialsForSkills([]);
658
+ expect(creds.map((c) => c.key)).toEqual(["ANTHROPIC_API_KEY"]);
659
+ });
660
+ it("includes credentials for specified skills", () => {
661
+ const creds = collectCredentialsForSkills(["github", "linear"]);
662
+ const keys = creds.map((c) => c.key);
663
+ expect(keys).toContain("ANTHROPIC_API_KEY");
664
+ expect(keys).toContain("GITHUB_TOKEN");
665
+ expect(keys).toContain("LINEAR_API_KEY");
666
+ expect(keys).toContain("LINEAR_TEAM_ID");
667
+ });
668
+ it("deduplicates credentials across skills", () => {
669
+ const creds = collectCredentialsForSkills(["github", "github"]);
670
+ const tokenCount = creds.filter((c) => c.key === "GITHUB_TOKEN").length;
671
+ expect(tokenCount).toBe(1);
672
+ });
673
+ it("skips unknown skills gracefully", () => {
674
+ const creds = collectCredentialsForSkills(["github", "nonexistent-skill"]);
675
+ const keys = creds.map((c) => c.key);
676
+ expect(keys).toContain("ANTHROPIC_API_KEY");
677
+ expect(keys).toContain("GITHUB_TOKEN");
678
+ expect(keys).toHaveLength(2);
679
+ });
680
+ it("ANTHROPIC_API_KEY is always first", () => {
681
+ const creds = collectCredentialsForSkills(["datadog", "github"]);
682
+ expect(creds[0].key).toBe("ANTHROPIC_API_KEY");
683
+ });
684
+ });
685
+ // ── extractSkillsFromYaml: block-style arrays + malformed input ───────
686
+ //
687
+ // Regression: an earlier regex-based implementation only matched inline
688
+ // arrays (`skills: [a, b]`). `yaml.stringify()` emits block style by
689
+ // default, so AI-generated `__custom` workflows silently dropped every
690
+ // skill and the wizard then asked for zero credentials. These tests pin
691
+ // the parser-based implementation to handle both forms.
692
+ describe("extractSkillsFromYaml — block-style arrays", () => {
693
+ it("extracts skills from block-style arrays (yaml.stringify default)", () => {
694
+ const yaml = `nodes:
695
+ review:
696
+ name: Code Review
697
+ skills:
698
+ - github
699
+ - linear
700
+ `;
701
+ const skills = extractSkillsFromYaml(yaml);
702
+ expect(skills.sort()).toEqual(["github", "linear"]);
703
+ });
704
+ it("deduplicates across block-style nodes", () => {
705
+ const yaml = `nodes:
706
+ first:
707
+ skills:
708
+ - github
709
+ - sentry
710
+ second:
711
+ skills:
712
+ - github
713
+ - linear
714
+ `;
715
+ const skills = extractSkillsFromYaml(yaml).sort();
716
+ expect(skills).toEqual(["github", "linear", "sentry"]);
717
+ });
718
+ it("handles mixed inline and block-style skills in the same doc", () => {
719
+ const yaml = `nodes:
720
+ a:
721
+ skills: [github, datadog]
722
+ b:
723
+ skills:
724
+ - linear
725
+ - jira
726
+ `;
727
+ const skills = extractSkillsFromYaml(yaml).sort();
728
+ expect(skills).toEqual(["datadog", "github", "jira", "linear"]);
729
+ });
730
+ it("ignores non-string skill entries defensively", () => {
731
+ const yaml = `nodes:
732
+ review:
733
+ skills:
734
+ - github
735
+ - 42
736
+ - null
737
+ - ""
738
+ `;
739
+ // Only "github" survives the typeof-string + non-empty filter
740
+ expect(extractSkillsFromYaml(yaml)).toEqual(["github"]);
741
+ });
742
+ it("returns empty array for malformed YAML (does not throw)", () => {
743
+ const yaml = `nodes:
744
+ review:
745
+ skills: [github, linear
746
+ # unclosed bracket above — invalid yaml
747
+ `;
748
+ expect(extractSkillsFromYaml(yaml)).toEqual([]);
749
+ });
750
+ it("returns empty array for YAML with no nodes key", () => {
751
+ expect(extractSkillsFromYaml("foo: bar\n")).toEqual([]);
752
+ });
753
+ it("returns empty array for empty/whitespace input", () => {
754
+ expect(extractSkillsFromYaml("")).toEqual([]);
755
+ expect(extractSkillsFromYaml(" \n")).toEqual([]);
756
+ });
757
+ it("returns empty array when nodes entries have no skills field", () => {
758
+ const yaml = `nodes:
759
+ review:
760
+ name: Code Review
761
+ instruction: Review the PR
762
+ `;
763
+ expect(extractSkillsFromYaml(yaml)).toEqual([]);
764
+ });
765
+ });
766
+ // ── Deprecated alias: runInit === runNew ──────────────────────────────
767
+ describe("runInit deprecation alias", () => {
768
+ it("re-exports runNew as runInit (identity pin)", () => {
769
+ // Callers using `import { runInit } from "@sweny-ai/core/new"` must
770
+ // continue to hit the same function during the deprecation window.
771
+ expect(runInit).toBe(runNew);
772
+ });
773
+ });
774
+ describe("SKILL_CREDENTIALS", () => {
775
+ it("has entries for all built-in skills", () => {
776
+ const expected = [
777
+ "github",
778
+ "gitlab",
779
+ "datadog",
780
+ "sentry",
781
+ "betterstack",
782
+ "newrelic",
783
+ "linear",
784
+ "jira",
785
+ "slack",
786
+ "discord",
787
+ "notification",
788
+ ];
789
+ for (const skill of expected) {
790
+ expect(SKILL_CREDENTIALS).toHaveProperty(skill);
791
+ }
792
+ });
793
+ it("every skill has valid credential entries", () => {
794
+ for (const [skill, creds] of Object.entries(SKILL_CREDENTIALS)) {
795
+ expect(Array.isArray(creds), `${skill} should have an array`).toBe(true);
796
+ for (const cred of creds) {
797
+ expect(typeof cred.key, `${skill}.${cred.key} should be a string`).toBe("string");
798
+ expect(cred.key, `${skill}.${cred.key} should be UPPER_SNAKE_CASE`).toMatch(/^[A-Z][A-Z0-9_]+$/);
799
+ }
800
+ }
801
+ });
802
+ });
@@ -0,0 +1,34 @@
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 type { Command } from "commander";
10
+ export interface PublishResult {
11
+ type: "workflow" | "skill";
12
+ id: string;
13
+ name: string;
14
+ outputPath?: string;
15
+ prUrl?: string;
16
+ }
17
+ export declare function validateWorkflowFile(filePath: string): {
18
+ valid: boolean;
19
+ id?: string;
20
+ name?: string;
21
+ nodeCount?: number;
22
+ edgeCount?: number;
23
+ errors: string[];
24
+ };
25
+ export declare function validateSkillDir(dirPath: string): {
26
+ valid: boolean;
27
+ id?: string;
28
+ name?: string;
29
+ hasInstruction: boolean;
30
+ hasMcp: boolean;
31
+ errors: string[];
32
+ };
33
+ export declare function runPublish(): Promise<PublishResult | null>;
34
+ export declare function registerPublishCommand(program: Command): void;