@savvy-web/cli 0.3.1 → 0.4.1
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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/changeset/commands/release-surface.ts
|
|
7
|
+
/**
|
|
8
|
+
* `release-surface` command -- list every path owned by a named package.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* For a given package name, emits the workspace directory, every
|
|
12
|
+
* `additionalScopes` glob and its materialized files, and every
|
|
13
|
+
* `versionFiles` entry and its targets. Useful for debugging "why is this
|
|
14
|
+
* path attributed to this package?" or "what's actually in this package's
|
|
15
|
+
* release surface right now?"
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```bash
|
|
19
|
+
* savvy changeset release-surface @savvy-web/changesets
|
|
20
|
+
* savvy changeset release-surface @scope/foo --json
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
const { ConfigInspector, ConfigurationError } = Changesets;
|
|
26
|
+
/* v8 ignore start -- CLI option definitions */
|
|
27
|
+
const packageArg = Args.text({ name: "package" });
|
|
28
|
+
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
29
|
+
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
30
|
+
/* v8 ignore stop */
|
|
31
|
+
/**
|
|
32
|
+
* Render a single package's resolved scope as human-readable text.
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
function renderHuman(pkg) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`Package: ${pkg.name} v${pkg.version}`);
|
|
39
|
+
lines.push(`Workspace: ${pkg.workspaceDir}`);
|
|
40
|
+
if (pkg.additionalScopes.length === 0 && pkg.versionFiles.length === 0) {
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push("(no additionalScopes or versionFiles — workspace dir is the entire release surface)");
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
if (pkg.additionalScopes.length > 0) {
|
|
46
|
+
lines.push("");
|
|
47
|
+
lines.push(`additionalScopes (${pkg.additionalScopes.length} glob${pkg.additionalScopes.length === 1 ? "" : "s"}):`);
|
|
48
|
+
for (const g of pkg.additionalScopes) lines.push(` - ${g}`);
|
|
49
|
+
lines.push(`Resolved files (${pkg.additionalScopeFiles.length}):`);
|
|
50
|
+
for (const f of pkg.additionalScopeFiles) lines.push(` ${f}`);
|
|
51
|
+
}
|
|
52
|
+
if (pkg.versionFiles.length > 0) {
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(`versionFiles (${pkg.versionFiles.length}):`);
|
|
55
|
+
for (const vf of pkg.versionFiles) {
|
|
56
|
+
lines.push(` ${vf.glob} → ${vf.paths.join(", ")}`);
|
|
57
|
+
for (const f of vf.matchedFiles) lines.push(` ${f}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve cwd, invoke `ConfigInspector.inspect`, find the named package's
|
|
64
|
+
* scope, and render it. Sets `process.exitCode = 1` on any error.
|
|
65
|
+
*
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
function runReleaseSurface(cwd, pkgName, json) {
|
|
69
|
+
return Effect.gen(function* () {
|
|
70
|
+
const inspector = yield* ConfigInspector;
|
|
71
|
+
const resolvedCwd = resolve(cwd);
|
|
72
|
+
const config = yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return Effect.fail(err);
|
|
75
|
+
}));
|
|
76
|
+
const scope = config.packages.find((p) => p.name === pkgName);
|
|
77
|
+
if (!scope) {
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return yield* Effect.fail(new ConfigurationError({
|
|
80
|
+
field: `packages["${pkgName}"]`,
|
|
81
|
+
reason: `Package "${pkgName}" is not declared in .changeset/config.json#packages. Declared packages: ${config.packages.map((p) => p.name).join(", ") || "(none)"}.`
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
const output = json ? JSON.stringify(scope, null, 2) : renderHuman(scope);
|
|
85
|
+
yield* Effect.log(output);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/* v8 ignore next 6 -- CLI registration */
|
|
89
|
+
const releaseSurfaceCommand = Command.make("release-surface", {
|
|
90
|
+
package: packageArg,
|
|
91
|
+
cwd: cwdOption,
|
|
92
|
+
json: jsonOption
|
|
93
|
+
}, ({ package: pkgName, cwd, json }) => runReleaseSurface(cwd, pkgName, json)).pipe(Command.withDescription("Print every path owned by a package — workspace dir, additionalScopes, versionFiles"));
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
export { releaseSurfaceCommand };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { requireValidConfig } from "../utils/config-gate.js";
|
|
2
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
3
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/changeset/commands/transform.ts
|
|
9
|
+
/**
|
|
10
|
+
* Transform command -- post-process CHANGELOG.md.
|
|
11
|
+
*
|
|
12
|
+
* Runs all remark transform plugins (section reordering, deduplication,
|
|
13
|
+
* contributor footnotes, issue link references, and format normalization)
|
|
14
|
+
* against a changelog file.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* The command supports three modes:
|
|
18
|
+
* - **Default** -- read the file, transform, and write back in place.
|
|
19
|
+
* - **`--dry-run` / `-n`** -- print the transformed output to stdout
|
|
20
|
+
* without writing.
|
|
21
|
+
* - **`--check` / `-c`** -- compare the transformed output against the
|
|
22
|
+
* original and exit with code 1 if they differ (useful in CI).
|
|
23
|
+
*
|
|
24
|
+
* Before any of those modes run, the command requires a valid
|
|
25
|
+
* `.changeset/config.json` (when one exists). A broken config indicates
|
|
26
|
+
* something structurally wrong with the project — refusing here surfaces
|
|
27
|
+
* that to the user rather than producing output the version step couldn't
|
|
28
|
+
* later corroborate.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```bash
|
|
32
|
+
* savvy changeset transform CHANGELOG.md
|
|
33
|
+
* savvy changeset transform --dry-run CHANGELOG.md
|
|
34
|
+
* savvy changeset transform --check CHANGELOG.md
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
const { ChangelogTransformer } = Changesets;
|
|
40
|
+
/* v8 ignore start -- CLI option definitions; handler tested via runTransform */
|
|
41
|
+
const fileArg = Args.file({ name: "file" }).pipe(Args.withDefault("CHANGELOG.md"));
|
|
42
|
+
const dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Print transformed output instead of writing"), Options.withDefault(false));
|
|
43
|
+
const checkOption = Options.boolean("check").pipe(Options.withAlias("c"), Options.withDescription("Exit 1 if file would change (for CI)"), Options.withDefault(false));
|
|
44
|
+
/* v8 ignore stop */
|
|
45
|
+
/**
|
|
46
|
+
* Run the remark transform pipeline on a single changelog file.
|
|
47
|
+
*
|
|
48
|
+
* Reads the file at `file`, applies all remark transform plugins via
|
|
49
|
+
* {@link ChangelogTransformer.transformContent}, and either writes the result
|
|
50
|
+
* back, prints it to stdout (`dryRun`), or checks for differences (`check`).
|
|
51
|
+
*
|
|
52
|
+
* @param file - Path to the CHANGELOG.md file (resolved relative to cwd)
|
|
53
|
+
* @param dryRun - When `true`, print transformed output instead of writing
|
|
54
|
+
* @param check - When `true`, exit with code 1 if the file would change
|
|
55
|
+
* @returns An Effect that performs the transformation
|
|
56
|
+
*
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
function runTransform(file, dryRun, check) {
|
|
60
|
+
return Effect.gen(function* () {
|
|
61
|
+
const resolved = resolve(file);
|
|
62
|
+
yield* requireValidConfig(dirname(resolved));
|
|
63
|
+
const content = yield* Effect.try(() => readFileSync(resolved, "utf-8"));
|
|
64
|
+
const result = ChangelogTransformer.transformContent(content);
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
yield* Effect.log(result);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (check) {
|
|
70
|
+
if (result !== content) {
|
|
71
|
+
yield* Effect.log(`${resolved} would be modified by transform.`);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
} else yield* Effect.log(`${resolved} is already formatted.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
yield* Effect.try(() => writeFileSync(resolved, result, "utf-8"));
|
|
77
|
+
yield* Effect.log(`Transformed ${resolved}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/* v8 ignore next 5 -- CLI registration; handler tested via runTransform */
|
|
81
|
+
const transformCommand = Command.make("transform", {
|
|
82
|
+
file: fileArg,
|
|
83
|
+
dryRun: dryRunOption,
|
|
84
|
+
check: checkOption
|
|
85
|
+
}, ({ file, dryRun, check }) => runTransform(file, dryRun, check)).pipe(Command.withDescription("Post-process CHANGELOG.md"));
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
export { transformCommand };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Args, Command } from "@effect/cli";
|
|
2
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/changeset/commands/validate-file.ts
|
|
6
|
+
/**
|
|
7
|
+
* Validate-file command — validate a single changeset file.
|
|
8
|
+
*
|
|
9
|
+
* Reads one `.md` file and runs the full lint pipeline against it,
|
|
10
|
+
* outputting machine-readable diagnostics. Designed for use in hooks
|
|
11
|
+
* and editor integrations where only one file needs checking.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```bash
|
|
15
|
+
* savvy changeset validate-file .changeset/cool-lions-sing.md
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const { ChangesetLinter } = Changesets;
|
|
21
|
+
/* v8 ignore next */
|
|
22
|
+
const fileArg = Args.file({ name: "file" });
|
|
23
|
+
/**
|
|
24
|
+
* Run lint validation on a single changeset file.
|
|
25
|
+
*
|
|
26
|
+
* Outputs one line per error in `file:line:col rule message` format.
|
|
27
|
+
* Logs "Valid." when the file passes. Sets `process.exitCode = 1`
|
|
28
|
+
* when errors are found or the file cannot be read.
|
|
29
|
+
*
|
|
30
|
+
* @param filePath - Path to the changeset `.md` file
|
|
31
|
+
* @returns An Effect that performs validation and logs results
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
function runValidateFile(filePath) {
|
|
36
|
+
return Effect.gen(function* () {
|
|
37
|
+
const result = yield* Effect.try(() => ChangesetLinter.validateFile(filePath)).pipe(Effect.catchAll((error) => Effect.gen(function* () {
|
|
38
|
+
yield* Effect.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return null;
|
|
41
|
+
})));
|
|
42
|
+
if (result === null) return;
|
|
43
|
+
for (const msg of result) yield* Effect.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
44
|
+
if (result.length > 0) process.exitCode = 1;
|
|
45
|
+
else yield* Effect.log("Valid.");
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/* v8 ignore next 3 -- CLI registration; handler tested via runValidateFile */
|
|
49
|
+
const validateFileCommand = Command.make("validate-file", { file: fileArg }, ({ file }) => runValidateFile(file)).pipe(Command.withDescription("Validate a single changeset file"));
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { validateFileCommand };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { requireValidConfig } from "../utils/config-gate.js";
|
|
2
|
+
import { Command, Options } from "@effect/cli";
|
|
3
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { PackageManagerDetector, WorkspaceDiscovery } from "workspaces-effect";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/changeset/commands/version.ts
|
|
11
|
+
/**
|
|
12
|
+
* Version command -- orchestrate `changeset version` and changelog transforms.
|
|
13
|
+
*
|
|
14
|
+
* Detects the package manager, validates the config, runs `changeset version`,
|
|
15
|
+
* discovers all workspace CHANGELOG.md files, transforms each one with the
|
|
16
|
+
* remark pipeline, and updates any configured version files.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The command performs six steps:
|
|
20
|
+
* 1. Detect the package manager (`pnpm`, `npm`, `yarn`, `bun`) via
|
|
21
|
+
* `PackageManagerDetector` from `workspaces-effect`.
|
|
22
|
+
* 2. **Require a valid `.changeset/config.json` via {@link ConfigInspector}**.
|
|
23
|
+
* If the config has overlap conflicts, unknown package keys, dual-shape,
|
|
24
|
+
* or schema errors, refuse to run and exit non-zero — a broken config
|
|
25
|
+
* means we cannot determine the right versions to write.
|
|
26
|
+
* 3. Run `changeset version` (skipped with `--dry-run`).
|
|
27
|
+
* 4. Discover all CHANGELOG.md files across workspace packages via
|
|
28
|
+
* `WorkspaceDiscovery` from `workspaces-effect`.
|
|
29
|
+
* 5. Transform each discovered changelog with
|
|
30
|
+
* {@link ChangelogTransformer.transformFile}.
|
|
31
|
+
* 6. Update version files using the **resolved** {@link InspectedConfig}
|
|
32
|
+
* via {@link VersionFiles.processResolvedVersionFiles}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```bash
|
|
36
|
+
* savvy changeset version
|
|
37
|
+
* savvy changeset version --dry-run
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
const { ChangelogTransformer, ConfigInspector, VersionFileError, VersionFiles } = Changesets;
|
|
43
|
+
/**
|
|
44
|
+
* Map package manager to the correct `changeset version` shell command.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
function getChangesetVersionCommand(pm) {
|
|
49
|
+
switch (pm) {
|
|
50
|
+
case "pnpm": return "pnpm exec changeset version";
|
|
51
|
+
case "yarn": return "yarn exec changeset version";
|
|
52
|
+
case "bun": return "bun x changeset version";
|
|
53
|
+
default: return "npx changeset version";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/* v8 ignore start -- CLI option definitions; handler tested via runVersion */
|
|
57
|
+
const dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Skip changeset version, only transform existing CHANGELOGs"), Options.withDefault(false));
|
|
58
|
+
/* v8 ignore stop */
|
|
59
|
+
/**
|
|
60
|
+
* Run the full version orchestration pipeline.
|
|
61
|
+
*
|
|
62
|
+
* Detects the package manager, validates the config, optionally runs
|
|
63
|
+
* `changeset version`, discovers and transforms all workspace changelogs,
|
|
64
|
+
* and updates version files using the resolved per-package scopes.
|
|
65
|
+
*
|
|
66
|
+
* @param dryRun - When `true`, skip `changeset version` and only transform
|
|
67
|
+
* existing CHANGELOG files
|
|
68
|
+
* @returns An Effect that performs the versioning pipeline
|
|
69
|
+
*
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
function runVersion(dryRun) {
|
|
73
|
+
return Effect.gen(function* () {
|
|
74
|
+
const cwd = process.cwd();
|
|
75
|
+
const pm = (yield* (yield* PackageManagerDetector).detect(cwd).pipe(Effect.catchAll(() => Effect.succeed({
|
|
76
|
+
type: "npm",
|
|
77
|
+
version: void 0
|
|
78
|
+
})))).type;
|
|
79
|
+
yield* Effect.log(`Detected package manager: ${pm}`);
|
|
80
|
+
yield* requireValidConfig(cwd);
|
|
81
|
+
if (!dryRun) {
|
|
82
|
+
const cmd = getChangesetVersionCommand(pm);
|
|
83
|
+
yield* Effect.log(`Running: ${cmd}`);
|
|
84
|
+
yield* Effect.try({
|
|
85
|
+
try: () => execSync(cmd, {
|
|
86
|
+
cwd,
|
|
87
|
+
stdio: "inherit"
|
|
88
|
+
}),
|
|
89
|
+
catch: (error) => /* @__PURE__ */ new Error(`changeset version failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
90
|
+
});
|
|
91
|
+
} else yield* Effect.log("Dry run: skipping changeset version");
|
|
92
|
+
const packages = yield* (yield* WorkspaceDiscovery).listPackages().pipe(Effect.catchAll(() => Effect.succeed([])));
|
|
93
|
+
const changelogs = [];
|
|
94
|
+
const seen = /* @__PURE__ */ new Set();
|
|
95
|
+
const resolvedCwd = resolve(cwd);
|
|
96
|
+
for (const pkg of packages) {
|
|
97
|
+
const changelogPath = join(pkg.path, "CHANGELOG.md");
|
|
98
|
+
if (existsSync(changelogPath) && !seen.has(pkg.path)) {
|
|
99
|
+
seen.add(pkg.path);
|
|
100
|
+
changelogs.push({
|
|
101
|
+
name: pkg.name,
|
|
102
|
+
path: pkg.path,
|
|
103
|
+
changelogPath
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!seen.has(resolvedCwd)) {
|
|
108
|
+
const rootChangelog = join(resolvedCwd, "CHANGELOG.md");
|
|
109
|
+
if (existsSync(rootChangelog)) {
|
|
110
|
+
let rootName = "root";
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(readFileSync(join(resolvedCwd, "package.json"), "utf-8"));
|
|
113
|
+
if (pkg.name) rootName = pkg.name;
|
|
114
|
+
} catch {}
|
|
115
|
+
changelogs.push({
|
|
116
|
+
name: rootName,
|
|
117
|
+
path: resolvedCwd,
|
|
118
|
+
changelogPath: rootChangelog
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (changelogs.length === 0) yield* Effect.log("No CHANGELOG.md files found.");
|
|
123
|
+
else {
|
|
124
|
+
yield* Effect.log(`Found ${changelogs.length} CHANGELOG.md file(s)`);
|
|
125
|
+
for (const entry of changelogs) {
|
|
126
|
+
yield* Effect.try({
|
|
127
|
+
try: () => ChangelogTransformer.transformFile(entry.changelogPath),
|
|
128
|
+
catch: (error) => /* @__PURE__ */ new Error(`Failed to transform ${entry.changelogPath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
129
|
+
});
|
|
130
|
+
yield* Effect.log(`Transformed ${entry.name} → ${entry.changelogPath}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!existsSync(join(resolvedCwd, ".changeset", "config.json"))) return;
|
|
134
|
+
const scopesWithVersionFiles = (yield* (yield* ConfigInspector).inspect(resolvedCwd)).packages.filter((p) => p.versionFiles.length > 0).map((p) => {
|
|
135
|
+
const fresh = readPackageVersionFromDisk(p.workspaceDir);
|
|
136
|
+
return fresh && fresh !== p.version ? {
|
|
137
|
+
...p,
|
|
138
|
+
version: fresh
|
|
139
|
+
} : p;
|
|
140
|
+
});
|
|
141
|
+
if (scopesWithVersionFiles.length === 0) return;
|
|
142
|
+
yield* Effect.log(`Found ${scopesWithVersionFiles.length} package${scopesWithVersionFiles.length === 1 ? "" : "s"} with versionFiles`);
|
|
143
|
+
const updates = yield* Effect.try({
|
|
144
|
+
try: () => VersionFiles.processResolvedVersionFiles(scopesWithVersionFiles, dryRun),
|
|
145
|
+
catch: (error) => {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
return new VersionFileError({
|
|
148
|
+
filePath: message.match(/Failed to update (.+?):/)?.[1] ?? cwd,
|
|
149
|
+
reason: message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
for (const update of updates) {
|
|
154
|
+
const action = dryRun ? "Would update" : "Updated";
|
|
155
|
+
yield* Effect.log(`${action} ${update.filePath} → ${update.version}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Read the `version` field from a workspace package's `package.json` on
|
|
161
|
+
* disk. Used to bypass cached state in `ConfigInspector` and
|
|
162
|
+
* `WorkspaceDiscovery` after `changeset version` has rewritten each
|
|
163
|
+
* workspace's manifest.
|
|
164
|
+
*
|
|
165
|
+
* @internal
|
|
166
|
+
*/
|
|
167
|
+
function readPackageVersionFromDisk(workspaceDir) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(readFileSync(join(workspaceDir, "package.json"), "utf-8")).version ?? null;
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/* v8 ignore next 4 -- CLI registration; handler tested via runVersion */
|
|
175
|
+
const versionCommand = Command.make("version", { dryRun: dryRunOption }, ({ dryRun }) => runVersion(dryRun)).pipe(Command.withDescription("Run changeset version and transform all CHANGELOGs"));
|
|
176
|
+
|
|
177
|
+
//#endregion
|
|
178
|
+
export { versionCommand };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { analyzeBranchCommand } from "./commands/analyze-branch.js";
|
|
2
|
+
import { classifyCommand } from "./commands/classify.js";
|
|
3
|
+
import { configShowCommand } from "./commands/config-show.js";
|
|
4
|
+
import { configValidateCommand } from "./commands/config-validate.js";
|
|
5
|
+
import { depsDetectCommand } from "./commands/deps-detect.js";
|
|
6
|
+
import { depsRegenCommand } from "./commands/deps-regen.js";
|
|
7
|
+
import { lintCommand } from "./commands/lint.js";
|
|
8
|
+
import { releaseSurfaceCommand } from "./commands/release-surface.js";
|
|
9
|
+
import { transformCommand } from "./commands/transform.js";
|
|
10
|
+
import { validateFileCommand } from "./commands/validate-file.js";
|
|
11
|
+
import { versionCommand } from "./commands/version.js";
|
|
12
|
+
import { runChangesetCheck } from "./commands/check.js";
|
|
13
|
+
import { runChangesetInit } from "./commands/init.js";
|
|
14
|
+
import { Command } from "@effect/cli";
|
|
15
|
+
|
|
16
|
+
//#region src/commands/changeset/index.ts
|
|
17
|
+
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
|
18
|
+
const configGroup = Command.make("config").pipe(Command.withSubcommands([configShowCommand, configValidateCommand]), Command.withDescription("Inspect or validate .changeset/config.json"));
|
|
19
|
+
const depsGroup = Command.make("deps").pipe(Command.withSubcommands([depsDetectCommand, depsRegenCommand]), Command.withDescription("Generate or regenerate dependency changesets"));
|
|
20
|
+
const _changesetCommand = Command.make("changeset").pipe(Command.withSubcommands([
|
|
21
|
+
lintCommand,
|
|
22
|
+
transformCommand,
|
|
23
|
+
validateFileCommand,
|
|
24
|
+
versionCommand,
|
|
25
|
+
classifyCommand,
|
|
26
|
+
analyzeBranchCommand,
|
|
27
|
+
releaseSurfaceCommand,
|
|
28
|
+
configGroup,
|
|
29
|
+
depsGroup
|
|
30
|
+
]), Command.withDescription("Section-aware changeset tooling"));
|
|
31
|
+
/**
|
|
32
|
+
* The `savvy changeset` command group for use in Task B7 root assembly.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
|
|
36
|
+
* errors from Effect's internal types. Task B7 should import and use this via
|
|
37
|
+
* `Command.withSubcommands([changesetCommand as never])` or re-infer the type.
|
|
38
|
+
*/
|
|
39
|
+
const changesetCommand = _changesetCommand;
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { changesetCommand };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Changesets } from "@savvy-web/silk-effects";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/changeset/utils/config-gate.ts
|
|
7
|
+
/**
|
|
8
|
+
* Shared "refuse on bad config" gate for CLI commands that must not run
|
|
9
|
+
* with an invalid `.changeset/config.json`.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Used by `version` and `transform` in 0.9.0. Both commands are entry
|
|
13
|
+
* points to release-shaped work; running them against a config that
|
|
14
|
+
* fails the new validation contract (overlap, unknown package keys,
|
|
15
|
+
* dual-shape, schema errors) would produce broken or inconsistent
|
|
16
|
+
* output.
|
|
17
|
+
*
|
|
18
|
+
* `lint` deliberately does **not** call this gate — it operates on
|
|
19
|
+
* changeset markdown files and must keep working while the user is
|
|
20
|
+
* iterating on a config fix.
|
|
21
|
+
*
|
|
22
|
+
* Behavior:
|
|
23
|
+
* - **No config present** (`.changeset/config.json` absent): the gate
|
|
24
|
+
* resolves to `void` and the caller proceeds. This preserves the
|
|
25
|
+
* pre-0.9.0 behavior where `transform` and `version` worked on
|
|
26
|
+
* projects that have not yet been bootstrapped.
|
|
27
|
+
* - **Config present**: the gate invokes
|
|
28
|
+
* {@link ConfigInspector.inspect}. On `ConfigurationError`, it sets
|
|
29
|
+
* `process.exitCode = 1` and propagates the error so the caller's
|
|
30
|
+
* `Effect.gen` short-circuits.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
const { ConfigInspector } = Changesets;
|
|
35
|
+
/**
|
|
36
|
+
* Require a valid `.changeset/config.json` (when one exists) before
|
|
37
|
+
* proceeding. Resolves to `void` on a clean config OR on a project that
|
|
38
|
+
* doesn't have a config at all; fails with {@link ConfigurationError} on
|
|
39
|
+
* an invalid config.
|
|
40
|
+
*
|
|
41
|
+
* @param cwd - Project root (will be resolved against the process cwd)
|
|
42
|
+
* @returns Effect that succeeds on valid/absent config, fails with
|
|
43
|
+
* {@link ConfigurationError} otherwise
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
function requireValidConfig(cwd) {
|
|
48
|
+
return Effect.gen(function* () {
|
|
49
|
+
const projectDir = resolve(cwd);
|
|
50
|
+
if (!existsSync(join(projectDir, ".changeset", "config.json"))) return;
|
|
51
|
+
yield* (yield* ConfigInspector).inspect(projectDir).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return Effect.fail(err);
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
export { requireValidConfig };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { runChangesetCheck } from "./changeset/commands/check.js";
|
|
2
|
+
import "./changeset/index.js";
|
|
3
|
+
import { runCommitCheck } from "./commit/check.js";
|
|
4
|
+
import { runLintCheck } from "./lint/check.js";
|
|
5
|
+
import { Command, Options } from "@effect/cli";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/check.ts
|
|
9
|
+
/**
|
|
10
|
+
* Unified `savvy check` orchestrator.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* Runs all three tool-specific check handlers — changeset, commit, lint —
|
|
14
|
+
* sequentially so each tool's console output is cleanly grouped rather than
|
|
15
|
+
* interleaved. Unlike the `init` orchestrator, check MUST NOT short-circuit:
|
|
16
|
+
* the user wants to see every failing check in a single pass. Uses
|
|
17
|
+
* `Effect.all` with `{ concurrency: 1, mode: "validate" }` to run all three
|
|
18
|
+
* regardless of individual failures and then surface the full set of errors.
|
|
19
|
+
*
|
|
20
|
+
* Runtime layer provision (ManagedSection, FileSystem, WorkspaceRoot,
|
|
21
|
+
* ToolDiscovery, etc.) is deferred to Task B7 (root `runCli`). The Effect
|
|
22
|
+
* returned by `checkCommand`'s handler therefore carries the full union of the
|
|
23
|
+
* three handlers' requirements in its R channel.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
/* v8 ignore start -- CLI option definitions; orchestration logic tested via runCheck */
|
|
28
|
+
const changesetDirOption = Options.text("changeset-dir").pipe(Options.withDescription("Path to the changeset directory"), Options.withDefault(".changeset"));
|
|
29
|
+
const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings from lint check"), Options.withDefault(false));
|
|
30
|
+
/* v8 ignore stop */
|
|
31
|
+
/**
|
|
32
|
+
* Run all three check step Effects without short-circuiting.
|
|
33
|
+
*
|
|
34
|
+
* All three checks always run, sequentially (concurrency 1) so per-tool
|
|
35
|
+
* output stays grouped. If any fail, the combined Effect fails with all
|
|
36
|
+
* accumulated errors. Uses `Effect.all` with `{ concurrency: 1, mode: "validate" }`
|
|
37
|
+
* to collect every failure rather than stopping at the first.
|
|
38
|
+
*
|
|
39
|
+
* @param steps - The three step Effects to run. Injected for testability.
|
|
40
|
+
* @returns An Effect that resolves to `void` on success, or fails with the
|
|
41
|
+
* union of all failing steps' errors.
|
|
42
|
+
*/
|
|
43
|
+
function runCheck(steps) {
|
|
44
|
+
return Effect.all([
|
|
45
|
+
steps.changeset,
|
|
46
|
+
steps.commit,
|
|
47
|
+
steps.lint
|
|
48
|
+
], {
|
|
49
|
+
concurrency: 1,
|
|
50
|
+
mode: "validate"
|
|
51
|
+
}).pipe(Effect.asVoid);
|
|
52
|
+
}
|
|
53
|
+
/* v8 ignore start -- CLI registration; orchestration logic tested via runCheck */
|
|
54
|
+
const _checkCommand = Command.make("check", {
|
|
55
|
+
changesetDir: changesetDirOption,
|
|
56
|
+
quiet: quietOption
|
|
57
|
+
}, (opts) => runCheck({
|
|
58
|
+
changeset: runChangesetCheck(opts.changesetDir),
|
|
59
|
+
commit: runCommitCheck(),
|
|
60
|
+
lint: runLintCheck({ quiet: opts.quiet })
|
|
61
|
+
})).pipe(Command.withDescription("Validate all Silk Suite tool configurations in one pass"));
|
|
62
|
+
/* v8 ignore stop */
|
|
63
|
+
/**
|
|
64
|
+
* The `savvy check` command for use in the Task B7 root assembly.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* Typed with `any` at the export boundary to avoid TypeScript declaration-emit
|
|
68
|
+
* errors from Effect's internal types. Task B7 should use this via
|
|
69
|
+
* `Command.withSubcommands([checkCommand as never])` or re-infer the type.
|
|
70
|
+
*/
|
|
71
|
+
const checkCommand = _checkCommand;
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { checkCommand, runCheck };
|