@savvy-web/silk-effects 0.1.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.
package/README.md CHANGED
@@ -3,16 +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
- - Discover config files using a priority-based search convention
15
- - Keep Biome `$schema` URLs in sync across config files
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
12
+ - Discover and resolve CLI tools globally or locally with version enforcement and caching
13
+ - Detect versioning strategy and format git tags from changeset configuration
14
+ - Locate config files and keep Biome schema URLs in sync across workspaces
16
15
 
17
16
  ## Installation
18
17
 
@@ -20,13 +19,37 @@ Shared [Effect](https://effect.website/) library providing Silk Suite convention
20
19
  pnpm add @savvy-web/silk-effects effect @effect/platform @effect/platform-node
21
20
  ```
22
21
 
23
- `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.
24
23
 
25
24
  ## Quick Start
26
25
 
26
+ All exports come from the package root:
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
+
27
50
  ```typescript
28
51
  import { Effect } from "effect";
29
- import { TargetResolver, TargetResolverLive } from "@savvy-web/silk-effects/publish";
52
+ import { TargetResolver, TargetResolverLive } from "@savvy-web/silk-effects";
30
53
 
31
54
  const targets = await Effect.runPromise(
32
55
  Effect.gen(function* () {
@@ -34,18 +57,91 @@ const targets = await Effect.runPromise(
34
57
  return yield* resolver.resolve(["npm", "github"]);
35
58
  }).pipe(Effect.provide(TargetResolverLive)),
36
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
+ );
37
85
  ```
38
86
 
39
- Modules that access the filesystem require a platform layer:
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.
40
90
 
41
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"
103
+ ```
104
+
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.
120
+
121
+ ```typescript
122
+ import { Effect } from "effect";
42
123
  import { NodeContext } from "@effect/platform-node";
43
- import { ManagedSection, ManagedSectionLive } from "@savvy-web/silk-effects/hooks";
124
+ import {
125
+ ManagedSection, ManagedSectionLive, SectionDefinition,
126
+ } from "@savvy-web/silk-effects";
127
+
128
+ // Define section identity
129
+ const def = SectionDefinition.make({ toolName: "LINT-STAGED" });
130
+
131
+ // Create a content block from the definition
132
+ const block = def.block("\nnpx lint-staged\n");
44
133
 
45
134
  await Effect.runPromise(
46
135
  Effect.gen(function* () {
47
- const section = yield* ManagedSection;
48
- yield* section.write(".husky/pre-commit", "silk", "\nnpx lint-staged\n");
136
+ const ms = yield* ManagedSection;
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
49
145
  }).pipe(
50
146
  Effect.provide(ManagedSectionLive),
51
147
  Effect.provide(NodeContext.layer),
@@ -53,22 +149,153 @@ await Effect.runPromise(
53
149
  );
54
150
  ```
55
151
 
56
- ## Modules
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
57
186
 
58
- Each module has its own entry point -- import only what you need:
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`).
59
188
 
60
- | Module | Entry Point | Platform Layer | Docs |
61
- | ------ | ----------- | -------------- | ---- |
62
- | Publish | `@savvy-web/silk-effects/publish` | No | [docs/publish.md](./docs/publish.md) |
63
- | Versioning | `@savvy-web/silk-effects/versioning` | Yes | [docs/versioning.md](./docs/versioning.md) |
64
- | Tags | `@savvy-web/silk-effects/tags` | No | [docs/tags.md](./docs/tags.md) |
65
- | Hooks | `@savvy-web/silk-effects/hooks` | Yes | [docs/hooks.md](./docs/hooks.md) |
66
- | Config | `@savvy-web/silk-effects/config` | Yes | [docs/config.md](./docs/config.md) |
67
- | Biome | `@savvy-web/silk-effects/biome` | Yes | [docs/biome.md](./docs/biome.md) |
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
+ ```
68
295
 
69
296
  ## Documentation
70
297
 
71
- 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).
72
299
 
73
300
  ## License
74
301