@savvy-web/silk 1.3.5 → 1.3.7

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.
@@ -0,0 +1,331 @@
1
+ const require_dependency_table = require('../utils/dependency-table.cjs');
2
+ const require_index = require('../../../../../../../node_modules/.pnpm/workspaces-effect@1.2.0_@effect_platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/index.cjs');
3
+ const require_config_inspector = require('./config-inspector.cjs');
4
+ const require_dep_diff = require('../utils/dep-diff.cjs');
5
+ const require_publishability = require('../utils/publishability.cjs');
6
+ const require_worktree_snapshot = require('../utils/worktree-snapshot.cjs');
7
+ const require_workspace_snapshot = require('./workspace-snapshot.cjs');
8
+ let effect = require("effect");
9
+ let node_path = require("node:path");
10
+ let node_fs = require("node:fs");
11
+
12
+ //#region ../silk-effects/dist/dev/pkg/changesets/services/deps-regen.js
13
+ /**
14
+ * `Changesets.DepsRegen` service — lift the `deps regen` / `deps detect`
15
+ * orchestration out of the CLI into a `Context.Tag` service with a
16
+ * `plan()` / `execute()` split.
17
+ *
18
+ * @remarks
19
+ * `plan()` computes the cumulative dependency diff (merge-base → working
20
+ * tree by default, or between two explicit refs), resolves `catalog:` /
21
+ * `workspace:` specifiers to concrete versions, drops `devDependency`
22
+ * rows (unless `includeDevDeps`), and returns a complete {@link RegenPlan}
23
+ * (target filenames + stale-changeset deletes) WITHOUT touching the
24
+ * filesystem. `execute()` applies a plan — writing the fresh changesets
25
+ * first, then deleting the stale pure-dependency ones (so an interrupted
26
+ * run loses nothing and is safely re-runnable).
27
+ *
28
+ * This is the single source of truth for regen/detect: the CLI commands
29
+ * and MCP tools are thin adapters over this service.
30
+ *
31
+ * @see {@link DepsRegen} for the service tag
32
+ * @see {@link DepsRegenLive} for the production layer
33
+ *
34
+ */
35
+ /** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
36
+ const EM_DASH = "—";
37
+ /** Whether a From/To cell holds a pnpm protocol specifier that could resolve to a version. */
38
+ const isProtocol = (v) => /^(?:catalog|workspace|npm|jsr|file|link|portal):/.test(v);
39
+ /**
40
+ * Resolve protocol From/To cells to concrete versions (raw-string fallback
41
+ * when unresolved or on resolver error), leave em-dash sentinels untouched,
42
+ * then optionally drop `devDependency` rows, and re-sort.
43
+ *
44
+ * @param diff - One workspace package's dependency-table rows.
45
+ * @param keepDevDeps - When `true`, retain `devDependency` rows; otherwise
46
+ * drop them unconditionally (the regen default).
47
+ * @returns An Effect yielding the transformed {@link WorkspaceDependencyDiff}.
48
+ *
49
+ * @public
50
+ */
51
+ const resolveDiffRows = (diff, keepDevDeps = false) => effect.Effect.gen(function* () {
52
+ const resolver = yield* require_index.CatalogResolver;
53
+ const resolveCell = (dep, value) => isProtocol(value) ? resolver.resolveSpecifier(dep, value).pipe(effect.Effect.map((opt) => effect.Option.getOrElse(opt, () => value)), effect.Effect.catchAll((error) => effect.Effect.logWarning(`DepsRegen: catalog resolution failed for "${dep}" (${value}); keeping raw specifier: ${String(error)}`).pipe(effect.Effect.as(value)))) : effect.Effect.succeed(value);
54
+ const rows = [];
55
+ for (const row of diff.rows) {
56
+ if (!keepDevDeps && row.type === "devDependency") continue;
57
+ const from = row.from === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.from);
58
+ const to = row.to === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.to);
59
+ rows.push({
60
+ ...row,
61
+ from,
62
+ to
63
+ });
64
+ }
65
+ return {
66
+ ...diff,
67
+ rows: require_dependency_table.sortDependencyRows(rows)
68
+ };
69
+ });
70
+ const ADJECTIVES = [
71
+ "brave",
72
+ "clever",
73
+ "swift",
74
+ "silver",
75
+ "lucky",
76
+ "happy",
77
+ "calm",
78
+ "bright",
79
+ "quiet",
80
+ "wild"
81
+ ];
82
+ const NOUNS = [
83
+ "dogs",
84
+ "cats",
85
+ "wolves",
86
+ "foxes",
87
+ "cups",
88
+ "ships",
89
+ "trees",
90
+ "owls",
91
+ "cranes",
92
+ "hills"
93
+ ];
94
+ const VERBS = [
95
+ "laugh",
96
+ "dream",
97
+ "fly",
98
+ "sing",
99
+ "dance",
100
+ "wander",
101
+ "soar",
102
+ "rest",
103
+ "leap",
104
+ "ponder"
105
+ ];
106
+ function pickRandomTriplet() {
107
+ return `${ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]}-${NOUNS[Math.floor(Math.random() * NOUNS.length)]}-${VERBS[Math.floor(Math.random() * VERBS.length)]}`;
108
+ }
109
+ /**
110
+ * Pick a `<adjective>-<noun>-<verb>` filename slug that does not collide
111
+ * with an existing `.changeset/*.md` OR with a slug already claimed
112
+ * earlier in the same {@link RegenPlan.toWrite} computation. `plan()`
113
+ * never writes to disk, so `existsSync` alone cannot see slugs chosen
114
+ * moments earlier in the same call — the `chosen` set closes that gap.
115
+ * The triplet space is 1,000 combinations, so a busy repo can plausibly
116
+ * exhaust it across runs; fall back to a timestamp suffix after 20
117
+ * unlucky picks.
118
+ *
119
+ * @param changesetDir - Directory checked via `existsSync` for on-disk collisions.
120
+ * @param chosen - Basenames (without extension) already picked within this plan;
121
+ * the picked candidate is added to this set before returning.
122
+ * @internal
123
+ */
124
+ function randomFilename(changesetDir, chosen) {
125
+ for (let i = 0; i < 20; i++) {
126
+ const candidate = pickRandomTriplet();
127
+ if (!chosen.has(candidate) && !(0, node_fs.existsSync)((0, node_path.join)(changesetDir, `${candidate}.md`))) {
128
+ chosen.add(candidate);
129
+ return candidate;
130
+ }
131
+ }
132
+ let attempt = 0;
133
+ let fallback = `${pickRandomTriplet()}-${Date.now()}`;
134
+ while (chosen.has(fallback) || (0, node_fs.existsSync)((0, node_path.join)(changesetDir, `${fallback}.md`))) fallback = `${pickRandomTriplet()}-${Date.now()}-${++attempt}`;
135
+ chosen.add(fallback);
136
+ return fallback;
137
+ }
138
+ /**
139
+ * Strict detection of "pure dependency changesets" per the documented
140
+ * rules: single-package frontmatter, single `## Dependencies` heading,
141
+ * no other body content beyond that section.
142
+ *
143
+ * @param content - Raw `.changeset/*.md` file contents.
144
+ * @returns `{ isPure, package }` — `isPure` is `true` only for a
145
+ * single-package, Dependencies-only changeset; `package` is the sole
146
+ * frontmatter package name (or `null` when not pure).
147
+ *
148
+ * @public
149
+ */
150
+ function isPureDependencyChangeset(content) {
151
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
152
+ if (!fmMatch) return {
153
+ isPure: false,
154
+ package: null
155
+ };
156
+ const frontmatter = fmMatch[1];
157
+ const body = (fmMatch[2] ?? "").trim();
158
+ const fmLines = frontmatter.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
159
+ if (fmLines.length !== 1) return {
160
+ isPure: false,
161
+ package: null
162
+ };
163
+ const pkgMatch = fmLines[0].match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
164
+ if (!pkgMatch) return {
165
+ isPure: false,
166
+ package: null
167
+ };
168
+ const pkg = pkgMatch[1];
169
+ const bodyTrimmed = body.replace(/^\s+/, "");
170
+ if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
171
+ isPure: false,
172
+ package: null
173
+ };
174
+ if ((bodyTrimmed.match(/^## /gm) ?? []).length !== 1) return {
175
+ isPure: false,
176
+ package: null
177
+ };
178
+ if (/^# /m.test(bodyTrimmed)) return {
179
+ isPure: false,
180
+ package: null
181
+ };
182
+ return {
183
+ isPure: true,
184
+ package: pkg
185
+ };
186
+ }
187
+ function listChangesetFiles(changesetDir) {
188
+ if (!(0, node_fs.existsSync)(changesetDir)) return [];
189
+ return (0, node_fs.readdirSync)(changesetDir).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => (0, node_path.join)(changesetDir, f));
190
+ }
191
+ function findPureDependencyChangesets(changesetDir) {
192
+ const result = [];
193
+ for (const file of listChangesetFiles(changesetDir)) {
194
+ let content;
195
+ try {
196
+ content = (0, node_fs.readFileSync)(file, "utf8");
197
+ } catch {
198
+ continue;
199
+ }
200
+ const detection = isPureDependencyChangeset(content);
201
+ if (detection.isPure && detection.package) result.push({
202
+ file,
203
+ package: detection.package
204
+ });
205
+ }
206
+ return result;
207
+ }
208
+ function findMixedDependencyChangesets(changesetDir) {
209
+ const result = [];
210
+ for (const file of listChangesetFiles(changesetDir)) {
211
+ let content;
212
+ try {
213
+ content = (0, node_fs.readFileSync)(file, "utf8");
214
+ } catch {
215
+ continue;
216
+ }
217
+ if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
218
+ }
219
+ return result;
220
+ }
221
+ /**
222
+ * Render a single-package, patch-bump changeset for a diff whose rows have
223
+ * already been resolved/filtered by {@link resolveDiffRows}.
224
+ */
225
+ function renderChangesetContent(diff) {
226
+ return `${`---\n"${diff.package}": patch\n---`}\n\n## Dependencies\n\n${require_dependency_table.serializeDependencyTableToMarkdown([...diff.rows])}\n`;
227
+ }
228
+ /**
229
+ * @internal
230
+ */
231
+ const DepsRegenBase = effect.Context.Tag("Changesets/DepsRegen")();
232
+ /**
233
+ * Effect service tag for {@link DepsRegenShape}.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * import { Effect } from "effect";
238
+ * import { Changesets } from "@savvy-web/silk-effects";
239
+ *
240
+ * const program = Effect.gen(function* () {
241
+ * const svc = yield* Changesets.DepsRegen;
242
+ * const plan = yield* svc.plan({ cwd: process.cwd() });
243
+ * return yield* svc.execute(plan);
244
+ * });
245
+ * ```
246
+ *
247
+ * @public
248
+ */
249
+ var DepsRegen = class extends DepsRegenBase {};
250
+ /**
251
+ * Build a {@link DepsRegenShape} that closes over already-resolved service
252
+ * implementations, keeping the public `plan`/`execute` signatures
253
+ * requirement-free (`R = never`).
254
+ */
255
+ function makeShape(reader, inspector, discovery, resolver, detector) {
256
+ const provideResolver = effect.Layer.succeed(require_index.CatalogResolver, resolver);
257
+ const provideDetector = effect.Layer.succeed(require_index.PublishabilityDetector, detector);
258
+ const plan = (options) => effect.Effect.gen(function* () {
259
+ const resolvedCwd = (0, node_path.resolve)(options.cwd);
260
+ const changesetDir = (0, node_path.join)(resolvedCwd, ".changeset");
261
+ let fromRef = options.from;
262
+ if (!fromRef) {
263
+ let baseBranch = options.base;
264
+ if (!baseBranch) baseBranch = (yield* inspector.inspect(resolvedCwd).pipe(effect.Effect.catchTag("ConfigurationError", () => effect.Effect.succeed({ baseBranch: "main" })))).baseBranch;
265
+ fromRef = yield* require_worktree_snapshot.gitMergeBase(resolvedCwd, baseBranch);
266
+ }
267
+ const rawDiffs = require_dep_diff.computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, fromRef), options.to ? yield* reader.snapshotAt(resolvedCwd, options.to) : require_worktree_snapshot.snapshotFromWorktree(resolvedCwd));
268
+ const targetPkg = options.package;
269
+ const publishable = yield* require_publishability.listPublishablePackageNames(yield* discovery.listPackages(resolvedCwd)).pipe(effect.Effect.provide(provideDetector));
270
+ const keepDevDeps = options.includeDevDeps === true;
271
+ const scoped = targetPkg ? rawDiffs.filter((d) => d.package === targetPkg) : rawDiffs.filter((d) => publishable.has(d.package));
272
+ const resolved = [];
273
+ for (const diff of scoped) {
274
+ const next = yield* resolveDiffRows(diff, keepDevDeps).pipe(effect.Effect.provide(provideResolver));
275
+ if (next.rows.length > 0) resolved.push(next);
276
+ }
277
+ const existingPure = findPureDependencyChangesets(changesetDir);
278
+ const skippedMixed = findMixedDependencyChangesets(changesetDir);
279
+ const toDelete = targetPkg ? existingPure.filter((p) => p.package === targetPkg) : existingPure.filter((p) => publishable.has(p.package));
280
+ const chosenFilenames = /* @__PURE__ */ new Set();
281
+ return {
282
+ toDelete,
283
+ toWrite: resolved.map((diff) => ({
284
+ file: (0, node_path.join)(changesetDir, `${randomFilename(changesetDir, chosenFilenames)}.md`),
285
+ package: diff.package,
286
+ diff
287
+ })),
288
+ skippedMixed
289
+ };
290
+ });
291
+ const execute = (plan) => effect.Effect.sync(() => {
292
+ const deleted = [];
293
+ const written = [];
294
+ for (const entry of plan.toWrite) {
295
+ (0, node_fs.writeFileSync)(entry.file, renderChangesetContent(entry.diff));
296
+ written.push(entry.file);
297
+ }
298
+ for (const entry of plan.toDelete) try {
299
+ (0, node_fs.unlinkSync)(entry.file);
300
+ deleted.push(entry.file);
301
+ } catch {}
302
+ return {
303
+ deleted,
304
+ written,
305
+ skippedMixed: plan.skippedMixed
306
+ };
307
+ });
308
+ return {
309
+ plan,
310
+ execute
311
+ };
312
+ }
313
+ /**
314
+ * Live layer for {@link DepsRegen}.
315
+ *
316
+ * Requires {@link WorkspaceSnapshotReader}, {@link ConfigInspector},
317
+ * `WorkspaceDiscovery`, `CatalogResolver`, and `PublishabilityDetector`
318
+ * (the last three from `workspaces-effect`).
319
+ *
320
+ * @public
321
+ */
322
+ const DepsRegenLive = effect.Layer.effect(DepsRegen, effect.Effect.gen(function* () {
323
+ return makeShape(yield* require_workspace_snapshot.WorkspaceSnapshotReader, yield* require_config_inspector.ConfigInspector, yield* require_index.WorkspaceDiscovery, yield* require_index.CatalogResolver, yield* require_index.PublishabilityDetector);
324
+ }));
325
+
326
+ //#endregion
327
+ exports.DepsRegen = DepsRegen;
328
+ exports.DepsRegenBase = DepsRegenBase;
329
+ exports.DepsRegenLive = DepsRegenLive;
330
+ exports.isPureDependencyChangeset = isPureDependencyChangeset;
331
+ exports.resolveDiffRows = resolveDiffRows;
@@ -0,0 +1,327 @@
1
+ import { serializeDependencyTableToMarkdown, sortDependencyRows } from "../utils/dependency-table.js";
2
+ import { CatalogResolver, PublishabilityDetector, WorkspaceDiscovery } from "../../../../../../../node_modules/.pnpm/workspaces-effect@1.2.0_@effect_platform@0.96.2_effect@3.21.4__effect@3.21.4/node_modules/workspaces-effect/index.js";
3
+ import { ConfigInspector } from "./config-inspector.js";
4
+ import { computeWorkspaceDependencyDiffs } from "../utils/dep-diff.js";
5
+ import { listPublishablePackageNames } from "../utils/publishability.js";
6
+ import { gitMergeBase, snapshotFromWorktree } from "../utils/worktree-snapshot.js";
7
+ import { WorkspaceSnapshotReader } from "./workspace-snapshot.js";
8
+ import { Context, Effect, Layer, Option } from "effect";
9
+ import { join, resolve } from "node:path";
10
+ import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
11
+
12
+ //#region ../silk-effects/dist/dev/pkg/changesets/services/deps-regen.js
13
+ /**
14
+ * `Changesets.DepsRegen` service — lift the `deps regen` / `deps detect`
15
+ * orchestration out of the CLI into a `Context.Tag` service with a
16
+ * `plan()` / `execute()` split.
17
+ *
18
+ * @remarks
19
+ * `plan()` computes the cumulative dependency diff (merge-base → working
20
+ * tree by default, or between two explicit refs), resolves `catalog:` /
21
+ * `workspace:` specifiers to concrete versions, drops `devDependency`
22
+ * rows (unless `includeDevDeps`), and returns a complete {@link RegenPlan}
23
+ * (target filenames + stale-changeset deletes) WITHOUT touching the
24
+ * filesystem. `execute()` applies a plan — writing the fresh changesets
25
+ * first, then deleting the stale pure-dependency ones (so an interrupted
26
+ * run loses nothing and is safely re-runnable).
27
+ *
28
+ * This is the single source of truth for regen/detect: the CLI commands
29
+ * and MCP tools are thin adapters over this service.
30
+ *
31
+ * @see {@link DepsRegen} for the service tag
32
+ * @see {@link DepsRegenLive} for the production layer
33
+ *
34
+ */
35
+ /** The em-dash sentinel (U+2014) used for added ("from") / removed ("to") cells. */
36
+ const EM_DASH = "—";
37
+ /** Whether a From/To cell holds a pnpm protocol specifier that could resolve to a version. */
38
+ const isProtocol = (v) => /^(?:catalog|workspace|npm|jsr|file|link|portal):/.test(v);
39
+ /**
40
+ * Resolve protocol From/To cells to concrete versions (raw-string fallback
41
+ * when unresolved or on resolver error), leave em-dash sentinels untouched,
42
+ * then optionally drop `devDependency` rows, and re-sort.
43
+ *
44
+ * @param diff - One workspace package's dependency-table rows.
45
+ * @param keepDevDeps - When `true`, retain `devDependency` rows; otherwise
46
+ * drop them unconditionally (the regen default).
47
+ * @returns An Effect yielding the transformed {@link WorkspaceDependencyDiff}.
48
+ *
49
+ * @public
50
+ */
51
+ const resolveDiffRows = (diff, keepDevDeps = false) => Effect.gen(function* () {
52
+ const resolver = yield* CatalogResolver;
53
+ const resolveCell = (dep, value) => isProtocol(value) ? resolver.resolveSpecifier(dep, value).pipe(Effect.map((opt) => Option.getOrElse(opt, () => value)), Effect.catchAll((error) => Effect.logWarning(`DepsRegen: catalog resolution failed for "${dep}" (${value}); keeping raw specifier: ${String(error)}`).pipe(Effect.as(value)))) : Effect.succeed(value);
54
+ const rows = [];
55
+ for (const row of diff.rows) {
56
+ if (!keepDevDeps && row.type === "devDependency") continue;
57
+ const from = row.from === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.from);
58
+ const to = row.to === EM_DASH ? EM_DASH : yield* resolveCell(row.dependency, row.to);
59
+ rows.push({
60
+ ...row,
61
+ from,
62
+ to
63
+ });
64
+ }
65
+ return {
66
+ ...diff,
67
+ rows: sortDependencyRows(rows)
68
+ };
69
+ });
70
+ const ADJECTIVES = [
71
+ "brave",
72
+ "clever",
73
+ "swift",
74
+ "silver",
75
+ "lucky",
76
+ "happy",
77
+ "calm",
78
+ "bright",
79
+ "quiet",
80
+ "wild"
81
+ ];
82
+ const NOUNS = [
83
+ "dogs",
84
+ "cats",
85
+ "wolves",
86
+ "foxes",
87
+ "cups",
88
+ "ships",
89
+ "trees",
90
+ "owls",
91
+ "cranes",
92
+ "hills"
93
+ ];
94
+ const VERBS = [
95
+ "laugh",
96
+ "dream",
97
+ "fly",
98
+ "sing",
99
+ "dance",
100
+ "wander",
101
+ "soar",
102
+ "rest",
103
+ "leap",
104
+ "ponder"
105
+ ];
106
+ function pickRandomTriplet() {
107
+ return `${ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]}-${NOUNS[Math.floor(Math.random() * NOUNS.length)]}-${VERBS[Math.floor(Math.random() * VERBS.length)]}`;
108
+ }
109
+ /**
110
+ * Pick a `<adjective>-<noun>-<verb>` filename slug that does not collide
111
+ * with an existing `.changeset/*.md` OR with a slug already claimed
112
+ * earlier in the same {@link RegenPlan.toWrite} computation. `plan()`
113
+ * never writes to disk, so `existsSync` alone cannot see slugs chosen
114
+ * moments earlier in the same call — the `chosen` set closes that gap.
115
+ * The triplet space is 1,000 combinations, so a busy repo can plausibly
116
+ * exhaust it across runs; fall back to a timestamp suffix after 20
117
+ * unlucky picks.
118
+ *
119
+ * @param changesetDir - Directory checked via `existsSync` for on-disk collisions.
120
+ * @param chosen - Basenames (without extension) already picked within this plan;
121
+ * the picked candidate is added to this set before returning.
122
+ * @internal
123
+ */
124
+ function randomFilename(changesetDir, chosen) {
125
+ for (let i = 0; i < 20; i++) {
126
+ const candidate = pickRandomTriplet();
127
+ if (!chosen.has(candidate) && !existsSync(join(changesetDir, `${candidate}.md`))) {
128
+ chosen.add(candidate);
129
+ return candidate;
130
+ }
131
+ }
132
+ let attempt = 0;
133
+ let fallback = `${pickRandomTriplet()}-${Date.now()}`;
134
+ while (chosen.has(fallback) || existsSync(join(changesetDir, `${fallback}.md`))) fallback = `${pickRandomTriplet()}-${Date.now()}-${++attempt}`;
135
+ chosen.add(fallback);
136
+ return fallback;
137
+ }
138
+ /**
139
+ * Strict detection of "pure dependency changesets" per the documented
140
+ * rules: single-package frontmatter, single `## Dependencies` heading,
141
+ * no other body content beyond that section.
142
+ *
143
+ * @param content - Raw `.changeset/*.md` file contents.
144
+ * @returns `{ isPure, package }` — `isPure` is `true` only for a
145
+ * single-package, Dependencies-only changeset; `package` is the sole
146
+ * frontmatter package name (or `null` when not pure).
147
+ *
148
+ * @public
149
+ */
150
+ function isPureDependencyChangeset(content) {
151
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
152
+ if (!fmMatch) return {
153
+ isPure: false,
154
+ package: null
155
+ };
156
+ const frontmatter = fmMatch[1];
157
+ const body = (fmMatch[2] ?? "").trim();
158
+ const fmLines = frontmatter.split(/\r?\n/).filter((l) => l.trim().length > 0 && !/^\s*#/.test(l));
159
+ if (fmLines.length !== 1) return {
160
+ isPure: false,
161
+ package: null
162
+ };
163
+ const pkgMatch = fmLines[0].match(/^\s*["']?([^"':\s]+)["']?\s*:\s*([a-z]+)\s*$/);
164
+ if (!pkgMatch) return {
165
+ isPure: false,
166
+ package: null
167
+ };
168
+ const pkg = pkgMatch[1];
169
+ const bodyTrimmed = body.replace(/^\s+/, "");
170
+ if (!/^## Dependencies\b/.test(bodyTrimmed)) return {
171
+ isPure: false,
172
+ package: null
173
+ };
174
+ if ((bodyTrimmed.match(/^## /gm) ?? []).length !== 1) return {
175
+ isPure: false,
176
+ package: null
177
+ };
178
+ if (/^# /m.test(bodyTrimmed)) return {
179
+ isPure: false,
180
+ package: null
181
+ };
182
+ return {
183
+ isPure: true,
184
+ package: pkg
185
+ };
186
+ }
187
+ function listChangesetFiles(changesetDir) {
188
+ if (!existsSync(changesetDir)) return [];
189
+ return readdirSync(changesetDir).filter((f) => f.endsWith(".md") && f !== "README.md").map((f) => join(changesetDir, f));
190
+ }
191
+ function findPureDependencyChangesets(changesetDir) {
192
+ const result = [];
193
+ for (const file of listChangesetFiles(changesetDir)) {
194
+ let content;
195
+ try {
196
+ content = readFileSync(file, "utf8");
197
+ } catch {
198
+ continue;
199
+ }
200
+ const detection = isPureDependencyChangeset(content);
201
+ if (detection.isPure && detection.package) result.push({
202
+ file,
203
+ package: detection.package
204
+ });
205
+ }
206
+ return result;
207
+ }
208
+ function findMixedDependencyChangesets(changesetDir) {
209
+ const result = [];
210
+ for (const file of listChangesetFiles(changesetDir)) {
211
+ let content;
212
+ try {
213
+ content = readFileSync(file, "utf8");
214
+ } catch {
215
+ continue;
216
+ }
217
+ if (/^## Dependencies\b/m.test(content) && !isPureDependencyChangeset(content).isPure) result.push(file);
218
+ }
219
+ return result;
220
+ }
221
+ /**
222
+ * Render a single-package, patch-bump changeset for a diff whose rows have
223
+ * already been resolved/filtered by {@link resolveDiffRows}.
224
+ */
225
+ function renderChangesetContent(diff) {
226
+ return `${`---\n"${diff.package}": patch\n---`}\n\n## Dependencies\n\n${serializeDependencyTableToMarkdown([...diff.rows])}\n`;
227
+ }
228
+ /**
229
+ * @internal
230
+ */
231
+ const DepsRegenBase = Context.Tag("Changesets/DepsRegen")();
232
+ /**
233
+ * Effect service tag for {@link DepsRegenShape}.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * import { Effect } from "effect";
238
+ * import { Changesets } from "@savvy-web/silk-effects";
239
+ *
240
+ * const program = Effect.gen(function* () {
241
+ * const svc = yield* Changesets.DepsRegen;
242
+ * const plan = yield* svc.plan({ cwd: process.cwd() });
243
+ * return yield* svc.execute(plan);
244
+ * });
245
+ * ```
246
+ *
247
+ * @public
248
+ */
249
+ var DepsRegen = class extends DepsRegenBase {};
250
+ /**
251
+ * Build a {@link DepsRegenShape} that closes over already-resolved service
252
+ * implementations, keeping the public `plan`/`execute` signatures
253
+ * requirement-free (`R = never`).
254
+ */
255
+ function makeShape(reader, inspector, discovery, resolver, detector) {
256
+ const provideResolver = Layer.succeed(CatalogResolver, resolver);
257
+ const provideDetector = Layer.succeed(PublishabilityDetector, detector);
258
+ const plan = (options) => Effect.gen(function* () {
259
+ const resolvedCwd = resolve(options.cwd);
260
+ const changesetDir = join(resolvedCwd, ".changeset");
261
+ let fromRef = options.from;
262
+ if (!fromRef) {
263
+ let baseBranch = options.base;
264
+ if (!baseBranch) baseBranch = (yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", () => Effect.succeed({ baseBranch: "main" })))).baseBranch;
265
+ fromRef = yield* gitMergeBase(resolvedCwd, baseBranch);
266
+ }
267
+ const rawDiffs = computeWorkspaceDependencyDiffs(yield* reader.snapshotAt(resolvedCwd, fromRef), options.to ? yield* reader.snapshotAt(resolvedCwd, options.to) : snapshotFromWorktree(resolvedCwd));
268
+ const targetPkg = options.package;
269
+ const publishable = yield* listPublishablePackageNames(yield* discovery.listPackages(resolvedCwd)).pipe(Effect.provide(provideDetector));
270
+ const keepDevDeps = options.includeDevDeps === true;
271
+ const scoped = targetPkg ? rawDiffs.filter((d) => d.package === targetPkg) : rawDiffs.filter((d) => publishable.has(d.package));
272
+ const resolved = [];
273
+ for (const diff of scoped) {
274
+ const next = yield* resolveDiffRows(diff, keepDevDeps).pipe(Effect.provide(provideResolver));
275
+ if (next.rows.length > 0) resolved.push(next);
276
+ }
277
+ const existingPure = findPureDependencyChangesets(changesetDir);
278
+ const skippedMixed = findMixedDependencyChangesets(changesetDir);
279
+ const toDelete = targetPkg ? existingPure.filter((p) => p.package === targetPkg) : existingPure.filter((p) => publishable.has(p.package));
280
+ const chosenFilenames = /* @__PURE__ */ new Set();
281
+ return {
282
+ toDelete,
283
+ toWrite: resolved.map((diff) => ({
284
+ file: join(changesetDir, `${randomFilename(changesetDir, chosenFilenames)}.md`),
285
+ package: diff.package,
286
+ diff
287
+ })),
288
+ skippedMixed
289
+ };
290
+ });
291
+ const execute = (plan) => Effect.sync(() => {
292
+ const deleted = [];
293
+ const written = [];
294
+ for (const entry of plan.toWrite) {
295
+ writeFileSync(entry.file, renderChangesetContent(entry.diff));
296
+ written.push(entry.file);
297
+ }
298
+ for (const entry of plan.toDelete) try {
299
+ unlinkSync(entry.file);
300
+ deleted.push(entry.file);
301
+ } catch {}
302
+ return {
303
+ deleted,
304
+ written,
305
+ skippedMixed: plan.skippedMixed
306
+ };
307
+ });
308
+ return {
309
+ plan,
310
+ execute
311
+ };
312
+ }
313
+ /**
314
+ * Live layer for {@link DepsRegen}.
315
+ *
316
+ * Requires {@link WorkspaceSnapshotReader}, {@link ConfigInspector},
317
+ * `WorkspaceDiscovery`, `CatalogResolver`, and `PublishabilityDetector`
318
+ * (the last three from `workspaces-effect`).
319
+ *
320
+ * @public
321
+ */
322
+ const DepsRegenLive = Layer.effect(DepsRegen, Effect.gen(function* () {
323
+ return makeShape(yield* WorkspaceSnapshotReader, yield* ConfigInspector, yield* WorkspaceDiscovery, yield* CatalogResolver, yield* PublishabilityDetector);
324
+ }));
325
+
326
+ //#endregion
327
+ export { DepsRegen, DepsRegenBase, DepsRegenLive, isPureDependencyChangeset, resolveDiffRows };