@savvy-web/cli 1.1.2 → 1.2.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/cli/index.js CHANGED
@@ -34,7 +34,8 @@ import { PackageManagerDetectorLive, WorkspaceDiscoveryLive, WorkspaceRootLive }
34
34
  * `ConfigDiscoveryLive`, `ToolDiscoveryLive`, and `VersioningStrategyLive`
35
35
  * (provided `ChangesetConfigReaderLive`).
36
36
  * - Changesets-namespace services — `Changesets.ConfigInspectorLive` (provided
37
- * `ChangesetConfigReaderLive`), `Changesets.WorkspaceSnapshotReaderLive`, and
37
+ * `ChangesetConfigReaderLive`), `Changesets.WorkspaceSnapshotReaderLive`,
38
+ * `Changesets.ReleasePlannerLive` (provided `ConfigInspectorLive`), and
38
39
  * `Changesets.BranchAnalyzerLive`, which shares the single `ConfigInspectorLive`
39
40
  * instance built once via `provideMerge`.
40
41
  *
@@ -56,7 +57,7 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
56
57
  ]));
57
58
  const cli = Command.run(rootCommand, {
58
59
  name: "savvy",
59
- version: "1.1.2"
60
+ version: "1.2.0"
60
61
  });
61
62
  /**
62
63
  * Shared base layer: workspace services, the changeset config reader, and the
@@ -93,18 +94,19 @@ const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, Manage
93
94
  * `Changesets.ConfigInspectorLive` needs `ChangesetConfigReader`,
94
95
  * `WorkspaceDiscovery`, and `FileSystem` (the last for its publishConfig-driven
95
96
  * fallback when no explicit `packages` record is configured);
96
- * `Changesets.BranchAnalyzerLive` needs `ConfigInspector`.
97
+ * `Changesets.BranchAnalyzerLive` needs `ConfigInspector`;
98
+ * `Changesets.ReleasePlannerLive` needs `ConfigInspector`.
97
99
  *
98
100
  * `ConfigInspectorLive` is built once via {@link Layer.provideMerge}: the merge
99
- * feeds that single `ConfigInspector` instance into `BranchAnalyzerLive` AND
100
- * re-exposes it for the surviving `config validate` handler that yields it
101
- * directly, so it is never constructed twice per run.
101
+ * feeds that single `ConfigInspector` instance into `ReleasePlannerLive` and
102
+ * `BranchAnalyzerLive` AND re-exposes it for the surviving `config validate`
103
+ * handler that yields it directly, so it is never constructed twice per run.
102
104
  *
103
105
  * `provideMerge(BaseLive)` feeds the remaining deps and re-exposes the base
104
106
  * services for handlers that yield them directly. `provideMerge(NodeContext.layer)`
105
107
  * supplies `FileSystem`, `Path`, and `CommandExecutor` to everything underneath.
106
108
  */
107
- const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ConfigInspectorLive));
109
+ const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ReleasePlannerLive), Layer.provideMerge(Changesets.ConfigInspectorLive));
108
110
  const AppLive = Layer.mergeAll(ToolDiscoveryLive, VersioningStrategyLive, InspectorAndAnalyzerLive).pipe(Layer.provideMerge(BaseLive), Layer.provideMerge(NodeContext.layer));
109
111
  /**
110
112
  * Bootstrap and run the `savvy` CLI application.
@@ -2,177 +2,44 @@ import { requireValidConfig } from "../utils/config-gate.js";
2
2
  import { Command, Options } from "@effect/cli";
3
3
  import { Changesets } from "@savvy-web/silk-effects";
4
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
5
 
10
6
  //#region src/commands/changeset/commands/version.ts
11
7
  /**
12
- * Version command -- orchestrate `changeset version` and changelog transforms.
8
+ * Version command -- natively apply pending changesets and report the result.
13
9
  *
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
- * ```
10
+ * Validates the config, then runs {@link ReleasePlanner.apply}, which bumps
11
+ * versions, writes + transforms CHANGELOGs, deletes consumed changesets, and
12
+ * updates configured versionFiles -- all without shelling out to a `changeset`
13
+ * CLI binary.
39
14
  *
40
15
  * @internal
41
16
  */
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
17
  /* 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));
18
+ const dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Compute and report the release without writing anything"), Options.withDefault(false));
58
19
  /* v8 ignore stop */
59
20
  /**
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
21
+ * Validate config, then natively apply (or dry-run) the release via
22
+ * {@link ReleasePlanner}.
69
23
  *
70
- * @internal
24
+ * @param dryRun - When `true`, write nothing; only report planned changes.
71
25
  */
72
26
  function runVersion(dryRun) {
73
27
  return Effect.gen(function* () {
74
28
  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
29
  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}`);
30
+ const result = yield* (yield* Changesets.ReleasePlanner).apply(cwd, { dryRun });
31
+ if (result.releases.length === 0) {
32
+ yield* Effect.log("No pending changesets.");
33
+ return;
156
34
  }
35
+ const verb = dryRun ? "Would release" : "Released";
36
+ for (const r of result.releases) yield* Effect.log(`${verb} ${r.name}: ${r.oldVersion} -> ${r.newVersion} (${r.type})`);
37
+ if (!dryRun) yield* Effect.log(`Touched ${result.touchedFiles.length} file(s)`);
38
+ for (const u of result.versionFileUpdates) yield* Effect.log(`${dryRun ? "Would update" : "Updated"} ${u.filePath} -> ${u.version}`);
157
39
  });
158
40
  }
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
41
  /* 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"));
42
+ const versionCommand = Command.make("version", { dryRun: dryRunOption }, ({ dryRun }) => runVersion(dryRun)).pipe(Command.withDescription("Apply pending changesets: bump versions and transform CHANGELOGs"));
176
43
 
177
44
  //#endregion
178
45
  export { versionCommand };
package/index.d.ts CHANGED
@@ -30,7 +30,8 @@ import { JsoncParseError } from "jsonc-effect";
30
30
  * `ConfigDiscoveryLive`, `ToolDiscoveryLive`, and `VersioningStrategyLive`
31
31
  * (provided `ChangesetConfigReaderLive`).
32
32
  * - Changesets-namespace services — `Changesets.ConfigInspectorLive` (provided
33
- * `ChangesetConfigReaderLive`), `Changesets.WorkspaceSnapshotReaderLive`, and
33
+ * `ChangesetConfigReaderLive`), `Changesets.WorkspaceSnapshotReaderLive`,
34
+ * `Changesets.ReleasePlannerLive` (provided `ConfigInspectorLive`), and
34
35
  * `Changesets.BranchAnalyzerLive`, which shares the single `ConfigInspectorLive`
35
36
  * instance built once via `provideMerge`.
36
37
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "The savvy CLI — unified commit, changeset, and lint commands for the Silk Suite",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/cli",
@@ -36,7 +36,7 @@
36
36
  "@effect/platform-node": "^0.107.0",
37
37
  "@effect/rpc": "^0.75.1",
38
38
  "@effect/sql": "^0.51.1",
39
- "@savvy-web/silk-effects": "1.3.1",
39
+ "@savvy-web/silk-effects": "1.4.0",
40
40
  "effect": "^3.21.3",
41
41
  "jsonc-effect": "^0.2.1",
42
42
  "workspaces-effect": "^1.2.0",