@savvy-web/cli 0.3.0 → 0.4.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/bin/savvy.d.ts +1 -0
- package/bin/savvy.js +17 -1
- package/cli/index.js +123 -0
- package/commands/changeset/commands/analyze-branch.js +108 -0
- package/commands/changeset/commands/check.js +71 -0
- package/commands/changeset/commands/classify.js +69 -0
- package/commands/changeset/commands/config-show.js +100 -0
- package/commands/changeset/commands/config-validate.js +63 -0
- package/commands/changeset/commands/deps-detect.js +103 -0
- package/commands/changeset/commands/deps-regen.js +277 -0
- package/commands/changeset/commands/init.js +634 -0
- package/commands/changeset/commands/lint.js +62 -0
- package/commands/changeset/commands/release-surface.js +96 -0
- package/commands/changeset/commands/transform.js +88 -0
- package/commands/changeset/commands/validate-file.js +52 -0
- package/commands/changeset/commands/version.js +178 -0
- package/commands/changeset/index.js +42 -0
- package/commands/changeset/utils/config-gate.js +59 -0
- package/commands/check.js +74 -0
- package/commands/clean.js +186 -0
- package/commands/commit/check.js +170 -0
- package/commands/commit/constants.js +10 -0
- package/commands/commit/hook.js +22 -0
- package/commands/commit/hooks/post-commit-verify.js +121 -0
- package/commands/commit/hooks/pre-commit-message.js +64 -0
- package/commands/commit/hooks/session-start.js +69 -0
- package/commands/commit/hooks/user-prompt-submit.js +42 -0
- package/commands/commit/index.js +20 -0
- package/commands/commit/init.js +127 -0
- package/commands/init.js +88 -0
- package/commands/lint/check.js +306 -0
- package/commands/lint/fmt.js +64 -0
- package/commands/lint/index.js +20 -0
- package/commands/lint/init.js +221 -0
- package/index.d.ts +237 -244
- package/index.js +14 -1
- package/package.json +39 -51
- package/841.js +0 -2394
- package/tsdoc-metadata.json +0 -11
package/841.js
DELETED
|
@@ -1,2394 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
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
|
-
import { Data, Effect, Layer, Option, Schema } from "effect";
|
|
5
|
-
import { PackageManagerDetector, PackageManagerDetectorLive, WorkspaceDiscovery, WorkspaceDiscoveryLive, WorkspaceRoot, WorkspaceRootLive } from "workspaces-effect";
|
|
6
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { execFile, execSync } from "node:child_process";
|
|
9
|
-
import { applyEdits, modify, parse } from "jsonc-effect";
|
|
10
|
-
import { FileSystem } from "@effect/platform";
|
|
11
|
-
import { chmod, glob, realpath, rm } from "node:fs/promises";
|
|
12
|
-
import { isDeepStrictEqual, promisify } from "node:util";
|
|
13
|
-
import { parse as external_yaml_parse, stringify } from "yaml";
|
|
14
|
-
const { BranchAnalyzer: BranchAnalyzer } = Changesets;
|
|
15
|
-
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
16
|
-
const baseOption = Options.text("base").pipe(Options.withDescription("Override the base branch (defaults to config baseBranch or origin/HEAD)"), Options.optional);
|
|
17
|
-
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
18
|
-
function renderHuman(analysis) {
|
|
19
|
-
const lines = [];
|
|
20
|
-
lines.push(`Base branch: ${analysis.baseBranch}`);
|
|
21
|
-
lines.push(`Merge base SHA: ${analysis.mergeBaseSha}`);
|
|
22
|
-
lines.push("");
|
|
23
|
-
if (analysis.packagesAffected.length > 0) {
|
|
24
|
-
lines.push(`Packages affected (${analysis.packagesAffected.length}):`);
|
|
25
|
-
for (const p of analysis.packagesAffected)lines.push(` ${p}`);
|
|
26
|
-
} else lines.push("Packages affected: (none)");
|
|
27
|
-
lines.push("");
|
|
28
|
-
if (0 === analysis.files.length) lines.push("Changes: (no files changed)");
|
|
29
|
-
else {
|
|
30
|
-
lines.push(`Changes (${analysis.files.length}):`);
|
|
31
|
-
const statusGlyph = {
|
|
32
|
-
added: "A",
|
|
33
|
-
modified: "M",
|
|
34
|
-
deleted: "D",
|
|
35
|
-
renamed: "R",
|
|
36
|
-
copied: "C",
|
|
37
|
-
typechange: "T",
|
|
38
|
-
unmerged: "U",
|
|
39
|
-
unknown: "?"
|
|
40
|
-
};
|
|
41
|
-
for (const f of analysis.files){
|
|
42
|
-
const glyph = statusGlyph[f.status] ?? "?";
|
|
43
|
-
const owner = f.package ?? "<unmapped>";
|
|
44
|
-
const reason = "workspace" === f.reason ? "workspace" : null !== f.reason ? `${f.reason.kind}: ${f.reason.glob}` : "—";
|
|
45
|
-
lines.push(` ${glyph} ${f.path}\t${owner}\t${reason}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (analysis.unmappedFiles.length > 0) {
|
|
49
|
-
lines.push("");
|
|
50
|
-
lines.push(`Unmapped (${analysis.unmappedFiles.length}):`);
|
|
51
|
-
for (const p of analysis.unmappedFiles)lines.push(` ${p}`);
|
|
52
|
-
}
|
|
53
|
-
return lines.join("\n");
|
|
54
|
-
}
|
|
55
|
-
function runAnalyzeBranch(cwd, base, json) {
|
|
56
|
-
return Effect.gen(function*() {
|
|
57
|
-
const analyzer = yield* BranchAnalyzer;
|
|
58
|
-
const resolvedCwd = resolve(cwd);
|
|
59
|
-
const baseBranch = Option.getOrUndefined(base);
|
|
60
|
-
const analysis = yield* analyzer.analyzeBranch(resolvedCwd, baseBranch ? {
|
|
61
|
-
baseBranch
|
|
62
|
-
} : void 0).pipe(Effect.catchTags({
|
|
63
|
-
ConfigurationError: (err)=>{
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
return Effect.fail(err);
|
|
66
|
-
},
|
|
67
|
-
GitError: (err)=>{
|
|
68
|
-
process.exitCode = 1;
|
|
69
|
-
return Effect.fail(err);
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
|
-
const output = json ? JSON.stringify(analysis, null, 2) : renderHuman(analysis);
|
|
73
|
-
yield* Effect.log(output);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
const analyzeBranchCommand = Command.make("analyze-branch", {
|
|
77
|
-
cwd: cwdOption,
|
|
78
|
-
base: baseOption,
|
|
79
|
-
json: jsonOption
|
|
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 { ConfigInspector: ConfigInspector } = Changesets;
|
|
82
|
-
const pathsArg = Args.text({
|
|
83
|
-
name: "path"
|
|
84
|
-
}).pipe(Args.repeated);
|
|
85
|
-
const classify_cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
86
|
-
const classify_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
87
|
-
function renderClassificationLine(c) {
|
|
88
|
-
if (null === c.package) return `${c.path}\t<unmapped>`;
|
|
89
|
-
if ("workspace" === c.reason) return `${c.path}\t${c.package}\tworkspace`;
|
|
90
|
-
if (null !== c.reason) return `${c.path}\t${c.package}\t${c.reason.kind}: ${c.reason.glob}`;
|
|
91
|
-
return `${c.path}\t${c.package}`;
|
|
92
|
-
}
|
|
93
|
-
function runClassify(cwd, paths, json) {
|
|
94
|
-
return Effect.gen(function*() {
|
|
95
|
-
const inspector = yield* ConfigInspector;
|
|
96
|
-
const resolvedCwd = resolve(cwd);
|
|
97
|
-
const classifications = yield* inspector.classify(resolvedCwd, paths).pipe(Effect.catchTag("ConfigurationError", (err)=>{
|
|
98
|
-
process.exitCode = 1;
|
|
99
|
-
return Effect.fail(err);
|
|
100
|
-
}));
|
|
101
|
-
if (json) return void (yield* Effect.log(JSON.stringify(classifications, null, 2)));
|
|
102
|
-
for (const c of classifications)yield* Effect.log(renderClassificationLine(c));
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
const classifyCommand = Command.make("classify", {
|
|
106
|
-
paths: pathsArg,
|
|
107
|
-
cwd: classify_cwdOption,
|
|
108
|
-
json: classify_jsonOption
|
|
109
|
-
}, ({ paths, cwd, json })=>runClassify(cwd, paths, json)).pipe(Command.withDescription("Map paths to their owning package per .changeset/config.json"));
|
|
110
|
-
const { ConfigInspector: config_show_ConfigInspector } = Changesets;
|
|
111
|
-
const dirArg = Args.directory({
|
|
112
|
-
name: "dir"
|
|
113
|
-
}).pipe(Args.withDefault("."));
|
|
114
|
-
const config_show_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
115
|
-
function config_show_renderHuman(config) {
|
|
116
|
-
const lines = [];
|
|
117
|
-
lines.push(`Config: ${config.configPath}`);
|
|
118
|
-
lines.push(`Project: ${config.projectDir}`);
|
|
119
|
-
lines.push(`Changelog: ${config.changelog ?? "(none)"}`);
|
|
120
|
-
lines.push(`Base: ${config.baseBranch}`);
|
|
121
|
-
lines.push(`Access: ${config.access}`);
|
|
122
|
-
if (config.ignore.length > 0) lines.push(`Ignore: ${config.ignore.join(", ")}`);
|
|
123
|
-
if (config.legacyVersionFilesUsed) {
|
|
124
|
-
lines.push("");
|
|
125
|
-
lines.push("⚠ This config still uses the deprecated top-level `versionFiles[]`.");
|
|
126
|
-
lines.push(" Migrate to `packages[<name>].versionFiles` — required for 1.0.0.");
|
|
127
|
-
}
|
|
128
|
-
lines.push("");
|
|
129
|
-
if (0 === config.packages.length) {
|
|
130
|
-
lines.push("Packages: (none declared)");
|
|
131
|
-
return lines.join("\n");
|
|
132
|
-
}
|
|
133
|
-
lines.push(`Packages (${config.packages.length}):`);
|
|
134
|
-
for (const pkg of config.packages){
|
|
135
|
-
lines.push("");
|
|
136
|
-
lines.push(` ${pkg.name} v${pkg.version}`);
|
|
137
|
-
lines.push(` workspace: ${pkg.workspaceDir}`);
|
|
138
|
-
if (pkg.additionalScopes.length > 0) {
|
|
139
|
-
lines.push(` additionalScopes (${pkg.additionalScopes.length}):`);
|
|
140
|
-
for (const g of pkg.additionalScopes)lines.push(` - ${g}`);
|
|
141
|
-
lines.push(` additionalScopeFiles (${pkg.additionalScopeFiles.length}):`);
|
|
142
|
-
for (const f of pkg.additionalScopeFiles)lines.push(` ${f}`);
|
|
143
|
-
}
|
|
144
|
-
if (pkg.versionFiles.length > 0) {
|
|
145
|
-
lines.push(` versionFiles (${pkg.versionFiles.length}):`);
|
|
146
|
-
for (const vf of pkg.versionFiles){
|
|
147
|
-
lines.push(` ${vf.glob} → ${vf.paths.join(", ")} (${vf.matchedFiles.length} file${1 === vf.matchedFiles.length ? "" : "s"} matched)`);
|
|
148
|
-
for (const f of vf.matchedFiles)lines.push(` ${f}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return lines.join("\n");
|
|
153
|
-
}
|
|
154
|
-
function runConfigShow(dir, json) {
|
|
155
|
-
return Effect.gen(function*() {
|
|
156
|
-
const inspector = yield* config_show_ConfigInspector;
|
|
157
|
-
const resolved = resolve(dir);
|
|
158
|
-
const config = yield* inspector.inspect(resolved).pipe(Effect.catchTag("ConfigurationError", (err)=>{
|
|
159
|
-
process.exitCode = 1;
|
|
160
|
-
return Effect.fail(err);
|
|
161
|
-
}));
|
|
162
|
-
const output = json ? JSON.stringify(config, null, 2) : config_show_renderHuman(config);
|
|
163
|
-
yield* Effect.log(output);
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
const configShowCommand = Command.make("show", {
|
|
167
|
-
dir: dirArg,
|
|
168
|
-
json: config_show_jsonOption
|
|
169
|
-
}, ({ dir, json })=>runConfigShow(dir, json)).pipe(Command.withDescription("Print the resolved .changeset/config.json"));
|
|
170
|
-
const { ConfigInspector: config_validate_ConfigInspector } = Changesets;
|
|
171
|
-
const config_validate_dirArg = Args.directory({
|
|
172
|
-
name: "dir"
|
|
173
|
-
}).pipe(Args.withDefault("."));
|
|
174
|
-
function runConfigValidate(dir) {
|
|
175
|
-
return Effect.gen(function*() {
|
|
176
|
-
const inspector = yield* config_validate_ConfigInspector;
|
|
177
|
-
const resolved = resolve(dir);
|
|
178
|
-
const result = yield* inspector.inspect(resolved).pipe(Effect.map((config)=>({
|
|
179
|
-
ok: true,
|
|
180
|
-
config
|
|
181
|
-
})), Effect.catchTag("ConfigurationError", (err)=>Effect.succeed({
|
|
182
|
-
ok: false,
|
|
183
|
-
field: err.field,
|
|
184
|
-
reason: err.reason
|
|
185
|
-
})));
|
|
186
|
-
if (result.ok) {
|
|
187
|
-
const { config } = result;
|
|
188
|
-
const pkgCount = config.packages.length;
|
|
189
|
-
const note = config.legacyVersionFilesUsed ? " (warning: legacy versionFiles in use)" : "";
|
|
190
|
-
yield* Effect.log(`OK ${config.configPath} — ${pkgCount} package${1 === pkgCount ? "" : "s"} declared${note}`);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
yield* Effect.log(`FAIL ${result.field}: ${result.reason}`);
|
|
194
|
-
process.exitCode = 1;
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
const configValidateCommand = Command.make("validate", {
|
|
198
|
-
dir: config_validate_dirArg
|
|
199
|
-
}, ({ dir })=>runConfigValidate(dir)).pipe(Command.withDescription("Validate .changeset/config.json without rendering it"));
|
|
200
|
-
const { ConfigInspector: deps_detect_ConfigInspector, WorkspaceSnapshotReader: WorkspaceSnapshotReader, computeWorkspaceDependencyDiffs: computeWorkspaceDependencyDiffs, gitMergeBase: gitMergeBase, listPublishablePackageNames: listPublishablePackageNames, serializeDependencyTableToMarkdown: serializeDependencyTableToMarkdown, snapshotFromWorktree: snapshotFromWorktree } = Changesets;
|
|
201
|
-
const fromOption = Options.text("from").pipe(Options.withDescription("Older ref to diff from (defaults to merge-base with base branch)"), Options.optional);
|
|
202
|
-
const toOption = Options.text("to").pipe(Options.withDescription("Newer ref to diff to (defaults to working tree)"), Options.optional);
|
|
203
|
-
const deps_detect_cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
204
|
-
const packageOption = Options.text("package").pipe(Options.withDescription("Restrict output to a single workspace package"), Options.optional);
|
|
205
|
-
const deps_detect_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON (default)"), Options.withDefault(false));
|
|
206
|
-
const markdownOption = Options.boolean("markdown").pipe(Options.withDescription("Emit one CSH005 markdown block per workspace package"), Options.withDefault(false));
|
|
207
|
-
function renderMarkdownBlocks(diffs) {
|
|
208
|
-
const blocks = [];
|
|
209
|
-
for (const diff of diffs){
|
|
210
|
-
const frontmatter = `---\n"${diff.package}": patch\n---`;
|
|
211
|
-
const table = serializeDependencyTableToMarkdown([
|
|
212
|
-
...diff.rows
|
|
213
|
-
]);
|
|
214
|
-
blocks.push(`${frontmatter}\n\n## Dependencies\n\n${table}\n`);
|
|
215
|
-
}
|
|
216
|
-
return blocks.join("\n");
|
|
217
|
-
}
|
|
218
|
-
function runDepsDetect(cwd, from, to, pkg, json, markdown) {
|
|
219
|
-
return Effect.gen(function*() {
|
|
220
|
-
const reader = yield* WorkspaceSnapshotReader;
|
|
221
|
-
const resolvedCwd = resolve(cwd);
|
|
222
|
-
let fromRef = Option.getOrUndefined(from);
|
|
223
|
-
if (!fromRef) {
|
|
224
|
-
const inspector = yield* deps_detect_ConfigInspector;
|
|
225
|
-
const inspected = yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", ()=>Effect.succeed({
|
|
226
|
-
baseBranch: "main"
|
|
227
|
-
})));
|
|
228
|
-
fromRef = yield* gitMergeBase(resolvedCwd, inspected.baseBranch).pipe(Effect.catchTag("GitError", (err)=>{
|
|
229
|
-
process.exitCode = 1;
|
|
230
|
-
return Effect.fail(err);
|
|
231
|
-
}));
|
|
232
|
-
}
|
|
233
|
-
const toRef = Option.getOrUndefined(to);
|
|
234
|
-
const beforeSnaps = yield* reader.snapshotAt(resolvedCwd, fromRef).pipe(Effect.catchTag("GitError", (err)=>{
|
|
235
|
-
process.exitCode = 1;
|
|
236
|
-
return Effect.fail(err);
|
|
237
|
-
}));
|
|
238
|
-
const afterSnaps = toRef ? yield* reader.snapshotAt(resolvedCwd, toRef).pipe(Effect.catchTag("GitError", (err)=>{
|
|
239
|
-
process.exitCode = 1;
|
|
240
|
-
return Effect.fail(err);
|
|
241
|
-
})) : snapshotFromWorktree(resolvedCwd);
|
|
242
|
-
let diffs = computeWorkspaceDependencyDiffs(beforeSnaps, afterSnaps);
|
|
243
|
-
const targetPkg = Option.getOrUndefined(pkg);
|
|
244
|
-
if (targetPkg) diffs = diffs.filter((d)=>d.package === targetPkg);
|
|
245
|
-
else {
|
|
246
|
-
const discovery = yield* WorkspaceDiscovery;
|
|
247
|
-
const livePackages = yield* discovery.listPackages(resolvedCwd).pipe(Effect.catchAll(()=>Effect.succeed([])));
|
|
248
|
-
const publishable = yield* listPublishablePackageNames(livePackages);
|
|
249
|
-
diffs = diffs.filter((d)=>publishable.has(d.package));
|
|
250
|
-
}
|
|
251
|
-
const emitMarkdown = markdown && !json;
|
|
252
|
-
if (emitMarkdown) return void (yield* Effect.log(renderMarkdownBlocks(diffs)));
|
|
253
|
-
yield* Effect.log(JSON.stringify(diffs, null, 2));
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
const depsDetectCommand = Command.make("detect", {
|
|
257
|
-
from: fromOption,
|
|
258
|
-
to: toOption,
|
|
259
|
-
cwd: deps_detect_cwdOption,
|
|
260
|
-
package: packageOption,
|
|
261
|
-
json: deps_detect_jsonOption,
|
|
262
|
-
markdown: markdownOption
|
|
263
|
-
}, ({ from, to, cwd, package: pkg, json, markdown })=>runDepsDetect(cwd, from, to, pkg, json, markdown)).pipe(Command.withDescription("Compute the dependency diff between two refs"));
|
|
264
|
-
const { ConfigInspector: deps_regen_ConfigInspector, WorkspaceSnapshotReader: deps_regen_WorkspaceSnapshotReader, computeWorkspaceDependencyDiffs: deps_regen_computeWorkspaceDependencyDiffs, gitMergeBase: deps_regen_gitMergeBase, listPublishablePackageNames: deps_regen_listPublishablePackageNames, serializeDependencyTableToMarkdown: deps_regen_serializeDependencyTableToMarkdown, snapshotFromWorktree: deps_regen_snapshotFromWorktree } = Changesets;
|
|
265
|
-
const deps_regen_cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
266
|
-
const deps_regen_baseOption = Options.text("base").pipe(Options.withDescription("Override the base branch (defaults to config baseBranch)"), Options.optional);
|
|
267
|
-
const deps_regen_packageOption = Options.text("package").pipe(Options.withDescription("Restrict regeneration to a single workspace package"), Options.optional);
|
|
268
|
-
const dryRunOption = Options.boolean("dry-run").pipe(Options.withDescription("Print the plan without writing or deleting"), Options.withDefault(false));
|
|
269
|
-
const deps_regen_jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit a structured plan as JSON"), Options.withDefault(false));
|
|
270
|
-
const ADJECTIVES = [
|
|
271
|
-
"brave",
|
|
272
|
-
"clever",
|
|
273
|
-
"swift",
|
|
274
|
-
"silver",
|
|
275
|
-
"lucky",
|
|
276
|
-
"happy",
|
|
277
|
-
"calm",
|
|
278
|
-
"bright",
|
|
279
|
-
"quiet",
|
|
280
|
-
"wild"
|
|
281
|
-
];
|
|
282
|
-
const NOUNS = [
|
|
283
|
-
"dogs",
|
|
284
|
-
"cats",
|
|
285
|
-
"wolves",
|
|
286
|
-
"foxes",
|
|
287
|
-
"cups",
|
|
288
|
-
"ships",
|
|
289
|
-
"trees",
|
|
290
|
-
"owls",
|
|
291
|
-
"cranes",
|
|
292
|
-
"hills"
|
|
293
|
-
];
|
|
294
|
-
const VERBS = [
|
|
295
|
-
"laugh",
|
|
296
|
-
"dream",
|
|
297
|
-
"fly",
|
|
298
|
-
"sing",
|
|
299
|
-
"dance",
|
|
300
|
-
"wander",
|
|
301
|
-
"soar",
|
|
302
|
-
"rest",
|
|
303
|
-
"leap",
|
|
304
|
-
"ponder"
|
|
305
|
-
];
|
|
306
|
-
function pickRandomTriplet() {
|
|
307
|
-
const a = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
308
|
-
const n = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
309
|
-
const v = VERBS[Math.floor(Math.random() * VERBS.length)];
|
|
310
|
-
return `${a}-${n}-${v}`;
|
|
311
|
-
}
|
|
312
|
-
function randomFilename(changesetDir) {
|
|
313
|
-
for(let i = 0; i < 20; i++){
|
|
314
|
-
const candidate = pickRandomTriplet();
|
|
315
|
-
if (!existsSync(join(changesetDir, `${candidate}.md`))) return candidate;
|
|
316
|
-
}
|
|
317
|
-
return `${pickRandomTriplet()}-${Date.now()}`;
|
|
318
|
-
}
|
|
319
|
-
function isPureDependencyChangeset(content) {
|
|
320
|
-
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
321
|
-
if (!fmMatch) return {
|
|
322
|
-
isPure: false,
|
|
323
|
-
package: null
|
|
324
|
-
};
|
|
325
|
-
const frontmatter = fmMatch[1];
|
|
326
|
-
const body = (fmMatch[2] ?? "").trim();
|
|
327
|
-
const fmLines = frontmatter.split(/\r?\n/).filter((l)=>l.trim().length > 0 && !/^\s*#/.test(l));
|
|
328
|
-
if (1 !== fmLines.length) return {
|
|
329
|
-
isPure: false,
|
|
330
|
-
package: null
|
|
331
|
-
};
|
|
332
|
-
const pkgLine = fmLines[0];
|
|
333
|
-
const pkgMatch = pkgLine.match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
|
|
334
|
-
if (!pkgMatch) return {
|
|
335
|
-
isPure: false,
|
|
336
|
-
package: null
|
|
337
|
-
};
|
|
338
|
-
const pkg = pkgMatch[1];
|
|
339
|
-
const bodyTrimmed = body.replace(/^\s+/, "");
|
|
340
|
-
if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
|
|
341
|
-
isPure: false,
|
|
342
|
-
package: null
|
|
343
|
-
};
|
|
344
|
-
const h2Matches = bodyTrimmed.match(/^## /gm) ?? [];
|
|
345
|
-
if (1 !== h2Matches.length) return {
|
|
346
|
-
isPure: false,
|
|
347
|
-
package: null
|
|
348
|
-
};
|
|
349
|
-
if (/^# /m.test(bodyTrimmed)) return {
|
|
350
|
-
isPure: false,
|
|
351
|
-
package: null
|
|
352
|
-
};
|
|
353
|
-
return {
|
|
354
|
-
isPure: true,
|
|
355
|
-
package: pkg
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function listChangesetFiles(changesetDir) {
|
|
359
|
-
if (!existsSync(changesetDir)) return [];
|
|
360
|
-
return readdirSync(changesetDir).filter((f)=>f.endsWith(".md") && "README.md" !== f).map((f)=>join(changesetDir, f));
|
|
361
|
-
}
|
|
362
|
-
function findPureDependencyChangesets(changesetDir) {
|
|
363
|
-
const result = [];
|
|
364
|
-
for (const file of listChangesetFiles(changesetDir)){
|
|
365
|
-
let content;
|
|
366
|
-
try {
|
|
367
|
-
content = readFileSync(file, "utf8");
|
|
368
|
-
} catch {
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
const detection = isPureDependencyChangeset(content);
|
|
372
|
-
if (detection.isPure && detection.package) result.push({
|
|
373
|
-
file,
|
|
374
|
-
package: detection.package
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return result;
|
|
378
|
-
}
|
|
379
|
-
function findMixedDependencyChangesets(changesetDir) {
|
|
380
|
-
const result = [];
|
|
381
|
-
for (const file of listChangesetFiles(changesetDir)){
|
|
382
|
-
let content;
|
|
383
|
-
try {
|
|
384
|
-
content = readFileSync(file, "utf8");
|
|
385
|
-
} catch {
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
|
|
389
|
-
}
|
|
390
|
-
return result;
|
|
391
|
-
}
|
|
392
|
-
function renderChangesetContent(diff) {
|
|
393
|
-
const frontmatter = `---\n"${diff.package}": patch\n---`;
|
|
394
|
-
const table = deps_regen_serializeDependencyTableToMarkdown([
|
|
395
|
-
...diff.rows
|
|
396
|
-
]);
|
|
397
|
-
return `${frontmatter}\n\n## Dependencies\n\n${table}\n`;
|
|
398
|
-
}
|
|
399
|
-
function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
400
|
-
return Effect.gen(function*() {
|
|
401
|
-
const resolvedCwd = resolve(cwd);
|
|
402
|
-
const changesetDir = join(resolvedCwd, ".changeset");
|
|
403
|
-
const reader = yield* deps_regen_WorkspaceSnapshotReader;
|
|
404
|
-
let baseBranch = Option.getOrUndefined(base);
|
|
405
|
-
if (!baseBranch) {
|
|
406
|
-
const inspector = yield* deps_regen_ConfigInspector;
|
|
407
|
-
const inspected = yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", ()=>Effect.succeed({
|
|
408
|
-
baseBranch: "main"
|
|
409
|
-
})));
|
|
410
|
-
baseBranch = inspected.baseBranch;
|
|
411
|
-
}
|
|
412
|
-
const mergeBase = yield* deps_regen_gitMergeBase(resolvedCwd, baseBranch).pipe(Effect.catchTag("GitError", (err)=>{
|
|
413
|
-
process.exitCode = 1;
|
|
414
|
-
return Effect.fail(err);
|
|
415
|
-
}));
|
|
416
|
-
const beforeSnaps = yield* reader.snapshotAt(resolvedCwd, mergeBase).pipe(Effect.catchTag("GitError", (err)=>{
|
|
417
|
-
process.exitCode = 1;
|
|
418
|
-
return Effect.fail(err);
|
|
419
|
-
}));
|
|
420
|
-
const afterSnaps = deps_regen_snapshotFromWorktree(resolvedCwd);
|
|
421
|
-
let diffs = deps_regen_computeWorkspaceDependencyDiffs(beforeSnaps, afterSnaps);
|
|
422
|
-
const targetPkg = Option.getOrUndefined(pkg);
|
|
423
|
-
const discovery = yield* WorkspaceDiscovery;
|
|
424
|
-
const livePackages = yield* discovery.listPackages(resolvedCwd).pipe(Effect.catchAll(()=>Effect.succeed([])));
|
|
425
|
-
const publishable = yield* deps_regen_listPublishablePackageNames(livePackages);
|
|
426
|
-
diffs = targetPkg ? diffs.filter((d)=>d.package === targetPkg) : diffs.filter((d)=>publishable.has(d.package));
|
|
427
|
-
const existingPure = findPureDependencyChangesets(changesetDir);
|
|
428
|
-
const skippedMixed = findMixedDependencyChangesets(changesetDir);
|
|
429
|
-
const toDelete = targetPkg ? existingPure.filter((p)=>p.package === targetPkg) : existingPure.filter((p)=>publishable.has(p.package));
|
|
430
|
-
const toWrite = diffs.map((diff)=>({
|
|
431
|
-
file: join(changesetDir, `${randomFilename(changesetDir)}.md`),
|
|
432
|
-
package: diff.package,
|
|
433
|
-
diff
|
|
434
|
-
}));
|
|
435
|
-
const plan = {
|
|
436
|
-
toDelete,
|
|
437
|
-
toWrite,
|
|
438
|
-
skippedMixed
|
|
439
|
-
};
|
|
440
|
-
if (dryRun) {
|
|
441
|
-
if (json) yield* Effect.log(JSON.stringify(plan, null, 2));
|
|
442
|
-
else yield* renderHumanPlan(plan);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
for (const entry of toDelete)try {
|
|
446
|
-
unlinkSync(entry.file);
|
|
447
|
-
} catch (error) {
|
|
448
|
-
yield* Effect.logWarning(`Failed to delete ${entry.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
449
|
-
}
|
|
450
|
-
for (const entry of toWrite)writeFileSync(entry.file, renderChangesetContent(entry.diff));
|
|
451
|
-
if (json) yield* Effect.log(JSON.stringify(plan, null, 2));
|
|
452
|
-
else yield* renderHumanPlan(plan);
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
function renderHumanPlan(plan) {
|
|
456
|
-
return Effect.gen(function*() {
|
|
457
|
-
if (0 === plan.toDelete.length && 0 === plan.toWrite.length) yield* Effect.log("No dependency changes to regenerate.");
|
|
458
|
-
else {
|
|
459
|
-
if (plan.toDelete.length > 0) {
|
|
460
|
-
yield* Effect.log(`Deleted ${plan.toDelete.length} pure dependency changeset(s):`);
|
|
461
|
-
for (const entry of plan.toDelete)yield* Effect.log(` - ${entry.file} (${entry.package})`);
|
|
462
|
-
}
|
|
463
|
-
if (plan.toWrite.length > 0) {
|
|
464
|
-
yield* Effect.log(`Wrote ${plan.toWrite.length} fresh dependency changeset(s):`);
|
|
465
|
-
for (const entry of plan.toWrite)yield* Effect.log(` + ${entry.file} (${entry.package} — ${entry.diff.rows.length} row${1 === entry.diff.rows.length ? "" : "s"})`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (plan.skippedMixed.length > 0) {
|
|
469
|
-
yield* Effect.log(`\nSkipped ${plan.skippedMixed.length} mixed changeset(s) (have Dependencies but also other content):`);
|
|
470
|
-
for (const file of plan.skippedMixed)yield* Effect.log(` ~ ${file}`);
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
const depsRegenCommand = Command.make("regen", {
|
|
475
|
-
cwd: deps_regen_cwdOption,
|
|
476
|
-
base: deps_regen_baseOption,
|
|
477
|
-
package: deps_regen_packageOption,
|
|
478
|
-
dryRun: dryRunOption,
|
|
479
|
-
json: deps_regen_jsonOption
|
|
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"));
|
|
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");
|
|
513
|
-
}
|
|
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}`);
|
|
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");
|
|
530
|
-
}
|
|
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);
|
|
549
|
-
});
|
|
550
|
-
}
|
|
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) {
|
|
558
|
-
return Effect.gen(function*() {
|
|
559
|
-
const projectDir = resolve(cwd);
|
|
560
|
-
const configPath = join(projectDir, ".changeset", "config.json");
|
|
561
|
-
if (!existsSync(configPath)) return;
|
|
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
|
-
}));
|
|
567
|
-
});
|
|
568
|
-
}
|
|
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) {
|
|
576
|
-
return Effect.gen(function*() {
|
|
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;
|
|
588
|
-
}
|
|
589
|
-
yield* Effect["try"](()=>writeFileSync(resolved, result, "utf-8"));
|
|
590
|
-
yield* Effect.log(`Transformed ${resolved}`);
|
|
591
|
-
});
|
|
592
|
-
}
|
|
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.");
|
|
613
|
-
});
|
|
614
|
-
}
|
|
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
|
-
}
|
|
630
|
-
}
|
|
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)}`)
|
|
653
|
-
});
|
|
654
|
-
}
|
|
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
|
-
});
|
|
669
|
-
}
|
|
670
|
-
}
|
|
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;
|
|
707
|
-
});
|
|
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
|
-
});
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
for (const update of updates){
|
|
721
|
-
const action = dryRun ? "Would update" : "Updated";
|
|
722
|
-
yield* Effect.log(`${action} ${update.filePath} → ${update.version}`);
|
|
723
|
-
}
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
function readPackageVersionFromDisk(workspaceDir) {
|
|
727
|
-
try {
|
|
728
|
-
const pkg = JSON.parse(readFileSync(join(workspaceDir, "package.json"), "utf-8"));
|
|
729
|
-
return pkg.version ?? null;
|
|
730
|
-
} catch {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
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) {
|
|
742
|
-
return Effect.gen(function*() {
|
|
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
|
-
]);
|
|
755
|
-
}
|
|
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}`);
|
|
759
|
-
}
|
|
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"
|
|
803
|
-
}
|
|
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
|
-
}
|
|
820
|
-
}
|
|
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
|
-
})
|
|
897
|
-
});
|
|
898
|
-
}
|
|
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;
|
|
907
|
-
}
|
|
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");
|
|
916
|
-
}
|
|
917
|
-
function warnIfLegacyVersionFiles(changesetDir) {
|
|
918
|
-
return Effect.gen(function*() {
|
|
919
|
-
const configPath = join(changesetDir, "config.json");
|
|
920
|
-
if (!existsSync(configPath)) return;
|
|
921
|
-
let parsed;
|
|
922
|
-
try {
|
|
923
|
-
parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
924
|
-
} catch {
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (detectLegacyVersionFiles(parsed)) yield* Effect.logWarning(legacyVersionFilesWarning(configPath));
|
|
928
|
-
});
|
|
929
|
-
}
|
|
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(", ")})`);
|
|
933
|
-
return Effect.gen(function*() {
|
|
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
|
-
}));
|
|
943
|
-
}
|
|
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
|
|
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";
|
|
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"
|
|
1037
|
-
}
|
|
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"
|
|
1047
|
-
}
|
|
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
|
-
});
|
|
1071
|
-
}
|
|
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`."
|
|
1075
|
-
});
|
|
1076
|
-
return issues;
|
|
1077
|
-
} catch {
|
|
1078
|
-
return [
|
|
1079
|
-
{
|
|
1080
|
-
file: ".changeset/config.json",
|
|
1081
|
-
message: "could not parse file"
|
|
1082
|
-
}
|
|
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(", ")})`
|
|
1092
|
-
}
|
|
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
|
-
}
|
|
1120
|
-
}
|
|
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
|
-
];
|
|
1129
|
-
try {
|
|
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;
|
|
1137
|
-
} catch {
|
|
1138
|
-
return [
|
|
1139
|
-
{
|
|
1140
|
-
file: ".changeset/.markdownlint.json",
|
|
1141
|
-
message: "could not parse file"
|
|
1142
|
-
}
|
|
1143
|
-
];
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
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"));
|
|
1201
|
-
const configGroup = Command.make("config").pipe(Command.withSubcommands([
|
|
1202
|
-
configShowCommand,
|
|
1203
|
-
configValidateCommand
|
|
1204
|
-
]), Command.withDescription("Inspect or validate .changeset/config.json"));
|
|
1205
|
-
const depsGroup = Command.make("deps").pipe(Command.withSubcommands([
|
|
1206
|
-
depsDetectCommand,
|
|
1207
|
-
depsRegenCommand
|
|
1208
|
-
]), Command.withDescription("Generate or regenerate dependency changesets"));
|
|
1209
|
-
const _changesetCommand = Command.make("changeset").pipe(Command.withSubcommands([
|
|
1210
|
-
lintCommand,
|
|
1211
|
-
transformCommand,
|
|
1212
|
-
validateFileCommand,
|
|
1213
|
-
versionCommand,
|
|
1214
|
-
classifyCommand,
|
|
1215
|
-
analyzeBranchCommand,
|
|
1216
|
-
releaseSurfaceCommand,
|
|
1217
|
-
configGroup,
|
|
1218
|
-
depsGroup
|
|
1219
|
-
]), Command.withDescription("Section-aware changeset tooling"));
|
|
1220
|
-
const changesetCommand = _changesetCommand;
|
|
1221
|
-
const HUSKY_HOOK_PATH = ".husky/commit-msg";
|
|
1222
|
-
const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
|
|
1223
|
-
const POST_MERGE_HOOK_PATH = ".husky/post-merge";
|
|
1224
|
-
const EXECUTABLE_MODE = 493;
|
|
1225
|
-
const DEFAULT_CONFIG_PATH = "lib/configs/commitlint.config.ts";
|
|
1226
|
-
const SECTION_DEF = SectionDefinition.make({
|
|
1227
|
-
toolName: "savvy-commit"
|
|
1228
|
-
});
|
|
1229
|
-
const COMMIT_MSG_HEADER = "#!/usr/bin/env sh\n# Commit-msg hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
1230
|
-
const HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
1231
|
-
function commitlintCommand(configPath) {
|
|
1232
|
-
return `commitlint --config "$ROOT/${configPath}" --edit "$1"`;
|
|
1233
|
-
}
|
|
1234
|
-
function savvyCommitBlock(configPath) {
|
|
1235
|
-
return savvyToolSection("savvy-commit", commitlintCommand(configPath));
|
|
1236
|
-
}
|
|
1237
|
-
function ensureHookFile(path, header) {
|
|
1238
|
-
return Effect.gen(function*() {
|
|
1239
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1240
|
-
const exists = yield* fs.exists(path);
|
|
1241
|
-
if (!exists) yield* fs.writeFileString(path, header);
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
const init_forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the commit-msg hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
1245
|
-
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the commitlint config file (from repo root)"), Options.withDefault(DEFAULT_CONFIG_PATH));
|
|
1246
|
-
const CONFIG_CONTENT = `import { CommitlintConfig } from "@savvy-web/silk/commitlint";
|
|
1247
|
-
|
|
1248
|
-
export default CommitlintConfig.silk();
|
|
1249
|
-
`;
|
|
1250
|
-
function makeExecutable(path) {
|
|
1251
|
-
return Effect.tryPromise({
|
|
1252
|
-
try: ()=>chmod(path, EXECUTABLE_MODE),
|
|
1253
|
-
catch: (e)=>new Error(String(e))
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
function runCommitInit(opts) {
|
|
1257
|
-
const { force, config } = opts;
|
|
1258
|
-
return Effect.gen(function*() {
|
|
1259
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1260
|
-
const ms = yield* ManagedSection;
|
|
1261
|
-
if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
|
|
1262
|
-
yield* Effect.log("Initializing commitlint configuration...\n");
|
|
1263
|
-
yield* fs.makeDirectory(".husky", {
|
|
1264
|
-
recursive: true
|
|
1265
|
-
});
|
|
1266
|
-
if (force) yield* fs.writeFileString(HUSKY_HOOK_PATH, COMMIT_MSG_HEADER);
|
|
1267
|
-
else yield* ensureHookFile(HUSKY_HOOK_PATH, COMMIT_MSG_HEADER);
|
|
1268
|
-
const commitResults = yield* ms.syncMany(HUSKY_HOOK_PATH, [
|
|
1269
|
-
SavvyBaseSection.block(savvyBasePreamble()),
|
|
1270
|
-
savvyCommitBlock(config)
|
|
1271
|
-
]);
|
|
1272
|
-
yield* makeExecutable(HUSKY_HOOK_PATH);
|
|
1273
|
-
yield* Effect.log(`✓ ${force ? "Replaced" : "Synced"} ${HUSKY_HOOK_PATH} (${commitResults.map((r)=>r._tag).join(", ")})`);
|
|
1274
|
-
for (const hookPath of [
|
|
1275
|
-
POST_CHECKOUT_HOOK_PATH,
|
|
1276
|
-
POST_MERGE_HOOK_PATH
|
|
1277
|
-
]){
|
|
1278
|
-
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
1279
|
-
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
1280
|
-
yield* makeExecutable(hookPath);
|
|
1281
|
-
yield* Effect.log(`✓ Synced ${hookPath}`);
|
|
1282
|
-
}
|
|
1283
|
-
const configExists = yield* fs.exists(config);
|
|
1284
|
-
if (configExists && !force) yield* Effect.log(`⚠ ${config} already exists (use --force to overwrite)`);
|
|
1285
|
-
else {
|
|
1286
|
-
const configDir = dirname(config);
|
|
1287
|
-
if (configDir && "." !== configDir) yield* fs.makeDirectory(configDir, {
|
|
1288
|
-
recursive: true
|
|
1289
|
-
});
|
|
1290
|
-
yield* fs.writeFileString(config, CONFIG_CONTENT);
|
|
1291
|
-
yield* Effect.log(`✓ Created ${config}`);
|
|
1292
|
-
}
|
|
1293
|
-
yield* Effect.log("\nDone! Install @commitlint/cli if not already installed.");
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
Command.make("init", {
|
|
1297
|
-
force: init_forceOption,
|
|
1298
|
-
config: configOption
|
|
1299
|
-
}, (opts)=>runCommitInit(opts)).pipe(Command.withDescription("Initialize commitlint configuration and husky hooks"));
|
|
1300
|
-
const CROSS_MARK = "✗";
|
|
1301
|
-
const BULLET = "•";
|
|
1302
|
-
const CONFIG_FILES = [
|
|
1303
|
-
"commitlint.config.ts",
|
|
1304
|
-
"commitlint.config.mts",
|
|
1305
|
-
"commitlint.config.cts",
|
|
1306
|
-
"commitlint.config.js",
|
|
1307
|
-
"commitlint.config.mjs",
|
|
1308
|
-
"commitlint.config.cjs",
|
|
1309
|
-
"lib/configs/commitlint.config.ts",
|
|
1310
|
-
"lib/configs/commitlint.config.mts",
|
|
1311
|
-
"lib/configs/commitlint.config.cts",
|
|
1312
|
-
"lib/configs/commitlint.config.js",
|
|
1313
|
-
"lib/configs/commitlint.config.mjs",
|
|
1314
|
-
"lib/configs/commitlint.config.cjs",
|
|
1315
|
-
".commitlintrc",
|
|
1316
|
-
".commitlintrc.json",
|
|
1317
|
-
".commitlintrc.yaml",
|
|
1318
|
-
".commitlintrc.yml",
|
|
1319
|
-
".commitlintrc.js",
|
|
1320
|
-
".commitlintrc.cjs",
|
|
1321
|
-
".commitlintrc.mjs",
|
|
1322
|
-
".commitlintrc.ts",
|
|
1323
|
-
".commitlintrc.cts",
|
|
1324
|
-
".commitlintrc.mts"
|
|
1325
|
-
];
|
|
1326
|
-
const DCO_FILE_PATH = "DCO";
|
|
1327
|
-
const STRATEGY_TO_FORMAT = {
|
|
1328
|
-
single: "semver",
|
|
1329
|
-
"fixed-group": "semver",
|
|
1330
|
-
independent: "packages"
|
|
1331
|
-
};
|
|
1332
|
-
function findConfigFile(fs) {
|
|
1333
|
-
return Effect.gen(function*() {
|
|
1334
|
-
for (const file of CONFIG_FILES)if (yield* fs.exists(file)) return file;
|
|
1335
|
-
return null;
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
function extractConfigPathFromManaged(managedContent) {
|
|
1339
|
-
const match = managedContent.match(/commitlint --config "\$ROOT\/([^"]+)"/);
|
|
1340
|
-
return match ? match[1] : null;
|
|
1341
|
-
}
|
|
1342
|
-
const detectReleaseFormat = Effect.gen(function*() {
|
|
1343
|
-
const versioning = yield* VersioningStrategy;
|
|
1344
|
-
const discovery = yield* WorkspaceDiscovery;
|
|
1345
|
-
const packages = yield* Effect.catchAll(discovery.listPackages(), ()=>Effect.succeed([]));
|
|
1346
|
-
const publishableNames = packages.filter((pkg)=>!pkg.private || pkg.publishConfig?.access !== void 0).map((pkg)=>pkg.name);
|
|
1347
|
-
const result = yield* Effect.catchAll(versioning.detect(publishableNames, process.cwd()), ()=>Effect.succeed({
|
|
1348
|
-
type: "single"
|
|
1349
|
-
}));
|
|
1350
|
-
return STRATEGY_TO_FORMAT[result.type] ?? "semver";
|
|
1351
|
-
});
|
|
1352
|
-
function runCommitCheck() {
|
|
1353
|
-
return Effect.gen(function*() {
|
|
1354
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1355
|
-
const ms = yield* ManagedSection;
|
|
1356
|
-
yield* Effect.log("Checking commitlint configuration...\n");
|
|
1357
|
-
const foundConfig = yield* findConfigFile(fs);
|
|
1358
|
-
if (foundConfig) yield* Effect.log(`✓ Config file: ${foundConfig}`);
|
|
1359
|
-
else yield* Effect.log(`${CROSS_MARK} No commitlint config file found`);
|
|
1360
|
-
const hasHuskyHook = yield* fs.exists(HUSKY_HOOK_PATH);
|
|
1361
|
-
if (hasHuskyHook) yield* Effect.log(`✓ Husky hook: ${HUSKY_HOOK_PATH}`);
|
|
1362
|
-
else yield* Effect.log(`${CROSS_MARK} No husky commit-msg hook found`);
|
|
1363
|
-
let sectionsHealthy = true;
|
|
1364
|
-
if (hasHuskyHook) {
|
|
1365
|
-
const baseStatus = yield* ms.check(HUSKY_HOOK_PATH, SavvyBaseSection.block(savvyBasePreamble()));
|
|
1366
|
-
if (CheckResult.$is("Found")(baseStatus) && baseStatus.isUpToDate) yield* Effect.log("✓ Base section: up-to-date");
|
|
1367
|
-
else if (CheckResult.$is("Found")(baseStatus)) {
|
|
1368
|
-
sectionsHealthy = false;
|
|
1369
|
-
yield* Effect.log("⚠ Base section: outdated (run 'savvy init' to update)");
|
|
1370
|
-
} else {
|
|
1371
|
-
sectionsHealthy = false;
|
|
1372
|
-
yield* Effect.log(`${BULLET} Base section: not found (run 'savvy init' to add)`);
|
|
1373
|
-
}
|
|
1374
|
-
const block = yield* ms.read(HUSKY_HOOK_PATH, SECTION_DEF);
|
|
1375
|
-
if (block) {
|
|
1376
|
-
const configPath = extractConfigPathFromManaged(block.content);
|
|
1377
|
-
if (configPath) {
|
|
1378
|
-
const status = yield* ms.check(HUSKY_HOOK_PATH, savvyCommitBlock(configPath));
|
|
1379
|
-
if (CheckResult.$is("Found")(status) && status.isUpToDate) yield* Effect.log("✓ Commit section: up-to-date");
|
|
1380
|
-
else {
|
|
1381
|
-
sectionsHealthy = false;
|
|
1382
|
-
yield* Effect.log("⚠ Commit section: outdated (run 'savvy init' to update)");
|
|
1383
|
-
}
|
|
1384
|
-
} else {
|
|
1385
|
-
sectionsHealthy = false;
|
|
1386
|
-
yield* Effect.log("⚠ Commit section: outdated (run 'savvy init' to update)");
|
|
1387
|
-
}
|
|
1388
|
-
} else {
|
|
1389
|
-
sectionsHealthy = false;
|
|
1390
|
-
yield* Effect.log(`${BULLET} Commit section: not found (run 'savvy init' to add)`);
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
for (const hookPath of [
|
|
1394
|
-
POST_CHECKOUT_HOOK_PATH,
|
|
1395
|
-
POST_MERGE_HOOK_PATH
|
|
1396
|
-
]){
|
|
1397
|
-
const hygieneExists = yield* fs.exists(hookPath);
|
|
1398
|
-
if (!hygieneExists) {
|
|
1399
|
-
sectionsHealthy = false;
|
|
1400
|
-
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} not found (run 'savvy init' to add)`);
|
|
1401
|
-
continue;
|
|
1402
|
-
}
|
|
1403
|
-
const hygieneStatus = yield* ms.check(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
1404
|
-
if (CheckResult.$is("Found")(hygieneStatus) && hygieneStatus.isUpToDate) yield* Effect.log(`✓ Hygiene hook: ${hookPath}`);
|
|
1405
|
-
else if (CheckResult.$is("Found")(hygieneStatus)) {
|
|
1406
|
-
sectionsHealthy = false;
|
|
1407
|
-
yield* Effect.log(`⚠ Hygiene hook: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
1408
|
-
} else {
|
|
1409
|
-
sectionsHealthy = false;
|
|
1410
|
-
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
const hasDCOFile = yield* fs.exists(DCO_FILE_PATH);
|
|
1414
|
-
if (hasDCOFile) yield* Effect.log(`✓ DCO file: ${DCO_FILE_PATH}`);
|
|
1415
|
-
else yield* Effect.log(`${BULLET} No DCO file (signoff not required)`);
|
|
1416
|
-
yield* Effect.log("\nDetected settings:");
|
|
1417
|
-
yield* Effect.log(` DCO required: ${Commitlint.detectDCO()}`);
|
|
1418
|
-
const releaseFormat = yield* detectReleaseFormat;
|
|
1419
|
-
yield* Effect.log(` Release format: ${releaseFormat}`);
|
|
1420
|
-
const scopes = yield* Effect.catchAll(Commitlint.detectScopes, ()=>Effect.succeed([]));
|
|
1421
|
-
const scopeDisplay = scopes.length > 0 ? scopes.join(", ") : "(none - not a monorepo or no packages found)";
|
|
1422
|
-
yield* Effect.log(` Detected scopes: ${scopeDisplay}`);
|
|
1423
|
-
yield* Effect.log("");
|
|
1424
|
-
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy;
|
|
1425
|
-
if (hasIssues) yield* Effect.log(`${CROSS_MARK} Commitlint needs configuration. Run: savvy init`);
|
|
1426
|
-
else yield* Effect.log("✓ Commitlint is configured correctly.");
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
Command.make("check", {}, ()=>runCommitCheck()).pipe(Command.withDescription("Check current commitlint configuration and detected settings"));
|
|
1430
|
-
const check_CHECK_MARK = "✓";
|
|
1431
|
-
const check_CROSS_MARK = "✗";
|
|
1432
|
-
const check_WARNING = "⚠";
|
|
1433
|
-
const check_BULLET = "•";
|
|
1434
|
-
const check_CONFIG_FILES = [
|
|
1435
|
-
"lint-staged.config.ts",
|
|
1436
|
-
"lint-staged.config.js",
|
|
1437
|
-
"lint-staged.config.mjs",
|
|
1438
|
-
"lint-staged.config.cjs",
|
|
1439
|
-
".lintstagedrc",
|
|
1440
|
-
".lintstagedrc.json",
|
|
1441
|
-
".lintstagedrc.yaml",
|
|
1442
|
-
".lintstagedrc.yml",
|
|
1443
|
-
".lintstagedrc.js",
|
|
1444
|
-
".lintstagedrc.cjs",
|
|
1445
|
-
".lintstagedrc.mjs"
|
|
1446
|
-
];
|
|
1447
|
-
const CONFIG_SEARCH_PATHS = [
|
|
1448
|
-
"lib/configs/lint-staged.config.ts",
|
|
1449
|
-
"lib/configs/lint-staged.config.js",
|
|
1450
|
-
...check_CONFIG_FILES
|
|
1451
|
-
];
|
|
1452
|
-
function check_findConfigFile(fs) {
|
|
1453
|
-
return Effect.gen(function*() {
|
|
1454
|
-
for (const file of CONFIG_SEARCH_PATHS)if (yield* fs.exists(file)) return file;
|
|
1455
|
-
return null;
|
|
1456
|
-
});
|
|
1457
|
-
}
|
|
1458
|
-
function findConfig(discovery, names) {
|
|
1459
|
-
return Effect.gen(function*() {
|
|
1460
|
-
for (const name of names){
|
|
1461
|
-
const result = yield* discovery.find(name);
|
|
1462
|
-
if (result) return result.path;
|
|
1463
|
-
}
|
|
1464
|
-
return null;
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
function check_extractConfigPathFromManaged(managedContent) {
|
|
1468
|
-
const match = managedContent.match(/lint-staged --config "\$ROOT\/([^"]+)"/);
|
|
1469
|
-
return match ? match[1] : null;
|
|
1470
|
-
}
|
|
1471
|
-
function checkMarkdownlintConfig(content) {
|
|
1472
|
-
return Effect.gen(function*() {
|
|
1473
|
-
const parsed = yield* parse(content);
|
|
1474
|
-
const schemaMatches = parsed.$schema === Lint.MARKDOWNLINT_SCHEMA;
|
|
1475
|
-
const existingConfig = parsed.config;
|
|
1476
|
-
const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, Lint.MARKDOWNLINT_CONFIG);
|
|
1477
|
-
return {
|
|
1478
|
-
exists: true,
|
|
1479
|
-
schemaMatches,
|
|
1480
|
-
configMatches,
|
|
1481
|
-
isUpToDate: schemaMatches && configMatches
|
|
1482
|
-
};
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
function checkBiomeSchemas() {
|
|
1486
|
-
return Effect.gen(function*() {
|
|
1487
|
-
const version = process.env.__BIOME_PEER_VERSION__;
|
|
1488
|
-
const statuses = [];
|
|
1489
|
-
if (!version) return {
|
|
1490
|
-
statuses,
|
|
1491
|
-
warnings: []
|
|
1492
|
-
};
|
|
1493
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1494
|
-
const warnings = [];
|
|
1495
|
-
const expectedSchema = `https://biomejs.dev/schemas/${version}/schema.json`;
|
|
1496
|
-
const configPaths = Lint.Biome.findAllConfigs();
|
|
1497
|
-
for (const configPath of configPaths){
|
|
1498
|
-
const content = yield* fs.readFileString(configPath);
|
|
1499
|
-
const parsed = yield* parse(content);
|
|
1500
|
-
const currentSchema = parsed.$schema;
|
|
1501
|
-
if (currentSchema === expectedSchema) statuses.push({
|
|
1502
|
-
path: configPath,
|
|
1503
|
-
matches: true
|
|
1504
|
-
});
|
|
1505
|
-
else {
|
|
1506
|
-
statuses.push({
|
|
1507
|
-
path: configPath,
|
|
1508
|
-
matches: false
|
|
1509
|
-
});
|
|
1510
|
-
warnings.push(`${check_WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy init' to update it.`);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
return {
|
|
1514
|
-
statuses,
|
|
1515
|
-
warnings
|
|
1516
|
-
};
|
|
1517
|
-
});
|
|
1518
|
-
}
|
|
1519
|
-
const check_quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings (for postinstall usage)"), Options.withDefault(false));
|
|
1520
|
-
function runLintCheck(opts) {
|
|
1521
|
-
const { quiet } = opts;
|
|
1522
|
-
return Effect.gen(function*() {
|
|
1523
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1524
|
-
const ms = yield* ManagedSection;
|
|
1525
|
-
const td = yield* ToolDiscovery;
|
|
1526
|
-
const discovery = yield* ConfigDiscovery;
|
|
1527
|
-
const warnings = [];
|
|
1528
|
-
const foundConfig = yield* check_findConfigFile(fs);
|
|
1529
|
-
const hasHuskyHook = yield* fs.exists(Lint.HUSKY_HOOK_PATH);
|
|
1530
|
-
let sectionsHealthy = true;
|
|
1531
|
-
let baseStatusLabel = "missing";
|
|
1532
|
-
let lintStatusLabel = "missing";
|
|
1533
|
-
let detectedConfigPath = null;
|
|
1534
|
-
if (hasHuskyHook) {
|
|
1535
|
-
const baseResult = yield* ms.check(Lint.HUSKY_HOOK_PATH, SavvyBaseSection.block(savvyBasePreamble()));
|
|
1536
|
-
if (CheckResult.$is("Found")(baseResult)) {
|
|
1537
|
-
baseStatusLabel = baseResult.isUpToDate ? "up-to-date" : "outdated";
|
|
1538
|
-
if (!baseResult.isUpToDate) sectionsHealthy = false;
|
|
1539
|
-
} else sectionsHealthy = false;
|
|
1540
|
-
const existing = yield* ms.read(Lint.HUSKY_HOOK_PATH, Lint.SavvyLintSectionDef);
|
|
1541
|
-
if (existing) {
|
|
1542
|
-
const configPath = check_extractConfigPathFromManaged(existing.content);
|
|
1543
|
-
detectedConfigPath = configPath;
|
|
1544
|
-
if (configPath) {
|
|
1545
|
-
const lintResult = yield* ms.check(Lint.HUSKY_HOOK_PATH, Lint.savvyLintBlock(configPath));
|
|
1546
|
-
if (CheckResult.$is("Found")(lintResult)) {
|
|
1547
|
-
lintStatusLabel = lintResult.isUpToDate ? "up-to-date" : "outdated";
|
|
1548
|
-
if (!lintResult.isUpToDate) sectionsHealthy = false;
|
|
1549
|
-
} else {
|
|
1550
|
-
lintStatusLabel = "outdated";
|
|
1551
|
-
sectionsHealthy = false;
|
|
1552
|
-
}
|
|
1553
|
-
} else {
|
|
1554
|
-
lintStatusLabel = "outdated";
|
|
1555
|
-
sectionsHealthy = false;
|
|
1556
|
-
}
|
|
1557
|
-
} else sectionsHealthy = false;
|
|
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).`);
|
|
1559
|
-
} else {
|
|
1560
|
-
sectionsHealthy = false;
|
|
1561
|
-
warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy init' to create it.`);
|
|
1562
|
-
}
|
|
1563
|
-
if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy init' to create one.`);
|
|
1564
|
-
const shellHookPaths = [
|
|
1565
|
-
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
1566
|
-
Lint.POST_MERGE_HOOK_PATH
|
|
1567
|
-
];
|
|
1568
|
-
const shellHookStatuses = [];
|
|
1569
|
-
for (const hookPath of shellHookPaths){
|
|
1570
|
-
const hookExists = yield* fs.exists(hookPath);
|
|
1571
|
-
if (!hookExists) {
|
|
1572
|
-
shellHookStatuses.push({
|
|
1573
|
-
path: hookPath,
|
|
1574
|
-
found: false,
|
|
1575
|
-
isUpToDate: false
|
|
1576
|
-
});
|
|
1577
|
-
continue;
|
|
1578
|
-
}
|
|
1579
|
-
const hygieneResult = yield* ms.check(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
1580
|
-
const found = CheckResult.$is("Found")(hygieneResult);
|
|
1581
|
-
const isUpToDate = CheckResult.$is("Found")(hygieneResult) && hygieneResult.isUpToDate;
|
|
1582
|
-
shellHookStatuses.push({
|
|
1583
|
-
path: hookPath,
|
|
1584
|
-
found,
|
|
1585
|
-
isUpToDate
|
|
1586
|
-
});
|
|
1587
|
-
if (found) {
|
|
1588
|
-
if (!isUpToDate) {
|
|
1589
|
-
sectionsHealthy = false;
|
|
1590
|
-
warnings.push(`${check_WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy init' to update.`);
|
|
1591
|
-
}
|
|
1592
|
-
} else {
|
|
1593
|
-
sectionsHealthy = false;
|
|
1594
|
-
warnings.push(`${check_WARNING} ${hookPath} has no savvy-hooks section.\n Run 'savvy init' to add it.`);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.catchAll(()=>Effect.succeed({
|
|
1598
|
-
statuses: [],
|
|
1599
|
-
warnings: [
|
|
1600
|
-
`${check_WARNING} Could not check biome $schema URLs.`
|
|
1601
|
-
]
|
|
1602
|
-
})));
|
|
1603
|
-
warnings.push(...biomeSchemaStatus.warnings);
|
|
1604
|
-
const hasMarkdownlintConfig = yield* fs.exists(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
1605
|
-
let markdownlintStatus = {
|
|
1606
|
-
exists: false,
|
|
1607
|
-
schemaMatches: false,
|
|
1608
|
-
configMatches: false,
|
|
1609
|
-
isUpToDate: false
|
|
1610
|
-
};
|
|
1611
|
-
if (hasMarkdownlintConfig) {
|
|
1612
|
-
const mdContent = yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
1613
|
-
markdownlintStatus = yield* checkMarkdownlintConfig(mdContent);
|
|
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.`);
|
|
1616
|
-
}
|
|
1617
|
-
if (quiet) {
|
|
1618
|
-
if (warnings.length > 0) for (const warning of warnings)yield* Effect.log(warning);
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
yield* Effect.log("Checking lint-staged configuration...\n");
|
|
1622
|
-
if (foundConfig) yield* Effect.log(`${check_CHECK_MARK} Config file: ${foundConfig}`);
|
|
1623
|
-
else yield* Effect.log(`${check_CROSS_MARK} No lint-staged config file found`);
|
|
1624
|
-
if (hasHuskyHook) yield* Effect.log(`${check_CHECK_MARK} Husky hook: ${Lint.HUSKY_HOOK_PATH}`);
|
|
1625
|
-
else yield* Effect.log(`${check_CROSS_MARK} No husky pre-commit hook found`);
|
|
1626
|
-
if (hasHuskyHook) {
|
|
1627
|
-
if ("up-to-date" === baseStatusLabel) yield* Effect.log(`${check_CHECK_MARK} Base section: up-to-date`);
|
|
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)`);
|
|
1630
|
-
const lintLabel = detectedConfigPath ? ` (config: ${detectedConfigPath})` : "";
|
|
1631
|
-
if ("up-to-date" === lintStatusLabel) yield* Effect.log(`${check_CHECK_MARK} Lint section: up-to-date${lintLabel}`);
|
|
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)`);
|
|
1634
|
-
}
|
|
1635
|
-
for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${check_CHECK_MARK} ${status.path}: up-to-date`);
|
|
1636
|
-
else yield* Effect.log(`${check_WARNING} ${status.path}: outdated (run 'savvy init' to update)`);
|
|
1637
|
-
else yield* Effect.log(`${check_BULLET} ${status.path}: savvy-hooks section not found`);
|
|
1638
|
-
yield* Effect.log("\nTool availability:");
|
|
1639
|
-
const biomeAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
1640
|
-
name: "biome"
|
|
1641
|
-
}));
|
|
1642
|
-
const biomeConfig = yield* findConfig(discovery, [
|
|
1643
|
-
"biome.jsonc",
|
|
1644
|
-
"biome.json"
|
|
1645
|
-
]);
|
|
1646
|
-
if (biomeAvailable) {
|
|
1647
|
-
const configInfo = biomeConfig ? ` (config: ${biomeConfig})` : "";
|
|
1648
|
-
yield* Effect.log(` ${check_CHECK_MARK} Biome${configInfo}`);
|
|
1649
|
-
} else yield* Effect.log(` ${check_BULLET} Biome: not installed`);
|
|
1650
|
-
const markdownAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
1651
|
-
name: "markdownlint-cli2"
|
|
1652
|
-
}));
|
|
1653
|
-
const markdownConfig = yield* findConfig(discovery, [
|
|
1654
|
-
".markdownlint-cli2.jsonc",
|
|
1655
|
-
".markdownlint-cli2.json",
|
|
1656
|
-
".markdownlint-cli2.yaml",
|
|
1657
|
-
".markdownlint-cli2.cjs",
|
|
1658
|
-
".markdownlint.jsonc",
|
|
1659
|
-
".markdownlint.json",
|
|
1660
|
-
".markdownlint.yaml"
|
|
1661
|
-
]);
|
|
1662
|
-
if (markdownAvailable) {
|
|
1663
|
-
const configInfo = markdownConfig ? ` (config: ${markdownConfig})` : "";
|
|
1664
|
-
yield* Effect.log(` ${check_CHECK_MARK} markdownlint-cli2${configInfo}`);
|
|
1665
|
-
} else yield* Effect.log(` ${check_BULLET} markdownlint-cli2: not installed`);
|
|
1666
|
-
const tsgoAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
1667
|
-
name: "tsgo"
|
|
1668
|
-
}));
|
|
1669
|
-
const tscAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
1670
|
-
name: "tsc"
|
|
1671
|
-
}));
|
|
1672
|
-
if (tsgoAvailable) yield* Effect.log(` ${check_CHECK_MARK} TypeScript (tsgo)`);
|
|
1673
|
-
else if (tscAvailable) yield* Effect.log(` ${check_CHECK_MARK} TypeScript (tsc)`);
|
|
1674
|
-
else yield* Effect.log(` ${check_BULLET} TypeScript: not installed`);
|
|
1675
|
-
if (hasMarkdownlintConfig) if (markdownlintStatus.isUpToDate) yield* Effect.log(` ${check_CHECK_MARK} ${Lint.MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
1676
|
-
else {
|
|
1677
|
-
const issues = [];
|
|
1678
|
-
if (!markdownlintStatus.schemaMatches) issues.push("$schema");
|
|
1679
|
-
if (!markdownlintStatus.configMatches) issues.push("config");
|
|
1680
|
-
yield* Effect.log(` ${check_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: ${issues.join(", ")} differ from template`);
|
|
1681
|
-
}
|
|
1682
|
-
else yield* Effect.log(` ${check_BULLET} ${Lint.MARKDOWNLINT_CONFIG_PATH}: not found`);
|
|
1683
|
-
for (const status of biomeSchemaStatus.statuses)if (status.matches) yield* Effect.log(` ${check_CHECK_MARK} ${status.path}: biome $schema up-to-date`);
|
|
1684
|
-
else yield* Effect.log(` ${check_WARNING} ${status.path}: biome $schema outdated (run 'savvy init' to update)`);
|
|
1685
|
-
yield* Effect.log("");
|
|
1686
|
-
const hasMarkdownlintIssues = hasMarkdownlintConfig && !markdownlintStatus.isUpToDate;
|
|
1687
|
-
const hasBiomeSchemaIssues = biomeSchemaStatus.statuses.some((s)=>!s.matches);
|
|
1688
|
-
const hasIssues = !foundConfig || !hasHuskyHook || !sectionsHealthy || hasMarkdownlintIssues || hasBiomeSchemaIssues;
|
|
1689
|
-
if (hasIssues) yield* Effect.log(`${check_WARNING} Some issues found. Run 'savvy init' to fix.`);
|
|
1690
|
-
else yield* Effect.log(`${check_CHECK_MARK} Lint-staged is configured correctly.`);
|
|
1691
|
-
});
|
|
1692
|
-
}
|
|
1693
|
-
Command.make("check", {
|
|
1694
|
-
quiet: check_quietOption
|
|
1695
|
-
}, (opts)=>runLintCheck(opts)).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
|
|
1696
|
-
const DEFAULT_CHANGESET_DIR = ".changeset";
|
|
1697
|
-
const changesetDirOption = Options.text("changeset-dir").pipe(Options.withDescription("Path to the changeset directory"), Options.withDefault(DEFAULT_CHANGESET_DIR));
|
|
1698
|
-
const commands_check_quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings from lint check"), Options.withDefault(false));
|
|
1699
|
-
function runCheck(steps) {
|
|
1700
|
-
return Effect.all([
|
|
1701
|
-
steps.changeset,
|
|
1702
|
-
steps.commit,
|
|
1703
|
-
steps.lint
|
|
1704
|
-
], {
|
|
1705
|
-
concurrency: 1,
|
|
1706
|
-
mode: "validate"
|
|
1707
|
-
}).pipe(Effect.asVoid);
|
|
1708
|
-
}
|
|
1709
|
-
const _checkCommand = Command.make("check", {
|
|
1710
|
-
changesetDir: changesetDirOption,
|
|
1711
|
-
quiet: commands_check_quietOption
|
|
1712
|
-
}, (opts)=>runCheck({
|
|
1713
|
-
changeset: runChangesetCheck(opts.changesetDir),
|
|
1714
|
-
commit: runCommitCheck(),
|
|
1715
|
-
lint: runLintCheck({
|
|
1716
|
-
quiet: opts.quiet
|
|
1717
|
-
})
|
|
1718
|
-
})).pipe(Command.withDescription("Validate all Silk Suite tool configurations in one pass"));
|
|
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;
|
|
1872
|
-
const execFileP = promisify(execFile);
|
|
1873
|
-
function buildPostCommitAdvice(i) {
|
|
1874
|
-
const lines = [];
|
|
1875
|
-
if (i.commitlintFailed) lines.push("The commit you just created fails commitlint validation. Run pnpm exec commitlint --last to see the violations, then fix with git commit --amend.");
|
|
1876
|
-
if (i.autoSignEnabled) {
|
|
1877
|
-
if ("N" === i.sigStatus) lines.push("The commit was unsigned but commit.gpgsign=true is configured. Re-sign with: git commit --amend --no-edit -S");
|
|
1878
|
-
else if ([
|
|
1879
|
-
"B",
|
|
1880
|
-
"X",
|
|
1881
|
-
"Y",
|
|
1882
|
-
"R",
|
|
1883
|
-
"E"
|
|
1884
|
-
].includes(i.sigStatus)) lines.push(`The commit signature status is '${i.sigStatus}' (B=bad, X=expired sig, Y=expired key, R=revoked, E=missing key). Investigate your signing setup and amend.`);
|
|
1885
|
-
}
|
|
1886
|
-
if (null !== i.branchTicketId && !i.bodyHasClosing) lines.push(`Branch implies ticket #${i.branchTicketId} but the commit body has no Closes/Fixes/Resolves trailer for it. If this commit closes #${i.branchTicketId}, amend with: git commit --amend --no-edit --trailer "Closes: #${i.branchTicketId}"`);
|
|
1887
|
-
return 0 === lines.length ? null : lines.join("\n\n");
|
|
1888
|
-
}
|
|
1889
|
-
function buildCommitlintInvocation(pm, configPath) {
|
|
1890
|
-
const tail = configPath ? [
|
|
1891
|
-
"commitlint",
|
|
1892
|
-
"--config",
|
|
1893
|
-
configPath,
|
|
1894
|
-
"--last"
|
|
1895
|
-
] : [
|
|
1896
|
-
"commitlint",
|
|
1897
|
-
"--last"
|
|
1898
|
-
];
|
|
1899
|
-
switch(pm){
|
|
1900
|
-
case "pnpm":
|
|
1901
|
-
return {
|
|
1902
|
-
command: "pnpm",
|
|
1903
|
-
args: [
|
|
1904
|
-
"exec",
|
|
1905
|
-
...tail
|
|
1906
|
-
]
|
|
1907
|
-
};
|
|
1908
|
-
case "yarn":
|
|
1909
|
-
return {
|
|
1910
|
-
command: "yarn",
|
|
1911
|
-
args: [
|
|
1912
|
-
"exec",
|
|
1913
|
-
...tail
|
|
1914
|
-
]
|
|
1915
|
-
};
|
|
1916
|
-
case "bun":
|
|
1917
|
-
return {
|
|
1918
|
-
command: "bunx",
|
|
1919
|
-
args: tail
|
|
1920
|
-
};
|
|
1921
|
-
case "npm":
|
|
1922
|
-
return {
|
|
1923
|
-
command: "npx",
|
|
1924
|
-
args: [
|
|
1925
|
-
"--no",
|
|
1926
|
-
"--",
|
|
1927
|
-
...tail
|
|
1928
|
-
]
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
async function getRepoRoot() {
|
|
1933
|
-
try {
|
|
1934
|
-
const { stdout } = await execFileP("git", [
|
|
1935
|
-
"rev-parse",
|
|
1936
|
-
"--show-toplevel"
|
|
1937
|
-
]);
|
|
1938
|
-
return stdout.trim();
|
|
1939
|
-
} catch {
|
|
1940
|
-
return process.cwd();
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
async function runCommitlintLast() {
|
|
1944
|
-
const root = await getRepoRoot();
|
|
1945
|
-
const pm = await Commitlint.detectPackageManager(root);
|
|
1946
|
-
const configPath = await Commitlint.readCommitlintConfigPath(root);
|
|
1947
|
-
const { command, args } = buildCommitlintInvocation(pm, configPath);
|
|
1948
|
-
try {
|
|
1949
|
-
await execFileP(command, args, {
|
|
1950
|
-
cwd: root
|
|
1951
|
-
});
|
|
1952
|
-
return false;
|
|
1953
|
-
} catch {
|
|
1954
|
-
return true;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
async function readSignatureStatus() {
|
|
1958
|
-
try {
|
|
1959
|
-
const { stdout } = await execFileP("git", [
|
|
1960
|
-
"log",
|
|
1961
|
-
"-1",
|
|
1962
|
-
"--format=%G?"
|
|
1963
|
-
]);
|
|
1964
|
-
return stdout.trim();
|
|
1965
|
-
} catch {
|
|
1966
|
-
return "N";
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
async function readLastCommitBody() {
|
|
1970
|
-
try {
|
|
1971
|
-
const { stdout } = await execFileP("git", [
|
|
1972
|
-
"log",
|
|
1973
|
-
"-1",
|
|
1974
|
-
"--format=%B"
|
|
1975
|
-
]);
|
|
1976
|
-
return stdout;
|
|
1977
|
-
} catch {
|
|
1978
|
-
return "";
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
const postCommitVerifyCommand = Command.make("post-commit-verify", {}, ()=>Effect.gen(function*() {
|
|
1982
|
-
const branch = yield* Commitlint.readBranchInfo();
|
|
1983
|
-
const signing = yield* Commitlint.readSigningDiagnostic();
|
|
1984
|
-
const commitlintFailed = yield* Effect.promise(runCommitlintLast);
|
|
1985
|
-
const sigStatus = yield* Effect.promise(readSignatureStatus);
|
|
1986
|
-
const body = yield* Effect.promise(readLastCommitBody);
|
|
1987
|
-
const bodyHasClosing = null !== branch.inferredTicketId && Commitlint.hasClosingTrailer(body, branch.inferredTicketId);
|
|
1988
|
-
const advice = buildPostCommitAdvice({
|
|
1989
|
-
commitlintFailed,
|
|
1990
|
-
sigStatus,
|
|
1991
|
-
autoSignEnabled: signing.autoSignEnabled,
|
|
1992
|
-
branchTicketId: branch.inferredTicketId,
|
|
1993
|
-
bodyHasClosing
|
|
1994
|
-
});
|
|
1995
|
-
if (null !== advice) yield* Effect.sync(()=>process.stdout.write(`${JSON.stringify(Commitlint.postToolUseAdvise(advice))}\n`));
|
|
1996
|
-
}).pipe(Effect.provide(Commitlint.HookSilencer))).pipe(Command.withDescription("Verify the most recent commit (commitlint replay + signature + closes trailer)"));
|
|
1997
|
-
async function evaluateMessage(command, ctx) {
|
|
1998
|
-
const parsed = Commitlint.parseBashCommand(command);
|
|
1999
|
-
if ("unknown" === parsed.kind) return Commitlint.preToolUseSilent();
|
|
2000
|
-
if (null === parsed.message) return Commitlint.preToolUseSilent();
|
|
2001
|
-
const hits = [];
|
|
2002
|
-
const collect = async (eff)=>{
|
|
2003
|
-
const h = await Effect.runPromise(eff);
|
|
2004
|
-
if (h) hits.push(h);
|
|
2005
|
-
};
|
|
2006
|
-
await collect(Commitlint.forbiddenContentRule.check({
|
|
2007
|
-
message: parsed.message
|
|
2008
|
-
}, void 0));
|
|
2009
|
-
await collect(Commitlint.planLeakageRule.check({
|
|
2010
|
-
message: parsed.message
|
|
2011
|
-
}, void 0));
|
|
2012
|
-
await collect(Commitlint.softWrapRule.check({
|
|
2013
|
-
message: parsed.message
|
|
2014
|
-
}, void 0));
|
|
2015
|
-
await collect(Commitlint.verbosityRule.check({
|
|
2016
|
-
message: parsed.message
|
|
2017
|
-
}, void 0));
|
|
2018
|
-
await collect(Commitlint.closesTrailerRule.check({
|
|
2019
|
-
message: parsed.message
|
|
2020
|
-
}, {
|
|
2021
|
-
branchInfo: ctx.branchInfo,
|
|
2022
|
-
openIssues: ctx.openIssues
|
|
2023
|
-
}));
|
|
2024
|
-
await collect(Commitlint.signingFlagConflictRule.check({
|
|
2025
|
-
flags: parsed.flags
|
|
2026
|
-
}, {
|
|
2027
|
-
autoSignEnabled: ctx.autoSignEnabled
|
|
2028
|
-
}));
|
|
2029
|
-
const { deny, advise } = Commitlint.partitionHits(hits);
|
|
2030
|
-
if (deny.length > 0) return Commitlint.preToolUseDeny([
|
|
2031
|
-
"The following rules denied this commit message:",
|
|
2032
|
-
...deny.map((h)=>`- ${h.message}`)
|
|
2033
|
-
].join("\n"));
|
|
2034
|
-
if (advise.length > 0) return Commitlint.preToolUseAdvise([
|
|
2035
|
-
"The commit message you are about to write has issues that should be addressed:",
|
|
2036
|
-
...advise.map((h)=>`- ${h.message}`)
|
|
2037
|
-
].join("\n"));
|
|
2038
|
-
return Commitlint.preToolUseSilent();
|
|
2039
|
-
}
|
|
2040
|
-
const preCommitMessageCommand = Command.make("pre-commit-message", {}, ()=>Effect.gen(function*() {
|
|
2041
|
-
const stdin = yield* Effect.promise(readStdin);
|
|
2042
|
-
let envelope;
|
|
2043
|
-
try {
|
|
2044
|
-
envelope = Schema.decodeUnknownSync(Commitlint.PreToolUseEnvelope)(JSON.parse(stdin));
|
|
2045
|
-
} catch {
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
const command = String(envelope.tool_input.command ?? "");
|
|
2049
|
-
if (!command) return;
|
|
2050
|
-
const branchInfo = yield* Commitlint.readBranchInfo();
|
|
2051
|
-
const signing = yield* Commitlint.readSigningDiagnostic();
|
|
2052
|
-
const issuesPath = resolve(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), Commitlint.ISSUES_CACHE_RELATIVE_PATH);
|
|
2053
|
-
const issues = (yield* Commitlint.readOpenIssuesFromCache(issuesPath)) ?? [];
|
|
2054
|
-
const out = yield* Effect.promise(()=>evaluateMessage(command, {
|
|
2055
|
-
branchInfo,
|
|
2056
|
-
openIssues: issues,
|
|
2057
|
-
autoSignEnabled: signing.autoSignEnabled
|
|
2058
|
-
}));
|
|
2059
|
-
if (null !== out) yield* Effect.sync(()=>process.stdout.write(`${JSON.stringify(out)}\n`));
|
|
2060
|
-
}).pipe(Effect.provide(Commitlint.HookSilencer))).pipe(Command.withDescription("Validate a candidate git commit / gh pr command's message"));
|
|
2061
|
-
async function readStdin() {
|
|
2062
|
-
const chunks = [];
|
|
2063
|
-
for await (const chunk of process.stdin)chunks.push(chunk);
|
|
2064
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2065
|
-
}
|
|
2066
|
-
const sessionStartCommand = Command.make("session-start", {}, ()=>Effect.gen(function*() {
|
|
2067
|
-
const branch = yield* Commitlint.readBranchInfo();
|
|
2068
|
-
const signing = yield* Commitlint.readSigningDiagnostic();
|
|
2069
|
-
const issuesCachePath = resolve(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), Commitlint.ISSUES_CACHE_RELATIVE_PATH);
|
|
2070
|
-
const issues = yield* Commitlint.readOrFetchOpenIssues(issuesCachePath);
|
|
2071
|
-
const blocks = [];
|
|
2072
|
-
blocks.push(buildSkillDirectiveBlock());
|
|
2073
|
-
if (branch.branch) blocks.push(buildBranchBlock(branch.branch, branch.inferredTicketId, issues));
|
|
2074
|
-
blocks.push(buildSigningBlock(signing));
|
|
2075
|
-
const ctx = `<EXTREMELY_IMPORTANT>\n${blocks.join("\n\n")}\n</EXTREMELY_IMPORTANT>`;
|
|
2076
|
-
const out = Commitlint.sessionStartContext(ctx);
|
|
2077
|
-
yield* Effect.sync(()=>process.stdout.write(`${JSON.stringify(out)}\n`));
|
|
2078
|
-
}).pipe(Effect.provide(Commitlint.HookSilencer))).pipe(Command.withDescription("Emit the SessionStart additionalContext for the commitlint plugin"));
|
|
2079
|
-
function buildSkillDirectiveBlock() {
|
|
2080
|
-
return "Before you run git commit, gh pr create, gh pr edit, amend a commit, or compose\nany commit message, you MUST invoke the commitlint:commit-create skill. It\ncontains the complete type enum, tdd scope grammar, subject and body rules, DCO\nsignoff format, Closes trailer pattern, and signing guidance.\n\nYOU DO NOT HAVE A CHOICE. Even if you believe the commit message is obvious,\nyou MUST load the skill first. This is not negotiable. You cannot rationalize\nyour way around it. The commit-msg hook will reject messages that violate the\nrules defined in that skill.";
|
|
2081
|
-
}
|
|
2082
|
-
function buildBranchBlock(branch, ticketId, issues) {
|
|
2083
|
-
const lines = [
|
|
2084
|
-
"<branch_context>",
|
|
2085
|
-
`<current_branch>${branch}</current_branch>`,
|
|
2086
|
-
`<inferred_ticket_id>${ticketId ?? "null"}</inferred_ticket_id>`
|
|
2087
|
-
];
|
|
2088
|
-
if (issues && issues.length > 0) {
|
|
2089
|
-
lines.push("<open_issues_in_repo>");
|
|
2090
|
-
for (const i of issues)lines.push(` - #${i.number} ${i.title}`);
|
|
2091
|
-
lines.push("</open_issues_in_repo>");
|
|
2092
|
-
}
|
|
2093
|
-
lines.push("</branch_context>");
|
|
2094
|
-
return lines.join("\n");
|
|
2095
|
-
}
|
|
2096
|
-
function buildSigningBlock(d) {
|
|
2097
|
-
const lines = [
|
|
2098
|
-
"<signing_diagnostic>",
|
|
2099
|
-
`<format>${d.format}</format>`,
|
|
2100
|
-
`<auto_sign_enabled>${d.autoSignEnabled}</auto_sign_enabled>`,
|
|
2101
|
-
`<signing_key_configured>${d.signingKeyConfigured}</signing_key_configured>`,
|
|
2102
|
-
`<key_resolves>${d.keyResolves}</key_resolves>`,
|
|
2103
|
-
`<agent_responsive>${d.agentResponsive}</agent_responsive>`,
|
|
2104
|
-
"<warnings>"
|
|
2105
|
-
];
|
|
2106
|
-
for (const w of d.warnings)lines.push(` - ${w}`);
|
|
2107
|
-
lines.push("</warnings>", "</signing_diagnostic>");
|
|
2108
|
-
return lines.join("\n");
|
|
2109
|
-
}
|
|
2110
|
-
const TRIGGER = /(\bcommit\b|\bcommitting\b|\bship (it|this)\b|\bwrap (it )?up\b|\b(create|open) a (pr|pull request)\b|\bfinalize\b|\/finalize\b|\bsquash\b|\bamend\b)/i;
|
|
2111
|
-
function reminderForPrompt(prompt) {
|
|
2112
|
-
if (!TRIGGER.test(prompt)) return null;
|
|
2113
|
-
return "<commit_reminder>\nBefore composing this commit message, invoke the commitlint:commit-create skill.\nIt defines the complete type enum, scope rules, DCO signoff format, and body\nconstraints enforced by @savvy-web/commitlint.\n</commit_reminder>";
|
|
2114
|
-
}
|
|
2115
|
-
const userPromptSubmitCommand = Command.make("user-prompt-submit", {}, ()=>Effect.gen(function*() {
|
|
2116
|
-
const stdin = yield* Effect.promise(user_prompt_submit_readStdin);
|
|
2117
|
-
let envelope;
|
|
2118
|
-
try {
|
|
2119
|
-
envelope = Schema.decodeUnknownSync(Commitlint.UserPromptSubmitEnvelope)(JSON.parse(stdin));
|
|
2120
|
-
} catch {
|
|
2121
|
-
return;
|
|
2122
|
-
}
|
|
2123
|
-
const reminder = reminderForPrompt(envelope.prompt);
|
|
2124
|
-
if (null === reminder) return;
|
|
2125
|
-
yield* Effect.sync(()=>process.stdout.write(`${JSON.stringify(Commitlint.userPromptSubmitContext(reminder))}\n`));
|
|
2126
|
-
}).pipe(Effect.provide(Commitlint.HookSilencer))).pipe(Command.withDescription("Inject a commit-quality reminder when the user prompt mentions commits"));
|
|
2127
|
-
async function user_prompt_submit_readStdin() {
|
|
2128
|
-
const chunks = [];
|
|
2129
|
-
for await (const chunk of process.stdin)chunks.push(chunk);
|
|
2130
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
2131
|
-
}
|
|
2132
|
-
const hookCommand = Command.make("hook").pipe(Command.withSubcommands([
|
|
2133
|
-
sessionStartCommand,
|
|
2134
|
-
preCommitMessageCommand,
|
|
2135
|
-
postCommitVerifyCommand,
|
|
2136
|
-
userPromptSubmitCommand
|
|
2137
|
-
])).pipe(Command.withDescription("Internal hook handlers used by the @savvy-web/commitlint plugin"));
|
|
2138
|
-
const _commitCommand = Command.make("commit").pipe(Command.withSubcommands([
|
|
2139
|
-
hookCommand
|
|
2140
|
-
]), Command.withDescription("Commit standards: config, checks, and Claude hook handlers"));
|
|
2141
|
-
const commitCommand = _commitCommand;
|
|
2142
|
-
const init_CHECK_MARK = "✓";
|
|
2143
|
-
const init_WARNING = "⚠";
|
|
2144
|
-
const init_EXECUTABLE_MODE = 493;
|
|
2145
|
-
const init_JSONC_FORMAT = {
|
|
2146
|
-
tabSize: 1,
|
|
2147
|
-
insertSpaces: false
|
|
2148
|
-
};
|
|
2149
|
-
const PRE_COMMIT_HEADER = "#!/usr/bin/env sh\n# Pre-commit hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
2150
|
-
const init_HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
2151
|
-
function presetIncludesShellScripts(preset) {
|
|
2152
|
-
return "minimal" !== preset;
|
|
2153
|
-
}
|
|
2154
|
-
function presetIncludesMarkdown(preset) {
|
|
2155
|
-
return "minimal" !== preset;
|
|
2156
|
-
}
|
|
2157
|
-
function generateConfigContent(preset) {
|
|
2158
|
-
return `/**
|
|
2159
|
-
* lint-staged configuration
|
|
2160
|
-
* Generated by savvy lint init
|
|
2161
|
-
*/
|
|
2162
|
-
import { Preset } from "@savvy-web/silk/lint";
|
|
2163
|
-
|
|
2164
|
-
export default Preset.${preset}();
|
|
2165
|
-
`;
|
|
2166
|
-
}
|
|
2167
|
-
function writeMarkdownlintConfig(fs, preset, force) {
|
|
2168
|
-
return Effect.gen(function*() {
|
|
2169
|
-
const configExists = yield* fs.exists(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
2170
|
-
const fullTemplate = JSON.stringify(Lint.MARKDOWNLINT_TEMPLATE, null, "\t");
|
|
2171
|
-
if (!configExists) {
|
|
2172
|
-
yield* fs.makeDirectory("lib/configs", {
|
|
2173
|
-
recursive: true
|
|
2174
|
-
});
|
|
2175
|
-
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
2176
|
-
yield* Effect.log(`${init_CHECK_MARK} Created ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
if ("silk" !== preset) return void (yield* Effect.log(`${init_CHECK_MARK} ${Lint.MARKDOWNLINT_CONFIG_PATH}: exists (not managed by ${preset} preset)`));
|
|
2180
|
-
if (force) {
|
|
2181
|
-
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
2182
|
-
yield* Effect.log(`${init_CHECK_MARK} Replaced ${Lint.MARKDOWNLINT_CONFIG_PATH} (--force)`);
|
|
2183
|
-
return;
|
|
2184
|
-
}
|
|
2185
|
-
const existingText = yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
2186
|
-
const existingParsed = yield* parse(existingText);
|
|
2187
|
-
let updatedText = existingText;
|
|
2188
|
-
let schemaUpdated = false;
|
|
2189
|
-
if (existingParsed.$schema !== Lint.MARKDOWNLINT_SCHEMA) {
|
|
2190
|
-
const edits = yield* modify(updatedText, [
|
|
2191
|
-
"$schema"
|
|
2192
|
-
], Lint.MARKDOWNLINT_SCHEMA, {
|
|
2193
|
-
formattingOptions: init_JSONC_FORMAT
|
|
2194
|
-
});
|
|
2195
|
-
updatedText = yield* applyEdits(updatedText, edits);
|
|
2196
|
-
schemaUpdated = true;
|
|
2197
|
-
}
|
|
2198
|
-
const existingConfig = existingParsed.config;
|
|
2199
|
-
const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, Lint.MARKDOWNLINT_CONFIG);
|
|
2200
|
-
if (!configMatches) {
|
|
2201
|
-
yield* Effect.log(`${init_WARNING} ${Lint.MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (use --force to overwrite)`);
|
|
2202
|
-
if (schemaUpdated) {
|
|
2203
|
-
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
2204
|
-
yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
2205
|
-
}
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2208
|
-
if (schemaUpdated) {
|
|
2209
|
-
yield* fs.writeFileString(Lint.MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
2210
|
-
yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${Lint.MARKDOWNLINT_CONFIG_PATH}`);
|
|
2211
|
-
} else yield* Effect.log(`${init_CHECK_MARK} ${Lint.MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
2212
|
-
});
|
|
2213
|
-
}
|
|
2214
|
-
function syncBiomeSchemas() {
|
|
2215
|
-
return Effect.gen(function*() {
|
|
2216
|
-
const version = process.env.__BIOME_PEER_VERSION__;
|
|
2217
|
-
if (!version) return;
|
|
2218
|
-
const syncer = yield* BiomeSchemaSync;
|
|
2219
|
-
const result = yield* syncer.sync(version);
|
|
2220
|
-
for (const configPath of result.current)yield* Effect.log(`${init_CHECK_MARK} ${configPath}: biome $schema up-to-date`);
|
|
2221
|
-
for (const configPath of result.updated)yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${configPath}`);
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
|
-
const lint_init_forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the pre-commit hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
2225
|
-
const init_configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(Lint.DEFAULT_CONFIG_PATH));
|
|
2226
|
-
const presetOption = Options.choice("preset", [
|
|
2227
|
-
"minimal",
|
|
2228
|
-
"standard",
|
|
2229
|
-
"silk"
|
|
2230
|
-
]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
2231
|
-
function init_makeExecutable(path) {
|
|
2232
|
-
return Effect.tryPromise({
|
|
2233
|
-
try: ()=>chmod(path, init_EXECUTABLE_MODE),
|
|
2234
|
-
catch: (e)=>new Error(String(e))
|
|
2235
|
-
});
|
|
2236
|
-
}
|
|
2237
|
-
function init_ensureHookFile(path, header) {
|
|
2238
|
-
return Effect.gen(function*() {
|
|
2239
|
-
const fs = yield* FileSystem.FileSystem;
|
|
2240
|
-
const exists = yield* fs.exists(path);
|
|
2241
|
-
if (!exists) yield* fs.writeFileString(path, header);
|
|
2242
|
-
});
|
|
2243
|
-
}
|
|
2244
|
-
function runLintInit(opts) {
|
|
2245
|
-
const { force, config, preset } = opts;
|
|
2246
|
-
return Effect.gen(function*() {
|
|
2247
|
-
const fs = yield* FileSystem.FileSystem;
|
|
2248
|
-
const ms = yield* ManagedSection;
|
|
2249
|
-
if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
|
|
2250
|
-
yield* Effect.log("Initializing lint-staged configuration...\n");
|
|
2251
|
-
yield* fs.makeDirectory(".husky", {
|
|
2252
|
-
recursive: true
|
|
2253
|
-
});
|
|
2254
|
-
if (force) yield* fs.writeFileString(Lint.HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
2255
|
-
else yield* init_ensureHookFile(Lint.HUSKY_HOOK_PATH, PRE_COMMIT_HEADER);
|
|
2256
|
-
const preCommitResults = yield* ms.syncMany(Lint.HUSKY_HOOK_PATH, [
|
|
2257
|
-
SavvyBaseSection.block(savvyBasePreamble()),
|
|
2258
|
-
Lint.savvyLintBlock(config)
|
|
2259
|
-
]);
|
|
2260
|
-
yield* init_makeExecutable(Lint.HUSKY_HOOK_PATH);
|
|
2261
|
-
yield* Effect.log(`${init_CHECK_MARK} ${force ? "Replaced" : "Synced"} ${Lint.HUSKY_HOOK_PATH} (${preCommitResults.map((r)=>r._tag).join(", ")})`);
|
|
2262
|
-
if (presetIncludesShellScripts(preset)) for (const hookPath of [
|
|
2263
|
-
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
2264
|
-
Lint.POST_MERGE_HOOK_PATH
|
|
2265
|
-
]){
|
|
2266
|
-
yield* init_ensureHookFile(hookPath, init_HYGIENE_HEADER);
|
|
2267
|
-
yield* ms.remove(hookPath, Lint.LegacySavvyLintHygieneDef);
|
|
2268
|
-
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
2269
|
-
yield* init_makeExecutable(hookPath);
|
|
2270
|
-
yield* Effect.log(`${init_CHECK_MARK} Synced ${hookPath}`);
|
|
2271
|
-
}
|
|
2272
|
-
if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
|
|
2273
|
-
yield* syncBiomeSchemas().pipe(Effect.catchTag("BiomeSyncError", (e)=>Effect.log(`${init_WARNING} Could not sync biome $schema: ${e.message}`)));
|
|
2274
|
-
const configExists = yield* fs.exists(config);
|
|
2275
|
-
if (configExists && !force) yield* Effect.log(`${init_WARNING} ${config} already exists (use --force to overwrite)`);
|
|
2276
|
-
else {
|
|
2277
|
-
const configDir = dirname(config);
|
|
2278
|
-
if (configDir && "." !== configDir) yield* fs.makeDirectory(configDir, {
|
|
2279
|
-
recursive: true
|
|
2280
|
-
});
|
|
2281
|
-
yield* fs.writeFileString(config, generateConfigContent(preset));
|
|
2282
|
-
yield* Effect.log(`${init_CHECK_MARK} Created ${config} (preset: ${preset})`);
|
|
2283
|
-
}
|
|
2284
|
-
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
2287
|
-
Command.make("init", {
|
|
2288
|
-
force: lint_init_forceOption,
|
|
2289
|
-
config: init_configOption,
|
|
2290
|
-
preset: presetOption
|
|
2291
|
-
}, (opts)=>runLintInit(opts)).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
|
|
2292
|
-
const DEFAULT_COMMIT_CONFIG = "lib/configs/commitlint.config.ts";
|
|
2293
|
-
const DEFAULT_LINT_CONFIG = "lib/configs/lint-staged.config.ts";
|
|
2294
|
-
const commands_init_forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing config files and hooks across all tools"), Options.withDefault(false));
|
|
2295
|
-
const commitConfigOption = Options.text("commit-config").pipe(Options.withDescription("Relative path for the commitlint config file"), Options.withDefault(DEFAULT_COMMIT_CONFIG));
|
|
2296
|
-
const lintConfigOption = Options.text("lint-config").pipe(Options.withDescription("Relative path for the lint-staged config file"), Options.withDefault(DEFAULT_LINT_CONFIG));
|
|
2297
|
-
const lintPresetOption = Options.choice("lint-preset", [
|
|
2298
|
-
"minimal",
|
|
2299
|
-
"standard",
|
|
2300
|
-
"silk"
|
|
2301
|
-
]).pipe(Options.withDescription("lint-staged preset: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
2302
|
-
function runInit(steps) {
|
|
2303
|
-
return Effect.gen(function*() {
|
|
2304
|
-
yield* steps.changeset;
|
|
2305
|
-
yield* steps.commit;
|
|
2306
|
-
yield* steps.lint;
|
|
2307
|
-
});
|
|
2308
|
-
}
|
|
2309
|
-
const _initCommand = Command.make("init", {
|
|
2310
|
-
force: commands_init_forceOption,
|
|
2311
|
-
commitConfig: commitConfigOption,
|
|
2312
|
-
lintConfig: lintConfigOption,
|
|
2313
|
-
lintPreset: lintPresetOption
|
|
2314
|
-
}, (opts)=>runInit({
|
|
2315
|
-
changeset: runChangesetInit({
|
|
2316
|
-
force: opts.force,
|
|
2317
|
-
quiet: false,
|
|
2318
|
-
skipMarkdownlint: false,
|
|
2319
|
-
check: false
|
|
2320
|
-
}),
|
|
2321
|
-
commit: runCommitInit({
|
|
2322
|
-
force: opts.force,
|
|
2323
|
-
config: opts.commitConfig
|
|
2324
|
-
}),
|
|
2325
|
-
lint: runLintInit({
|
|
2326
|
-
force: opts.force,
|
|
2327
|
-
config: opts.lintConfig,
|
|
2328
|
-
preset: opts.lintPreset
|
|
2329
|
-
})
|
|
2330
|
-
})).pipe(Command.withDescription("Bootstrap a repository for all Silk Suite tools in one pass"));
|
|
2331
|
-
const commands_init_initCommand = _initCommand;
|
|
2332
|
-
const YAML_STRINGIFY_OPTIONS = {
|
|
2333
|
-
indent: 2,
|
|
2334
|
-
lineWidth: 0,
|
|
2335
|
-
singleQuote: false
|
|
2336
|
-
};
|
|
2337
|
-
const filesArg = Args.repeated(Args.file({
|
|
2338
|
-
name: "files",
|
|
2339
|
-
exists: "yes"
|
|
2340
|
-
}));
|
|
2341
|
-
const packageJsonCommand = Command.make("package-json", {
|
|
2342
|
-
files: filesArg
|
|
2343
|
-
}, ({ files })=>Effect.sync(()=>{
|
|
2344
|
-
for (const filepath of files){
|
|
2345
|
-
const content = readFileSync(filepath, "utf-8");
|
|
2346
|
-
const sorted = Lint.PackageJson.sortContent(content);
|
|
2347
|
-
if (sorted !== content) writeFileSync(filepath, sorted, "utf-8");
|
|
2348
|
-
}
|
|
2349
|
-
}));
|
|
2350
|
-
const pnpmWorkspaceCommand = Command.make("pnpm-workspace", {}, ()=>Effect.sync(()=>{
|
|
2351
|
-
const filepath = "pnpm-workspace.yaml";
|
|
2352
|
-
if (!existsSync(filepath)) return;
|
|
2353
|
-
const content = readFileSync(filepath, "utf-8");
|
|
2354
|
-
const parsed = external_yaml_parse(content);
|
|
2355
|
-
const sorted = Lint.PnpmWorkspace.sortContent(parsed);
|
|
2356
|
-
const formatted = stringify(sorted, YAML_STRINGIFY_OPTIONS);
|
|
2357
|
-
writeFileSync(filepath, formatted, "utf-8");
|
|
2358
|
-
}));
|
|
2359
|
-
const yamlCommand = Command.make("yaml", {
|
|
2360
|
-
files: filesArg
|
|
2361
|
-
}, ({ files })=>Effect.gen(function*() {
|
|
2362
|
-
for (const filepath of files)yield* Effect.promise(()=>Lint.Yaml.formatFile(filepath));
|
|
2363
|
-
}));
|
|
2364
|
-
const _fmtCommand = Command.make("fmt").pipe(Command.withSubcommands([
|
|
2365
|
-
packageJsonCommand,
|
|
2366
|
-
pnpmWorkspaceCommand,
|
|
2367
|
-
yamlCommand
|
|
2368
|
-
]));
|
|
2369
|
-
const fmtCommand = _fmtCommand;
|
|
2370
|
-
const _lintCommand = Command.make("lint").pipe(Command.withSubcommands([
|
|
2371
|
-
fmtCommand
|
|
2372
|
-
]), Command.withDescription("Code-quality: lint-staged config, checks, and in-place formatting"));
|
|
2373
|
-
const lint_lintCommand = _lintCommand;
|
|
2374
|
-
const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
2375
|
-
commands_init_initCommand,
|
|
2376
|
-
commands_check_checkCommand,
|
|
2377
|
-
cleanCommand,
|
|
2378
|
-
commitCommand,
|
|
2379
|
-
changesetCommand,
|
|
2380
|
-
lint_lintCommand
|
|
2381
|
-
]));
|
|
2382
|
-
const cli = Command.run(rootCommand, {
|
|
2383
|
-
name: "savvy",
|
|
2384
|
-
version: "0.3.0"
|
|
2385
|
-
});
|
|
2386
|
-
const WorkspaceLive = Layer.mergeAll(WorkspaceRootLive, PackageManagerDetectorLive, WorkspaceDiscoveryLive.pipe(Layer.provide(WorkspaceRootLive)));
|
|
2387
|
-
const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, ManagedSectionLive, BiomeSchemaSyncLive, ConfigDiscoveryLive, SilkPublishabilityDetectorLive, Changesets.WorkspaceSnapshotReaderLive);
|
|
2388
|
-
const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ConfigInspectorLive));
|
|
2389
|
-
const AppLive = Layer.mergeAll(ToolDiscoveryLive, VersioningStrategyLive, InspectorAndAnalyzerLive).pipe(Layer.provideMerge(BaseLive), Layer.provideMerge(NodeContext.layer));
|
|
2390
|
-
function runCli() {
|
|
2391
|
-
const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(AppLive));
|
|
2392
|
-
NodeRuntime.runMain(main);
|
|
2393
|
-
}
|
|
2394
|
-
export { changesetCommand, commands_check_checkCommand as checkCommand, commands_init_initCommand as initCommand, commitCommand, lint_lintCommand as lintCommand, runChangesetCheck, runChangesetInit, runCheck, runCli, runCommitCheck, runCommitInit, runInit, runLintCheck, runLintInit };
|