@savvy-web/silk-effects 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/README.md +242 -28
  2. 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/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.1",
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": {