@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
|
@@ -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 "./
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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;
|