@savvy-web/silk-effects 4.2.5 → 5.0.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.
Files changed (41) hide show
  1. package/README.md +73 -192
  2. package/changesets/api/changelog.js +1 -2
  3. package/changesets/changelog/index.js +8 -10
  4. package/changesets/errors.js +0 -1
  5. package/changesets/index.js +1 -4
  6. package/changesets/services/changelog.js +1 -1
  7. package/changesets/services/config-inspector.js +36 -17
  8. package/changesets/services/github.js +1 -1
  9. package/changesets/utils/dependency-table.js +1 -1
  10. package/index.d.ts +95 -1398
  11. package/index.js +1 -20
  12. package/lint/cli/sections.js +9 -8
  13. package/package.json +7 -5
  14. package/schemas/SavvySections.js +26 -9
  15. package/schemas/VersioningSchemas.js +1 -32
  16. package/schemas/WorkspaceAnalysisSchemas.js +4 -5
  17. package/services/SilkPublishability.js +1 -1
  18. package/services/SilkWorkspaceAnalyzer.js +13 -16
  19. package/turbo/services/TurboInspector.js +11 -11
  20. package/changesets/services/markdown.js +0 -93
  21. package/errors/SectionParseError.js +0 -17
  22. package/errors/SectionValidationError.js +0 -17
  23. package/errors/SectionWriteError.js +0 -17
  24. package/errors/TagFormatError.js +0 -21
  25. package/errors/ToolNotFoundError.js +0 -12
  26. package/errors/ToolResolutionError.js +0 -12
  27. package/errors/ToolVersionMismatchError.js +0 -12
  28. package/errors/VersioningDetectionError.js +0 -21
  29. package/schemas/CommentStyle.js +0 -17
  30. package/schemas/ResolvedTool.js +0 -99
  31. package/schemas/SectionBlock.js +0 -71
  32. package/schemas/SectionDefinition.js +0 -123
  33. package/schemas/SectionResults.js +0 -21
  34. package/schemas/TagStrategySchemas.js +0 -19
  35. package/schemas/ToolDefinition.js +0 -40
  36. package/schemas/ToolResults.js +0 -28
  37. package/services/ManagedSection.js +0 -289
  38. package/services/TagStrategy.js +0 -56
  39. package/services/ToolDiscovery.js +0 -232
  40. package/services/VersioningStrategy.js +0 -69
  41. package/utils/ToolCommand.js +0 -67
@@ -1,99 +0,0 @@
1
- import { ToolCommand } from "../utils/ToolCommand.js";
2
- import { ToolSource } from "./ToolResults.js";
3
- import { Equal, Hash, Schema } from "effect";
4
- import { ChildProcess } from "effect/unstable/process";
5
-
6
- //#region src/schemas/ResolvedTool.ts
7
- const PackageManager = Schema.Literals([
8
- "npm",
9
- "pnpm",
10
- "yarn",
11
- "bun"
12
- ]);
13
- /**
14
- * Result of resolving a {@link ToolDefinition}.
15
- *
16
- * Provides `exec` and `dlx` to build commands for the resolved tool.
17
- *
18
- * @since 0.2.0
19
- * @public
20
- */
21
- var ResolvedTool = class ResolvedTool extends Schema.TaggedClass()("ResolvedTool", {
22
- name: Schema.String,
23
- source: ToolSource,
24
- version: Schema.Option(Schema.String),
25
- globalVersion: Schema.Option(Schema.String),
26
- localVersion: Schema.Option(Schema.String),
27
- packageManager: PackageManager,
28
- mismatch: Schema.Boolean
29
- }) {
30
- get isGlobal() {
31
- return this.source === "global";
32
- }
33
- get isLocal() {
34
- return this.source === "local";
35
- }
36
- get hasVersionMismatch() {
37
- return this.mismatch;
38
- }
39
- exec(...args) {
40
- if (this.source === "global") return new ToolCommand(ChildProcess.make(this.name, args));
41
- switch (this.packageManager) {
42
- case "pnpm": return new ToolCommand(ChildProcess.make("pnpm", [
43
- "exec",
44
- this.name,
45
- ...args
46
- ]));
47
- case "npm": return new ToolCommand(ChildProcess.make("npx", [
48
- "--no",
49
- "--",
50
- this.name,
51
- ...args
52
- ]));
53
- case "yarn": return new ToolCommand(ChildProcess.make("yarn", [
54
- "exec",
55
- this.name,
56
- ...args
57
- ]));
58
- case "bun": return new ToolCommand(ChildProcess.make("bun", [
59
- "x",
60
- "--no-install",
61
- this.name,
62
- ...args
63
- ]));
64
- }
65
- }
66
- dlx(...args) {
67
- switch (this.packageManager) {
68
- case "pnpm": return new ToolCommand(ChildProcess.make("pnpm", [
69
- "dlx",
70
- this.name,
71
- ...args
72
- ]));
73
- case "npm": return new ToolCommand(ChildProcess.make("npx", [this.name, ...args]));
74
- case "yarn": return new ToolCommand(ChildProcess.make("yarn", [
75
- "dlx",
76
- this.name,
77
- ...args
78
- ]));
79
- case "bun": return new ToolCommand(ChildProcess.make("bun", [
80
- "x",
81
- this.name,
82
- ...args
83
- ]));
84
- }
85
- }
86
- [Equal.symbol](that) {
87
- if (!(that instanceof ResolvedTool)) return false;
88
- return this.name === that.name && this.source === that.source && Equal.equals(this.version, that.version);
89
- }
90
- [Hash.symbol]() {
91
- let h = Hash.hash(this.name);
92
- h = Hash.combine(h, Hash.hash(this.source));
93
- h = Hash.combine(h, Hash.hash(this.version));
94
- return Hash.optimize(h);
95
- }
96
- };
97
-
98
- //#endregion
99
- export { ResolvedTool };
@@ -1,71 +0,0 @@
1
- import { CommentStyle } from "./CommentStyle.js";
2
- import { SectionDiff } from "./SectionResults.js";
3
- import { Equal, Function, Hash, Schema } from "effect";
4
-
5
- //#region src/schemas/SectionBlock.ts
6
- /**
7
- * The content between managed section markers.
8
- *
9
- * `Equal` compares normalized content only (trimmed, whitespace-collapsed).
10
- * Use `diff` to compute line-level differences.
11
- *
12
- * @since 0.2.0
13
- * @public
14
- */
15
- var SectionBlock = class SectionBlock extends Schema.TaggedClass()("SectionBlock", {
16
- toolName: Schema.String,
17
- commentStyle: CommentStyle,
18
- content: Schema.String
19
- }) {
20
- static diff = Function.dual(2, (self, that) => self.diff(that));
21
- static prepend = Function.dual(2, (self, lines) => self.prepend(lines));
22
- static append = Function.dual(2, (self, lines) => self.append(lines));
23
- get text() {
24
- return this.content;
25
- }
26
- get normalized() {
27
- return this.content.trim().replace(/\s+/g, " ");
28
- }
29
- get rendered() {
30
- const begin = `${this.commentStyle} --- BEGIN ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
31
- const end = `${this.commentStyle} --- END ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
32
- return `${begin}\n${this.content}\n${end}`;
33
- }
34
- prepend(lines) {
35
- return SectionBlock.make({
36
- toolName: this.toolName,
37
- commentStyle: this.commentStyle,
38
- content: `${lines}\n${this.content}`
39
- });
40
- }
41
- append(lines) {
42
- return SectionBlock.make({
43
- toolName: this.toolName,
44
- commentStyle: this.commentStyle,
45
- content: `${this.content}\n${lines}`
46
- });
47
- }
48
- diff(that) {
49
- if (this.normalized === that.normalized) return SectionDiff.Unchanged();
50
- const selfLines = this.content.trim().split("\n");
51
- const thatLines = that.content.trim().split("\n");
52
- const selfSet = new Set(selfLines);
53
- const thatSet = new Set(thatLines);
54
- const removed = selfLines.filter((line) => !thatSet.has(line));
55
- const added = thatLines.filter((line) => !selfSet.has(line));
56
- return SectionDiff.Changed({
57
- added,
58
- removed
59
- });
60
- }
61
- [Equal.symbol](that) {
62
- if (!(that instanceof SectionBlock)) return false;
63
- return this.normalized === that.normalized;
64
- }
65
- [Hash.symbol]() {
66
- return Hash.optimize(Hash.hash(this.normalized));
67
- }
68
- };
69
-
70
- //#endregion
71
- export { SectionBlock };
@@ -1,123 +0,0 @@
1
- import { SectionValidationError } from "../errors/SectionValidationError.js";
2
- import { CommentStyle } from "./CommentStyle.js";
3
- import { SectionDiff } from "./SectionResults.js";
4
- import { SectionBlock } from "./SectionBlock.js";
5
- import { Effect, Equal, Function, Hash, Schema } from "effect";
6
-
7
- //#region src/schemas/SectionDefinition.ts
8
- /**
9
- * Identity envelope for a managed section type.
10
- *
11
- * `Equal` compares on `toolName` + `commentStyle`.
12
- * Use {@link SectionDefinition.block | block()} to create a {@link SectionBlock},
13
- * or `generate()` for a typed factory.
14
- *
15
- * @since 0.2.0
16
- * @public
17
- */
18
- var SectionDefinition = class SectionDefinition extends Schema.TaggedClass()("SectionDefinition", {
19
- toolName: Schema.String,
20
- commentStyle: CommentStyle.pipe(Schema.withDecodingDefaultType(Effect.succeed("#")), Schema.withConstructorDefault(Effect.succeed("#")))
21
- }) {
22
- _validate;
23
- static generate = Function.dual(2, (self, fn) => {
24
- return (config) => self.block(fn(config));
25
- });
26
- static generateEffect = Function.dual(2, (self, fn) => {
27
- return (config) => Effect.flatMap(fn(config), (content) => {
28
- try {
29
- return Effect.succeed(self.block(content));
30
- } catch (e) {
31
- return Effect.fail(e);
32
- }
33
- });
34
- });
35
- static withValidation = Function.dual(2, (self, fn) => {
36
- const copy = SectionDefinition.make({
37
- toolName: self.toolName,
38
- commentStyle: self.commentStyle
39
- });
40
- copy._validate = fn;
41
- return copy;
42
- });
43
- static diff = Function.dual(2, (self, that) => self.diff(that));
44
- block(content) {
45
- const block = SectionBlock.make({
46
- toolName: this.toolName,
47
- commentStyle: this.commentStyle,
48
- content
49
- });
50
- if (this._validate && !this._validate(block)) throw new SectionValidationError({
51
- toolName: this.toolName,
52
- reason: "Content failed validation"
53
- });
54
- return block;
55
- }
56
- generate(fn) {
57
- return (config) => this.block(fn(config));
58
- }
59
- generateEffect(fn) {
60
- return (config) => Effect.flatMap(fn(config), (content) => {
61
- try {
62
- return Effect.succeed(this.block(content));
63
- } catch (e) {
64
- return Effect.fail(e);
65
- }
66
- });
67
- }
68
- diff(that) {
69
- if (Equal.equals(this, that)) return SectionDiff.Unchanged();
70
- return SectionDiff.Changed({
71
- added: [that.toolName !== this.toolName ? `toolName: ${that.toolName}` : "", that.commentStyle !== this.commentStyle ? `commentStyle: ${that.commentStyle}` : ""].filter(Boolean),
72
- removed: [that.toolName !== this.toolName ? `toolName: ${this.toolName}` : "", that.commentStyle !== this.commentStyle ? `commentStyle: ${this.commentStyle}` : ""].filter(Boolean)
73
- });
74
- }
75
- get beginMarker() {
76
- return `${this.commentStyle} --- BEGIN ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
77
- }
78
- get endMarker() {
79
- return `${this.commentStyle} --- END ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
80
- }
81
- [Equal.symbol](that) {
82
- if (!(that instanceof SectionDefinition)) return false;
83
- return this.toolName === that.toolName && this.commentStyle === that.commentStyle;
84
- }
85
- [Hash.symbol]() {
86
- return Hash.optimize(Hash.combine(Hash.hash(this.toolName), Hash.hash(this.commentStyle)));
87
- }
88
- };
89
- /**
90
- * Convenience section definition for shell hooks.
91
- *
92
- * `commentStyle` is always `"#"` — only `toolName` is required.
93
- *
94
- * @since 0.2.0
95
- * @public
96
- */
97
- var ShellSectionDefinition = class extends Schema.TaggedClass()("ShellSectionDefinition", { toolName: Schema.String }) {
98
- get commentStyle() {
99
- return "#";
100
- }
101
- block(content) {
102
- return SectionBlock.make({
103
- toolName: this.toolName,
104
- commentStyle: "#",
105
- content
106
- });
107
- }
108
- generate(fn) {
109
- return (config) => this.block(fn(config));
110
- }
111
- generateEffect(fn) {
112
- return (config) => Effect.map(fn(config), (content) => this.block(content));
113
- }
114
- get beginMarker() {
115
- return `# --- BEGIN ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
116
- }
117
- get endMarker() {
118
- return `# --- END ${this.toolName.toUpperCase()} MANAGED SECTION ---`;
119
- }
120
- };
121
-
122
- //#endregion
123
- export { SectionDefinition, ShellSectionDefinition };
@@ -1,21 +0,0 @@
1
- import { Data } from "effect";
2
-
3
- //#region src/schemas/SectionResults.ts
4
- /**
5
- * @since 0.2.0
6
- * @public
7
- */
8
- const SectionDiff = Data.taggedEnum();
9
- /**
10
- * @since 0.2.0
11
- * @public
12
- */
13
- const SyncResult = Data.taggedEnum();
14
- /**
15
- * @since 0.2.0
16
- * @public
17
- */
18
- const CheckResult = Data.taggedEnum();
19
-
20
- //#endregion
21
- export { CheckResult, SectionDiff, SyncResult };
@@ -1,19 +0,0 @@
1
- import { Schema } from "effect";
2
-
3
- //#region src/schemas/TagStrategySchemas.ts
4
- /**
5
- * Git tag naming strategy for a workspace.
6
- *
7
- * @remarks
8
- * - `"single"` — one shared tag for the entire release (e.g. `1.2.3`).
9
- * - `"scoped"` — a per-package tag that includes the package name (e.g. `@my-org/pkg@1.2.3`).
10
- *
11
- * Determined by `TagStrategy.determine` based on the {@link (VersioningStrategyResult:type)}.
12
- *
13
- * @since 0.1.0
14
- * @public
15
- */
16
- const TagStrategyType = Schema.Literals(["single", "scoped"]);
17
-
18
- //#endregion
19
- export { TagStrategyType };
@@ -1,40 +0,0 @@
1
- import { ResolutionPolicy, SourceRequirement, VersionExtractor } from "./ToolResults.js";
2
- import { Equal, Hash, Schema } from "effect";
3
-
4
- //#region src/schemas/ToolDefinition.ts
5
- const NameSchema = Schema.Struct({ name: Schema.String });
6
- /**
7
- * Declares a CLI tool's identity and resolution constraints.
8
- *
9
- * `Equal` compares on `name` only (identity).
10
- *
11
- * @since 0.2.0
12
- * @public
13
- */
14
- var ToolDefinition = class ToolDefinition {
15
- _tag = "ToolDefinition";
16
- name;
17
- versionExtractor;
18
- policy;
19
- source;
20
- constructor(name, versionExtractor, policy, source) {
21
- this.name = name;
22
- this.versionExtractor = versionExtractor;
23
- this.policy = policy;
24
- this.source = source;
25
- }
26
- static make(options) {
27
- Schema.decodeUnknownSync(NameSchema)({ name: options.name });
28
- return new ToolDefinition(options.name, options.versionExtractor ?? VersionExtractor.Flag({ flag: "--version" }), options.policy ?? ResolutionPolicy.Report(), options.source ?? SourceRequirement.Any());
29
- }
30
- [Equal.symbol](that) {
31
- if (!(that instanceof ToolDefinition)) return false;
32
- return this.name === that.name;
33
- }
34
- [Hash.symbol]() {
35
- return Hash.optimize(Hash.hash(this.name));
36
- }
37
- };
38
-
39
- //#endregion
40
- export { ToolDefinition };
@@ -1,28 +0,0 @@
1
- import { Data, Schema } from "effect";
2
-
3
- //#region src/schemas/ToolResults.ts
4
- /**
5
- * Where a tool was resolved from.
6
- *
7
- * @since 0.2.0
8
- * @public
9
- */
10
- const ToolSource = Schema.Literals(["global", "local"]);
11
- /**
12
- * @since 0.2.0
13
- * @public
14
- */
15
- const VersionExtractor = Data.taggedEnum();
16
- /**
17
- * @since 0.2.0
18
- * @public
19
- */
20
- const ResolutionPolicy = Data.taggedEnum();
21
- /**
22
- * @since 0.2.0
23
- * @public
24
- */
25
- const SourceRequirement = Data.taggedEnum();
26
-
27
- //#endregion
28
- export { ResolutionPolicy, SourceRequirement, ToolSource, VersionExtractor };
@@ -1,289 +0,0 @@
1
- import { SectionParseError } from "../errors/SectionParseError.js";
2
- import { SectionWriteError } from "../errors/SectionWriteError.js";
3
- import { CheckResult, SyncResult } from "../schemas/SectionResults.js";
4
- import { SectionBlock } from "../schemas/SectionBlock.js";
5
- import { Context, Effect, Equal, FileSystem, Function, Layer } from "effect";
6
-
7
- //#region src/services/ManagedSection.ts
8
- function beginMarker(toolName, commentStyle) {
9
- return `${commentStyle} --- BEGIN ${toolName.toUpperCase()} MANAGED SECTION ---`;
10
- }
11
- function endMarker(toolName, commentStyle) {
12
- return `${commentStyle} --- END ${toolName.toUpperCase()} MANAGED SECTION ---`;
13
- }
14
- function parseContent(content, toolName, commentStyle) {
15
- const begin = beginMarker(toolName, commentStyle);
16
- const end = endMarker(toolName, commentStyle);
17
- const beginIndex = content.indexOf(begin);
18
- const endIndex = content.indexOf(end);
19
- if (beginIndex === -1 || endIndex === -1 || endIndex <= beginIndex) return null;
20
- let managed = content.slice(beginIndex + begin.length, endIndex);
21
- if (managed.startsWith("\n")) managed = managed.slice(1);
22
- if (managed.endsWith("\n")) managed = managed.slice(0, -1);
23
- return {
24
- before: content.slice(0, beginIndex),
25
- managed,
26
- after: content.slice(endIndex + end.length)
27
- };
28
- }
29
- function assembleContent(before, managed, after, toolName, commentStyle) {
30
- return `${before}${beginMarker(toolName, commentStyle)}\n${managed}\n${endMarker(toolName, commentStyle)}${after}`;
31
- }
32
- const BEGIN_MARKER_RE = /^(#|\/\/) --- BEGIN (.+?) MANAGED SECTION ---$/gm;
33
- function sectionKey(toolName, commentStyle) {
34
- return `${toolName.toUpperCase()}::${commentStyle}`;
35
- }
36
- /**
37
- * Locate every managed section in `content`, in document order, across all tools and
38
- * comment styles. Unterminated begin markers are skipped.
39
- */
40
- function findAllSections(content) {
41
- const results = [];
42
- for (const match of content.matchAll(BEGIN_MARKER_RE)) {
43
- const style = match[1];
44
- const name = match[2];
45
- const beginStart = match.index;
46
- const beginEnd = beginStart + match[0].length;
47
- const end = `${style} --- END ${name} MANAGED SECTION ---`;
48
- const endIdx = content.indexOf(end, beginEnd);
49
- if (endIdx === -1) continue;
50
- let inner = content.slice(beginEnd, endIdx);
51
- if (inner.startsWith("\n")) inner = inner.slice(1);
52
- if (inner.endsWith("\n")) inner = inner.slice(0, -1);
53
- results.push({
54
- key: sectionKey(name, style),
55
- commentStyle: style,
56
- content: inner,
57
- raw: content.slice(beginStart, endIdx + end.length),
58
- start: beginStart,
59
- end: endIdx + end.length
60
- });
61
- }
62
- return results;
63
- }
64
- /**
65
- * Service for managing delimited sections in user-editable files.
66
- *
67
- * All methods use dual API (data-first and data-last).
68
- * Identity-only operations (`read`, `isManaged`) take a {@link SectionDefinition}.
69
- * Content operations (`write`, `sync`, `check`) take a {@link SectionBlock}.
70
- *
71
- * @since 0.2.0
72
- * @public
73
- */
74
- var ManagedSection = class extends Context.Service()("@savvy-web/silk-effects/ManagedSection") {};
75
- /**
76
- * Live implementation of {@link ManagedSection} backed by the core `FileSystem` service.
77
- *
78
- * @since 0.2.0
79
- * @public
80
- */
81
- const ManagedSectionLive = Layer.effect(ManagedSection, Effect.gen(function* () {
82
- const fs = yield* FileSystem.FileSystem;
83
- const read = Function.dual(2, (path, definition) => Effect.gen(function* () {
84
- if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return null;
85
- const parsed = parseContent(yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionParseError({
86
- path,
87
- reason: String(cause)
88
- }))), definition.toolName, definition.commentStyle);
89
- if (parsed === null) return null;
90
- return SectionBlock.make({
91
- toolName: definition.toolName,
92
- commentStyle: definition.commentStyle,
93
- content: parsed.managed
94
- });
95
- }));
96
- const isManaged = Function.dual(2, (path, definition) => Effect.gen(function* () {
97
- if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return false;
98
- const raw = yield* fs.readFileString(path).pipe(Effect.orElseSucceed(() => ""));
99
- const begin = beginMarker(definition.toolName, definition.commentStyle);
100
- const end = endMarker(definition.toolName, definition.commentStyle);
101
- const beginIdx = raw.indexOf(begin);
102
- const endIdx = raw.indexOf(end);
103
- return beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx;
104
- }));
105
- const write = Function.dual(2, (path, block) => Effect.gen(function* () {
106
- const exists = yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false));
107
- let fileContent;
108
- if (exists) {
109
- const raw = yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
110
- path,
111
- reason: String(cause)
112
- })));
113
- const parsed = parseContent(raw, block.toolName, block.commentStyle);
114
- if (parsed !== null) fileContent = assembleContent(parsed.before, block.content, parsed.after, block.toolName, block.commentStyle);
115
- else {
116
- const trimmed = raw.trimEnd();
117
- const begin = beginMarker(block.toolName, block.commentStyle);
118
- const end = endMarker(block.toolName, block.commentStyle);
119
- fileContent = `${trimmed}\n\n${begin}\n${block.content}\n${end}\n`;
120
- }
121
- } else {
122
- const begin = beginMarker(block.toolName, block.commentStyle);
123
- const end = endMarker(block.toolName, block.commentStyle);
124
- fileContent = `${begin}\n${block.content}\n${end}\n`;
125
- }
126
- yield* fs.writeFileString(path, fileContent).pipe(Effect.mapError((cause) => new SectionWriteError({
127
- path,
128
- reason: String(cause)
129
- })));
130
- }));
131
- return {
132
- read,
133
- write,
134
- isManaged,
135
- sync: Function.dual(2, (path, block) => Effect.gen(function* () {
136
- const onDisk = yield* read(path, {
137
- toolName: block.toolName,
138
- commentStyle: block.commentStyle
139
- }).pipe(Effect.mapError((cause) => new SectionWriteError({
140
- path,
141
- reason: String(cause)
142
- })));
143
- if (onDisk === null) {
144
- yield* write(path, block);
145
- return SyncResult.Created();
146
- }
147
- if (Equal.equals(onDisk, block)) return SyncResult.Unchanged();
148
- const d = SectionBlock.diff(onDisk, block);
149
- yield* write(path, block);
150
- return SyncResult.Updated({ diff: d });
151
- })),
152
- syncMany: Function.dual(2, (path, blocks) => Effect.gen(function* () {
153
- const original = (yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))) ? yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
154
- path,
155
- reason: String(cause)
156
- }))) : "";
157
- const keyOf = (b) => sectionKey(b.toolName, b.commentStyle);
158
- const found = findAllSections(original);
159
- const onDiskByKey = /* @__PURE__ */ new Map();
160
- for (const f of found) if (!onDiskByKey.has(f.key)) onDiskByKey.set(f.key, f);
161
- const results = blocks.map((block) => {
162
- const onDisk = onDiskByKey.get(keyOf(block));
163
- if (onDisk === void 0) return SyncResult.Created();
164
- const current = SectionBlock.make({
165
- toolName: block.toolName,
166
- commentStyle: block.commentStyle,
167
- content: onDisk.content
168
- });
169
- if (Equal.equals(current, block)) return SyncResult.Unchanged();
170
- return SyncResult.Updated({ diff: SectionBlock.diff(current, block) });
171
- });
172
- const items = [];
173
- let cursor = 0;
174
- for (const f of found) {
175
- items.push({
176
- kind: "text",
177
- value: original.slice(cursor, f.start)
178
- });
179
- items.push({
180
- kind: "section",
181
- key: f.key,
182
- raw: f.raw,
183
- render: null
184
- });
185
- cursor = f.end;
186
- }
187
- items.push({
188
- kind: "text",
189
- value: original.slice(cursor)
190
- });
191
- const targetKeys = new Set(blocks.map(keyOf));
192
- const slotIndices = [];
193
- items.forEach((item, idx) => {
194
- if (item.kind === "section" && targetKeys.has(item.key)) slotIndices.push(idx);
195
- });
196
- const itemIndexByDeclared = /* @__PURE__ */ new Map();
197
- let slotCursor = 0;
198
- blocks.forEach((block, declaredIdx) => {
199
- if (!onDiskByKey.has(keyOf(block))) return;
200
- if (slotCursor >= slotIndices.length) return;
201
- const itemIdx = slotIndices[slotCursor];
202
- const item = items[itemIdx];
203
- if (item.kind === "section") item.render = block;
204
- itemIndexByDeclared.set(declaredIdx, itemIdx);
205
- slotCursor += 1;
206
- });
207
- const beforeAnchor = /* @__PURE__ */ new Map();
208
- const afterAnchor = /* @__PURE__ */ new Map();
209
- const appendList = [];
210
- const pushInto = (map, anchor, block) => {
211
- const arr = map.get(anchor) ?? [];
212
- arr.push(block);
213
- map.set(anchor, arr);
214
- };
215
- blocks.forEach((block, i) => {
216
- if (onDiskByKey.has(keyOf(block))) return;
217
- let placed = false;
218
- for (let j = i + 1; j < blocks.length && !placed; j += 1) {
219
- const itemIdx = itemIndexByDeclared.get(j);
220
- if (itemIdx !== void 0) {
221
- pushInto(beforeAnchor, itemIdx, block);
222
- placed = true;
223
- }
224
- }
225
- for (let j = i - 1; j >= 0 && !placed; j -= 1) {
226
- const itemIdx = itemIndexByDeclared.get(j);
227
- if (itemIdx !== void 0) {
228
- pushInto(afterAnchor, itemIdx, block);
229
- placed = true;
230
- }
231
- }
232
- if (!placed) appendList.push(block);
233
- });
234
- const out = [];
235
- items.forEach((item, idx) => {
236
- if (item.kind === "text") {
237
- out.push(item.value);
238
- return;
239
- }
240
- for (const block of beforeAnchor.get(idx) ?? []) out.push(block.rendered, "\n\n");
241
- out.push(item.render === null ? item.raw : item.render.rendered);
242
- for (const block of afterAnchor.get(idx) ?? []) out.push("\n\n", block.rendered);
243
- });
244
- let output = out.join("");
245
- for (const block of appendList) output = output.trim() === "" ? `${block.rendered}\n` : `${output.replace(/\n+$/, "")}\n\n${block.rendered}\n`;
246
- if (output !== "" && !output.endsWith("\n")) output += "\n";
247
- if (output !== original) yield* fs.writeFileString(path, output).pipe(Effect.mapError((cause) => new SectionWriteError({
248
- path,
249
- reason: String(cause)
250
- })));
251
- return results;
252
- })),
253
- check: Function.dual(2, (path, block) => Effect.gen(function* () {
254
- const onDisk = yield* read(path, {
255
- toolName: block.toolName,
256
- commentStyle: block.commentStyle
257
- });
258
- if (onDisk === null) return CheckResult.NotFound();
259
- const isUpToDate = Equal.equals(onDisk, block);
260
- const d = SectionBlock.diff(onDisk, block);
261
- return CheckResult.Found({
262
- isUpToDate,
263
- diff: d
264
- });
265
- })),
266
- remove: Function.dual(2, (path, definition) => Effect.gen(function* () {
267
- if (!(yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)))) return false;
268
- const parsed = parseContent(yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new SectionWriteError({
269
- path,
270
- reason: String(cause)
271
- }))), definition.toolName, definition.commentStyle);
272
- if (parsed === null) return false;
273
- const before = parsed.before.replace(/\n+$/, "");
274
- const after = parsed.after.replace(/^\n+/, "");
275
- let next;
276
- if (before !== "" && after !== "") next = `${before}\n\n${after}`;
277
- else if (before !== "") next = `${before}\n`;
278
- else next = after;
279
- yield* fs.writeFileString(path, next).pipe(Effect.mapError((cause) => new SectionWriteError({
280
- path,
281
- reason: String(cause)
282
- })));
283
- return true;
284
- }))
285
- };
286
- }));
287
-
288
- //#endregion
289
- export { ManagedSection, ManagedSectionLive };