@savvy-web/silk-effects 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +242 -28
  2. package/index.js +8 -5
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -3,18 +3,15 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@savvy-web/silk-effects)](https://www.npmjs.com/package/@savvy-web/silk-effects)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Shared [Effect](https://effect.website/) library providing Silk Suite conventions for publishability detection, versioning strategy, tag formatting, managed sections, config discovery, and Biome schema synchronization. Platform-agnostic: consumers provide their own runtime layer (Node.js, Bun, etc.).
6
+ Shared [Effect](https://effect.website/) library providing Silk Suite conventions for publishability detection, versioning strategy, tag formatting, managed file sections, config discovery, Biome schema synchronization, and CLI tool resolution. Platform-agnostic -- consumers provide their own runtime layer (`NodeContext`, `BunContext`, etc.).
7
7
 
8
8
  ## Features
9
9
 
10
- - Resolve publish targets from shorthand strings, URLs, or objects with sensible defaults
11
- - Detect versioning strategy (single, fixed-group, independent) from changeset config
12
- - Format git tags consistently based on workspace structure
13
- - Manage tool-owned sections inside user-editable files without clobbering user content
14
- - Define reusable section identities with typed content factories (`SectionDefinition`)
10
+ - Resolve publish targets and detect publishability from `package.json` with multi-registry support
11
+ - Manage tool-owned sections in user-editable files without clobbering surrounding content
15
12
  - Discover and resolve CLI tools globally or locally with version enforcement and caching
16
- - Discover config files using a priority-based search convention
17
- - Keep Biome `$schema` URLs in sync across config files
13
+ - Detect versioning strategy and format git tags from changeset configuration
14
+ - Locate config files and keep Biome schema URLs in sync across workspaces
18
15
 
19
16
  ## Installation
20
17
 
@@ -22,12 +19,34 @@ Shared [Effect](https://effect.website/) library providing Silk Suite convention
22
19
  pnpm add @savvy-web/silk-effects effect @effect/platform @effect/platform-node
23
20
  ```
24
21
 
25
- `effect` is a peer dependency -- install it alongside the package.
22
+ `effect` is a peer dependency. Install a platform package (`@effect/platform-node`, `@effect/platform-bun`) matching your runtime.
26
23
 
27
24
  ## Quick Start
28
25
 
29
26
  All exports come from the package root:
30
27
 
28
+ ```typescript
29
+ import {
30
+ TargetResolver, TargetResolverLive,
31
+ ManagedSection, ManagedSectionLive, SectionDefinition,
32
+ ToolDiscovery, ToolDiscoveryLive, ToolDefinition,
33
+ } from "@savvy-web/silk-effects";
34
+ ```
35
+
36
+ ## Services
37
+
38
+ The 9 services are grouped by which platform layers they require.
39
+
40
+ ---
41
+
42
+ ### No Platform Layer Required
43
+
44
+ These services are pure logic -- no filesystem or shell access needed.
45
+
46
+ #### TargetResolver
47
+
48
+ Resolve raw publish-target values into fully-normalized `ResolvedTarget` records. Supports shorthands (`"npm"`, `"github"`, `"jsr"`), custom registry URLs, and object configs. Auth strategy (OIDC vs token) is auto-detected from the registry hostname.
49
+
31
50
  ```typescript
32
51
  import { Effect } from "effect";
33
52
  import { TargetResolver, TargetResolverLive } from "@savvy-web/silk-effects";
@@ -38,26 +57,91 @@ const targets = await Effect.runPromise(
38
57
  return yield* resolver.resolve(["npm", "github"]);
39
58
  }).pipe(Effect.provide(TargetResolverLive)),
40
59
  );
60
+ // => ResolvedTarget[] with registry, auth, provenance, etc.
61
+ ```
62
+
63
+ #### SilkPublishabilityPlugin
64
+
65
+ Detect whether a package is publishable from its `package.json` and resolve its targets. Delegates to `TargetResolver` internally.
66
+
67
+ Rules: `private: true` with no `publishConfig` is not publishable. A `publishConfig.targets` array resolves each entry. A bare `publishConfig.registry` resolves a single target. Default falls back to `"npm"`.
68
+
69
+ ```typescript
70
+ import { Effect } from "effect";
71
+ import {
72
+ SilkPublishabilityPlugin, SilkPublishabilityPluginLive,
73
+ TargetResolverLive,
74
+ } from "@savvy-web/silk-effects";
75
+
76
+ const targets = await Effect.runPromise(
77
+ Effect.gen(function* () {
78
+ const plugin = yield* SilkPublishabilityPlugin;
79
+ return yield* plugin.detect(packageJson);
80
+ }).pipe(
81
+ Effect.provide(SilkPublishabilityPluginLive),
82
+ Effect.provide(TargetResolverLive),
83
+ ),
84
+ );
85
+ ```
86
+
87
+ #### TagStrategy
88
+
89
+ Determine git-tag naming strategy and format tag strings. Strategy is `"single"` (one publishable package, tags like `1.2.3`) or `"scoped"` (multiple packages, tags like `@scope/pkg@1.2.3`). Tag format follows strict SemVer 2.0.0 with no `v` prefix.
90
+
91
+ ```typescript
92
+ import { Effect } from "effect";
93
+ import { TagStrategy, TagStrategyLive } from "@savvy-web/silk-effects";
94
+
95
+ const tag = await Effect.runPromise(
96
+ Effect.gen(function* () {
97
+ const ts = yield* TagStrategy;
98
+ const strategy = yield* ts.determine(versioningResult);
99
+ return yield* ts.formatTag("@savvy-web/silk-effects", "0.2.0", strategy);
100
+ }).pipe(Effect.provide(TagStrategyLive)),
101
+ );
102
+ // => "@savvy-web/silk-effects@0.2.0"
41
103
  ```
42
104
 
43
- Services that access the filesystem require a platform layer:
105
+ ---
106
+
107
+ ### FileSystem Layer Required
108
+
109
+ These services read or write files. Provide a platform layer such as `NodeContext.layer` or `BunContext.layer`.
110
+
111
+ #### ManagedSection
112
+
113
+ Manage tool-owned delimited sections inside user-editable files. Sections are bounded by markers like `# --- BEGIN TOOL MANAGED SECTION ---` / `# --- END ... ---`. User content outside the markers is never touched.
114
+
115
+ **SectionDefinition** is a value object representing section identity (tool name + comment style). It creates `SectionBlock` instances that hold the actual content. Definitions support typed content factories via `generate()` and `generateEffect()`.
116
+
117
+ **SectionBlock** represents the content between markers. It supports `diff()`, `prepend()`, and `append()` operations and uses normalized content for equality comparison.
118
+
119
+ Methods: `read`, `write`, `sync`, `check`, `isManaged` -- all support dual API (data-first and data-last) for pipe composition.
44
120
 
45
121
  ```typescript
46
122
  import { Effect } from "effect";
47
123
  import { NodeContext } from "@effect/platform-node";
48
124
  import {
49
- ManagedSection,
50
- ManagedSectionLive,
51
- SectionDefinition,
125
+ ManagedSection, ManagedSectionLive, SectionDefinition,
52
126
  } from "@savvy-web/silk-effects";
53
127
 
54
- const def = SectionDefinition.make({ toolName: "MY-TOOL" });
128
+ // Define section identity
129
+ const def = SectionDefinition.make({ toolName: "LINT-STAGED" });
130
+
131
+ // Create a content block from the definition
55
132
  const block = def.block("\nnpx lint-staged\n");
56
133
 
57
134
  await Effect.runPromise(
58
135
  Effect.gen(function* () {
59
136
  const ms = yield* ManagedSection;
60
- yield* ms.sync(".husky/pre-commit", block);
137
+
138
+ // Sync: creates the section if missing, updates if changed, no-op if identical
139
+ const result = yield* ms.sync(".husky/pre-commit", block);
140
+ // => SyncResult: Created | Updated | Unchanged
141
+
142
+ // Check: compare file content against expected block
143
+ const check = yield* ms.check(".husky/pre-commit", block);
144
+ // => CheckResult: Found | NotFound
61
145
  }).pipe(
62
146
  Effect.provide(ManagedSectionLive),
63
147
  Effect.provide(NodeContext.layer),
@@ -65,23 +149,153 @@ await Effect.runPromise(
65
149
  );
66
150
  ```
67
151
 
68
- ## Services
152
+ `SectionDefinition` also supports `//` comment style for JavaScript/TypeScript files:
153
+
154
+ ```typescript
155
+ const jsDef = SectionDefinition.make({ toolName: "MY-TOOL", commentStyle: "//" });
156
+ ```
157
+
158
+ Use `ShellSectionDefinition` when the comment style is always `#` and should not be configurable.
159
+
160
+ #### VersioningStrategy
161
+
162
+ Classify the versioning strategy from changeset configuration. Outputs `"single"` (0-1 publishable packages), `"fixed-group"` (all packages in one fixed group), or `"independent"` (multiple packages, not in a single group). Falls back gracefully if config is missing.
163
+
164
+ ```typescript
165
+ import { Effect } from "effect";
166
+ import { NodeContext } from "@effect/platform-node";
167
+ import {
168
+ VersioningStrategy, VersioningStrategyLive,
169
+ ChangesetConfigReaderLive,
170
+ } from "@savvy-web/silk-effects";
171
+
172
+ const result = await Effect.runPromise(
173
+ Effect.gen(function* () {
174
+ const vs = yield* VersioningStrategy;
175
+ return yield* vs.detect(publishablePackages, process.cwd());
176
+ }).pipe(
177
+ Effect.provide(VersioningStrategyLive),
178
+ Effect.provide(ChangesetConfigReaderLive),
179
+ Effect.provide(NodeContext.layer),
180
+ ),
181
+ );
182
+ // => { strategy: "single" | "fixed-group" | "independent", ... }
183
+ ```
184
+
185
+ #### ChangesetConfigReader
69
186
 
70
- | Service | Platform Layer | Description |
71
- | ------- | -------------- | ----------- |
72
- | `TargetResolver` | No | Resolve publish targets from shorthand strings or objects |
73
- | `SilkPublishabilityPlugin` | No | Detect publishable packages from `package.json` |
74
- | `TagStrategy` | No | Determine and format git tags by versioning strategy |
75
- | `VersioningStrategy` | Yes (FileSystem) | Detect versioning strategy from changeset config |
76
- | `ChangesetConfigReader` | Yes (FileSystem) | Read and parse changeset configuration |
77
- | `ManagedSection` | Yes (FileSystem) | Read/write/sync/check tool-owned sections in user files |
78
- | `ConfigDiscovery` | Yes (FileSystem) | Locate config files by priority-based search |
79
- | `BiomeSchemaSync` | Yes (FileSystem) | Keep Biome `$schema` URLs in sync across config files |
80
- | `ToolDiscovery` | Yes (CommandExecutor) | Locate CLI tools globally or locally, with caching |
187
+ Read and decode `.changeset/config.json`. Auto-detects whether the project uses `@savvy-web/changesets` (returning `SilkChangesetConfig` with `_isSilk: true`) or standard changesets (returning `ChangesetConfig`).
188
+
189
+ ```typescript
190
+ import { Effect } from "effect";
191
+ import { NodeContext } from "@effect/platform-node";
192
+ import {
193
+ ChangesetConfigReader, ChangesetConfigReaderLive,
194
+ } from "@savvy-web/silk-effects";
195
+
196
+ const config = await Effect.runPromise(
197
+ Effect.gen(function* () {
198
+ const reader = yield* ChangesetConfigReader;
199
+ return yield* reader.read(process.cwd());
200
+ }).pipe(
201
+ Effect.provide(ChangesetConfigReaderLive),
202
+ Effect.provide(NodeContext.layer),
203
+ ),
204
+ );
205
+ ```
206
+
207
+ #### ConfigDiscovery
208
+
209
+ Locate config files using a priority-based search convention. Checks `lib/configs/{name}` (shared configs) first, then `{cwd}/{name}` (local override).
210
+
211
+ ```typescript
212
+ import { Effect } from "effect";
213
+ import { NodeContext } from "@effect/platform-node";
214
+ import { ConfigDiscovery, ConfigDiscoveryLive } from "@savvy-web/silk-effects";
215
+
216
+ const result = await Effect.runPromise(
217
+ Effect.gen(function* () {
218
+ const cd = yield* ConfigDiscovery;
219
+ return yield* cd.find("biome.jsonc");
220
+ // => { path: "/project/biome.jsonc", source: "root" } | null
221
+ }).pipe(
222
+ Effect.provide(ConfigDiscoveryLive),
223
+ Effect.provide(NodeContext.layer),
224
+ ),
225
+ );
226
+ ```
227
+
228
+ #### BiomeSchemaSync
229
+
230
+ Keep Biome config `$schema` URLs current. Locates `biome.json` or `biome.jsonc`, compares the `$schema` value against the expected URL for the given version, and optionally updates in place. Strips semver range prefixes.
231
+
232
+ ```typescript
233
+ import { Effect } from "effect";
234
+ import { NodeContext } from "@effect/platform-node";
235
+ import { BiomeSchemaSync, BiomeSchemaSyncLive } from "@savvy-web/silk-effects";
236
+
237
+ await Effect.runPromise(
238
+ Effect.gen(function* () {
239
+ const bss = yield* BiomeSchemaSync;
240
+ const result = yield* bss.sync("2.0.0");
241
+ // => { updated: true, skipped: false, current: "2.0.0" }
242
+ }).pipe(
243
+ Effect.provide(BiomeSchemaSyncLive),
244
+ Effect.provide(NodeContext.layer),
245
+ ),
246
+ );
247
+ ```
248
+
249
+ ---
250
+
251
+ ### FileSystem + CommandExecutor Layer Required
252
+
253
+ #### ToolDiscovery
254
+
255
+ Locate CLI tools globally (PATH) or locally (via package manager), extract versions, enforce constraints, and cache results.
256
+
257
+ **ToolDefinition** configures how a tool is resolved: `VersionExtractor` (Flag, Json, or None), `ResolutionPolicy` (Report, PreferLocal, PreferGlobal, RequireMatch), and `SourceRequirement` (Any, OnlyLocal, OnlyGlobal, Both). Equality is based on tool name only.
258
+
259
+ **ResolvedTool** is the result of resolution. It carries the tool's name, source (`"global"` or `"local"`), version, and package manager. Its `exec()` and `dlx()` methods return a **ToolCommand** -- a wrapper around `@effect/platform` `Command` with instance-method ergonomics (`string()`, `lines()`, `exitCode()`, `stream()`).
260
+
261
+ ```typescript
262
+ import { Effect } from "effect";
263
+ import { NodeContext } from "@effect/platform-node";
264
+ import {
265
+ ToolDiscovery, ToolDiscoveryLive, ToolDefinition,
266
+ } from "@savvy-web/silk-effects";
267
+
268
+ const output = await Effect.runPromise(
269
+ Effect.gen(function* () {
270
+ const td = yield* ToolDiscovery;
271
+
272
+ // Resolve a tool (results are cached by name)
273
+ const biome = yield* td.resolve(ToolDefinition.make({ name: "biome" }));
274
+
275
+ // Check availability without throwing
276
+ const hasBiome = yield* td.isAvailable(ToolDefinition.make({ name: "biome" }));
277
+
278
+ // Execute the resolved tool
279
+ return yield* biome.exec("check", ".").string();
280
+ }).pipe(
281
+ Effect.provide(ToolDiscoveryLive),
282
+ Effect.provide(NodeContext.layer),
283
+ ),
284
+ );
285
+ ```
286
+
287
+ Use `require()` to fail with a descriptive error if the tool is not found:
288
+
289
+ ```typescript
290
+ const biome = yield* td.require(
291
+ ToolDefinition.make({ name: "biome" }),
292
+ "Biome is required for linting",
293
+ );
294
+ ```
81
295
 
82
296
  ## Documentation
83
297
 
84
- For service API reference, schemas, error types, and advanced usage, see [docs/](./docs/).
298
+ For architecture details, service patterns, and design rationale, see the [design documentation](./.claude/design/silk-effects/architecture.md).
85
299
 
86
300
  ## License
87
301
 
package/index.js CHANGED
@@ -202,7 +202,7 @@ class SectionBlock extends Schema.TaggedClass()("SectionBlock", {
202
202
  get rendered() {
203
203
  const begin = `${this.commentStyle} --- BEGIN ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
204
204
  const end = `${this.commentStyle} --- END ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
205
- return `${begin}${this.content}${end}`;
205
+ return `${begin}\n${this.content}\n${end}`;
206
206
  }
207
207
  prepend(lines) {
208
208
  return SectionBlock.make({
@@ -560,16 +560,19 @@ function parseContent(content, toolName, commentStyle) {
560
560
  const beginIndex = content.indexOf(begin);
561
561
  const endIndex = content.indexOf(end);
562
562
  if (-1 === beginIndex || -1 === endIndex || endIndex <= beginIndex) return null;
563
+ let managed = content.slice(beginIndex + begin.length, endIndex);
564
+ if (managed.startsWith("\n")) managed = managed.slice(1);
565
+ if (managed.endsWith("\n")) managed = managed.slice(0, -1);
563
566
  return {
564
567
  before: content.slice(0, beginIndex),
565
- managed: content.slice(beginIndex + begin.length, endIndex),
568
+ managed,
566
569
  after: content.slice(endIndex + end.length)
567
570
  };
568
571
  }
569
572
  function assembleContent(before, managed, after, toolName, commentStyle) {
570
573
  const begin = beginMarker(toolName, commentStyle);
571
574
  const end = endMarker(toolName, commentStyle);
572
- return `${before}${begin}${managed}${end}${after}`;
575
+ return `${before}${begin}\n${managed}\n${end}${after}`;
573
576
  }
574
577
  class ManagedSection extends Context.Tag("@savvy-web/silk-effects/ManagedSection")() {
575
578
  }
@@ -614,12 +617,12 @@ const ManagedSectionLive = Layer.effect(ManagedSection, Effect.gen(function*() {
614
617
  const trimmed = raw.trimEnd();
615
618
  const begin = beginMarker(block.toolName, block.commentStyle);
616
619
  const end = endMarker(block.toolName, block.commentStyle);
617
- fileContent = `${trimmed}\n\n${begin}${block.content}${end}\n`;
620
+ fileContent = `${trimmed}\n\n${begin}\n${block.content}\n${end}\n`;
618
621
  }
619
622
  } else {
620
623
  const begin = beginMarker(block.toolName, block.commentStyle);
621
624
  const end = endMarker(block.toolName, block.commentStyle);
622
- fileContent = `${begin}${block.content}${end}\n`;
625
+ fileContent = `${begin}\n${block.content}\n${end}\n`;
623
626
  }
624
627
  yield* fs.writeFileString(path, fileContent).pipe(Effect.mapError((cause)=>new SectionWriteError({
625
628
  path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/silk-effects",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "Shared Effect library for Silk Suite conventions",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/silk-effects",
@@ -30,7 +30,7 @@
30
30
  "@effect/platform": "^0.96.0",
31
31
  "jsonc-effect": "^0.2.1",
32
32
  "semver-effect": "^0.2.1",
33
- "workspaces-effect": "^0.2.0",
33
+ "workspaces-effect": "^0.3.0",
34
34
  "yaml-effect": "^0.2.3"
35
35
  },
36
36
  "peerDependencies": {