@savvy-web/cli 1.3.5 → 1.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/cli/index.js CHANGED
@@ -6,9 +6,9 @@ import { initCommand } from "../commands/init.js";
6
6
  import { lintCommand } from "../commands/lint/index.js";
7
7
  import { Command } from "@effect/cli";
8
8
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
9
- import { BiomeSchemaSyncLive, ChangesetConfigReaderLive, Changesets, ConfigDiscoveryLive, ManagedSectionLive, SilkPublishabilityDetectorLive, ToolDiscoveryLive, VersioningStrategyLive } from "@savvy-web/silk-effects";
9
+ import { BiomeSchemaSyncLive, ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, ConfigDiscoveryLive, ManagedSectionLive, SilkPublishabilityDetectorLive, ToolDiscoveryLive, VersioningStrategyLive } from "@savvy-web/silk-effects";
10
10
  import { Effect, Layer } from "effect";
11
- import { PackageManagerDetectorLive, WorkspaceDiscoveryLive, WorkspaceRootLive } from "workspaces-effect";
11
+ import { PackageManagerDetectorLive, PointInTimeWorkspaceLive, PublishabilityDetectorLive, WorkspaceDiscoveryLive, WorkspaceRootLive } from "workspaces-effect";
12
12
 
13
13
  //#region src/cli/index.ts
14
14
  /**
@@ -34,10 +34,9 @@ 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`,
38
- * `Changesets.ReleasePlannerLive` (provided `ConfigInspectorLive`), and
39
- * `Changesets.BranchAnalyzerLive`, which shares the single `ConfigInspectorLive`
40
- * instance built once via `provideMerge`.
37
+ * `ChangesetConfigReaderLive`), `Changesets.ReleasePlannerLive` (provided
38
+ * `ConfigInspectorLive`), and `Changesets.BranchAnalyzerLive`, which shares
39
+ * the single `ConfigInspectorLive` instance built once via `provideMerge`.
41
40
  *
42
41
  * The CLI version is injected at build time via `process.env.__PACKAGE_VERSION__`.
43
42
  *
@@ -57,7 +56,7 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
57
56
  ]));
58
57
  const cli = Command.run(rootCommand, {
59
58
  name: "savvy",
60
- version: "1.3.5"
59
+ version: "1.4.0"
61
60
  });
62
61
  /**
63
62
  * Shared base layer: workspace services, the changeset config reader, and the
@@ -77,12 +76,12 @@ const cli = Command.run(rootCommand, {
77
76
  const WorkspaceLive = Layer.mergeAll(WorkspaceRootLive, PackageManagerDetectorLive, WorkspaceDiscoveryLive.pipe(Layer.provide(WorkspaceRootLive)));
78
77
  /**
79
78
  * Base layer membership: silk-effects leaf services (`ManagedSection`,
80
- * `BiomeSchemaSync`, `ConfigDiscovery`, `SilkPublishabilityDetector`,
81
- * `WorkspaceSnapshotReader`) that depend only on the platform, plus the
82
- * changeset base layers (`WorkspaceLive`, `ChangesetConfigReader`) that
83
- * `AppLive`'s upper services build upon.
79
+ * `BiomeSchemaSync`, `ConfigDiscovery`, `SilkPublishabilityDetector`) that
80
+ * depend only on the platform, plus the changeset base layers
81
+ * (`WorkspaceLive`, `ChangesetConfigReader`) that `AppLive`'s upper services
82
+ * build upon.
84
83
  */
85
- const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, ManagedSectionLive, BiomeSchemaSyncLive, ConfigDiscoveryLive, SilkPublishabilityDetectorLive, Changesets.WorkspaceSnapshotReaderLive);
84
+ const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, ManagedSectionLive, BiomeSchemaSyncLive, ConfigDiscoveryLive, SilkPublishabilityDetectorLive);
86
85
  /**
87
86
  * Merged runtime Layer stack — the union of the three source CLIs' stacks with
88
87
  * every inter-layer dependency satisfied.
@@ -102,12 +101,34 @@ const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, Manage
102
101
  * `BranchAnalyzerLive` AND re-exposes it for the surviving `config validate`
103
102
  * handler that yields it directly, so it is never constructed twice per run.
104
103
  *
104
+ * `Changesets.DepsRegenLive` (the `deps regen`/`deps detect` orchestration
105
+ * service) needs `ConfigInspector`, `WorkspaceDiscovery` (from
106
+ * `BaseLive`/`InspectorAndAnalyzerLive`), plus `PointInTimeWorkspace` and
107
+ * `PublishabilityDetector` from `workspaces-effect`, and `ChangesetConfig`
108
+ * (provided its own `ChangesetConfigReaderLive`, since `Layer.mergeAll` does
109
+ * not cross-feed sibling layers) — none of which the other commands need, so
110
+ * they're composed only for `DepsRegenGroupLive`. `PointInTimeWorkspaceLive`
111
+ * in turn needs `WorkspaceRoot`, `WorkspaceDiscovery`, `CommandExecutor`,
112
+ * `FileSystem`, `Path` — the workspace pair supplied by `WorkspaceLive`, the
113
+ * platform trio by `NodeContext.layer` below.
114
+ * `DepsRegenGroupLive` reuses the exact `InspectorAndAnalyzerLive` layer
115
+ * reference (not a fresh copy), so Effect's layer memoization builds that
116
+ * single `ConfigInspector` instance once and shares it here too.
117
+ *
105
118
  * `provideMerge(BaseLive)` feeds the remaining deps and re-exposes the base
106
119
  * services for handlers that yield them directly. `provideMerge(NodeContext.layer)`
107
120
  * supplies `FileSystem`, `Path`, and `CommandExecutor` to everything underneath.
108
121
  */
109
122
  const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provideMerge(Changesets.ReleasePlannerLive), Layer.provideMerge(Changesets.ConfigInspectorLive));
110
- const AppLive = Layer.mergeAll(ToolDiscoveryLive, VersioningStrategyLive, InspectorAndAnalyzerLive).pipe(Layer.provideMerge(BaseLive), Layer.provideMerge(NodeContext.layer));
123
+ /**
124
+ * `Changesets.DepsRegen`, fully composed except for the base services
125
+ * (`WorkspaceDiscovery`, platform layers) supplied by `BaseLive` /
126
+ * `NodeContext.layer`. `PointInTimeWorkspaceLive` is provided `WorkspaceLive`
127
+ * so its `WorkspaceRoot`/`WorkspaceDiscovery` requirements resolve; the
128
+ * remaining `CommandExecutor`/`FileSystem`/`Path` flow up to `NodeContext.layer`.
129
+ */
130
+ const DepsRegenGroupLive = Changesets.DepsRegenLive.pipe(Layer.provide(InspectorAndAnalyzerLive), Layer.provide(PointInTimeWorkspaceLive.pipe(Layer.provide(WorkspaceLive))), Layer.provide(PublishabilityDetectorLive), Layer.provide(ChangesetConfigLive.pipe(Layer.provide(ChangesetConfigReaderLive))));
131
+ const AppLive = Layer.mergeAll(ToolDiscoveryLive, VersioningStrategyLive, InspectorAndAnalyzerLive, DepsRegenGroupLive).pipe(Layer.provideMerge(BaseLive), Layer.provideMerge(NodeContext.layer));
111
132
  /**
112
133
  * Bootstrap and run the `savvy` CLI application.
113
134
  *
@@ -1,24 +1,21 @@
1
1
  import { Command, Options } from "@effect/cli";
2
2
  import { Changesets } from "@savvy-web/silk-effects";
3
3
  import { Console, Effect, Option } from "effect";
4
- import { WorkspaceDiscovery } from "workspaces-effect";
5
- import { resolve } from "node:path";
6
4
 
7
5
  //#region src/commands/changeset/commands/deps-detect.ts
8
6
  /**
9
7
  * `deps detect` command — read-only dependency-diff inspection.
10
8
  *
11
9
  * @remarks
12
- * Computes the per-workspace-package dependency changes between two git
13
- * refs and renders them either as structured JSON (one row per change)
14
- * or as ready-to-paste CSH005 markdown. No file writes.
10
+ * A thin adapter over {@link Changesets.DepsRegen}: this command calls
11
+ * `plan({ includeDevDeps: true, ... })` the read-only path, so devDeps
12
+ * stay in the diff and no file is written or deleted — then renders
13
+ * `plan.toWrite`'s diffs either as structured JSON (one row per change) or
14
+ * as ready-to-paste CSH005 markdown. No file writes.
15
15
  *
16
16
  * Defaults:
17
17
  * - `--from` → `git merge-base <baseBranch> HEAD`
18
18
  * - `--to` → working tree (i.e., `HEAD` plus staged + unstaged + untracked).
19
- * Passed as the special value
20
- * `WORKTREE` to {@link WorkspaceSnapshotReader} — implementations resolve
21
- * this against the live working tree rather than `git show`.
22
19
  *
23
20
  * @example
24
21
  * ```bash
@@ -29,7 +26,7 @@ import { resolve } from "node:path";
29
26
  *
30
27
  * @internal
31
28
  */
32
- const { ConfigInspector, WorkspaceSnapshotReader, computeWorkspaceDependencyDiffs, gitMergeBase, listPublishablePackageNames, serializeDependencyTableToMarkdown, snapshotFromWorktree } = Changesets;
29
+ const { DepsRegen, serializeDependencyTableToMarkdown } = Changesets;
33
30
  /* v8 ignore start -- CLI option definitions */
34
31
  const fromOption = Options.text("from").pipe(Options.withDescription("Older ref to diff from (defaults to merge-base with base branch)"), Options.optional);
35
32
  const toOption = Options.text("to").pipe(Options.withDescription("Newer ref to diff to (defaults to working tree)"), Options.optional);
@@ -61,27 +58,22 @@ function renderMarkdownBlocks(diffs) {
61
58
  */
62
59
  function runDepsDetect(cwd, from, to, pkg, json, markdown) {
63
60
  return Effect.gen(function* () {
64
- const reader = yield* WorkspaceSnapshotReader;
65
- const resolvedCwd = resolve(cwd);
66
- let fromRef = Option.getOrUndefined(from);
67
- if (!fromRef) fromRef = yield* gitMergeBase(resolvedCwd, (yield* (yield* ConfigInspector).inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", () => Effect.succeed({ baseBranch: "main" })))).baseBranch).pipe(Effect.catchTag("GitError", (err) => {
68
- process.exitCode = 1;
69
- return Effect.fail(err);
70
- }));
71
- const toRef = Option.getOrUndefined(to);
72
- let diffs = computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, fromRef).pipe(Effect.catchTag("GitError", (err) => {
73
- process.exitCode = 1;
74
- return Effect.fail(err);
75
- })), toRef ? yield* reader.snapshotAt(resolvedCwd, toRef).pipe(Effect.catchTag("GitError", (err) => {
76
- process.exitCode = 1;
77
- return Effect.fail(err);
78
- })) : snapshotFromWorktree(resolvedCwd));
79
- const targetPkg = Option.getOrUndefined(pkg);
80
- if (targetPkg) diffs = diffs.filter((d) => d.package === targetPkg);
81
- else {
82
- const publishable = yield* listPublishablePackageNames(yield* (yield* WorkspaceDiscovery).listPackages(resolvedCwd).pipe(Effect.catchAll(() => Effect.succeed([]))));
83
- diffs = diffs.filter((d) => publishable.has(d.package));
84
- }
61
+ const diffs = (yield* (yield* DepsRegen).plan({
62
+ cwd,
63
+ includeDevDeps: true,
64
+ ...Option.isSome(pkg) ? { package: pkg.value } : {},
65
+ ...Option.isSome(from) ? { from: from.value } : {},
66
+ ...Option.isSome(to) ? { to: to.value } : {}
67
+ }).pipe(Effect.catchTags({
68
+ GitError: (err) => {
69
+ process.exitCode = 1;
70
+ return Effect.fail(err);
71
+ },
72
+ GitReadError: (err) => {
73
+ process.exitCode = 1;
74
+ return Effect.fail(err);
75
+ }
76
+ }))).toWrite.map((entry) => entry.diff);
85
77
  if (markdown && !json) {
86
78
  yield* Effect.log(renderMarkdownBlocks(diffs));
87
79
  return;
@@ -1,9 +1,6 @@
1
1
  import { Command, Options } from "@effect/cli";
2
2
  import { Changesets } from "@savvy-web/silk-effects";
3
3
  import { Console, Effect, Option } from "effect";
4
- import { WorkspaceDiscovery } from "workspaces-effect";
5
- import { join, resolve } from "node:path";
6
- import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
7
4
 
8
5
  //#region src/commands/changeset/commands/deps-regen.ts
9
6
  /**
@@ -12,7 +9,14 @@ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from
12
9
  * cumulative dep diff from base to working tree.
13
10
  *
14
11
  * @remarks
15
- * **The single-package-per-changeset rule.** This command enforces our
12
+ * This is a thin adapter over {@link Changesets.DepsRegen}: the plan/execute
13
+ * orchestration (pure-dependency-changeset detection, protocol-specifier
14
+ * resolution, filename generation, and the filesystem writes themselves)
15
+ * lives in `@savvy-web/silk-effects`. This command only translates CLI
16
+ * options into a `plan()` call, conditionally applies the plan via
17
+ * `execute()`, and renders the resulting {@link RegenPlan}.
18
+ *
19
+ * **The single-package-per-changeset rule.** The service enforces our
16
20
  * convention that each `.changeset/*.md` file lists exactly one package
17
21
  * in its frontmatter. `@changesets/cli` technically supports multi-package
18
22
  * frontmatter, but our agent (and this command) always produces single-
@@ -28,18 +32,9 @@ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from
28
32
  * Anything else (multi-package frontmatter, additional sections, comments,
29
33
  * `### Sub-headings`, etc.) is treated as "mixed" and left untouched.
30
34
  * That's the safe default — if a human authored something idiosyncratic,
31
- * we don't clobber it.
32
- *
33
- * **The algorithm:**
34
- *
35
- * 1. Compute dep diff from `merge-base(baseBranch, HEAD)` to working
36
- * tree, grouped by workspace package.
37
- * 2. Find every pure-dependency changeset (strict definition).
38
- * 3. Delete every one of them — even those for packages with no current
39
- * dep changes (their changeset is stale by definition).
40
- * 4. Write a fresh `<adjective>-<noun>-<verb>.md` per workspace package
41
- * that has current dep changes: single-package frontmatter, `patch`
42
- * bump, one `## Dependencies` section, one CSH005 table.
35
+ * we don't clobber it. `devDependency` rows are dropped by the service
36
+ * (the regen default) and protocol specifiers (`catalog:`/`workspace:`)
37
+ * are resolved to concrete versions.
43
38
  *
44
39
  * @example
45
40
  * ```bash
@@ -50,7 +45,7 @@ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from
50
45
  *
51
46
  * @internal
52
47
  */
53
- const { ConfigInspector, WorkspaceSnapshotReader, computeWorkspaceDependencyDiffs, gitMergeBase, listPublishablePackageNames, serializeDependencyTableToMarkdown, snapshotFromWorktree } = Changesets;
48
+ const { DepsRegen } = Changesets;
54
49
  /* v8 ignore start -- CLI option definitions */
55
50
  const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
56
51
  const baseOption = Options.text("base").pipe(Options.withDescription("Override the base branch (defaults to config baseBranch)"), Options.optional);
@@ -58,141 +53,6 @@ const packageOption = Options.text("package").pipe(Options.withDescription("Rest
58
53
  const dryRunOption = Options.boolean("dry-run").pipe(Options.withDescription("Print the plan without writing or deleting"), Options.withDefault(false));
59
54
  const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit a structured plan as JSON"), Options.withDefault(false));
60
55
  /* v8 ignore stop */
61
- const ADJECTIVES = [
62
- "brave",
63
- "clever",
64
- "swift",
65
- "silver",
66
- "lucky",
67
- "happy",
68
- "calm",
69
- "bright",
70
- "quiet",
71
- "wild"
72
- ];
73
- const NOUNS = [
74
- "dogs",
75
- "cats",
76
- "wolves",
77
- "foxes",
78
- "cups",
79
- "ships",
80
- "trees",
81
- "owls",
82
- "cranes",
83
- "hills"
84
- ];
85
- const VERBS = [
86
- "laugh",
87
- "dream",
88
- "fly",
89
- "sing",
90
- "dance",
91
- "wander",
92
- "soar",
93
- "rest",
94
- "leap",
95
- "ponder"
96
- ];
97
- function pickRandomTriplet() {
98
- return `${ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]}-${NOUNS[Math.floor(Math.random() * NOUNS.length)]}-${VERBS[Math.floor(Math.random() * VERBS.length)]}`;
99
- }
100
- /**
101
- * Pick a `<adjective>-<noun>-<verb>` filename slug that does not collide
102
- * with an existing `.changeset/*.md`. The triplet space is 1,000
103
- * combinations, so a busy repo can plausibly exhaust it across runs;
104
- * fall back to a timestamp suffix after 20 unlucky picks.
105
- *
106
- * @internal
107
- */
108
- function randomFilename(changesetDir) {
109
- for (let i = 0; i < 20; i++) {
110
- const candidate = pickRandomTriplet();
111
- if (!existsSync(join(changesetDir, `${candidate}.md`))) return candidate;
112
- }
113
- return `${pickRandomTriplet()}-${Date.now()}`;
114
- }
115
- /**
116
- * Strict detection of "pure dependency changesets" per the documented
117
- * rules: single-package frontmatter, single `## Dependencies` heading,
118
- * no other body content beyond that section.
119
- *
120
- * @internal
121
- */
122
- function isPureDependencyChangeset(content) {
123
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
124
- if (!fmMatch) return {
125
- isPure: false,
126
- package: null
127
- };
128
- const frontmatter = fmMatch[1];
129
- const body = (fmMatch[2] ?? "").trim();
130
- const fmLines = frontmatter.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
131
- if (fmLines.length !== 1) return {
132
- isPure: false,
133
- package: null
134
- };
135
- const pkgMatch = fmLines[0].match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
136
- if (!pkgMatch) return {
137
- isPure: false,
138
- package: null
139
- };
140
- const pkg = pkgMatch[1];
141
- const bodyTrimmed = body.replace(/^\s+/, "");
142
- if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
143
- isPure: false,
144
- package: null
145
- };
146
- if ((bodyTrimmed.match(/^## /gm) ?? []).length !== 1) return {
147
- isPure: false,
148
- package: null
149
- };
150
- if (/^# /m.test(bodyTrimmed)) return {
151
- isPure: false,
152
- package: null
153
- };
154
- return {
155
- isPure: true,
156
- package: pkg
157
- };
158
- }
159
- function listChangesetFiles(changesetDir) {
160
- if (!existsSync(changesetDir)) return [];
161
- return readdirSync(changesetDir).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => join(changesetDir, f));
162
- }
163
- function findPureDependencyChangesets(changesetDir) {
164
- const result = [];
165
- for (const file of listChangesetFiles(changesetDir)) {
166
- let content;
167
- try {
168
- content = readFileSync(file, "utf8");
169
- } catch {
170
- continue;
171
- }
172
- const detection = isPureDependencyChangeset(content);
173
- if (detection.isPure && detection.package) result.push({
174
- file,
175
- package: detection.package
176
- });
177
- }
178
- return result;
179
- }
180
- function findMixedDependencyChangesets(changesetDir) {
181
- const result = [];
182
- for (const file of listChangesetFiles(changesetDir)) {
183
- let content;
184
- try {
185
- content = readFileSync(file, "utf8");
186
- } catch {
187
- continue;
188
- }
189
- if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
190
- }
191
- return result;
192
- }
193
- function renderChangesetContent(diff) {
194
- return `${`---\n"${diff.package}": patch\n---`}\n\n## Dependencies\n\n${serializeDependencyTableToMarkdown([...diff.rows])}\n`;
195
- }
196
56
  /**
197
57
  * Handler exported for direct invocation in tests.
198
58
  *
@@ -200,47 +60,22 @@ function renderChangesetContent(diff) {
200
60
  */
201
61
  function runDepsRegen(cwd, base, pkg, dryRun, json) {
202
62
  return Effect.gen(function* () {
203
- const resolvedCwd = resolve(cwd);
204
- const changesetDir = join(resolvedCwd, ".changeset");
205
- const reader = yield* WorkspaceSnapshotReader;
206
- let baseBranch = Option.getOrUndefined(base);
207
- if (!baseBranch) baseBranch = (yield* (yield* ConfigInspector).inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", () => Effect.succeed({ baseBranch: "main" })))).baseBranch;
208
- const mergeBase = yield* gitMergeBase(resolvedCwd, baseBranch).pipe(Effect.catchTag("GitError", (err) => {
209
- process.exitCode = 1;
210
- return Effect.fail(err);
211
- }));
212
- let diffs = computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, mergeBase).pipe(Effect.catchTag("GitError", (err) => {
213
- process.exitCode = 1;
214
- return Effect.fail(err);
215
- })), snapshotFromWorktree(resolvedCwd));
216
- const targetPkg = Option.getOrUndefined(pkg);
217
- const publishable = yield* listPublishablePackageNames(yield* (yield* WorkspaceDiscovery).listPackages(resolvedCwd).pipe(Effect.catchAll(() => Effect.succeed([]))));
218
- if (targetPkg) diffs = diffs.filter((d) => d.package === targetPkg);
219
- else diffs = diffs.filter((d) => publishable.has(d.package));
220
- const existingPure = findPureDependencyChangesets(changesetDir);
221
- const skippedMixed = findMixedDependencyChangesets(changesetDir);
222
- const toDelete = targetPkg ? existingPure.filter((p) => p.package === targetPkg) : existingPure.filter((p) => publishable.has(p.package));
223
- const toWrite = diffs.map((diff) => ({
224
- file: join(changesetDir, `${randomFilename(changesetDir)}.md`),
225
- package: diff.package,
226
- diff
63
+ const service = yield* DepsRegen;
64
+ const plan = yield* service.plan({
65
+ cwd,
66
+ ...Option.isSome(base) ? { base: base.value } : {},
67
+ ...Option.isSome(pkg) ? { package: pkg.value } : {}
68
+ }).pipe(Effect.catchTags({
69
+ GitError: (err) => {
70
+ process.exitCode = 1;
71
+ return Effect.fail(err);
72
+ },
73
+ GitReadError: (err) => {
74
+ process.exitCode = 1;
75
+ return Effect.fail(err);
76
+ }
227
77
  }));
228
- const plan = {
229
- toDelete,
230
- toWrite,
231
- skippedMixed
232
- };
233
- if (dryRun) {
234
- if (json) yield* Console.log(JSON.stringify(plan, null, 2));
235
- else yield* renderHumanPlan(plan);
236
- return;
237
- }
238
- for (const entry of toDelete) try {
239
- unlinkSync(entry.file);
240
- } catch (error) {
241
- yield* Effect.logWarning(`Failed to delete ${entry.file}: ${error instanceof Error ? error.message : String(error)}`);
242
- }
243
- for (const entry of toWrite) writeFileSync(entry.file, renderChangesetContent(entry.diff));
78
+ if (!dryRun) yield* service.execute(plan);
244
79
  if (json) yield* Console.log(JSON.stringify(plan, null, 2));
245
80
  else yield* renderHumanPlan(plan);
246
81
  });
@@ -274,4 +109,4 @@ const depsRegenCommand = Command.make("regen", {
274
109
  }, ({ 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"));
275
110
 
276
111
  //#endregion
277
- export { depsRegenCommand, isPureDependencyChangeset, runDepsRegen };
112
+ export { depsRegenCommand, runDepsRegen };
package/index.d.ts CHANGED
@@ -30,10 +30,9 @@ 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`,
34
- * `Changesets.ReleasePlannerLive` (provided `ConfigInspectorLive`), and
35
- * `Changesets.BranchAnalyzerLive`, which shares the single `ConfigInspectorLive`
36
- * instance built once via `provideMerge`.
33
+ * `ChangesetConfigReaderLive`), `Changesets.ReleasePlannerLive` (provided
34
+ * `ConfigInspectorLive`), and `Changesets.BranchAnalyzerLive`, which shares
35
+ * the single `ConfigInspectorLive` instance built once via `provideMerge`.
37
36
  *
38
37
  * The CLI version is injected at build time via `process.env.__PACKAGE_VERSION__`.
39
38
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/cli",
3
- "version": "1.3.5",
3
+ "version": "1.4.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,10 +36,10 @@
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.5.2",
39
+ "@savvy-web/silk-effects": "2.0.0",
40
40
  "effect": "^3.21.4",
41
- "jsonc-effect": "^0.2.1",
42
- "workspaces-effect": "^1.2.0",
41
+ "jsonc-effect": "^0.3.0",
42
+ "workspaces-effect": "^2.0.0",
43
43
  "yaml": "^2.9.0"
44
44
  }
45
45
  }