@savvy-web/cli 0.2.0 → 0.3.0
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/841.js +850 -688
- package/README.md +80 -0
- package/package.json +2 -1
package/841.js
CHANGED
|
@@ -3,12 +3,12 @@ import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
|
3
3
|
import { BiomeSchemaSync, BiomeSchemaSyncLive, ChangesetConfigReaderLive, Changesets, CheckResult, Commitlint, ConfigDiscovery, ConfigDiscoveryLive, Lint, ManagedSection, ManagedSectionLive, SavvyBaseSection, SavvyHooksSection, SectionDefinition, SilkPublishabilityDetectorLive, ToolDefinition, ToolDiscovery, ToolDiscoveryLive, VersioningStrategy, VersioningStrategyLive, savvyBasePreamble, savvyHooksHygiene, savvyToolSection } from "@savvy-web/silk-effects";
|
|
4
4
|
import { Data, Effect, Layer, Option, Schema } from "effect";
|
|
5
5
|
import { PackageManagerDetector, PackageManagerDetectorLive, WorkspaceDiscovery, WorkspaceDiscoveryLive, WorkspaceRoot, WorkspaceRootLive } from "workspaces-effect";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { execFile, execSync } from "node:child_process";
|
|
9
9
|
import { applyEdits, modify, parse } from "jsonc-effect";
|
|
10
10
|
import { FileSystem } from "@effect/platform";
|
|
11
|
-
import { chmod } from "node:fs/promises";
|
|
11
|
+
import { chmod, glob, realpath, rm } from "node:fs/promises";
|
|
12
12
|
import { isDeepStrictEqual, promisify } from "node:util";
|
|
13
13
|
import { parse as external_yaml_parse, stringify } from "yaml";
|
|
14
14
|
const { BranchAnalyzer: BranchAnalyzer } = Changesets;
|
|
@@ -78,40 +78,6 @@ const analyzeBranchCommand = Command.make("analyze-branch", {
|
|
|
78
78
|
base: baseOption,
|
|
79
79
|
json: jsonOption
|
|
80
80
|
}, ({ cwd, base, json })=>runAnalyzeBranch(cwd, base, json)).pipe(Command.withDescription("Diff the current branch and classify every changed file by owning package"));
|
|
81
|
-
const { ChangesetLinter: ChangesetLinter } = Changesets;
|
|
82
|
-
const dirArg = Args.directory({
|
|
83
|
-
name: "dir"
|
|
84
|
-
}).pipe(Args.withDefault(".changeset"));
|
|
85
|
-
function runChangesetCheck(dir) {
|
|
86
|
-
return Effect.gen(function*() {
|
|
87
|
-
const resolved = resolve(dir);
|
|
88
|
-
const messages = yield* Effect["try"]({
|
|
89
|
-
try: ()=>ChangesetLinter.validate(resolved),
|
|
90
|
-
catch: (e)=>new Error(String(e))
|
|
91
|
-
});
|
|
92
|
-
const byFile = new Map();
|
|
93
|
-
for (const msg of messages){
|
|
94
|
-
const existing = byFile.get(msg.file);
|
|
95
|
-
if (existing) existing.push(msg);
|
|
96
|
-
else byFile.set(msg.file, [
|
|
97
|
-
msg
|
|
98
|
-
]);
|
|
99
|
-
}
|
|
100
|
-
for (const [file, fileMessages] of byFile){
|
|
101
|
-
yield* Effect.log(`\n${file}`);
|
|
102
|
-
for (const msg of fileMessages)yield* Effect.log(` ${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
103
|
-
}
|
|
104
|
-
const errorCount = messages.length;
|
|
105
|
-
const filesWithErrors = byFile.size;
|
|
106
|
-
if (errorCount > 0) {
|
|
107
|
-
yield* Effect.log(`\n${filesWithErrors} file(s) with errors, ${errorCount} error(s) found`);
|
|
108
|
-
process.exitCode = 1;
|
|
109
|
-
} else yield* Effect.log("All changeset files passed validation.");
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
const checkCommand = Command.make("check", {
|
|
113
|
-
dir: dirArg
|
|
114
|
-
}, ({ dir })=>runChangesetCheck(dir)).pipe(Command.withDescription("Full changeset validation with summary"));
|
|
115
81
|
const { ConfigInspector: ConfigInspector } = Changesets;
|
|
116
82
|
const pathsArg = Args.text({
|
|
117
83
|
name: "path"
|
|
@@ -142,7 +108,7 @@ const classifyCommand = Command.make("classify", {
|
|
|
142
108
|
json: classify_jsonOption
|
|
143
109
|
}, ({ paths, cwd, json })=>runClassify(cwd, paths, json)).pipe(Command.withDescription("Map paths to their owning package per .changeset/config.json"));
|
|
144
110
|
const { ConfigInspector: config_show_ConfigInspector } = Changesets;
|
|
145
|
-
const
|
|
111
|
+
const dirArg = Args.directory({
|
|
146
112
|
name: "dir"
|
|
147
113
|
}).pipe(Args.withDefault("."));
|
|
148
114
|
const config_show_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
@@ -198,7 +164,7 @@ function runConfigShow(dir, json) {
|
|
|
198
164
|
});
|
|
199
165
|
}
|
|
200
166
|
const configShowCommand = Command.make("show", {
|
|
201
|
-
dir:
|
|
167
|
+
dir: dirArg,
|
|
202
168
|
json: config_show_jsonOption
|
|
203
169
|
}, ({ dir, json })=>runConfigShow(dir, json)).pipe(Command.withDescription("Print the resolved .changeset/config.json"));
|
|
204
170
|
const { ConfigInspector: config_validate_ConfigInspector } = Changesets;
|
|
@@ -512,677 +478,726 @@ const depsRegenCommand = Command.make("regen", {
|
|
|
512
478
|
dryRun: dryRunOption,
|
|
513
479
|
json: deps_regen_jsonOption
|
|
514
480
|
}, ({ cwd, base, package: pkg, dryRun, json })=>runDepsRegen(cwd, base, pkg, dryRun, json)).pipe(Command.withDescription("Delete pure dependency changesets and regenerate them from the current diff"));
|
|
515
|
-
const {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
];
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
481
|
+
const { ChangesetLinter: ChangesetLinter } = Changesets;
|
|
482
|
+
const lint_dirArg = Args.directory({
|
|
483
|
+
name: "dir"
|
|
484
|
+
}).pipe(Args.withDefault(".changeset"));
|
|
485
|
+
const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output errors, no summary"), Options.withDefault(false));
|
|
486
|
+
function runLint(dir, quiet) {
|
|
487
|
+
return Effect.gen(function*() {
|
|
488
|
+
const resolved = resolve(dir);
|
|
489
|
+
const messages = yield* Effect["try"](()=>ChangesetLinter.validate(resolved));
|
|
490
|
+
for (const msg of messages)yield* Effect.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
491
|
+
if (!quiet && 0 === messages.length) yield* Effect.log("No lint errors found.");
|
|
492
|
+
if (messages.length > 0) process.exitCode = 1;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const lintCommand = Command.make("lint", {
|
|
496
|
+
dir: lint_dirArg,
|
|
497
|
+
quiet: quietOption
|
|
498
|
+
}, ({ dir, quiet })=>runLint(dir, quiet)).pipe(Command.withDescription("Validate changeset files"));
|
|
499
|
+
const { ConfigInspector: release_surface_ConfigInspector, ConfigurationError: ConfigurationError } = Changesets;
|
|
500
|
+
const packageArg = Args.text({
|
|
501
|
+
name: "package"
|
|
502
|
+
});
|
|
503
|
+
const release_surface_cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
504
|
+
const release_surface_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
505
|
+
function release_surface_renderHuman(pkg) {
|
|
506
|
+
const lines = [];
|
|
507
|
+
lines.push(`Package: ${pkg.name} v${pkg.version}`);
|
|
508
|
+
lines.push(`Workspace: ${pkg.workspaceDir}`);
|
|
509
|
+
if (0 === pkg.additionalScopes.length && 0 === pkg.versionFiles.length) {
|
|
510
|
+
lines.push("");
|
|
511
|
+
lines.push("(no additionalScopes or versionFiles — workspace dir is the entire release surface)");
|
|
512
|
+
return lines.join("\n");
|
|
547
513
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
514
|
+
if (pkg.additionalScopes.length > 0) {
|
|
515
|
+
lines.push("");
|
|
516
|
+
lines.push(`additionalScopes (${pkg.additionalScopes.length} glob${1 === pkg.additionalScopes.length ? "" : "s"}):`);
|
|
517
|
+
for (const g of pkg.additionalScopes)lines.push(` - ${g}`);
|
|
518
|
+
lines.push(`Resolved files (${pkg.additionalScopeFiles.length}):`);
|
|
519
|
+
for (const f of pkg.additionalScopeFiles)lines.push(` ${f}`);
|
|
553
520
|
}
|
|
521
|
+
if (pkg.versionFiles.length > 0) {
|
|
522
|
+
lines.push("");
|
|
523
|
+
lines.push(`versionFiles (${pkg.versionFiles.length}):`);
|
|
524
|
+
for (const vf of pkg.versionFiles){
|
|
525
|
+
lines.push(` ${vf.glob} → ${vf.paths.join(", ")}`);
|
|
526
|
+
for (const f of vf.matchedFiles)lines.push(` ${f}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return lines.join("\n");
|
|
554
530
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
tabSize: 1,
|
|
574
|
-
insertSpaces: false
|
|
575
|
-
};
|
|
576
|
-
function resolveWorkspaceRoot(cwd) {
|
|
577
|
-
return WorkspaceRoot.pipe(Effect.flatMap((wr)=>wr.find(cwd)), Effect.catchAll(()=>Effect.succeed(cwd)));
|
|
578
|
-
}
|
|
579
|
-
function findMarkdownlintConfig(root) {
|
|
580
|
-
for (const configPath of MARKDOWNLINT_CONFIG_PATHS)if (existsSync(join(root, configPath))) return configPath;
|
|
581
|
-
return null;
|
|
582
|
-
}
|
|
583
|
-
function ensureChangesetDir(root) {
|
|
584
|
-
return Effect["try"]({
|
|
585
|
-
try: ()=>{
|
|
586
|
-
const dir = join(root, ".changeset");
|
|
587
|
-
mkdirSync(dir, {
|
|
588
|
-
recursive: true
|
|
589
|
-
});
|
|
590
|
-
return dir;
|
|
591
|
-
},
|
|
592
|
-
catch: (error)=>new InitError({
|
|
593
|
-
step: ".changeset directory",
|
|
594
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
595
|
-
})
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
function handleConfig(changesetDir, repoSlug, force) {
|
|
599
|
-
return Effect["try"]({
|
|
600
|
-
try: ()=>{
|
|
601
|
-
const configPath = join(changesetDir, "config.json");
|
|
602
|
-
if (force || !existsSync(configPath)) {
|
|
603
|
-
const config = {
|
|
604
|
-
...DEFAULT_CONFIG,
|
|
605
|
-
changelog: [
|
|
606
|
-
CHANGELOG_ENTRY,
|
|
607
|
-
{
|
|
608
|
-
repo: repoSlug
|
|
609
|
-
}
|
|
610
|
-
]
|
|
611
|
-
};
|
|
612
|
-
writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
|
|
613
|
-
return force ? "Overwrote .changeset/config.json" : "Created .changeset/config.json";
|
|
614
|
-
}
|
|
615
|
-
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
616
|
-
const currentOptions = Array.isArray(existing.changelog) && "object" == typeof existing.changelog[1] && null !== existing.changelog[1] ? existing.changelog[1] : {};
|
|
617
|
-
existing.changelog = [
|
|
618
|
-
CHANGELOG_ENTRY,
|
|
619
|
-
{
|
|
620
|
-
...currentOptions,
|
|
621
|
-
repo: repoSlug
|
|
622
|
-
}
|
|
623
|
-
];
|
|
624
|
-
writeFileSync(configPath, `${JSON.stringify(existing, null, "\t")}\n`);
|
|
625
|
-
return "Patched changelog in .changeset/config.json";
|
|
626
|
-
},
|
|
627
|
-
catch: (error)=>new InitError({
|
|
628
|
-
step: ".changeset/config.json",
|
|
629
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
630
|
-
})
|
|
531
|
+
function runReleaseSurface(cwd, pkgName, json) {
|
|
532
|
+
return Effect.gen(function*() {
|
|
533
|
+
const inspector = yield* release_surface_ConfigInspector;
|
|
534
|
+
const resolvedCwd = resolve(cwd);
|
|
535
|
+
const config = yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", (err)=>{
|
|
536
|
+
process.exitCode = 1;
|
|
537
|
+
return Effect.fail(err);
|
|
538
|
+
}));
|
|
539
|
+
const scope = config.packages.find((p)=>p.name === pkgName);
|
|
540
|
+
if (!scope) {
|
|
541
|
+
process.exitCode = 1;
|
|
542
|
+
return yield* Effect.fail(new ConfigurationError({
|
|
543
|
+
field: `packages["${pkgName}"]`,
|
|
544
|
+
reason: `Package "${pkgName}" is not declared in .changeset/config.json#packages. Declared packages: ${config.packages.map((p)=>p.name).join(", ") || "(none)"}.`
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
const output = json ? JSON.stringify(scope, null, 2) : release_surface_renderHuman(scope);
|
|
548
|
+
yield* Effect.log(output);
|
|
631
549
|
});
|
|
632
550
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
return Array.isArray(options.versionFiles) && options.versionFiles.length > 0;
|
|
641
|
-
}
|
|
642
|
-
function legacyVersionFilesWarning(configPath) {
|
|
643
|
-
return [
|
|
644
|
-
`DEPRECATION: ${configPath} uses the legacy top-level \`versionFiles[]\` array.`,
|
|
645
|
-
" Migrate each entry to `changelog[1].packages[<entry.package>].versionFiles`",
|
|
646
|
-
" and remove the top-level field. Run `savvy changeset config show --json`",
|
|
647
|
-
" to see the normalized form, or check the 0.9.0 release notes for examples.",
|
|
648
|
-
" Removed in @savvy-web/changesets 1.0.0."
|
|
649
|
-
].join("\n");
|
|
650
|
-
}
|
|
651
|
-
function warnIfLegacyVersionFiles(changesetDir) {
|
|
551
|
+
const releaseSurfaceCommand = Command.make("release-surface", {
|
|
552
|
+
package: packageArg,
|
|
553
|
+
cwd: release_surface_cwdOption,
|
|
554
|
+
json: release_surface_jsonOption
|
|
555
|
+
}, ({ package: pkgName, cwd, json })=>runReleaseSurface(cwd, pkgName, json)).pipe(Command.withDescription("Print every path owned by a package — workspace dir, additionalScopes, versionFiles"));
|
|
556
|
+
const { ConfigInspector: config_gate_ConfigInspector } = Changesets;
|
|
557
|
+
function requireValidConfig(cwd) {
|
|
652
558
|
return Effect.gen(function*() {
|
|
653
|
-
const
|
|
559
|
+
const projectDir = resolve(cwd);
|
|
560
|
+
const configPath = join(projectDir, ".changeset", "config.json");
|
|
654
561
|
if (!existsSync(configPath)) return;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
if (detectLegacyVersionFiles(parsed)) yield* Effect.logWarning(legacyVersionFilesWarning(configPath));
|
|
562
|
+
const inspector = yield* config_gate_ConfigInspector;
|
|
563
|
+
yield* inspector.inspect(projectDir).pipe(Effect.catchTag("ConfigurationError", (err)=>{
|
|
564
|
+
process.exitCode = 1;
|
|
565
|
+
return Effect.fail(err);
|
|
566
|
+
}));
|
|
662
567
|
});
|
|
663
568
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
569
|
+
const { ChangelogTransformer: ChangelogTransformer } = Changesets;
|
|
570
|
+
const fileArg = Args.file({
|
|
571
|
+
name: "file"
|
|
572
|
+
}).pipe(Args.withDefault("CHANGELOG.md"));
|
|
573
|
+
const transform_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Print transformed output instead of writing"), Options.withDefault(false));
|
|
574
|
+
const checkOption = Options.boolean("check").pipe(Options.withAlias("c"), Options.withDescription("Exit 1 if file would change (for CI)"), Options.withDefault(false));
|
|
575
|
+
function runTransform(file, dryRun, check) {
|
|
667
576
|
return Effect.gen(function*() {
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
try
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (!Array.isArray(parsed.customRules) || !parsed.customRules.includes(CUSTOM_RULES_ENTRY)) if (Array.isArray(parsed.customRules)) {
|
|
680
|
-
const currentArray = parsed.customRules;
|
|
681
|
-
const edits = yield* modify(text, [
|
|
682
|
-
"customRules",
|
|
683
|
-
currentArray.length
|
|
684
|
-
], CUSTOM_RULES_ENTRY, {
|
|
685
|
-
formattingOptions: JSONC_FORMAT
|
|
686
|
-
});
|
|
687
|
-
text = yield* applyEdits(text, edits);
|
|
688
|
-
} else {
|
|
689
|
-
const edits = yield* modify(text, [
|
|
690
|
-
"customRules"
|
|
691
|
-
], [
|
|
692
|
-
CUSTOM_RULES_ENTRY
|
|
693
|
-
], {
|
|
694
|
-
formattingOptions: JSONC_FORMAT
|
|
695
|
-
});
|
|
696
|
-
text = yield* applyEdits(text, edits);
|
|
697
|
-
}
|
|
698
|
-
parsed = yield* parse(text);
|
|
699
|
-
const currentConfig = parsed.config;
|
|
700
|
-
if ("object" != typeof currentConfig || null === currentConfig) {
|
|
701
|
-
const edits = yield* modify(text, [
|
|
702
|
-
"config"
|
|
703
|
-
], {}, {
|
|
704
|
-
formattingOptions: JSONC_FORMAT
|
|
705
|
-
});
|
|
706
|
-
text = yield* applyEdits(text, edits);
|
|
707
|
-
}
|
|
708
|
-
parsed = yield* parse(text);
|
|
709
|
-
const config = parsed.config;
|
|
710
|
-
for (const rule of RULE_NAMES)if (!(rule in config)) {
|
|
711
|
-
const edits = yield* modify(text, [
|
|
712
|
-
"config",
|
|
713
|
-
rule
|
|
714
|
-
], false, {
|
|
715
|
-
formattingOptions: JSONC_FORMAT
|
|
716
|
-
});
|
|
717
|
-
text = yield* applyEdits(text, edits);
|
|
718
|
-
}
|
|
719
|
-
try {
|
|
720
|
-
writeFileSync(fullPath, text);
|
|
721
|
-
} catch (error) {
|
|
722
|
-
return yield* Effect.fail(new InitError({
|
|
723
|
-
step: "markdownlint config",
|
|
724
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
725
|
-
}));
|
|
577
|
+
const resolved = resolve(file);
|
|
578
|
+
yield* requireValidConfig(dirname(resolved));
|
|
579
|
+
const content = yield* Effect["try"](()=>readFileSync(resolved, "utf-8"));
|
|
580
|
+
const result = ChangelogTransformer.transformContent(content);
|
|
581
|
+
if (dryRun) return void (yield* Effect.log(result));
|
|
582
|
+
if (check) {
|
|
583
|
+
if (result !== content) {
|
|
584
|
+
yield* Effect.log(`${resolved} would be modified by transform.`);
|
|
585
|
+
process.exitCode = 1;
|
|
586
|
+
} else yield* Effect.log(`${resolved} is already formatted.`);
|
|
587
|
+
return;
|
|
726
588
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
return Effect.fail(new InitError({
|
|
731
|
-
step: "markdownlint config",
|
|
732
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
733
|
-
}));
|
|
734
|
-
}));
|
|
589
|
+
yield* Effect["try"](()=>writeFileSync(resolved, result, "utf-8"));
|
|
590
|
+
yield* Effect.log(`Transformed ${resolved}`);
|
|
591
|
+
});
|
|
735
592
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
step: ".changeset/.markdownlint.json",
|
|
757
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
758
|
-
})
|
|
593
|
+
const transformCommand = Command.make("transform", {
|
|
594
|
+
file: fileArg,
|
|
595
|
+
dryRun: transform_dryRunOption,
|
|
596
|
+
check: checkOption
|
|
597
|
+
}, ({ file, dryRun, check })=>runTransform(file, dryRun, check)).pipe(Command.withDescription("Post-process CHANGELOG.md"));
|
|
598
|
+
const { ChangesetLinter: validate_file_ChangesetLinter } = Changesets;
|
|
599
|
+
const validate_file_fileArg = Args.file({
|
|
600
|
+
name: "file"
|
|
601
|
+
});
|
|
602
|
+
function runValidateFile(filePath) {
|
|
603
|
+
return Effect.gen(function*() {
|
|
604
|
+
const result = yield* Effect["try"](()=>validate_file_ChangesetLinter.validateFile(filePath)).pipe(Effect.catchAll((error)=>Effect.gen(function*() {
|
|
605
|
+
yield* Effect.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
|
+
process.exitCode = 1;
|
|
607
|
+
return null;
|
|
608
|
+
})));
|
|
609
|
+
if (null === result) return;
|
|
610
|
+
for (const msg of result)yield* Effect.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
611
|
+
if (result.length > 0) process.exitCode = 1;
|
|
612
|
+
else yield* Effect.log("Valid.");
|
|
759
613
|
});
|
|
760
614
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
615
|
+
const validateFileCommand = Command.make("validate-file", {
|
|
616
|
+
file: validate_file_fileArg
|
|
617
|
+
}, ({ file })=>runValidateFile(file)).pipe(Command.withDescription("Validate a single changeset file"));
|
|
618
|
+
const { ChangelogTransformer: version_ChangelogTransformer, ConfigInspector: version_ConfigInspector, VersionFileError: VersionFileError, VersionFiles: VersionFiles } = Changesets;
|
|
619
|
+
function getChangesetVersionCommand(pm) {
|
|
620
|
+
switch(pm){
|
|
621
|
+
case "pnpm":
|
|
622
|
+
return "pnpm exec changeset version";
|
|
623
|
+
case "yarn":
|
|
624
|
+
return "yarn exec changeset version";
|
|
625
|
+
case "bun":
|
|
626
|
+
return "bun x changeset version";
|
|
627
|
+
default:
|
|
628
|
+
return "npx changeset version";
|
|
629
|
+
}
|
|
770
630
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
const options = Array.isArray(changelog) ? changelog[1] : void 0;
|
|
794
|
-
if (options && "object" == typeof options && "versionFiles" in options) {
|
|
795
|
-
const result = Schema.decodeUnknownEither(LegacyVersionFilesSchema)(options.versionFiles);
|
|
796
|
-
if ("Left" === result._tag) issues.push({
|
|
797
|
-
file: ".changeset/config.json",
|
|
798
|
-
message: "versionFiles config is invalid"
|
|
631
|
+
const version_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Skip changeset version, only transform existing CHANGELOGs"), Options.withDefault(false));
|
|
632
|
+
function runVersion(dryRun) {
|
|
633
|
+
return Effect.gen(function*() {
|
|
634
|
+
const cwd = process.cwd();
|
|
635
|
+
const detector = yield* PackageManagerDetector;
|
|
636
|
+
const detected = yield* detector.detect(cwd).pipe(Effect.catchAll(()=>Effect.succeed({
|
|
637
|
+
type: "npm",
|
|
638
|
+
version: void 0
|
|
639
|
+
})));
|
|
640
|
+
const pm = detected.type;
|
|
641
|
+
yield* Effect.log(`Detected package manager: ${pm}`);
|
|
642
|
+
yield* requireValidConfig(cwd);
|
|
643
|
+
if (dryRun) yield* Effect.log("Dry run: skipping changeset version");
|
|
644
|
+
else {
|
|
645
|
+
const cmd = getChangesetVersionCommand(pm);
|
|
646
|
+
yield* Effect.log(`Running: ${cmd}`);
|
|
647
|
+
yield* Effect["try"]({
|
|
648
|
+
try: ()=>execSync(cmd, {
|
|
649
|
+
cwd,
|
|
650
|
+
stdio: "inherit"
|
|
651
|
+
}),
|
|
652
|
+
catch: (error)=>new Error(`changeset version failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
799
653
|
});
|
|
800
654
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
{
|
|
809
|
-
|
|
810
|
-
|
|
655
|
+
const discovery = yield* WorkspaceDiscovery;
|
|
656
|
+
const packages = yield* discovery.listPackages().pipe(Effect.catchAll(()=>Effect.succeed([])));
|
|
657
|
+
const changelogs = [];
|
|
658
|
+
const seen = new Set();
|
|
659
|
+
const resolvedCwd = resolve(cwd);
|
|
660
|
+
for (const pkg of packages){
|
|
661
|
+
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
662
|
+
if (existsSync(changelogPath) && !seen.has(pkg.path)) {
|
|
663
|
+
seen.add(pkg.path);
|
|
664
|
+
changelogs.push({
|
|
665
|
+
name: pkg.name,
|
|
666
|
+
path: pkg.path,
|
|
667
|
+
changelogPath
|
|
668
|
+
});
|
|
811
669
|
}
|
|
812
|
-
];
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
function checkBaseMarkdownlint(root) {
|
|
816
|
-
const foundPath = findMarkdownlintConfig(root);
|
|
817
|
-
if (!foundPath) return [
|
|
818
|
-
{
|
|
819
|
-
file: "markdownlint config",
|
|
820
|
-
message: `not found (checked ${MARKDOWNLINT_CONFIG_PATHS.join(", ")})`
|
|
821
670
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
671
|
+
if (!seen.has(resolvedCwd)) {
|
|
672
|
+
const rootChangelog = join(resolvedCwd, "CHANGELOG.md");
|
|
673
|
+
if (existsSync(rootChangelog)) {
|
|
674
|
+
let rootName = "root";
|
|
675
|
+
try {
|
|
676
|
+
const pkg = JSON.parse(readFileSync(join(resolvedCwd, "package.json"), "utf-8"));
|
|
677
|
+
if (pkg.name) rootName = pkg.name;
|
|
678
|
+
} catch {}
|
|
679
|
+
changelogs.push({
|
|
680
|
+
name: rootName,
|
|
681
|
+
path: resolvedCwd,
|
|
682
|
+
changelogPath: rootChangelog
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (0 === changelogs.length) yield* Effect.log("No CHANGELOG.md files found.");
|
|
687
|
+
else {
|
|
688
|
+
yield* Effect.log(`Found ${changelogs.length} CHANGELOG.md file(s)`);
|
|
689
|
+
for (const entry of changelogs){
|
|
690
|
+
yield* Effect["try"]({
|
|
691
|
+
try: ()=>version_ChangelogTransformer.transformFile(entry.changelogPath),
|
|
692
|
+
catch: (error)=>new Error(`Failed to transform ${entry.changelogPath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
693
|
+
});
|
|
694
|
+
yield* Effect.log(`Transformed ${entry.name} → ${entry.changelogPath}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const configPath = join(resolvedCwd, ".changeset", "config.json");
|
|
698
|
+
if (!existsSync(configPath)) return;
|
|
699
|
+
const inspector = yield* version_ConfigInspector;
|
|
700
|
+
const inspected = yield* inspector.inspect(resolvedCwd);
|
|
701
|
+
const scopesWithVersionFiles = inspected.packages.filter((p)=>p.versionFiles.length > 0).map((p)=>{
|
|
702
|
+
const fresh = readPackageVersionFromDisk(p.workspaceDir);
|
|
703
|
+
return fresh && fresh !== p.version ? {
|
|
704
|
+
...p,
|
|
705
|
+
version: fresh
|
|
706
|
+
} : p;
|
|
839
707
|
});
|
|
840
|
-
return
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
message
|
|
708
|
+
if (0 === scopesWithVersionFiles.length) return;
|
|
709
|
+
yield* Effect.log(`Found ${scopesWithVersionFiles.length} package${1 === scopesWithVersionFiles.length ? "" : "s"} with versionFiles`);
|
|
710
|
+
const updates = yield* Effect["try"]({
|
|
711
|
+
try: ()=>VersionFiles.processResolvedVersionFiles(scopesWithVersionFiles, dryRun),
|
|
712
|
+
catch: (error)=>{
|
|
713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
714
|
+
return new VersionFileError({
|
|
715
|
+
filePath: message.match(/Failed to update (.+?):/)?.[1] ?? cwd,
|
|
716
|
+
reason: message
|
|
717
|
+
});
|
|
846
718
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const mdlintPath = join(changesetDir, ".markdownlint.json");
|
|
852
|
-
if (!existsSync(mdlintPath)) return [
|
|
853
|
-
{
|
|
854
|
-
file: ".changeset/.markdownlint.json",
|
|
855
|
-
message: "file does not exist"
|
|
719
|
+
});
|
|
720
|
+
for (const update of updates){
|
|
721
|
+
const action = dryRun ? "Would update" : "Updated";
|
|
722
|
+
yield* Effect.log(`${action} ${update.filePath} → ${update.version}`);
|
|
856
723
|
}
|
|
857
|
-
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function readPackageVersionFromDisk(workspaceDir) {
|
|
858
727
|
try {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
for (const rule of RULE_NAMES)if (true !== existing[rule]) issues.push({
|
|
862
|
-
file: ".changeset/.markdownlint.json",
|
|
863
|
-
message: `rule "${rule}" is not enabled`
|
|
864
|
-
});
|
|
865
|
-
return issues;
|
|
728
|
+
const pkg = JSON.parse(readFileSync(join(workspaceDir, "package.json"), "utf-8"));
|
|
729
|
+
return pkg.version ?? null;
|
|
866
730
|
} catch {
|
|
867
|
-
return
|
|
868
|
-
{
|
|
869
|
-
file: ".changeset/.markdownlint.json",
|
|
870
|
-
message: "could not parse file"
|
|
871
|
-
}
|
|
872
|
-
];
|
|
731
|
+
return null;
|
|
873
732
|
}
|
|
874
733
|
}
|
|
875
|
-
|
|
876
|
-
|
|
734
|
+
const versionCommand = Command.make("version", {
|
|
735
|
+
dryRun: version_dryRunOption
|
|
736
|
+
}, ({ dryRun })=>runVersion(dryRun)).pipe(Command.withDescription("Run changeset version and transform all CHANGELOGs"));
|
|
737
|
+
const { ChangesetLinter: check_ChangesetLinter } = Changesets;
|
|
738
|
+
const check_dirArg = Args.directory({
|
|
739
|
+
name: "dir"
|
|
740
|
+
}).pipe(Args.withDefault(".changeset"));
|
|
741
|
+
function runChangesetCheck(dir) {
|
|
877
742
|
return Effect.gen(function*() {
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
];
|
|
890
|
-
if (0 === issues.length) return void (yield* Effect.log("All @savvy-web/changesets config files are up to date."));
|
|
891
|
-
for (const issue of issues)yield* Effect.logWarning(`${issue.file}: ${issue.message}`);
|
|
892
|
-
yield* Effect.logWarning('Run "savvy changeset init --force" to fix.');
|
|
893
|
-
return;
|
|
743
|
+
const resolved = resolve(dir);
|
|
744
|
+
const messages = yield* Effect["try"]({
|
|
745
|
+
try: ()=>check_ChangesetLinter.validate(resolved),
|
|
746
|
+
catch: (e)=>new Error(String(e))
|
|
747
|
+
});
|
|
748
|
+
const byFile = new Map();
|
|
749
|
+
for (const msg of messages){
|
|
750
|
+
const existing = byFile.get(msg.file);
|
|
751
|
+
if (existing) existing.push(msg);
|
|
752
|
+
else byFile.set(msg.file, [
|
|
753
|
+
msg
|
|
754
|
+
]);
|
|
894
755
|
}
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const configResult = yield* handleConfig(changesetDir, repoSlug, force).pipe(Effect.either);
|
|
899
|
-
if ("Right" === configResult._tag) {
|
|
900
|
-
yield* Effect.log(configResult.right);
|
|
901
|
-
if (!quiet) yield* warnIfLegacyVersionFiles(changesetDir);
|
|
902
|
-
} else errors.push(configResult.left);
|
|
903
|
-
if (!skipMarkdownlint) {
|
|
904
|
-
const baseResult = yield* handleBaseMarkdownlint(root).pipe(Effect.either);
|
|
905
|
-
if ("Right" === baseResult._tag) yield* Effect.log(baseResult.right);
|
|
906
|
-
else errors.push(baseResult.left);
|
|
756
|
+
for (const [file, fileMessages] of byFile){
|
|
757
|
+
yield* Effect.log(`\n${file}`);
|
|
758
|
+
for (const msg of fileMessages)yield* Effect.log(` ${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
907
759
|
}
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
760
|
+
const errorCount = messages.length;
|
|
761
|
+
const filesWithErrors = byFile.size;
|
|
762
|
+
if (errorCount > 0) {
|
|
763
|
+
yield* Effect.log(`\n${filesWithErrors} file(s) with errors, ${errorCount} error(s) found`);
|
|
764
|
+
process.exitCode = 1;
|
|
765
|
+
} else yield* Effect.log("All changeset files passed validation.");
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
Command.make("check", {
|
|
769
|
+
dir: check_dirArg
|
|
770
|
+
}, ({ dir })=>runChangesetCheck(dir)).pipe(Command.withDescription("Full changeset validation with summary"));
|
|
771
|
+
const { LegacyVersionFilesSchema: LegacyVersionFilesSchema } = Changesets;
|
|
772
|
+
const CHANGELOG_ENTRY = "@savvy-web/silk/changesets/changelog";
|
|
773
|
+
const LEGACY_CHANGELOG_ENTRY = "@savvy-web/changesets/changelog";
|
|
774
|
+
const ACCEPTED_CHANGELOG_ENTRIES = [
|
|
775
|
+
CHANGELOG_ENTRY,
|
|
776
|
+
LEGACY_CHANGELOG_ENTRY
|
|
777
|
+
];
|
|
778
|
+
const CUSTOM_RULES_ENTRY = "@savvy-web/silk/changesets/markdownlint";
|
|
779
|
+
const LEGACY_CUSTOM_RULES_ENTRY = "@savvy-web/changesets/markdownlint";
|
|
780
|
+
const ACCEPTED_CUSTOM_RULES_ENTRIES = [
|
|
781
|
+
CUSTOM_RULES_ENTRY,
|
|
782
|
+
LEGACY_CUSTOM_RULES_ENTRY
|
|
783
|
+
];
|
|
784
|
+
const MARKDOWNLINT_CONFIG_PATHS = [
|
|
785
|
+
"lib/configs/.markdownlint-cli2.jsonc",
|
|
786
|
+
"lib/configs/.markdownlint-cli2.json",
|
|
787
|
+
".markdownlint-cli2.jsonc",
|
|
788
|
+
".markdownlint-cli2.json"
|
|
789
|
+
];
|
|
790
|
+
const RULE_NAMES = [
|
|
791
|
+
"changeset-heading-hierarchy",
|
|
792
|
+
"changeset-required-sections",
|
|
793
|
+
"changeset-content-structure",
|
|
794
|
+
"changeset-uncategorized-content",
|
|
795
|
+
"changeset-dependency-table-format"
|
|
796
|
+
];
|
|
797
|
+
const DEFAULT_CONFIG = {
|
|
798
|
+
$schema: "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
799
|
+
changelog: [
|
|
800
|
+
CHANGELOG_ENTRY,
|
|
801
|
+
{
|
|
802
|
+
repo: "owner/repo"
|
|
915
803
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
804
|
+
],
|
|
805
|
+
commit: false,
|
|
806
|
+
access: "restricted",
|
|
807
|
+
baseBranch: "main",
|
|
808
|
+
updateInternalDependencies: "patch",
|
|
809
|
+
ignore: [],
|
|
810
|
+
privatePackages: {
|
|
811
|
+
tag: true,
|
|
812
|
+
version: true
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
const InitErrorBase = Data.TaggedError("InitError");
|
|
816
|
+
class InitError extends InitErrorBase {
|
|
817
|
+
get message() {
|
|
818
|
+
return `Init failed at ${this.step}: ${this.reason}`;
|
|
819
|
+
}
|
|
923
820
|
}
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}).
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
821
|
+
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing config files"));
|
|
822
|
+
const init_quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Silence warnings, always exit 0"));
|
|
823
|
+
const skipMarkdownlintOption = Options.boolean("skip-markdownlint").pipe(Options.withDescription("Skip registering rules in base markdownlint config"));
|
|
824
|
+
const init_checkOption = Options.boolean("check").pipe(Options.withDescription("Check configuration without writing (for postinstall scripts)"));
|
|
825
|
+
function detectGitHubRepo(cwd) {
|
|
826
|
+
try {
|
|
827
|
+
const url = execSync("git remote get-url origin", {
|
|
828
|
+
cwd,
|
|
829
|
+
encoding: "utf-8"
|
|
830
|
+
}).trim();
|
|
831
|
+
const https = url.match(/github\.com\/([^/]+)\/([^/.]+)/);
|
|
832
|
+
if (https) return `${https[1]}/${https[2]}`;
|
|
833
|
+
const ssh = url.match(/github\.com:([^/]+)\/([^/.]+)/);
|
|
834
|
+
if (ssh) return `${ssh[1]}/${ssh[2]}`;
|
|
835
|
+
} catch {}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
const JSONC_FORMAT = {
|
|
839
|
+
tabSize: 1,
|
|
840
|
+
insertSpaces: false
|
|
841
|
+
};
|
|
842
|
+
function resolveWorkspaceRoot(cwd) {
|
|
843
|
+
return WorkspaceRoot.pipe(Effect.flatMap((wr)=>wr.find(cwd)), Effect.catchAll(()=>Effect.succeed(cwd)));
|
|
844
|
+
}
|
|
845
|
+
function findMarkdownlintConfig(root) {
|
|
846
|
+
for (const configPath of MARKDOWNLINT_CONFIG_PATHS)if (existsSync(join(root, configPath))) return configPath;
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
function ensureChangesetDir(root) {
|
|
850
|
+
return Effect["try"]({
|
|
851
|
+
try: ()=>{
|
|
852
|
+
const dir = join(root, ".changeset");
|
|
853
|
+
mkdirSync(dir, {
|
|
854
|
+
recursive: true
|
|
855
|
+
});
|
|
856
|
+
return dir;
|
|
857
|
+
},
|
|
858
|
+
catch: (error)=>new InitError({
|
|
859
|
+
step: ".changeset directory",
|
|
860
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
function handleConfig(changesetDir, repoSlug, force) {
|
|
865
|
+
return Effect["try"]({
|
|
866
|
+
try: ()=>{
|
|
867
|
+
const configPath = join(changesetDir, "config.json");
|
|
868
|
+
if (force || !existsSync(configPath)) {
|
|
869
|
+
const config = {
|
|
870
|
+
...DEFAULT_CONFIG,
|
|
871
|
+
changelog: [
|
|
872
|
+
CHANGELOG_ENTRY,
|
|
873
|
+
{
|
|
874
|
+
repo: repoSlug
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
};
|
|
878
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`);
|
|
879
|
+
return force ? "Overwrote .changeset/config.json" : "Created .changeset/config.json";
|
|
880
|
+
}
|
|
881
|
+
const existing = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
882
|
+
const currentOptions = Array.isArray(existing.changelog) && "object" == typeof existing.changelog[1] && null !== existing.changelog[1] ? existing.changelog[1] : {};
|
|
883
|
+
existing.changelog = [
|
|
884
|
+
CHANGELOG_ENTRY,
|
|
885
|
+
{
|
|
886
|
+
...currentOptions,
|
|
887
|
+
repo: repoSlug
|
|
888
|
+
}
|
|
889
|
+
];
|
|
890
|
+
writeFileSync(configPath, `${JSON.stringify(existing, null, "\t")}\n`);
|
|
891
|
+
return "Patched changelog in .changeset/config.json";
|
|
892
|
+
},
|
|
893
|
+
catch: (error)=>new InitError({
|
|
894
|
+
step: ".changeset/config.json",
|
|
895
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
896
|
+
})
|
|
942
897
|
});
|
|
943
898
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const release_surface_cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
953
|
-
const release_surface_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
954
|
-
function release_surface_renderHuman(pkg) {
|
|
955
|
-
const lines = [];
|
|
956
|
-
lines.push(`Package: ${pkg.name} v${pkg.version}`);
|
|
957
|
-
lines.push(`Workspace: ${pkg.workspaceDir}`);
|
|
958
|
-
if (0 === pkg.additionalScopes.length && 0 === pkg.versionFiles.length) {
|
|
959
|
-
lines.push("");
|
|
960
|
-
lines.push("(no additionalScopes or versionFiles — workspace dir is the entire release surface)");
|
|
961
|
-
return lines.join("\n");
|
|
962
|
-
}
|
|
963
|
-
if (pkg.additionalScopes.length > 0) {
|
|
964
|
-
lines.push("");
|
|
965
|
-
lines.push(`additionalScopes (${pkg.additionalScopes.length} glob${1 === pkg.additionalScopes.length ? "" : "s"}):`);
|
|
966
|
-
for (const g of pkg.additionalScopes)lines.push(` - ${g}`);
|
|
967
|
-
lines.push(`Resolved files (${pkg.additionalScopeFiles.length}):`);
|
|
968
|
-
for (const f of pkg.additionalScopeFiles)lines.push(` ${f}`);
|
|
969
|
-
}
|
|
970
|
-
if (pkg.versionFiles.length > 0) {
|
|
971
|
-
lines.push("");
|
|
972
|
-
lines.push(`versionFiles (${pkg.versionFiles.length}):`);
|
|
973
|
-
for (const vf of pkg.versionFiles){
|
|
974
|
-
lines.push(` ${vf.glob} → ${vf.paths.join(", ")}`);
|
|
975
|
-
for (const f of vf.matchedFiles)lines.push(` ${f}`);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return lines.join("\n");
|
|
899
|
+
function detectLegacyVersionFiles(config) {
|
|
900
|
+
if ("object" != typeof config || null === config) return false;
|
|
901
|
+
const cfg = config;
|
|
902
|
+
const changelog = cfg.changelog;
|
|
903
|
+
if (!Array.isArray(changelog) || changelog.length < 2) return false;
|
|
904
|
+
const options = changelog[1];
|
|
905
|
+
if ("object" != typeof options || null === options) return false;
|
|
906
|
+
return Array.isArray(options.versionFiles) && options.versionFiles.length > 0;
|
|
979
907
|
}
|
|
980
|
-
function
|
|
981
|
-
return
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const scope = config.packages.find((p)=>p.name === pkgName);
|
|
989
|
-
if (!scope) {
|
|
990
|
-
process.exitCode = 1;
|
|
991
|
-
return yield* Effect.fail(new ConfigurationError({
|
|
992
|
-
field: `packages["${pkgName}"]`,
|
|
993
|
-
reason: `Package "${pkgName}" is not declared in .changeset/config.json#packages. Declared packages: ${config.packages.map((p)=>p.name).join(", ") || "(none)"}.`
|
|
994
|
-
}));
|
|
995
|
-
}
|
|
996
|
-
const output = json ? JSON.stringify(scope, null, 2) : release_surface_renderHuman(scope);
|
|
997
|
-
yield* Effect.log(output);
|
|
998
|
-
});
|
|
908
|
+
function legacyVersionFilesWarning(configPath) {
|
|
909
|
+
return [
|
|
910
|
+
`DEPRECATION: ${configPath} uses the legacy top-level \`versionFiles[]\` array.`,
|
|
911
|
+
" Migrate each entry to `changelog[1].packages[<entry.package>].versionFiles`",
|
|
912
|
+
" and remove the top-level field. Run `savvy changeset config show --json`",
|
|
913
|
+
" to see the normalized form, or check the 0.9.0 release notes for examples.",
|
|
914
|
+
" Removed in @savvy-web/changesets 1.0.0."
|
|
915
|
+
].join("\n");
|
|
999
916
|
}
|
|
1000
|
-
|
|
1001
|
-
package: packageArg,
|
|
1002
|
-
cwd: release_surface_cwdOption,
|
|
1003
|
-
json: release_surface_jsonOption
|
|
1004
|
-
}, ({ package: pkgName, cwd, json })=>runReleaseSurface(cwd, pkgName, json)).pipe(Command.withDescription("Print every path owned by a package — workspace dir, additionalScopes, versionFiles"));
|
|
1005
|
-
const { ConfigInspector: config_gate_ConfigInspector } = Changesets;
|
|
1006
|
-
function requireValidConfig(cwd) {
|
|
917
|
+
function warnIfLegacyVersionFiles(changesetDir) {
|
|
1007
918
|
return Effect.gen(function*() {
|
|
1008
|
-
const
|
|
1009
|
-
const configPath = join(projectDir, ".changeset", "config.json");
|
|
919
|
+
const configPath = join(changesetDir, "config.json");
|
|
1010
920
|
if (!existsSync(configPath)) return;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}));
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1018
|
-
const { ChangelogTransformer: ChangelogTransformer } = Changesets;
|
|
1019
|
-
const fileArg = Args.file({
|
|
1020
|
-
name: "file"
|
|
1021
|
-
}).pipe(Args.withDefault("CHANGELOG.md"));
|
|
1022
|
-
const transform_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Print transformed output instead of writing"), Options.withDefault(false));
|
|
1023
|
-
const transform_checkOption = Options.boolean("check").pipe(Options.withAlias("c"), Options.withDescription("Exit 1 if file would change (for CI)"), Options.withDefault(false));
|
|
1024
|
-
function runTransform(file, dryRun, check) {
|
|
1025
|
-
return Effect.gen(function*() {
|
|
1026
|
-
const resolved = resolve(file);
|
|
1027
|
-
yield* requireValidConfig(dirname(resolved));
|
|
1028
|
-
const content = yield* Effect["try"](()=>readFileSync(resolved, "utf-8"));
|
|
1029
|
-
const result = ChangelogTransformer.transformContent(content);
|
|
1030
|
-
if (dryRun) return void (yield* Effect.log(result));
|
|
1031
|
-
if (check) {
|
|
1032
|
-
if (result !== content) {
|
|
1033
|
-
yield* Effect.log(`${resolved} would be modified by transform.`);
|
|
1034
|
-
process.exitCode = 1;
|
|
1035
|
-
} else yield* Effect.log(`${resolved} is already formatted.`);
|
|
921
|
+
let parsed;
|
|
922
|
+
try {
|
|
923
|
+
parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
924
|
+
} catch {
|
|
1036
925
|
return;
|
|
1037
926
|
}
|
|
1038
|
-
yield* Effect
|
|
1039
|
-
yield* Effect.log(`Transformed ${resolved}`);
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
const transformCommand = Command.make("transform", {
|
|
1043
|
-
file: fileArg,
|
|
1044
|
-
dryRun: transform_dryRunOption,
|
|
1045
|
-
check: transform_checkOption
|
|
1046
|
-
}, ({ file, dryRun, check })=>runTransform(file, dryRun, check)).pipe(Command.withDescription("Post-process CHANGELOG.md"));
|
|
1047
|
-
const { ChangesetLinter: validate_file_ChangesetLinter } = Changesets;
|
|
1048
|
-
const validate_file_fileArg = Args.file({
|
|
1049
|
-
name: "file"
|
|
1050
|
-
});
|
|
1051
|
-
function runValidateFile(filePath) {
|
|
1052
|
-
return Effect.gen(function*() {
|
|
1053
|
-
const result = yield* Effect["try"](()=>validate_file_ChangesetLinter.validateFile(filePath)).pipe(Effect.catchAll((error)=>Effect.gen(function*() {
|
|
1054
|
-
yield* Effect.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1055
|
-
process.exitCode = 1;
|
|
1056
|
-
return null;
|
|
1057
|
-
})));
|
|
1058
|
-
if (null === result) return;
|
|
1059
|
-
for (const msg of result)yield* Effect.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
1060
|
-
if (result.length > 0) process.exitCode = 1;
|
|
1061
|
-
else yield* Effect.log("Valid.");
|
|
927
|
+
if (detectLegacyVersionFiles(parsed)) yield* Effect.logWarning(legacyVersionFilesWarning(configPath));
|
|
1062
928
|
});
|
|
1063
929
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
const { ChangelogTransformer: version_ChangelogTransformer, ConfigInspector: version_ConfigInspector, VersionFileError: VersionFileError, VersionFiles: VersionFiles } = Changesets;
|
|
1068
|
-
function getChangesetVersionCommand(pm) {
|
|
1069
|
-
switch(pm){
|
|
1070
|
-
case "pnpm":
|
|
1071
|
-
return "pnpm exec changeset version";
|
|
1072
|
-
case "yarn":
|
|
1073
|
-
return "yarn exec changeset version";
|
|
1074
|
-
case "bun":
|
|
1075
|
-
return "bun x changeset version";
|
|
1076
|
-
default:
|
|
1077
|
-
return "npx changeset version";
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
const version_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Skip changeset version, only transform existing CHANGELOGs"), Options.withDefault(false));
|
|
1081
|
-
function runVersion(dryRun) {
|
|
930
|
+
function handleBaseMarkdownlint(root) {
|
|
931
|
+
const foundPath = findMarkdownlintConfig(root);
|
|
932
|
+
if (!foundPath) return Effect.succeed(`Warning: no markdownlint config found (checked ${MARKDOWNLINT_CONFIG_PATHS.join(", ")})`);
|
|
1082
933
|
return Effect.gen(function*() {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (dryRun) yield* Effect.log("Dry run: skipping changeset version");
|
|
1093
|
-
else {
|
|
1094
|
-
const cmd = getChangesetVersionCommand(pm);
|
|
1095
|
-
yield* Effect.log(`Running: ${cmd}`);
|
|
1096
|
-
yield* Effect["try"]({
|
|
1097
|
-
try: ()=>execSync(cmd, {
|
|
1098
|
-
cwd,
|
|
1099
|
-
stdio: "inherit"
|
|
1100
|
-
}),
|
|
1101
|
-
catch: (error)=>new Error(`changeset version failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
1102
|
-
});
|
|
934
|
+
const fullPath = join(root, foundPath);
|
|
935
|
+
let text;
|
|
936
|
+
try {
|
|
937
|
+
text = readFileSync(fullPath, "utf-8");
|
|
938
|
+
} catch (error) {
|
|
939
|
+
return yield* Effect.fail(new InitError({
|
|
940
|
+
step: "markdownlint config",
|
|
941
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
942
|
+
}));
|
|
1103
943
|
}
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
944
|
+
let parsed = yield* parse(text);
|
|
945
|
+
const currentRules = Array.isArray(parsed.customRules) ? parsed.customRules : null;
|
|
946
|
+
if (null === currentRules) {
|
|
947
|
+
const edits = yield* modify(text, [
|
|
948
|
+
"customRules"
|
|
949
|
+
], [
|
|
950
|
+
CUSTOM_RULES_ENTRY
|
|
951
|
+
], {
|
|
952
|
+
formattingOptions: JSONC_FORMAT
|
|
953
|
+
});
|
|
954
|
+
text = yield* applyEdits(text, edits);
|
|
955
|
+
} else {
|
|
956
|
+
const desired = currentRules.filter((r)=>r !== LEGACY_CUSTOM_RULES_ENTRY && r !== CUSTOM_RULES_ENTRY);
|
|
957
|
+
desired.push(CUSTOM_RULES_ENTRY);
|
|
958
|
+
const changed = desired.length !== currentRules.length || desired.some((r, i)=>r !== currentRules[i]);
|
|
959
|
+
if (changed) {
|
|
960
|
+
const edits = yield* modify(text, [
|
|
961
|
+
"customRules"
|
|
962
|
+
], desired, {
|
|
963
|
+
formattingOptions: JSONC_FORMAT
|
|
1117
964
|
});
|
|
965
|
+
text = yield* applyEdits(text, edits);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
parsed = yield* parse(text);
|
|
969
|
+
const currentConfig = parsed.config;
|
|
970
|
+
if ("object" != typeof currentConfig || null === currentConfig) {
|
|
971
|
+
const edits = yield* modify(text, [
|
|
972
|
+
"config"
|
|
973
|
+
], {}, {
|
|
974
|
+
formattingOptions: JSONC_FORMAT
|
|
975
|
+
});
|
|
976
|
+
text = yield* applyEdits(text, edits);
|
|
977
|
+
}
|
|
978
|
+
parsed = yield* parse(text);
|
|
979
|
+
const config = parsed.config;
|
|
980
|
+
for (const rule of RULE_NAMES)if (!(rule in config)) {
|
|
981
|
+
const edits = yield* modify(text, [
|
|
982
|
+
"config",
|
|
983
|
+
rule
|
|
984
|
+
], false, {
|
|
985
|
+
formattingOptions: JSONC_FORMAT
|
|
986
|
+
});
|
|
987
|
+
text = yield* applyEdits(text, edits);
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
writeFileSync(fullPath, text);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
return yield* Effect.fail(new InitError({
|
|
993
|
+
step: "markdownlint config",
|
|
994
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
995
|
+
}));
|
|
996
|
+
}
|
|
997
|
+
return `Updated ${foundPath}`;
|
|
998
|
+
}).pipe(Effect.catchAll((error)=>{
|
|
999
|
+
if (error instanceof InitError) return Effect.fail(error);
|
|
1000
|
+
return Effect.fail(new InitError({
|
|
1001
|
+
step: "markdownlint config",
|
|
1002
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1003
|
+
}));
|
|
1004
|
+
}));
|
|
1005
|
+
}
|
|
1006
|
+
function handleChangesetMarkdownlint(changesetDir, root, force) {
|
|
1007
|
+
return Effect["try"]({
|
|
1008
|
+
try: ()=>{
|
|
1009
|
+
const mdlintPath = join(changesetDir, ".markdownlint.json");
|
|
1010
|
+
const baseConfig = findMarkdownlintConfig(root);
|
|
1011
|
+
if (force || !existsSync(mdlintPath)) {
|
|
1012
|
+
const mdlintConfig = {};
|
|
1013
|
+
if (baseConfig) mdlintConfig.extends = `../${baseConfig}`;
|
|
1014
|
+
mdlintConfig.default = false;
|
|
1015
|
+
mdlintConfig.MD041 = false;
|
|
1016
|
+
for (const rule of RULE_NAMES)mdlintConfig[rule] = true;
|
|
1017
|
+
writeFileSync(mdlintPath, `${JSON.stringify(mdlintConfig, null, "\t")}\n`);
|
|
1018
|
+
return force ? "Overwrote .changeset/.markdownlint.json" : "Created .changeset/.markdownlint.json";
|
|
1118
1019
|
}
|
|
1020
|
+
const existing = JSON.parse(readFileSync(mdlintPath, "utf-8"));
|
|
1021
|
+
for (const rule of RULE_NAMES)existing[rule] = true;
|
|
1022
|
+
writeFileSync(mdlintPath, `${JSON.stringify(existing, null, "\t")}\n`);
|
|
1023
|
+
return "Patched rules in .changeset/.markdownlint.json";
|
|
1024
|
+
},
|
|
1025
|
+
catch: (error)=>new InitError({
|
|
1026
|
+
step: ".changeset/.markdownlint.json",
|
|
1027
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1028
|
+
})
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function checkChangesetDir(root) {
|
|
1032
|
+
const dir = join(root, ".changeset");
|
|
1033
|
+
if (!existsSync(dir)) return [
|
|
1034
|
+
{
|
|
1035
|
+
file: ".changeset/",
|
|
1036
|
+
message: "directory does not exist"
|
|
1119
1037
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
name: rootName,
|
|
1130
|
-
path: resolvedCwd,
|
|
1131
|
-
changelogPath: rootChangelog
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1038
|
+
];
|
|
1039
|
+
return [];
|
|
1040
|
+
}
|
|
1041
|
+
function checkConfig(changesetDir, repoSlug) {
|
|
1042
|
+
const configPath = join(changesetDir, "config.json");
|
|
1043
|
+
if (!existsSync(configPath)) return [
|
|
1044
|
+
{
|
|
1045
|
+
file: ".changeset/config.json",
|
|
1046
|
+
message: "file does not exist"
|
|
1134
1047
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1048
|
+
];
|
|
1049
|
+
try {
|
|
1050
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1051
|
+
const issues = [];
|
|
1052
|
+
const changelog = config.changelog;
|
|
1053
|
+
const entry = Array.isArray(changelog) ? changelog[0] : changelog;
|
|
1054
|
+
const repo = Array.isArray(changelog) ? changelog[1]?.repo : void 0;
|
|
1055
|
+
if (ACCEPTED_CHANGELOG_ENTRIES.includes(entry)) {
|
|
1056
|
+
if (repo !== repoSlug) issues.push({
|
|
1057
|
+
file: ".changeset/config.json",
|
|
1058
|
+
message: `changelog repo is "${repo ?? "(not set)"}", expected "${repoSlug}"`
|
|
1059
|
+
});
|
|
1060
|
+
} else issues.push({
|
|
1061
|
+
file: ".changeset/config.json",
|
|
1062
|
+
message: `changelog formatter is "${entry}", expected "${CHANGELOG_ENTRY}"`
|
|
1063
|
+
});
|
|
1064
|
+
const options = Array.isArray(changelog) ? changelog[1] : void 0;
|
|
1065
|
+
if (options && "object" == typeof options && "versionFiles" in options) {
|
|
1066
|
+
const result = Schema.decodeUnknownEither(LegacyVersionFilesSchema)(options.versionFiles);
|
|
1067
|
+
if ("Left" === result._tag) issues.push({
|
|
1068
|
+
file: ".changeset/config.json",
|
|
1069
|
+
message: "versionFiles config is invalid"
|
|
1070
|
+
});
|
|
1145
1071
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
const inspected = yield* inspector.inspect(resolvedCwd);
|
|
1150
|
-
const scopesWithVersionFiles = inspected.packages.filter((p)=>p.versionFiles.length > 0).map((p)=>{
|
|
1151
|
-
const fresh = readPackageVersionFromDisk(p.workspaceDir);
|
|
1152
|
-
return fresh && fresh !== p.version ? {
|
|
1153
|
-
...p,
|
|
1154
|
-
version: fresh
|
|
1155
|
-
} : p;
|
|
1072
|
+
if (detectLegacyVersionFiles(config)) issues.push({
|
|
1073
|
+
file: ".changeset/config.json",
|
|
1074
|
+
message: "uses the legacy top-level `versionFiles[]` array (deprecated; removed in 1.0.0). Migrate to `packages[<name>].versionFiles`."
|
|
1156
1075
|
});
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return new VersionFileError({
|
|
1164
|
-
filePath: message.match(/Failed to update (.+?):/)?.[1] ?? cwd,
|
|
1165
|
-
reason: message
|
|
1166
|
-
});
|
|
1076
|
+
return issues;
|
|
1077
|
+
} catch {
|
|
1078
|
+
return [
|
|
1079
|
+
{
|
|
1080
|
+
file: ".changeset/config.json",
|
|
1081
|
+
message: "could not parse file"
|
|
1167
1082
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1083
|
+
];
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function checkBaseMarkdownlint(root) {
|
|
1087
|
+
const foundPath = findMarkdownlintConfig(root);
|
|
1088
|
+
if (!foundPath) return [
|
|
1089
|
+
{
|
|
1090
|
+
file: "markdownlint config",
|
|
1091
|
+
message: `not found (checked ${MARKDOWNLINT_CONFIG_PATHS.join(", ")})`
|
|
1172
1092
|
}
|
|
1173
|
-
|
|
1093
|
+
];
|
|
1094
|
+
try {
|
|
1095
|
+
const raw = readFileSync(join(root, foundPath), "utf-8");
|
|
1096
|
+
const parsed = Effect.runSync(parse(raw));
|
|
1097
|
+
const issues = [];
|
|
1098
|
+
if (!Array.isArray(parsed.customRules) || !parsed.customRules.some((r)=>ACCEPTED_CUSTOM_RULES_ENTRIES.includes(r))) issues.push({
|
|
1099
|
+
file: foundPath,
|
|
1100
|
+
message: `customRules does not include ${CUSTOM_RULES_ENTRY}`
|
|
1101
|
+
});
|
|
1102
|
+
const config = parsed.config;
|
|
1103
|
+
if ("object" != typeof config || null === config) issues.push({
|
|
1104
|
+
file: foundPath,
|
|
1105
|
+
message: "config section is missing"
|
|
1106
|
+
});
|
|
1107
|
+
else for (const rule of RULE_NAMES)if (!(rule in config)) issues.push({
|
|
1108
|
+
file: foundPath,
|
|
1109
|
+
message: `rule "${rule}" is not configured`
|
|
1110
|
+
});
|
|
1111
|
+
return issues;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return [
|
|
1114
|
+
{
|
|
1115
|
+
file: foundPath,
|
|
1116
|
+
message: "could not parse file"
|
|
1117
|
+
}
|
|
1118
|
+
];
|
|
1119
|
+
}
|
|
1174
1120
|
}
|
|
1175
|
-
function
|
|
1121
|
+
function checkChangesetMarkdownlint(changesetDir) {
|
|
1122
|
+
const mdlintPath = join(changesetDir, ".markdownlint.json");
|
|
1123
|
+
if (!existsSync(mdlintPath)) return [
|
|
1124
|
+
{
|
|
1125
|
+
file: ".changeset/.markdownlint.json",
|
|
1126
|
+
message: "file does not exist"
|
|
1127
|
+
}
|
|
1128
|
+
];
|
|
1176
1129
|
try {
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1130
|
+
const existing = JSON.parse(readFileSync(mdlintPath, "utf-8"));
|
|
1131
|
+
const issues = [];
|
|
1132
|
+
for (const rule of RULE_NAMES)if (true !== existing[rule]) issues.push({
|
|
1133
|
+
file: ".changeset/.markdownlint.json",
|
|
1134
|
+
message: `rule "${rule}" is not enabled`
|
|
1135
|
+
});
|
|
1136
|
+
return issues;
|
|
1179
1137
|
} catch {
|
|
1180
|
-
return
|
|
1138
|
+
return [
|
|
1139
|
+
{
|
|
1140
|
+
file: ".changeset/.markdownlint.json",
|
|
1141
|
+
message: "could not parse file"
|
|
1142
|
+
}
|
|
1143
|
+
];
|
|
1181
1144
|
}
|
|
1182
1145
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1146
|
+
function runChangesetInit(opts) {
|
|
1147
|
+
const { force, quiet, skipMarkdownlint, check } = opts;
|
|
1148
|
+
return Effect.gen(function*() {
|
|
1149
|
+
const root = yield* resolveWorkspaceRoot(process.cwd());
|
|
1150
|
+
const repo = detectGitHubRepo(root);
|
|
1151
|
+
if (!repo && !quiet) yield* Effect.log("Warning: could not detect GitHub repo from git remote, using placeholder");
|
|
1152
|
+
const repoSlug = repo ?? "owner/repo";
|
|
1153
|
+
if (check) {
|
|
1154
|
+
const changesetDir = join(root, ".changeset");
|
|
1155
|
+
const issues = [
|
|
1156
|
+
...checkChangesetDir(root),
|
|
1157
|
+
...checkConfig(changesetDir, repoSlug),
|
|
1158
|
+
...skipMarkdownlint ? [] : checkBaseMarkdownlint(root),
|
|
1159
|
+
...checkChangesetMarkdownlint(changesetDir)
|
|
1160
|
+
];
|
|
1161
|
+
if (0 === issues.length) return void (yield* Effect.log("All @savvy-web/changesets config files are up to date."));
|
|
1162
|
+
for (const issue of issues)yield* Effect.logWarning(`${issue.file}: ${issue.message}`);
|
|
1163
|
+
yield* Effect.logWarning('Run "savvy init --force" to fix.');
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const changesetDir = yield* ensureChangesetDir(root);
|
|
1167
|
+
yield* Effect.log("Ensured .changeset/ directory");
|
|
1168
|
+
const errors = [];
|
|
1169
|
+
const configResult = yield* handleConfig(changesetDir, repoSlug, force).pipe(Effect.either);
|
|
1170
|
+
if ("Right" === configResult._tag) {
|
|
1171
|
+
yield* Effect.log(configResult.right);
|
|
1172
|
+
if (!quiet) yield* warnIfLegacyVersionFiles(changesetDir);
|
|
1173
|
+
} else errors.push(configResult.left);
|
|
1174
|
+
if (!skipMarkdownlint) {
|
|
1175
|
+
const baseResult = yield* handleBaseMarkdownlint(root).pipe(Effect.either);
|
|
1176
|
+
if ("Right" === baseResult._tag) yield* Effect.log(baseResult.right);
|
|
1177
|
+
else errors.push(baseResult.left);
|
|
1178
|
+
}
|
|
1179
|
+
const mdlintResult = yield* handleChangesetMarkdownlint(changesetDir, root, force).pipe(Effect.either);
|
|
1180
|
+
if ("Right" === mdlintResult._tag) yield* Effect.log(mdlintResult.right);
|
|
1181
|
+
else errors.push(mdlintResult.left);
|
|
1182
|
+
if (errors.length > 0) {
|
|
1183
|
+
for (const err of errors)yield* Effect.logError(err.message);
|
|
1184
|
+
if (!quiet) process.exitCode = 1;
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
yield* Effect.log("Init complete.");
|
|
1188
|
+
}).pipe(Effect.catchAll((error)=>Effect.gen(function*() {
|
|
1189
|
+
if (!quiet) {
|
|
1190
|
+
yield* Effect.logError(error instanceof InitError ? error.message : `Init failed: ${String(error)}`);
|
|
1191
|
+
process.exitCode = 1;
|
|
1192
|
+
}
|
|
1193
|
+
})));
|
|
1194
|
+
}
|
|
1195
|
+
Command.make("init", {
|
|
1196
|
+
force: forceOption,
|
|
1197
|
+
quiet: init_quietOption,
|
|
1198
|
+
skipMarkdownlint: skipMarkdownlintOption,
|
|
1199
|
+
check: init_checkOption
|
|
1200
|
+
}, (opts)=>runChangesetInit(opts)).pipe(Command.withDescription("Bootstrap a repo for @savvy-web/changesets"));
|
|
1186
1201
|
const configGroup = Command.make("config").pipe(Command.withSubcommands([
|
|
1187
1202
|
configShowCommand,
|
|
1188
1203
|
configValidateCommand
|
|
@@ -1192,8 +1207,6 @@ const depsGroup = Command.make("deps").pipe(Command.withSubcommands([
|
|
|
1192
1207
|
depsRegenCommand
|
|
1193
1208
|
]), Command.withDescription("Generate or regenerate dependency changesets"));
|
|
1194
1209
|
const _changesetCommand = Command.make("changeset").pipe(Command.withSubcommands([
|
|
1195
|
-
initCommand,
|
|
1196
|
-
checkCommand,
|
|
1197
1210
|
lintCommand,
|
|
1198
1211
|
transformCommand,
|
|
1199
1212
|
validateFileCommand,
|
|
@@ -1280,7 +1293,7 @@ function runCommitInit(opts) {
|
|
|
1280
1293
|
yield* Effect.log("\nDone! Install @commitlint/cli if not already installed.");
|
|
1281
1294
|
});
|
|
1282
1295
|
}
|
|
1283
|
-
|
|
1296
|
+
Command.make("init", {
|
|
1284
1297
|
force: init_forceOption,
|
|
1285
1298
|
config: configOption
|
|
1286
1299
|
}, (opts)=>runCommitInit(opts)).pipe(Command.withDescription("Initialize commitlint configuration and husky hooks"));
|
|
@@ -1353,10 +1366,10 @@ function runCommitCheck() {
|
|
|
1353
1366
|
if (CheckResult.$is("Found")(baseStatus) && baseStatus.isUpToDate) yield* Effect.log("✓ Base section: up-to-date");
|
|
1354
1367
|
else if (CheckResult.$is("Found")(baseStatus)) {
|
|
1355
1368
|
sectionsHealthy = false;
|
|
1356
|
-
yield* Effect.log("⚠ Base section: outdated (run 'savvy
|
|
1369
|
+
yield* Effect.log("⚠ Base section: outdated (run 'savvy init' to update)");
|
|
1357
1370
|
} else {
|
|
1358
1371
|
sectionsHealthy = false;
|
|
1359
|
-
yield* Effect.log(`${BULLET} Base section: not found (run 'savvy
|
|
1372
|
+
yield* Effect.log(`${BULLET} Base section: not found (run 'savvy init' to add)`);
|
|
1360
1373
|
}
|
|
1361
1374
|
const block = yield* ms.read(HUSKY_HOOK_PATH, SECTION_DEF);
|
|
1362
1375
|
if (block) {
|
|
@@ -1366,15 +1379,15 @@ function runCommitCheck() {
|
|
|
1366
1379
|
if (CheckResult.$is("Found")(status) && status.isUpToDate) yield* Effect.log("✓ Commit section: up-to-date");
|
|
1367
1380
|
else {
|
|
1368
1381
|
sectionsHealthy = false;
|
|
1369
|
-
yield* Effect.log("⚠ Commit section: outdated (run 'savvy
|
|
1382
|
+
yield* Effect.log("⚠ Commit section: outdated (run 'savvy init' to update)");
|
|
1370
1383
|
}
|
|
1371
1384
|
} else {
|
|
1372
1385
|
sectionsHealthy = false;
|
|
1373
|
-
yield* Effect.log("⚠ Commit section: outdated (run 'savvy
|
|
1386
|
+
yield* Effect.log("⚠ Commit section: outdated (run 'savvy init' to update)");
|
|
1374
1387
|
}
|
|
1375
1388
|
} else {
|
|
1376
1389
|
sectionsHealthy = false;
|
|
1377
|
-
yield* Effect.log(`${BULLET} Commit section: not found (run 'savvy
|
|
1390
|
+
yield* Effect.log(`${BULLET} Commit section: not found (run 'savvy init' to add)`);
|
|
1378
1391
|
}
|
|
1379
1392
|
}
|
|
1380
1393
|
for (const hookPath of [
|
|
@@ -1384,17 +1397,17 @@ function runCommitCheck() {
|
|
|
1384
1397
|
const hygieneExists = yield* fs.exists(hookPath);
|
|
1385
1398
|
if (!hygieneExists) {
|
|
1386
1399
|
sectionsHealthy = false;
|
|
1387
|
-
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} not found (run 'savvy
|
|
1400
|
+
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} not found (run 'savvy init' to add)`);
|
|
1388
1401
|
continue;
|
|
1389
1402
|
}
|
|
1390
1403
|
const hygieneStatus = yield* ms.check(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
1391
1404
|
if (CheckResult.$is("Found")(hygieneStatus) && hygieneStatus.isUpToDate) yield* Effect.log(`✓ Hygiene hook: ${hookPath}`);
|
|
1392
1405
|
else if (CheckResult.$is("Found")(hygieneStatus)) {
|
|
1393
1406
|
sectionsHealthy = false;
|
|
1394
|
-
yield* Effect.log(`⚠ Hygiene hook: ${hookPath} outdated (run 'savvy
|
|
1407
|
+
yield* Effect.log(`⚠ Hygiene hook: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
1395
1408
|
} else {
|
|
1396
1409
|
sectionsHealthy = false;
|
|
1397
|
-
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} section not found (run 'savvy
|
|
1410
|
+
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
1398
1411
|
}
|
|
1399
1412
|
}
|
|
1400
1413
|
const hasDCOFile = yield* fs.exists(DCO_FILE_PATH);
|
|
@@ -1409,11 +1422,11 @@ function runCommitCheck() {
|
|
|
1409
1422
|
yield* Effect.log(` Detected scopes: ${scopeDisplay}`);
|
|
1410
1423
|
yield* Effect.log("");
|
|
1411
1424
|
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy;
|
|
1412
|
-
if (hasIssues) yield* Effect.log(`${CROSS_MARK} Commitlint needs configuration. Run: savvy
|
|
1425
|
+
if (hasIssues) yield* Effect.log(`${CROSS_MARK} Commitlint needs configuration. Run: savvy init`);
|
|
1413
1426
|
else yield* Effect.log("✓ Commitlint is configured correctly.");
|
|
1414
1427
|
});
|
|
1415
1428
|
}
|
|
1416
|
-
|
|
1429
|
+
Command.make("check", {}, ()=>runCommitCheck()).pipe(Command.withDescription("Check current commitlint configuration and detected settings"));
|
|
1417
1430
|
const check_CHECK_MARK = "✓";
|
|
1418
1431
|
const check_CROSS_MARK = "✗";
|
|
1419
1432
|
const check_WARNING = "⚠";
|
|
@@ -1494,7 +1507,7 @@ function checkBiomeSchemas() {
|
|
|
1494
1507
|
path: configPath,
|
|
1495
1508
|
matches: false
|
|
1496
1509
|
});
|
|
1497
|
-
warnings.push(`${check_WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy
|
|
1510
|
+
warnings.push(`${check_WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy init' to update it.`);
|
|
1498
1511
|
}
|
|
1499
1512
|
}
|
|
1500
1513
|
return {
|
|
@@ -1542,12 +1555,12 @@ function runLintCheck(opts) {
|
|
|
1542
1555
|
sectionsHealthy = false;
|
|
1543
1556
|
}
|
|
1544
1557
|
} else sectionsHealthy = false;
|
|
1545
|
-
if ("up-to-date" !== baseStatusLabel || "up-to-date" !== lintStatusLabel) warnings.push(`${check_WARNING} Your ${Lint.HUSKY_HOOK_PATH} managed sections are out of date.\n Run 'savvy
|
|
1558
|
+
if ("up-to-date" !== baseStatusLabel || "up-to-date" !== lintStatusLabel) warnings.push(`${check_WARNING} Your ${Lint.HUSKY_HOOK_PATH} managed sections are out of date.\n Run 'savvy init' to update (preserves your custom hooks).`);
|
|
1546
1559
|
} else {
|
|
1547
1560
|
sectionsHealthy = false;
|
|
1548
|
-
warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy
|
|
1561
|
+
warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy init' to create it.`);
|
|
1549
1562
|
}
|
|
1550
|
-
if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy
|
|
1563
|
+
if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy init' to create one.`);
|
|
1551
1564
|
const shellHookPaths = [
|
|
1552
1565
|
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
1553
1566
|
Lint.POST_MERGE_HOOK_PATH
|
|
@@ -1574,11 +1587,11 @@ function runLintCheck(opts) {
|
|
|
1574
1587
|
if (found) {
|
|
1575
1588
|
if (!isUpToDate) {
|
|
1576
1589
|
sectionsHealthy = false;
|
|
1577
|
-
warnings.push(`${check_WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy
|
|
1590
|
+
warnings.push(`${check_WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy init' to update.`);
|
|
1578
1591
|
}
|
|
1579
1592
|
} else {
|
|
1580
1593
|
sectionsHealthy = false;
|
|
1581
|
-
warnings.push(`${check_WARNING} ${hookPath} has no savvy-hooks section.\n Run 'savvy
|
|
1594
|
+
warnings.push(`${check_WARNING} ${hookPath} has no savvy-hooks section.\n Run 'savvy init' to add it.`);
|
|
1582
1595
|
}
|
|
1583
1596
|
}
|
|
1584
1597
|
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.catchAll(()=>Effect.succeed({
|
|
@@ -1598,8 +1611,8 @@ function runLintCheck(opts) {
|
|
|
1598
1611
|
if (hasMarkdownlintConfig) {
|
|
1599
1612
|
const mdContent = yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
1600
1613
|
markdownlintStatus = yield* checkMarkdownlintConfig(mdContent);
|
|
1601
|
-
if (!markdownlintStatus.schemaMatches) warnings.push(`${check_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: $schema differs from template.\n Run 'savvy
|
|
1602
|
-
if (!markdownlintStatus.configMatches) warnings.push(`${check_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template.\n Run 'savvy
|
|
1614
|
+
if (!markdownlintStatus.schemaMatches) warnings.push(`${check_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: $schema differs from template.\n Run 'savvy init' to update it.`);
|
|
1615
|
+
if (!markdownlintStatus.configMatches) warnings.push(`${check_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template.\n Run 'savvy init --force' to overwrite.`);
|
|
1603
1616
|
}
|
|
1604
1617
|
if (quiet) {
|
|
1605
1618
|
if (warnings.length > 0) for (const warning of warnings)yield* Effect.log(warning);
|
|
@@ -1612,15 +1625,15 @@ function runLintCheck(opts) {
|
|
|
1612
1625
|
else yield* Effect.log(`${check_CROSS_MARK} No husky pre-commit hook found`);
|
|
1613
1626
|
if (hasHuskyHook) {
|
|
1614
1627
|
if ("up-to-date" === baseStatusLabel) yield* Effect.log(`${check_CHECK_MARK} Base section: up-to-date`);
|
|
1615
|
-
else if ("outdated" === baseStatusLabel) yield* Effect.log(`${check_WARNING} Base section: outdated (run 'savvy
|
|
1616
|
-
else yield* Effect.log(`${check_BULLET} Base section: not found (run 'savvy
|
|
1628
|
+
else if ("outdated" === baseStatusLabel) yield* Effect.log(`${check_WARNING} Base section: outdated (run 'savvy init' to update)`);
|
|
1629
|
+
else yield* Effect.log(`${check_BULLET} Base section: not found (run 'savvy init' to add)`);
|
|
1617
1630
|
const lintLabel = detectedConfigPath ? ` (config: ${detectedConfigPath})` : "";
|
|
1618
1631
|
if ("up-to-date" === lintStatusLabel) yield* Effect.log(`${check_CHECK_MARK} Lint section: up-to-date${lintLabel}`);
|
|
1619
|
-
else if ("outdated" === lintStatusLabel) yield* Effect.log(`${check_WARNING} Lint section: outdated (run 'savvy
|
|
1620
|
-
else yield* Effect.log(`${check_BULLET} Lint section: not found (run 'savvy
|
|
1632
|
+
else if ("outdated" === lintStatusLabel) yield* Effect.log(`${check_WARNING} Lint section: outdated (run 'savvy init' to update)`);
|
|
1633
|
+
else yield* Effect.log(`${check_BULLET} Lint section: not found (run 'savvy init' to add)`);
|
|
1621
1634
|
}
|
|
1622
1635
|
for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${check_CHECK_MARK} ${status.path}: up-to-date`);
|
|
1623
|
-
else yield* Effect.log(`${check_WARNING} ${status.path}: outdated (run 'savvy
|
|
1636
|
+
else yield* Effect.log(`${check_WARNING} ${status.path}: outdated (run 'savvy init' to update)`);
|
|
1624
1637
|
else yield* Effect.log(`${check_BULLET} ${status.path}: savvy-hooks section not found`);
|
|
1625
1638
|
yield* Effect.log("\nTool availability:");
|
|
1626
1639
|
const biomeAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
@@ -1668,16 +1681,16 @@ function runLintCheck(opts) {
|
|
|
1668
1681
|
}
|
|
1669
1682
|
else yield* Effect.log(` ${check_BULLET} ${Lint.MARKDOWNLINT_CONFIG_PATH}: not found`);
|
|
1670
1683
|
for (const status of biomeSchemaStatus.statuses)if (status.matches) yield* Effect.log(` ${check_CHECK_MARK} ${status.path}: biome $schema up-to-date`);
|
|
1671
|
-
else yield* Effect.log(` ${check_WARNING} ${status.path}: biome $schema outdated (run 'savvy
|
|
1684
|
+
else yield* Effect.log(` ${check_WARNING} ${status.path}: biome $schema outdated (run 'savvy init' to update)`);
|
|
1672
1685
|
yield* Effect.log("");
|
|
1673
1686
|
const hasMarkdownlintIssues = hasMarkdownlintConfig && !markdownlintStatus.isUpToDate;
|
|
1674
1687
|
const hasBiomeSchemaIssues = biomeSchemaStatus.statuses.some((s)=>!s.matches);
|
|
1675
1688
|
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy || hasMarkdownlintIssues || hasBiomeSchemaIssues;
|
|
1676
|
-
if (hasIssues) yield* Effect.log(`${check_WARNING} Some issues found. Run 'savvy
|
|
1689
|
+
if (hasIssues) yield* Effect.log(`${check_WARNING} Some issues found. Run 'savvy init' to fix.`);
|
|
1677
1690
|
else yield* Effect.log(`${check_CHECK_MARK} Lint-staged is configured correctly.`);
|
|
1678
1691
|
});
|
|
1679
1692
|
}
|
|
1680
|
-
|
|
1693
|
+
Command.make("check", {
|
|
1681
1694
|
quiet: check_quietOption
|
|
1682
1695
|
}, (opts)=>runLintCheck(opts)).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
|
|
1683
1696
|
const DEFAULT_CHANGESET_DIR = ".changeset";
|
|
@@ -1704,6 +1717,158 @@ const _checkCommand = Command.make("check", {
|
|
|
1704
1717
|
})
|
|
1705
1718
|
})).pipe(Command.withDescription("Validate all Silk Suite tool configurations in one pass"));
|
|
1706
1719
|
const commands_check_checkCommand = _checkCommand;
|
|
1720
|
+
const DEFAULT_GLOBS = [
|
|
1721
|
+
"dist",
|
|
1722
|
+
".turbo",
|
|
1723
|
+
"coverage",
|
|
1724
|
+
"node_modules",
|
|
1725
|
+
".rslib"
|
|
1726
|
+
];
|
|
1727
|
+
const NO_DESCEND = new Set([
|
|
1728
|
+
"node_modules",
|
|
1729
|
+
".git"
|
|
1730
|
+
]);
|
|
1731
|
+
class CleanError extends Data.TaggedError("CleanError") {
|
|
1732
|
+
}
|
|
1733
|
+
function collectTargets(pkgPath, patterns) {
|
|
1734
|
+
return Effect.tryPromise({
|
|
1735
|
+
try: async ()=>{
|
|
1736
|
+
const rootReal = await realpath(pkgPath);
|
|
1737
|
+
const seen = new Map();
|
|
1738
|
+
for await (const entry of glob(patterns, {
|
|
1739
|
+
cwd: pkgPath,
|
|
1740
|
+
withFileTypes: true,
|
|
1741
|
+
exclude: (dirent)=>NO_DESCEND.has(dirent.name) && dirent.isDirectory()
|
|
1742
|
+
})){
|
|
1743
|
+
const abs = join(entry.parentPath, entry.name);
|
|
1744
|
+
let real;
|
|
1745
|
+
try {
|
|
1746
|
+
real = await realpath(abs);
|
|
1747
|
+
} catch {
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
if (real !== rootReal && real.startsWith(rootReal + sep)) {
|
|
1751
|
+
if (real !== join(rootReal, "package.json")) seen.set(abs, {
|
|
1752
|
+
path: abs,
|
|
1753
|
+
kind: entry.isDirectory() ? "dir" : "file"
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return [
|
|
1758
|
+
...seen.values()
|
|
1759
|
+
];
|
|
1760
|
+
},
|
|
1761
|
+
catch: (e)=>new CleanError({
|
|
1762
|
+
step: `glob ${pkgPath}`,
|
|
1763
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
1764
|
+
})
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
const REMOVE_CONCURRENCY = 8;
|
|
1768
|
+
function removeTargets(targets, dryRun) {
|
|
1769
|
+
return Effect.gen(function*() {
|
|
1770
|
+
const results = yield* Effect.forEach(targets, (target)=>dryRun ? Effect.succeed({
|
|
1771
|
+
target,
|
|
1772
|
+
reason: null
|
|
1773
|
+
}) : Effect.tryPromise(()=>rm(target.path, {
|
|
1774
|
+
recursive: true,
|
|
1775
|
+
force: true
|
|
1776
|
+
})).pipe(Effect.match({
|
|
1777
|
+
onSuccess: ()=>({
|
|
1778
|
+
target,
|
|
1779
|
+
reason: null
|
|
1780
|
+
}),
|
|
1781
|
+
onFailure: (e)=>({
|
|
1782
|
+
target,
|
|
1783
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
1784
|
+
})
|
|
1785
|
+
})), {
|
|
1786
|
+
concurrency: REMOVE_CONCURRENCY
|
|
1787
|
+
});
|
|
1788
|
+
return {
|
|
1789
|
+
removed: results.filter((r)=>null === r.reason).map((r)=>r.target),
|
|
1790
|
+
failed: results.filter((r)=>null !== r.reason).map((r)=>({
|
|
1791
|
+
target: r.target,
|
|
1792
|
+
reason: r.reason
|
|
1793
|
+
}))
|
|
1794
|
+
};
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
const clean_CHECK_MARK = "✓";
|
|
1798
|
+
const clean_BULLET = "•";
|
|
1799
|
+
const WARN_MARK = "⚠";
|
|
1800
|
+
function parseGlobs(raw) {
|
|
1801
|
+
const parts = raw.split(",").map((s)=>s.trim()).filter((s)=>s.length > 0);
|
|
1802
|
+
return parts.length > 0 ? parts : DEFAULT_GLOBS;
|
|
1803
|
+
}
|
|
1804
|
+
function runClean(opts) {
|
|
1805
|
+
const patterns = parseGlobs(opts.globs);
|
|
1806
|
+
return Effect.gen(function*() {
|
|
1807
|
+
const discovery = yield* WorkspaceDiscovery;
|
|
1808
|
+
const packages = yield* discovery.listPackages(process.cwd()).pipe(Effect.mapError((e)=>new CleanError({
|
|
1809
|
+
step: "discover workspaces",
|
|
1810
|
+
reason: e.message
|
|
1811
|
+
})));
|
|
1812
|
+
const leaves = packages.filter((p)=>!p.isRootWorkspace);
|
|
1813
|
+
const roots = packages.filter((p)=>p.isRootWorkspace);
|
|
1814
|
+
const ordered = [
|
|
1815
|
+
...leaves,
|
|
1816
|
+
...roots
|
|
1817
|
+
];
|
|
1818
|
+
const planned = yield* Effect.forEach(ordered, (pkg)=>collectTargets(pkg.path, patterns).pipe(Effect.map((targets)=>({
|
|
1819
|
+
pkg,
|
|
1820
|
+
targets
|
|
1821
|
+
}))));
|
|
1822
|
+
const seen = new Set();
|
|
1823
|
+
const groups = planned.map(({ pkg, targets })=>{
|
|
1824
|
+
const unique = targets.filter((t)=>{
|
|
1825
|
+
if (seen.has(t.path)) return false;
|
|
1826
|
+
seen.add(t.path);
|
|
1827
|
+
return true;
|
|
1828
|
+
});
|
|
1829
|
+
return {
|
|
1830
|
+
pkg,
|
|
1831
|
+
targets: unique
|
|
1832
|
+
};
|
|
1833
|
+
});
|
|
1834
|
+
const leafGroups = groups.filter((g)=>!g.pkg.isRootWorkspace);
|
|
1835
|
+
const rootGroups = groups.filter((g)=>g.pkg.isRootWorkspace);
|
|
1836
|
+
const verb = opts.dryRun ? "would remove" : "removed";
|
|
1837
|
+
let total = 0;
|
|
1838
|
+
const failures = [];
|
|
1839
|
+
for (const phase of [
|
|
1840
|
+
leafGroups,
|
|
1841
|
+
rootGroups
|
|
1842
|
+
]){
|
|
1843
|
+
const reports = yield* Effect.forEach(phase, (g)=>removeTargets(g.targets, opts.dryRun).pipe(Effect.map((report)=>({
|
|
1844
|
+
g,
|
|
1845
|
+
report
|
|
1846
|
+
}))));
|
|
1847
|
+
for (const { g, report } of reports)if (0 !== g.targets.length) {
|
|
1848
|
+
yield* Effect.log(`\n${"." === g.pkg.relativePath ? "<root>" : g.pkg.relativePath}`);
|
|
1849
|
+
for (const t of report.removed)yield* Effect.log(` ${clean_BULLET} ${verb} [${t.kind}] ${t.path}`);
|
|
1850
|
+
for (const f of report.failed)yield* Effect.log(` ${WARN_MARK} failed [${f.target.kind}] ${f.target.path}: ${f.reason}`);
|
|
1851
|
+
total += report.removed.length;
|
|
1852
|
+
failures.push(...report.failed);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
yield* Effect.log(`\n${clean_CHECK_MARK} ${opts.dryRun ? "Would remove" : "Removed"} ${total} item(s).`);
|
|
1856
|
+
if (failures.length > 0) {
|
|
1857
|
+
for (const f of failures)yield* Effect.logError(`Failed to remove ${f.target.path}: ${f.reason}`);
|
|
1858
|
+
return yield* Effect.fail(new CleanError({
|
|
1859
|
+
step: "remove",
|
|
1860
|
+
reason: `${failures.length} target(s) could not be removed`
|
|
1861
|
+
}));
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
const globsOption = Options.text("globs").pipe(Options.withAlias("g"), Options.withDescription(`Comma-separated glob patterns to remove from each workspace root (default: ${DEFAULT_GLOBS.join(",")})`), Options.withDefault(DEFAULT_GLOBS.join(",")));
|
|
1866
|
+
const clean_dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Report what would be removed without deleting anything"), Options.withDefault(false));
|
|
1867
|
+
const _cleanCommand = Command.make("clean", {
|
|
1868
|
+
globs: globsOption,
|
|
1869
|
+
dryRun: clean_dryRunOption
|
|
1870
|
+
}, (opts)=>runClean(opts)).pipe(Command.withDescription("Remove build/cache artifacts across the workspace (leaves first, root last)"));
|
|
1871
|
+
const cleanCommand = _cleanCommand;
|
|
1707
1872
|
const execFileP = promisify(execFile);
|
|
1708
1873
|
function buildPostCommitAdvice(i) {
|
|
1709
1874
|
const lines = [];
|
|
@@ -1971,8 +2136,6 @@ const hookCommand = Command.make("hook").pipe(Command.withSubcommands([
|
|
|
1971
2136
|
userPromptSubmitCommand
|
|
1972
2137
|
])).pipe(Command.withDescription("Internal hook handlers used by the @savvy-web/commitlint plugin"));
|
|
1973
2138
|
const _commitCommand = Command.make("commit").pipe(Command.withSubcommands([
|
|
1974
|
-
init_initCommand,
|
|
1975
|
-
check_checkCommand,
|
|
1976
2139
|
hookCommand
|
|
1977
2140
|
]), Command.withDescription("Commit standards: config, checks, and Claude hook handlers"));
|
|
1978
2141
|
const commitCommand = _commitCommand;
|
|
@@ -2121,7 +2284,7 @@ function runLintInit(opts) {
|
|
|
2121
2284
|
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
2122
2285
|
});
|
|
2123
2286
|
}
|
|
2124
|
-
|
|
2287
|
+
Command.make("init", {
|
|
2125
2288
|
force: lint_init_forceOption,
|
|
2126
2289
|
config: init_configOption,
|
|
2127
2290
|
preset: presetOption
|
|
@@ -2205,21 +2368,20 @@ const _fmtCommand = Command.make("fmt").pipe(Command.withSubcommands([
|
|
|
2205
2368
|
]));
|
|
2206
2369
|
const fmtCommand = _fmtCommand;
|
|
2207
2370
|
const _lintCommand = Command.make("lint").pipe(Command.withSubcommands([
|
|
2208
|
-
lint_init_initCommand,
|
|
2209
|
-
lint_check_checkCommand,
|
|
2210
2371
|
fmtCommand
|
|
2211
2372
|
]), Command.withDescription("Code-quality: lint-staged config, checks, and in-place formatting"));
|
|
2212
2373
|
const lint_lintCommand = _lintCommand;
|
|
2213
2374
|
const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
2214
2375
|
commands_init_initCommand,
|
|
2215
2376
|
commands_check_checkCommand,
|
|
2377
|
+
cleanCommand,
|
|
2216
2378
|
commitCommand,
|
|
2217
2379
|
changesetCommand,
|
|
2218
2380
|
lint_lintCommand
|
|
2219
2381
|
]));
|
|
2220
2382
|
const cli = Command.run(rootCommand, {
|
|
2221
2383
|
name: "savvy",
|
|
2222
|
-
version: "0.
|
|
2384
|
+
version: "0.3.0"
|
|
2223
2385
|
});
|
|
2224
2386
|
const WorkspaceLive = Layer.mergeAll(WorkspaceRootLive, PackageManagerDetectorLive, WorkspaceDiscoveryLive.pipe(Layer.provide(WorkspaceRootLive)));
|
|
2225
2387
|
const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, ManagedSectionLive, BiomeSchemaSyncLive, ConfigDiscoveryLive, SilkPublishabilityDetectorLive, Changesets.WorkspaceSnapshotReaderLive);
|