@savvy-web/silk-effects 4.2.6 → 5.0.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 +73 -192
- package/changesets/api/changelog.js +1 -2
- package/changesets/changelog/index.js +8 -10
- package/changesets/errors.js +0 -1
- package/changesets/index.js +1 -4
- package/changesets/services/changelog.js +1 -1
- package/changesets/services/github.js +1 -1
- package/changesets/utils/dependency-table.js +1 -1
- package/index.d.ts +95 -1398
- package/index.js +1 -20
- package/lint/cli/sections.js +9 -8
- package/package.json +7 -5
- package/schemas/SavvySections.js +26 -9
- package/schemas/VersioningSchemas.js +1 -32
- package/schemas/WorkspaceAnalysisSchemas.js +4 -5
- package/services/SilkPublishability.js +1 -1
- package/services/SilkWorkspaceAnalyzer.js +13 -16
- package/turbo/services/TurboInspector.js +11 -11
- package/changesets/services/markdown.js +0 -93
- package/errors/SectionParseError.js +0 -17
- package/errors/SectionValidationError.js +0 -17
- package/errors/SectionWriteError.js +0 -17
- package/errors/TagFormatError.js +0 -21
- package/errors/ToolNotFoundError.js +0 -12
- package/errors/ToolResolutionError.js +0 -12
- package/errors/ToolVersionMismatchError.js +0 -12
- package/errors/VersioningDetectionError.js +0 -21
- package/schemas/CommentStyle.js +0 -17
- package/schemas/ResolvedTool.js +0 -99
- package/schemas/SectionBlock.js +0 -71
- package/schemas/SectionDefinition.js +0 -123
- package/schemas/SectionResults.js +0 -21
- package/schemas/TagStrategySchemas.js +0 -19
- package/schemas/ToolDefinition.js +0 -40
- package/schemas/ToolResults.js +0 -28
- package/services/ManagedSection.js +0 -289
- package/services/TagStrategy.js +0 -56
- package/services/ToolDiscovery.js +0 -232
- package/services/VersioningStrategy.js +0 -69
- package/utils/ToolCommand.js +0 -67
package/schemas/ResolvedTool.js
DELETED
|
@@ -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 };
|
package/schemas/SectionBlock.js
DELETED
|
@@ -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 };
|
package/schemas/ToolResults.js
DELETED
|
@@ -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 };
|