@sweny-ai/core 0.1.62 → 0.1.64
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__/cascade.test.d.ts +11 -0
- package/dist/__tests__/cascade.test.js +449 -0
- package/dist/cli/main.js +4 -4
- package/dist/cli/marketplace.d.ts +103 -0
- package/dist/cli/marketplace.js +290 -0
- package/dist/cli/marketplace.test.d.ts +1 -0
- package/dist/cli/marketplace.test.js +355 -0
- package/dist/cli/new.d.ts +35 -2
- package/dist/cli/new.js +156 -51
- package/dist/cli/new.test.js +215 -2
- package/dist/executor.js +105 -26
- package/dist/schema.d.ts +724 -0
- package/dist/schema.js +51 -1
- package/dist/source-resolver.d.ts +22 -0
- package/dist/source-resolver.js +174 -0
- package/dist/sources.d.ts +9 -6
- package/dist/sources.js +13 -146
- package/dist/sources.test.js +2 -1
- package/dist/sources.validation.test.js +2 -1
- package/dist/templates.js +2 -1
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
package/dist/cli/new.d.ts
CHANGED
|
@@ -76,6 +76,23 @@ export declare function collectCredentials(selections: InitSelections): Credenti
|
|
|
76
76
|
* Returns provider + normalised remote (e.g. "github.com/owner/repo"), or null.
|
|
77
77
|
*/
|
|
78
78
|
export declare function detectGitRemote(cwd: string): GitRemoteInfo | null;
|
|
79
|
+
/**
|
|
80
|
+
* Infer source-control-provider from git remote detection.
|
|
81
|
+
*/
|
|
82
|
+
export declare function inferSourceControl(gitInfo: GitRemoteInfo | null): string;
|
|
83
|
+
/**
|
|
84
|
+
* Infer issue-tracker-provider from skills in the workflow.
|
|
85
|
+
*
|
|
86
|
+
* Explicit issue-tracker skills win. Otherwise we default to `github-issues`
|
|
87
|
+
* regardless of source-control provider: we don't ship a gitlab issues
|
|
88
|
+
* provider (yet), and `github-issues` is a sensible "use the same platform
|
|
89
|
+
* as source control" default for the supported case.
|
|
90
|
+
*/
|
|
91
|
+
export declare function inferIssueTracker(skills: string[], _sourceControl: string): string;
|
|
92
|
+
/**
|
|
93
|
+
* Infer observability-provider from skills in the workflow.
|
|
94
|
+
*/
|
|
95
|
+
export declare function inferObservability(skills: string[]): string | null;
|
|
79
96
|
/**
|
|
80
97
|
* Generate .sweny.yml content from inferred providers.
|
|
81
98
|
*/
|
|
@@ -89,12 +106,25 @@ export declare function buildEnvTemplate(credentials: Credential[]): string;
|
|
|
89
106
|
* Only includes credentials WITHOUT defaults as secrets.
|
|
90
107
|
*/
|
|
91
108
|
export declare function buildActionWorkflow(credentials: Credential[], cronExpression: string): string;
|
|
109
|
+
/** Write .sweny.yml if missing. Returns true if created, false if file existed. */
|
|
110
|
+
export declare function writeSwenyYmlIfMissing(cwd: string, sourceControl: string, observability: string | null, issueTracker: string): boolean;
|
|
111
|
+
/** Append credential keys to .env that aren't already present. Returns count appended. */
|
|
112
|
+
export declare function appendMissingEnvKeys(cwd: string, credentials: Credential[]): number;
|
|
113
|
+
/** Write workflow YAML into .sweny/workflows/<id>.yml. Returns info about what happened. */
|
|
114
|
+
export declare function writeWorkflowFile(cwd: string, id: string, yaml: string, options?: {
|
|
115
|
+
overwrite?: boolean;
|
|
116
|
+
}): {
|
|
117
|
+
written: boolean;
|
|
118
|
+
exists?: boolean;
|
|
119
|
+
path: string;
|
|
120
|
+
};
|
|
92
121
|
/**
|
|
93
122
|
* Interactive workflow creation wizard — workflow-first approach.
|
|
94
123
|
*
|
|
95
124
|
* Flow:
|
|
96
125
|
* 1. Detect git remote (used to infer source-control provider)
|
|
97
|
-
* 2. Pick one of
|
|
126
|
+
* 2. Pick one of five branches:
|
|
127
|
+
* - `__marketplace` → browse published workflows at swenyai/workflows, then delegate to `installMarketplaceWorkflow`
|
|
98
128
|
* - A built-in `WORKFLOW_TEMPLATES` entry → write that workflow file
|
|
99
129
|
* - `__custom` → LLM generates a workflow from a user description
|
|
100
130
|
* - `__e2e` → delegate to the e2e browser-testing wizard in `./e2e.ts`
|
|
@@ -108,4 +138,7 @@ export declare function buildActionWorkflow(credentials: Credential[], cronExpre
|
|
|
108
138
|
*
|
|
109
139
|
* Safe to re-run in an existing project to add additional workflows.
|
|
110
140
|
*/
|
|
111
|
-
export declare function runNew(
|
|
141
|
+
export declare function runNew(options?: {
|
|
142
|
+
marketplaceId?: string;
|
|
143
|
+
skipIntro?: boolean;
|
|
144
|
+
}): Promise<void>;
|
package/dist/cli/new.js
CHANGED
|
@@ -291,7 +291,7 @@ export function detectGitRemote(cwd) {
|
|
|
291
291
|
/**
|
|
292
292
|
* Infer source-control-provider from git remote detection.
|
|
293
293
|
*/
|
|
294
|
-
function inferSourceControl(gitInfo) {
|
|
294
|
+
export function inferSourceControl(gitInfo) {
|
|
295
295
|
return gitInfo?.provider ?? "github";
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
@@ -302,7 +302,7 @@ function inferSourceControl(gitInfo) {
|
|
|
302
302
|
* provider (yet), and `github-issues` is a sensible "use the same platform
|
|
303
303
|
* as source control" default for the supported case.
|
|
304
304
|
*/
|
|
305
|
-
function inferIssueTracker(skills, _sourceControl) {
|
|
305
|
+
export function inferIssueTracker(skills, _sourceControl) {
|
|
306
306
|
if (skills.includes("linear"))
|
|
307
307
|
return "linear";
|
|
308
308
|
if (skills.includes("jira"))
|
|
@@ -312,7 +312,7 @@ function inferIssueTracker(skills, _sourceControl) {
|
|
|
312
312
|
/**
|
|
313
313
|
* Infer observability-provider from skills in the workflow.
|
|
314
314
|
*/
|
|
315
|
-
function inferObservability(skills) {
|
|
315
|
+
export function inferObservability(skills) {
|
|
316
316
|
if (skills.includes("datadog"))
|
|
317
317
|
return "datadog";
|
|
318
318
|
if (skills.includes("sentry"))
|
|
@@ -396,6 +396,51 @@ export function buildActionWorkflow(credentials, cronExpression) {
|
|
|
396
396
|
lines.push("");
|
|
397
397
|
return lines.join("\n");
|
|
398
398
|
}
|
|
399
|
+
// ── Pure write helpers (exported for marketplace install path) ────────
|
|
400
|
+
/** Write .sweny.yml if missing. Returns true if created, false if file existed. */
|
|
401
|
+
export function writeSwenyYmlIfMissing(cwd, sourceControl, observability, issueTracker) {
|
|
402
|
+
const configPath = path.join(cwd, ".sweny.yml");
|
|
403
|
+
if (fs.existsSync(configPath))
|
|
404
|
+
return false;
|
|
405
|
+
fs.writeFileSync(configPath, buildSwenyYml(sourceControl, observability, issueTracker), "utf-8");
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
/** Append credential keys to .env that aren't already present. Returns count appended. */
|
|
409
|
+
export function appendMissingEnvKeys(cwd, credentials) {
|
|
410
|
+
const envPath = path.join(cwd, ".env");
|
|
411
|
+
if (!fs.existsSync(envPath)) {
|
|
412
|
+
fs.writeFileSync(envPath, buildEnvTemplate(credentials), "utf-8");
|
|
413
|
+
return credentials.length;
|
|
414
|
+
}
|
|
415
|
+
const existing = fs.readFileSync(envPath, "utf-8");
|
|
416
|
+
const definedKeys = new Set();
|
|
417
|
+
for (const rawLine of existing.split("\n")) {
|
|
418
|
+
const line = rawLine.trimStart().replace(/^#\s*/, "");
|
|
419
|
+
const eq = line.indexOf("=");
|
|
420
|
+
if (eq <= 0)
|
|
421
|
+
continue;
|
|
422
|
+
const key = line.slice(0, eq).trim();
|
|
423
|
+
if (/^[A-Z_][A-Z0-9_]*$/i.test(key))
|
|
424
|
+
definedKeys.add(key);
|
|
425
|
+
}
|
|
426
|
+
const newKeys = credentials.filter((c) => !definedKeys.has(c.key));
|
|
427
|
+
if (newKeys.length === 0)
|
|
428
|
+
return 0;
|
|
429
|
+
fs.appendFileSync(envPath, "\n" + buildEnvTemplate(newKeys), "utf-8");
|
|
430
|
+
return newKeys.length;
|
|
431
|
+
}
|
|
432
|
+
/** Write workflow YAML into .sweny/workflows/<id>.yml. Returns info about what happened. */
|
|
433
|
+
export function writeWorkflowFile(cwd, id, yaml, options) {
|
|
434
|
+
const dir = path.join(cwd, ".sweny", "workflows");
|
|
435
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
436
|
+
const target = path.join(dir, `${id}.yml`);
|
|
437
|
+
const overwrite = options?.overwrite ?? false;
|
|
438
|
+
if (fs.existsSync(target) && !overwrite) {
|
|
439
|
+
return { written: false, exists: true, path: target };
|
|
440
|
+
}
|
|
441
|
+
fs.writeFileSync(target, yaml, "utf-8");
|
|
442
|
+
return { written: true, path: target };
|
|
443
|
+
}
|
|
399
444
|
// ── Interactive wizard ────────────────────────────────────────────────
|
|
400
445
|
function cancel() {
|
|
401
446
|
p.cancel("Setup cancelled.");
|
|
@@ -406,7 +451,8 @@ function cancel() {
|
|
|
406
451
|
*
|
|
407
452
|
* Flow:
|
|
408
453
|
* 1. Detect git remote (used to infer source-control provider)
|
|
409
|
-
* 2. Pick one of
|
|
454
|
+
* 2. Pick one of five branches:
|
|
455
|
+
* - `__marketplace` → browse published workflows at swenyai/workflows, then delegate to `installMarketplaceWorkflow`
|
|
410
456
|
* - A built-in `WORKFLOW_TEMPLATES` entry → write that workflow file
|
|
411
457
|
* - `__custom` → LLM generates a workflow from a user description
|
|
412
458
|
* - `__e2e` → delegate to the e2e browser-testing wizard in `./e2e.ts`
|
|
@@ -420,8 +466,63 @@ function cancel() {
|
|
|
420
466
|
*
|
|
421
467
|
* Safe to re-run in an existing project to add additional workflows.
|
|
422
468
|
*/
|
|
423
|
-
export async function runNew() {
|
|
469
|
+
export async function runNew(options) {
|
|
424
470
|
const cwd = process.cwd();
|
|
471
|
+
// ── Marketplace install fast path ───────────────────────────────────
|
|
472
|
+
if (options?.marketplaceId) {
|
|
473
|
+
const { installMarketplaceWorkflow } = await import("./marketplace.js");
|
|
474
|
+
const allSkills = configuredSkills(process.env, cwd);
|
|
475
|
+
const hasAgent = !!process.env.ANTHROPIC_API_KEY;
|
|
476
|
+
const claude = hasAgent ? new ClaudeClient({ maxTurns: 3, cwd, logger: consoleLogger }) : null;
|
|
477
|
+
if (!options.skipIntro)
|
|
478
|
+
p.intro(`Installing ${options.marketplaceId} from marketplace`);
|
|
479
|
+
// Pre-compute source-control from git-remote (same method as the wizard path).
|
|
480
|
+
// Don't pre-compute issue-tracker / observability — those are skill-based,
|
|
481
|
+
// and marketplace.ts infers them after the workflow is fetched.
|
|
482
|
+
const gitInfo = detectGitRemote(cwd);
|
|
483
|
+
const inferredSourceControl = inferSourceControl(gitInfo);
|
|
484
|
+
// Check for existing workflow file and prompt before fetching.
|
|
485
|
+
const existingWorkflowPath = path.join(cwd, ".sweny", "workflows", `${options.marketplaceId}.yml`);
|
|
486
|
+
let overwrite = false;
|
|
487
|
+
if (fs.existsSync(existingWorkflowPath)) {
|
|
488
|
+
const confirmed = await p.confirm({
|
|
489
|
+
message: `.sweny/workflows/${options.marketplaceId}.yml already exists. Overwrite?`,
|
|
490
|
+
initialValue: false,
|
|
491
|
+
});
|
|
492
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
493
|
+
p.log.info(`Workflow already exists at ${path.relative(cwd, existingWorkflowPath)}. Not overwritten.`);
|
|
494
|
+
p.cancel("Setup cancelled.");
|
|
495
|
+
process.exit(0);
|
|
496
|
+
}
|
|
497
|
+
overwrite = true;
|
|
498
|
+
}
|
|
499
|
+
let result;
|
|
500
|
+
try {
|
|
501
|
+
result = await installMarketplaceWorkflow(options.marketplaceId, {
|
|
502
|
+
cwd,
|
|
503
|
+
availableSkills: allSkills,
|
|
504
|
+
claude,
|
|
505
|
+
logger: consoleLogger,
|
|
506
|
+
inferredSourceControl,
|
|
507
|
+
overwrite,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
if (!result.installed) {
|
|
515
|
+
p.cancel("Setup cancelled.");
|
|
516
|
+
process.exit(0);
|
|
517
|
+
}
|
|
518
|
+
if (result.adapted)
|
|
519
|
+
p.log.success("Adapted workflow for your project");
|
|
520
|
+
if (result.addedEnvKeys > 0)
|
|
521
|
+
p.log.success(`Appended ${result.addedEnvKeys} key(s) to .env`);
|
|
522
|
+
p.log.success(`Created ${path.relative(cwd, result.workflowPath)}`);
|
|
523
|
+
p.outro("Workflow installed!");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
425
526
|
// ── Fresh vs existing detection ─────────────────────────────────────
|
|
426
527
|
const configPath = path.join(cwd, ".sweny.yml");
|
|
427
528
|
const hasExistingConfig = fs.existsSync(configPath);
|
|
@@ -441,6 +542,11 @@ export async function runNew() {
|
|
|
441
542
|
const templateChoice = await p.select({
|
|
442
543
|
message: "What do you want to do?",
|
|
443
544
|
options: [
|
|
545
|
+
{
|
|
546
|
+
value: "__marketplace",
|
|
547
|
+
label: "Browse marketplace",
|
|
548
|
+
hint: "install a published workflow from swenyai/workflows",
|
|
549
|
+
},
|
|
444
550
|
...WORKFLOW_TEMPLATES.map((t) => ({
|
|
445
551
|
value: t.id,
|
|
446
552
|
label: t.name,
|
|
@@ -453,6 +559,35 @@ export async function runNew() {
|
|
|
453
559
|
});
|
|
454
560
|
if (p.isCancel(templateChoice))
|
|
455
561
|
cancel();
|
|
562
|
+
// ── Marketplace short-circuit: fetch index, pick, delegate ──────────
|
|
563
|
+
if (templateChoice === "__marketplace") {
|
|
564
|
+
const { fetchMarketplaceIndex } = await import("./marketplace.js");
|
|
565
|
+
const spinner = p.spinner();
|
|
566
|
+
spinner.start("Fetching marketplace index…");
|
|
567
|
+
let entries;
|
|
568
|
+
try {
|
|
569
|
+
entries = await fetchMarketplaceIndex();
|
|
570
|
+
spinner.stop(`Found ${entries.length} workflow(s)`);
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
spinner.stop("Failed");
|
|
574
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const pick = await p.select({
|
|
578
|
+
message: "Which workflow?",
|
|
579
|
+
options: entries.map((e) => ({
|
|
580
|
+
value: e.id,
|
|
581
|
+
label: e.name,
|
|
582
|
+
hint: e.description,
|
|
583
|
+
})),
|
|
584
|
+
});
|
|
585
|
+
if (p.isCancel(pick))
|
|
586
|
+
cancel();
|
|
587
|
+
// Delegate to the marketplace install path
|
|
588
|
+
// Wizard already showed its intro — skip the fast-path intro to avoid doubling.
|
|
589
|
+
return runNew({ marketplaceId: pick, skipIntro: true });
|
|
590
|
+
}
|
|
456
591
|
// ── E2E short-circuit: delegate to the e2e wizard ────────────────────
|
|
457
592
|
if (templateChoice === "__e2e") {
|
|
458
593
|
await runE2eInit({ skipIntro: true });
|
|
@@ -519,60 +654,30 @@ export async function runNew() {
|
|
|
519
654
|
process.exit(0);
|
|
520
655
|
}
|
|
521
656
|
// ── Step 4: Write files ─────────────────────────────────────────────
|
|
522
|
-
// 1. Write .sweny.yml (only if fresh
|
|
523
|
-
if (
|
|
524
|
-
fs.writeFileSync(configPath, buildSwenyYml(sourceControl, observability, issueTracker), "utf-8");
|
|
657
|
+
// 1. Write .sweny.yml (only if fresh)
|
|
658
|
+
if (writeSwenyYmlIfMissing(cwd, sourceControl, observability, issueTracker)) {
|
|
525
659
|
p.log.success("Created .sweny.yml");
|
|
526
660
|
}
|
|
527
661
|
else {
|
|
528
662
|
p.log.info(".sweny.yml already exists — keeping existing config");
|
|
529
663
|
}
|
|
530
|
-
// 2.
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
for (const rawLine of existing.split("\n")) {
|
|
540
|
-
const line = rawLine.trimStart().replace(/^#\s*/, "");
|
|
541
|
-
const eq = line.indexOf("=");
|
|
542
|
-
if (eq <= 0)
|
|
543
|
-
continue;
|
|
544
|
-
const key = line.slice(0, eq).trim();
|
|
545
|
-
if (/^[A-Z_][A-Z0-9_]*$/i.test(key)) {
|
|
546
|
-
definedKeys.add(key);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const newKeys = [];
|
|
550
|
-
for (const cred of credentials) {
|
|
551
|
-
if (!definedKeys.has(cred.key)) {
|
|
552
|
-
newKeys.push(cred);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
if (newKeys.length > 0) {
|
|
556
|
-
const appendBlock = "\n" + buildEnvTemplate(newKeys);
|
|
557
|
-
fs.appendFileSync(envPath, appendBlock, "utf-8");
|
|
558
|
-
p.log.success(`Appended ${newKeys.length} new key(s) to .env`);
|
|
559
|
-
addedNewKeys = true;
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
p.log.info(".env already contains all required keys — skipped");
|
|
563
|
-
}
|
|
664
|
+
// 2. Append missing .env keys
|
|
665
|
+
const envExistedBefore = fs.existsSync(path.join(cwd, ".env"));
|
|
666
|
+
const addedCount = appendMissingEnvKeys(cwd, credentials);
|
|
667
|
+
const addedNewKeys = addedCount > 0;
|
|
668
|
+
if (!envExistedBefore) {
|
|
669
|
+
p.log.success("Created .env");
|
|
670
|
+
}
|
|
671
|
+
else if (addedCount > 0) {
|
|
672
|
+
p.log.success(`Appended ${addedCount} new key(s) to .env`);
|
|
564
673
|
}
|
|
565
674
|
else {
|
|
566
|
-
|
|
567
|
-
p.log.success("Created .env");
|
|
568
|
-
addedNewKeys = true;
|
|
675
|
+
p.log.info(".env already contains all required keys — skipped");
|
|
569
676
|
}
|
|
570
|
-
// 3. Workflow template
|
|
677
|
+
// 3. Workflow template
|
|
571
678
|
if (template) {
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
const templatePath = path.join(workflowDir, `${template.id}.yml`);
|
|
575
|
-
if (fs.existsSync(templatePath)) {
|
|
679
|
+
const firstAttempt = writeWorkflowFile(cwd, template.id, template.yaml, { overwrite: false });
|
|
680
|
+
if (firstAttempt.exists) {
|
|
576
681
|
const overwrite = await p.confirm({
|
|
577
682
|
message: `.sweny/workflows/${template.id}.yml already exists. Overwrite?`,
|
|
578
683
|
initialValue: false,
|
|
@@ -584,8 +689,8 @@ export async function runNew() {
|
|
|
584
689
|
p.outro("Done.");
|
|
585
690
|
return;
|
|
586
691
|
}
|
|
692
|
+
writeWorkflowFile(cwd, template.id, template.yaml, { overwrite: true });
|
|
587
693
|
}
|
|
588
|
-
fs.writeFileSync(templatePath, template.yaml, "utf-8");
|
|
589
694
|
p.log.success(`Created .sweny/workflows/${template.id}.yml`);
|
|
590
695
|
}
|
|
591
696
|
// 4. .gitignore check
|
package/dist/cli/new.test.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
2
|
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, collectCredentialsForSkills, extractSkillsFromYaml, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, PROVIDER_CREDENTIALS, SKILL_CREDENTIALS, } from "./new.js";
|
|
6
|
+
import { collectCredentials, collectCredentialsForSkills, extractSkillsFromYaml, detectGitRemote, buildSwenyYml, buildEnvTemplate, buildActionWorkflow, runNew, PROVIDER_CREDENTIALS, SKILL_CREDENTIALS, writeSwenyYmlIfMissing, appendMissingEnvKeys, writeWorkflowFile, } from "./new.js";
|
|
7
7
|
// ── detectGitRemote ────────────────────────────────────────────────────
|
|
8
8
|
describe("detectGitRemote", () => {
|
|
9
9
|
const tmpDirs = [];
|
|
@@ -828,3 +828,216 @@ describe("SKILL_CREDENTIALS", () => {
|
|
|
828
828
|
}
|
|
829
829
|
});
|
|
830
830
|
});
|
|
831
|
+
describe("writeSwenyYmlIfMissing", () => {
|
|
832
|
+
const tmpDirs = [];
|
|
833
|
+
afterEach(() => {
|
|
834
|
+
for (const d of tmpDirs)
|
|
835
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
836
|
+
tmpDirs.length = 0;
|
|
837
|
+
});
|
|
838
|
+
it("creates .sweny.yml when missing", () => {
|
|
839
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
840
|
+
tmpDirs.push(dir);
|
|
841
|
+
const created = writeSwenyYmlIfMissing(dir, "github", "datadog", "github-issues");
|
|
842
|
+
expect(created).toBe(true);
|
|
843
|
+
expect(fs.existsSync(path.join(dir, ".sweny.yml"))).toBe(true);
|
|
844
|
+
});
|
|
845
|
+
it("does not overwrite existing .sweny.yml", () => {
|
|
846
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
847
|
+
tmpDirs.push(dir);
|
|
848
|
+
fs.writeFileSync(path.join(dir, ".sweny.yml"), "custom: value\n");
|
|
849
|
+
const created = writeSwenyYmlIfMissing(dir, "github", null, "github-issues");
|
|
850
|
+
expect(created).toBe(false);
|
|
851
|
+
expect(fs.readFileSync(path.join(dir, ".sweny.yml"), "utf-8")).toBe("custom: value\n");
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
describe("appendMissingEnvKeys", () => {
|
|
855
|
+
const tmpDirs = [];
|
|
856
|
+
afterEach(() => {
|
|
857
|
+
for (const d of tmpDirs)
|
|
858
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
859
|
+
tmpDirs.length = 0;
|
|
860
|
+
});
|
|
861
|
+
it("creates .env from scratch", () => {
|
|
862
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
863
|
+
tmpDirs.push(dir);
|
|
864
|
+
const creds = [{ key: "FOO", hint: "set FOO" }];
|
|
865
|
+
const added = appendMissingEnvKeys(dir, creds);
|
|
866
|
+
expect(added).toBe(1);
|
|
867
|
+
expect(fs.readFileSync(path.join(dir, ".env"), "utf-8")).toContain("FOO=");
|
|
868
|
+
});
|
|
869
|
+
it("skips already-present keys (including commented placeholders)", () => {
|
|
870
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
871
|
+
tmpDirs.push(dir);
|
|
872
|
+
fs.writeFileSync(path.join(dir, ".env"), "# FOO=set-me\nBAR=baz\n");
|
|
873
|
+
const creds = [{ key: "FOO" }, { key: "BAR" }, { key: "QUUX" }];
|
|
874
|
+
const added = appendMissingEnvKeys(dir, creds);
|
|
875
|
+
expect(added).toBe(1);
|
|
876
|
+
const final = fs.readFileSync(path.join(dir, ".env"), "utf-8");
|
|
877
|
+
expect(final).toContain("QUUX=");
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
describe("writeWorkflowFile", () => {
|
|
881
|
+
const tmpDirs = [];
|
|
882
|
+
afterEach(() => {
|
|
883
|
+
for (const d of tmpDirs)
|
|
884
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
885
|
+
tmpDirs.length = 0;
|
|
886
|
+
});
|
|
887
|
+
it("writes a new workflow file", () => {
|
|
888
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
889
|
+
tmpDirs.push(dir);
|
|
890
|
+
const result = writeWorkflowFile(dir, "pr-review", "id: pr-review\n");
|
|
891
|
+
expect(result).toEqual({ written: true, path: path.join(dir, ".sweny", "workflows", "pr-review.yml") });
|
|
892
|
+
expect(fs.existsSync(result.path)).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
it("returns exists=true when file already present and overwrite=false", () => {
|
|
895
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
896
|
+
tmpDirs.push(dir);
|
|
897
|
+
fs.mkdirSync(path.join(dir, ".sweny", "workflows"), { recursive: true });
|
|
898
|
+
fs.writeFileSync(path.join(dir, ".sweny", "workflows", "pr-review.yml"), "old\n");
|
|
899
|
+
const result = writeWorkflowFile(dir, "pr-review", "new\n", { overwrite: false });
|
|
900
|
+
expect(result).toEqual({ written: false, exists: true, path: expect.any(String) });
|
|
901
|
+
expect(fs.readFileSync(result.path, "utf-8")).toBe("old\n");
|
|
902
|
+
});
|
|
903
|
+
it("overwrites when overwrite=true", () => {
|
|
904
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-writer-"));
|
|
905
|
+
tmpDirs.push(dir);
|
|
906
|
+
fs.mkdirSync(path.join(dir, ".sweny", "workflows"), { recursive: true });
|
|
907
|
+
fs.writeFileSync(path.join(dir, ".sweny", "workflows", "pr-review.yml"), "old\n");
|
|
908
|
+
const result = writeWorkflowFile(dir, "pr-review", "new\n", { overwrite: true });
|
|
909
|
+
expect(result).toEqual({ written: true, path: expect.any(String) });
|
|
910
|
+
expect(fs.readFileSync(result.path, "utf-8")).toBe("new\n");
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
vi.mock("./marketplace.js", async () => {
|
|
914
|
+
const actual = await vi.importActual("./marketplace.js");
|
|
915
|
+
return {
|
|
916
|
+
...actual,
|
|
917
|
+
installMarketplaceWorkflow: vi.fn(),
|
|
918
|
+
fetchMarketplaceIndex: vi.fn(),
|
|
919
|
+
};
|
|
920
|
+
});
|
|
921
|
+
vi.mock("@clack/prompts", async () => {
|
|
922
|
+
const actual = await vi.importActual("@clack/prompts");
|
|
923
|
+
return {
|
|
924
|
+
...actual,
|
|
925
|
+
intro: vi.fn(),
|
|
926
|
+
outro: vi.fn(),
|
|
927
|
+
cancel: vi.fn(),
|
|
928
|
+
isCancel: vi.fn(() => false),
|
|
929
|
+
select: vi.fn(),
|
|
930
|
+
confirm: vi.fn(),
|
|
931
|
+
text: vi.fn(),
|
|
932
|
+
spinner: vi.fn(() => ({ start: vi.fn(), stop: vi.fn() })),
|
|
933
|
+
log: {
|
|
934
|
+
info: vi.fn(),
|
|
935
|
+
success: vi.fn(),
|
|
936
|
+
warn: vi.fn(),
|
|
937
|
+
error: vi.fn(),
|
|
938
|
+
message: vi.fn(),
|
|
939
|
+
},
|
|
940
|
+
note: vi.fn(),
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
describe("runNew with marketplaceId", () => {
|
|
944
|
+
const tmpDirs = [];
|
|
945
|
+
afterEach(() => {
|
|
946
|
+
for (const d of tmpDirs)
|
|
947
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
948
|
+
tmpDirs.length = 0;
|
|
949
|
+
vi.restoreAllMocks();
|
|
950
|
+
vi.clearAllMocks();
|
|
951
|
+
});
|
|
952
|
+
function tmp() {
|
|
953
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-new-mkt-"));
|
|
954
|
+
tmpDirs.push(dir);
|
|
955
|
+
return dir;
|
|
956
|
+
}
|
|
957
|
+
it("calls installMarketplaceWorkflow with pre-computed provider fields and overwrite:false when file absent", async () => {
|
|
958
|
+
const cwd = tmp();
|
|
959
|
+
vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
|
960
|
+
const mkt = await import("./marketplace.js");
|
|
961
|
+
mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
|
|
962
|
+
installed: true,
|
|
963
|
+
adapted: false,
|
|
964
|
+
workflowPath: path.join(cwd, ".sweny", "workflows", "pr-review.yml"),
|
|
965
|
+
mismatches: [],
|
|
966
|
+
addedEnvKeys: 0,
|
|
967
|
+
});
|
|
968
|
+
await runNew({ marketplaceId: "pr-review" });
|
|
969
|
+
expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.objectContaining({
|
|
970
|
+
cwd,
|
|
971
|
+
inferredSourceControl: expect.any(String),
|
|
972
|
+
overwrite: false,
|
|
973
|
+
}));
|
|
974
|
+
// inferredIssueTracker / inferredObservability are skill-based — runNew
|
|
975
|
+
// must NOT pre-compute them (it has no skills list), so marketplace.ts
|
|
976
|
+
// can fall back to skill-based inference after fetching the workflow.
|
|
977
|
+
const callArgs = mkt.installMarketplaceWorkflow.mock.calls[0][1];
|
|
978
|
+
expect("inferredIssueTracker" in callArgs).toBe(false);
|
|
979
|
+
expect("inferredObservability" in callArgs).toBe(false);
|
|
980
|
+
});
|
|
981
|
+
it("passes overwrite:true to installMarketplaceWorkflow when user confirms overwrite", async () => {
|
|
982
|
+
const cwd = tmp();
|
|
983
|
+
vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
|
984
|
+
// Pre-create the workflow file so the exists check triggers
|
|
985
|
+
const wfDir = path.join(cwd, ".sweny", "workflows");
|
|
986
|
+
fs.mkdirSync(wfDir, { recursive: true });
|
|
987
|
+
fs.writeFileSync(path.join(wfDir, "pr-review.yml"), "# existing\n", "utf-8");
|
|
988
|
+
const mkt = await import("./marketplace.js");
|
|
989
|
+
const p = await import("@clack/prompts");
|
|
990
|
+
mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
|
|
991
|
+
installed: true,
|
|
992
|
+
adapted: false,
|
|
993
|
+
workflowPath: path.join(wfDir, "pr-review.yml"),
|
|
994
|
+
mismatches: [],
|
|
995
|
+
addedEnvKeys: 0,
|
|
996
|
+
});
|
|
997
|
+
// User confirms overwrite
|
|
998
|
+
p.confirm.mockResolvedValueOnce(true);
|
|
999
|
+
await runNew({ marketplaceId: "pr-review" });
|
|
1000
|
+
expect(p.confirm).toHaveBeenCalled();
|
|
1001
|
+
expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.objectContaining({ overwrite: true }));
|
|
1002
|
+
});
|
|
1003
|
+
it("exits without installing when user declines overwrite for existing workflow", async () => {
|
|
1004
|
+
const cwd = tmp();
|
|
1005
|
+
vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
|
1006
|
+
// Pre-create the workflow file so the exists check triggers
|
|
1007
|
+
const wfDir = path.join(cwd, ".sweny", "workflows");
|
|
1008
|
+
fs.mkdirSync(wfDir, { recursive: true });
|
|
1009
|
+
fs.writeFileSync(path.join(wfDir, "pr-review.yml"), "# existing\n", "utf-8");
|
|
1010
|
+
const mkt = await import("./marketplace.js");
|
|
1011
|
+
const p = await import("@clack/prompts");
|
|
1012
|
+
// User declines overwrite
|
|
1013
|
+
p.confirm.mockResolvedValueOnce(false);
|
|
1014
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((_code) => {
|
|
1015
|
+
throw new Error("process.exit called");
|
|
1016
|
+
});
|
|
1017
|
+
await expect(runNew({ marketplaceId: "pr-review" })).rejects.toThrow("process.exit called");
|
|
1018
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
1019
|
+
// installMarketplaceWorkflow must NOT have been called
|
|
1020
|
+
expect(mkt.installMarketplaceWorkflow).not.toHaveBeenCalled();
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
describe("runNew wizard — browse marketplace branch", () => {
|
|
1024
|
+
afterEach(() => vi.restoreAllMocks());
|
|
1025
|
+
it("fetches index and delegates to installMarketplaceWorkflow when user picks a marketplace entry", async () => {
|
|
1026
|
+
const p = await import("@clack/prompts");
|
|
1027
|
+
const mkt = await import("./marketplace.js");
|
|
1028
|
+
p.select.mockResolvedValueOnce("__marketplace").mockResolvedValueOnce("pr-review");
|
|
1029
|
+
mkt.fetchMarketplaceIndex.mockResolvedValueOnce([
|
|
1030
|
+
{ id: "pr-review", name: "PR Review", description: "Review PRs", skills: ["github"] },
|
|
1031
|
+
]);
|
|
1032
|
+
mkt.installMarketplaceWorkflow.mockResolvedValueOnce({
|
|
1033
|
+
installed: true,
|
|
1034
|
+
adapted: false,
|
|
1035
|
+
workflowPath: "/tmp/.sweny/workflows/pr-review.yml",
|
|
1036
|
+
mismatches: [],
|
|
1037
|
+
addedEnvKeys: 0,
|
|
1038
|
+
});
|
|
1039
|
+
await runNew();
|
|
1040
|
+
expect(mkt.fetchMarketplaceIndex).toHaveBeenCalled();
|
|
1041
|
+
expect(mkt.installMarketplaceWorkflow).toHaveBeenCalledWith("pr-review", expect.any(Object));
|
|
1042
|
+
});
|
|
1043
|
+
});
|