@danieljvdm/dev-kit 0.11.3 → 0.13.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.
- package/README.md +111 -78
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/build-effect-clis/SKILL.md +61 -0
- package/skills/build-effect-clis/agents/openai.yaml +4 -0
- package/skills/build-effect-clis/references/command-design.md +93 -0
- package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
- package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
- package/skills/dev-kit/SKILL.md +77 -48
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +86 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +3 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +18 -16
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +181 -131
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
package/src/catalog.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
3
|
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
discoverPackageSkills,
|
|
7
|
+
resolvePackageSkillSelector,
|
|
8
|
+
type DiscoveredPackageSkill,
|
|
9
|
+
} from "./package-skill-source.ts";
|
|
6
10
|
import { observePath, type Digest } from "./path-digest.ts";
|
|
11
|
+
import { packageSkillInstallName } from "./skill-selector.ts";
|
|
7
12
|
import {
|
|
8
13
|
SkillSourcesLockSchema,
|
|
9
14
|
type LockedSkillSource,
|
|
@@ -45,7 +50,7 @@ export type ResolvedSkillSource = {
|
|
|
45
50
|
};
|
|
46
51
|
};
|
|
47
52
|
|
|
48
|
-
class CatalogError extends Schema.
|
|
53
|
+
class CatalogError extends Schema.TaggedError<CatalogError>()("CatalogError", {
|
|
49
54
|
message: Schema.String,
|
|
50
55
|
}) {}
|
|
51
56
|
|
|
@@ -61,7 +66,7 @@ const runGit = Effect.fn("runCatalogGit")(function* (cwd: string, args: Readonly
|
|
|
61
66
|
]);
|
|
62
67
|
|
|
63
68
|
if (exitCode !== 0) {
|
|
64
|
-
return yield*
|
|
69
|
+
return yield* CatalogError.make({
|
|
65
70
|
message: `git ${args.join(" ")} failed: ${output.trim()}`,
|
|
66
71
|
});
|
|
67
72
|
}
|
|
@@ -80,11 +85,11 @@ const readCatalogLock = Effect.fn("readCatalogLock")(function* (packageRoot: str
|
|
|
80
85
|
const value = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
81
86
|
|
|
82
87
|
if (errors.length > 0) {
|
|
83
|
-
return yield*
|
|
88
|
+
return yield* CatalogError.make({ message: `invalid skill catalog lock: ${lockPath}` });
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
return yield* Schema.decodeUnknownEffect(SkillSourcesLockSchema)(value).pipe(
|
|
87
|
-
Effect.mapError((error) =>
|
|
92
|
+
Effect.mapError((error) => CatalogError.make({ message: error.message })),
|
|
88
93
|
);
|
|
89
94
|
});
|
|
90
95
|
|
|
@@ -145,7 +150,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
145
150
|
|
|
146
151
|
for (const candidate of discovery.candidates) {
|
|
147
152
|
skills.push({
|
|
148
|
-
name: candidate.name,
|
|
153
|
+
name: packageSkillInstallName(candidate.package, candidate.name),
|
|
149
154
|
selector: candidate.selector,
|
|
150
155
|
description: candidate.description,
|
|
151
156
|
source: candidate.package,
|
|
@@ -159,7 +164,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
159
164
|
);
|
|
160
165
|
|
|
161
166
|
if (duplicates.length > 0) {
|
|
162
|
-
return yield*
|
|
167
|
+
return yield* CatalogError.make({
|
|
163
168
|
message: `duplicate catalog skill selector: ${duplicates[0]?.selector ?? "unknown"}`,
|
|
164
169
|
});
|
|
165
170
|
}
|
|
@@ -171,13 +176,12 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
171
176
|
);
|
|
172
177
|
|
|
173
178
|
if (duplicateFamily !== undefined) {
|
|
174
|
-
return yield*
|
|
179
|
+
return yield* CatalogError.make({
|
|
175
180
|
message: `duplicate catalog family: ${duplicateFamily[0]}`,
|
|
176
181
|
});
|
|
177
182
|
}
|
|
178
183
|
const families: Readonly<Record<string, ReadonlyArray<string>>> = {
|
|
179
|
-
effect: ["effect-ts"],
|
|
180
|
-
"effect-atom-data-fetching": ["effect-ts"],
|
|
184
|
+
effect: ["effect-ts", "effect-architecture-audit", "build-effect-apis", "build-effect-clis"],
|
|
181
185
|
...Object.fromEntries(externalFamilies),
|
|
182
186
|
};
|
|
183
187
|
|
|
@@ -206,6 +210,57 @@ const stripFrontmatterKeys = (text: string, keys: ReadonlyArray<string>): string
|
|
|
206
210
|
return `---\n${lines.join("\n")}\n---${frontmatter[2]}${text.slice(frontmatter[0].length)}`;
|
|
207
211
|
};
|
|
208
212
|
|
|
213
|
+
const rewriteFrontmatterName = (text: string, name: string): string => {
|
|
214
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
215
|
+
|
|
216
|
+
if (!frontmatter?.[1]) return text;
|
|
217
|
+
let replacing = false;
|
|
218
|
+
const lines = frontmatter[1].split(/\r?\n/).flatMap((line) => {
|
|
219
|
+
const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1];
|
|
220
|
+
|
|
221
|
+
if (key !== undefined) replacing = key === "name";
|
|
222
|
+
|
|
223
|
+
return replacing ? (key === undefined ? [] : [`name: ${name}`]) : [line];
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return `---\n${lines.join("\n")}\n---${frontmatter[2]}${text.slice(frontmatter[0].length)}`;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const materializePackageSkill = Effect.fn("materializePackageSkill")(function* (
|
|
230
|
+
projectDir: string,
|
|
231
|
+
skill: DiscoveredPackageSkill,
|
|
232
|
+
digest: Digest,
|
|
233
|
+
cache: boolean,
|
|
234
|
+
) {
|
|
235
|
+
const fs = yield* FileSystem.FileSystem;
|
|
236
|
+
const path = yield* Path.Path;
|
|
237
|
+
const installName = packageSkillInstallName(skill.package, skill.name);
|
|
238
|
+
const root = cache
|
|
239
|
+
? path.join(projectDir, ".dev-kit", "cache", "package-skills", installName)
|
|
240
|
+
: path.join(
|
|
241
|
+
yield* fs.makeTempDirectoryScoped({ prefix: "dev-kit-package-plan-" }),
|
|
242
|
+
installName,
|
|
243
|
+
);
|
|
244
|
+
const staged = path.join(root, "skill");
|
|
245
|
+
const ready = path.join(root, ".ready");
|
|
246
|
+
const stamp = `${skill.version}\n${digest}\n`;
|
|
247
|
+
|
|
248
|
+
if (!(yield* fs.exists(ready)) || (yield* fs.readFileString(ready)) !== stamp) {
|
|
249
|
+
yield* fs.remove(root, { force: true, recursive: true });
|
|
250
|
+
yield* fs.makeDirectory(root, { recursive: true });
|
|
251
|
+
yield* fs.copy(skill.path, staged, { overwrite: true });
|
|
252
|
+
const document = path.join(staged, "SKILL.md");
|
|
253
|
+
|
|
254
|
+
yield* fs.writeFileString(
|
|
255
|
+
document,
|
|
256
|
+
rewriteFrontmatterName(yield* fs.readFileString(document), installName),
|
|
257
|
+
);
|
|
258
|
+
yield* fs.writeFileString(ready, stamp);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return staged;
|
|
262
|
+
});
|
|
263
|
+
|
|
209
264
|
const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
210
265
|
projectDir: string,
|
|
211
266
|
source: LockedSkillSource,
|
|
@@ -234,14 +289,14 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
234
289
|
const actual = yield* runGit(checkout, ["rev-parse", "HEAD"]);
|
|
235
290
|
|
|
236
291
|
if (actual !== source.resolved) {
|
|
237
|
-
return yield*
|
|
292
|
+
return yield* CatalogError.make({
|
|
238
293
|
message: `source ${source.id} resolved to ${actual}, expected ${source.resolved}`,
|
|
239
294
|
});
|
|
240
295
|
}
|
|
241
296
|
const symlinks = yield* runGit(checkout, ["ls-files", "--stage", "--", source.skillsPath]);
|
|
242
297
|
|
|
243
298
|
if (symlinks.split(/\r?\n/).some((line) => line.startsWith("120000 "))) {
|
|
244
|
-
return yield*
|
|
299
|
+
return yield* CatalogError.make({
|
|
245
300
|
message: `source ${source.id} contains symlinks; refusing to install it`,
|
|
246
301
|
});
|
|
247
302
|
}
|
|
@@ -251,7 +306,7 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
251
306
|
const observation = yield* observePath(from);
|
|
252
307
|
|
|
253
308
|
if (observation.kind !== "directory") {
|
|
254
|
-
return yield*
|
|
309
|
+
return yield* CatalogError.make({
|
|
255
310
|
message: `source ${source.id} is missing skill ${skill}`,
|
|
256
311
|
});
|
|
257
312
|
}
|
|
@@ -275,7 +330,7 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
275
330
|
approvedDigest !== undefined &&
|
|
276
331
|
(observation.kind !== "directory" || observation.digest !== approvedDigest)
|
|
277
332
|
) {
|
|
278
|
-
return yield*
|
|
333
|
+
return yield* CatalogError.make({
|
|
279
334
|
message: `cached skill ${skill} does not match the approved catalog; remove ${root} and retry`,
|
|
280
335
|
});
|
|
281
336
|
}
|
|
@@ -324,10 +379,10 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
324
379
|
const observation = yield* observePath(resolved.path);
|
|
325
380
|
|
|
326
381
|
if (observation.kind !== "directory") {
|
|
327
|
-
return yield*
|
|
382
|
+
return yield* CatalogError.make({ message: `package skill is missing: ${selector}` });
|
|
328
383
|
}
|
|
329
384
|
sources.set(selector, {
|
|
330
|
-
path: resolved.
|
|
385
|
+
path: yield* materializePackageSkill(projectDir, resolved, observation.digest, cache),
|
|
331
386
|
linkPath: resolved.linkPath,
|
|
332
387
|
catalog: {
|
|
333
388
|
package: resolved.package,
|
package/src/effect-source.ts
CHANGED
|
@@ -28,7 +28,20 @@ export type EffectSourcePlan = {
|
|
|
28
28
|
readonly tag: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const EffectSourcePlanJsonSchema = Schema.fromJsonString(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
action: Schema.Literals(["sync", "unchanged", "skipped"]),
|
|
34
|
+
checkoutDir: Schema.String,
|
|
35
|
+
packageName: Schema.String,
|
|
36
|
+
packageVersion: Schema.String,
|
|
37
|
+
path: Schema.String,
|
|
38
|
+
projectDir: Schema.String,
|
|
39
|
+
repository: Schema.String,
|
|
40
|
+
tag: Schema.String,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export class EffectSourceDependencyError extends Schema.TaggedError<EffectSourceDependencyError>()(
|
|
32
45
|
"EffectSourceDependencyError",
|
|
33
46
|
{ packageName: Schema.String },
|
|
34
47
|
) {
|
|
@@ -37,12 +50,12 @@ export class EffectSourceDependencyError extends Schema.TaggedErrorClass<EffectS
|
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
export class EffectSourceCheckoutError extends Schema.
|
|
53
|
+
export class EffectSourceCheckoutError extends Schema.TaggedError<EffectSourceCheckoutError>()(
|
|
41
54
|
"EffectSourceCheckoutError",
|
|
42
55
|
{ message: Schema.String },
|
|
43
56
|
) {}
|
|
44
57
|
|
|
45
|
-
class EffectSourceCommandError extends Schema.
|
|
58
|
+
class EffectSourceCommandError extends Schema.TaggedError<EffectSourceCommandError>()(
|
|
46
59
|
"EffectSourceCommandError",
|
|
47
60
|
{ command: Schema.String, exitCode: Schema.Int, output: Schema.String },
|
|
48
61
|
) {
|
|
@@ -76,7 +89,7 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
|
|
|
76
89
|
const trimmed = output.trim();
|
|
77
90
|
|
|
78
91
|
if (exitCode !== 0) {
|
|
79
|
-
return yield*
|
|
92
|
+
return yield* EffectSourceCommandError.make({
|
|
80
93
|
command: [command, ...args].join(" "),
|
|
81
94
|
exitCode,
|
|
82
95
|
output: trimmed,
|
|
@@ -104,12 +117,12 @@ const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function*
|
|
|
104
117
|
.readFileString(manifestPath)
|
|
105
118
|
.pipe(
|
|
106
119
|
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
107
|
-
Effect.fail(
|
|
120
|
+
Effect.fail(EffectSourceDependencyError.make({ packageName })),
|
|
108
121
|
),
|
|
109
122
|
);
|
|
110
123
|
|
|
111
|
-
return yield* Schema.
|
|
112
|
-
Effect.mapError(() =>
|
|
124
|
+
return yield* Schema.decodeEffect(PackageVersionSchema)(contents).pipe(
|
|
125
|
+
Effect.mapError(() => EffectSourceDependencyError.make({ packageName })),
|
|
113
126
|
Effect.map((manifest) => manifest.version),
|
|
114
127
|
);
|
|
115
128
|
});
|
|
@@ -121,7 +134,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
121
134
|
const path = yield* Path.Path;
|
|
122
135
|
|
|
123
136
|
if (candidate.length === 0 || path.isAbsolute(candidate)) {
|
|
124
|
-
return yield*
|
|
137
|
+
return yield* EffectSourceCheckoutError.make({
|
|
125
138
|
message: `Effect source path must be a non-empty project-relative path: ${candidate}`,
|
|
126
139
|
});
|
|
127
140
|
}
|
|
@@ -134,7 +147,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
134
147
|
relative.startsWith(`..${path.sep}`) ||
|
|
135
148
|
path.isAbsolute(relative)
|
|
136
149
|
) {
|
|
137
|
-
return yield*
|
|
150
|
+
return yield* EffectSourceCheckoutError.make({
|
|
138
151
|
message: `Effect source path resolves outside the project: ${candidate}`,
|
|
139
152
|
});
|
|
140
153
|
}
|
|
@@ -143,7 +156,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
143
156
|
for (const segment of relative.split(path.sep).slice(0, -1)) {
|
|
144
157
|
ancestor = path.join(ancestor, segment);
|
|
145
158
|
if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
|
|
146
|
-
return yield*
|
|
159
|
+
return yield* EffectSourceCheckoutError.make({
|
|
147
160
|
message: `Effect source path has a symlink ancestor: ${ancestor}`,
|
|
148
161
|
});
|
|
149
162
|
}
|
|
@@ -163,31 +176,30 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
163
176
|
const fs = yield* FileSystem.FileSystem;
|
|
164
177
|
|
|
165
178
|
if ((yield* observeSymbolicLink(checkoutDir)).kind === "symlink") {
|
|
166
|
-
return yield*
|
|
179
|
+
return yield* EffectSourceCheckoutError.make({
|
|
167
180
|
message: `Effect source destination is a symlink: ${checkoutDir}`,
|
|
168
181
|
});
|
|
169
182
|
}
|
|
170
183
|
if (!(yield* fs.exists(checkoutDir))) return "sync" as const;
|
|
171
184
|
|
|
172
185
|
const actualRoot = yield* runGit(checkoutDir, ["rev-parse", "--show-toplevel"]).pipe(
|
|
173
|
-
Effect.mapError(
|
|
174
|
-
(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}),
|
|
186
|
+
Effect.mapError(() =>
|
|
187
|
+
EffectSourceCheckoutError.make({
|
|
188
|
+
message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
|
|
189
|
+
}),
|
|
178
190
|
),
|
|
179
191
|
);
|
|
180
192
|
const expectedRoot = yield* fs.realPath(checkoutDir);
|
|
181
193
|
|
|
182
194
|
if ((yield* fs.realPath(actualRoot)) !== expectedRoot) {
|
|
183
|
-
return yield*
|
|
195
|
+
return yield* EffectSourceCheckoutError.make({
|
|
184
196
|
message: `Effect source destination is nested inside another Git checkout: ${checkoutDir}`,
|
|
185
197
|
});
|
|
186
198
|
}
|
|
187
199
|
const remote = yield* runGit(checkoutDir, ["remote", "get-url", "origin"]);
|
|
188
200
|
|
|
189
201
|
if (remote !== repository) {
|
|
190
|
-
return yield*
|
|
202
|
+
return yield* EffectSourceCheckoutError.make({
|
|
191
203
|
message: `Effect source origin is ${remote}; expected ${repository}`,
|
|
192
204
|
});
|
|
193
205
|
}
|
|
@@ -208,7 +220,7 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
208
220
|
const dirty = yield* runGit(checkoutDir, ["status", "--porcelain", "--untracked-files=all"]);
|
|
209
221
|
|
|
210
222
|
if (dirty.length > 0) {
|
|
211
|
-
return yield*
|
|
223
|
+
return yield* EffectSourceCheckoutError.make({
|
|
212
224
|
message: `Effect source checkout has local changes; refusing to switch ${checkoutDir} to ${tag}`,
|
|
213
225
|
});
|
|
214
226
|
}
|
|
@@ -225,14 +237,14 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
|
|
|
225
237
|
const packageName = options.packageName ?? "effect";
|
|
226
238
|
|
|
227
239
|
if (!isTypeScriptPackageName(packageName)) {
|
|
228
|
-
return yield*
|
|
240
|
+
return yield* EffectSourceCheckoutError.make({
|
|
229
241
|
message: `invalid Effect source package name: ${packageName}`,
|
|
230
242
|
});
|
|
231
243
|
}
|
|
232
244
|
const repository = options.repository ?? DEFAULT_EFFECT_REPOSITORY;
|
|
233
245
|
|
|
234
246
|
if (repository.length === 0) {
|
|
235
|
-
return yield*
|
|
247
|
+
return yield* EffectSourceCheckoutError.make({
|
|
236
248
|
message: "Effect source repository cannot be empty",
|
|
237
249
|
});
|
|
238
250
|
}
|
|
@@ -289,7 +301,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
|
|
|
289
301
|
staged,
|
|
290
302
|
]);
|
|
291
303
|
if (yield* fs.exists(plan.checkoutDir)) {
|
|
292
|
-
return yield*
|
|
304
|
+
return yield* EffectSourceCheckoutError.make({
|
|
293
305
|
message: `Effect source destination appeared while cloning: ${plan.checkoutDir}`,
|
|
294
306
|
});
|
|
295
307
|
}
|
|
@@ -344,9 +356,19 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
|
|
|
344
356
|
}
|
|
345
357
|
yield* acquireProjectProcessLock(plan.projectDir);
|
|
346
358
|
const replanned = yield* planEffectSource(options);
|
|
359
|
+
const [plannedSignature, replannedSignature] = yield* Effect.all([
|
|
360
|
+
Schema.encodeEffect(EffectSourcePlanJsonSchema)(plan),
|
|
361
|
+
Schema.encodeEffect(EffectSourcePlanJsonSchema)(replanned),
|
|
362
|
+
]).pipe(
|
|
363
|
+
Effect.mapError((error) =>
|
|
364
|
+
EffectSourceCheckoutError.make({
|
|
365
|
+
message: `could not encode Effect source plan: ${error.message}`,
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
368
|
+
);
|
|
347
369
|
|
|
348
|
-
if (
|
|
349
|
-
return yield*
|
|
370
|
+
if (plannedSignature !== replannedSignature) {
|
|
371
|
+
return yield* EffectSourceCheckoutError.make({
|
|
350
372
|
message: "Effect source checkout changed after planning; rerun the command",
|
|
351
373
|
});
|
|
352
374
|
}
|
package/src/effect-tsgo.ts
CHANGED
|
@@ -5,10 +5,47 @@ import { printStatus, withSpinner } from "./cli-ui.ts";
|
|
|
5
5
|
import { acquireProjectProcessLock } from "./project-process-lock.ts";
|
|
6
6
|
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
7
7
|
|
|
8
|
-
export const EFFECT_TSGO_VERSION = "0.
|
|
8
|
+
export const EFFECT_TSGO_VERSION = "0.33.0";
|
|
9
9
|
export const EFFECT_TSGO_TYPESCRIPT_VERSION = "7.0.2";
|
|
10
10
|
export const EFFECT_TSGO_PLUGIN_NAME = "@effect/language-service";
|
|
11
11
|
|
|
12
|
+
export type EffectTsgoDiagnosticSeverity = "off" | "error" | "warning" | "message" | "suggestion";
|
|
13
|
+
|
|
14
|
+
export type EffectTsgoPluginConfig = {
|
|
15
|
+
readonly name: typeof EFFECT_TSGO_PLUGIN_NAME;
|
|
16
|
+
readonly diagnosticSeverity: Readonly<Record<string, EffectTsgoDiagnosticSeverity>>;
|
|
17
|
+
readonly overrides: ReadonlyArray<{
|
|
18
|
+
readonly include: ReadonlyArray<string>;
|
|
19
|
+
readonly options: {
|
|
20
|
+
readonly diagnosticSeverity: Readonly<Record<string, EffectTsgoDiagnosticSeverity>>;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Recommended diagnostics for projects using the Effect TypeScript-Go plugin. */
|
|
26
|
+
export const recommendedEffectTsgoPlugin = {
|
|
27
|
+
name: EFFECT_TSGO_PLUGIN_NAME,
|
|
28
|
+
diagnosticSeverity: {
|
|
29
|
+
anyUnknownInErrorContext: "warning",
|
|
30
|
+
instanceOfSchema: "suggestion",
|
|
31
|
+
nestedEffectGenYield: "suggestion",
|
|
32
|
+
newSchemaClass: "suggestion",
|
|
33
|
+
preferSchemaTypeProperty: "suggestion",
|
|
34
|
+
unsafeEffectTypeAssertion: "warning",
|
|
35
|
+
},
|
|
36
|
+
overrides: [
|
|
37
|
+
{
|
|
38
|
+
include: ["src/**/*.ts"],
|
|
39
|
+
options: {
|
|
40
|
+
diagnosticSeverity: {
|
|
41
|
+
nodeBuiltinImport: "warning",
|
|
42
|
+
preferSchemaOverJson: "suggestion",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
} as const satisfies EffectTsgoPluginConfig;
|
|
48
|
+
|
|
12
49
|
export type EffectTsgoPatchOptions = {
|
|
13
50
|
readonly dryRun?: boolean;
|
|
14
51
|
readonly force?: boolean;
|
|
@@ -18,6 +55,7 @@ export type EffectTsgoPatchOptions = {
|
|
|
18
55
|
|
|
19
56
|
export type EffectTsgoPatchPlan = {
|
|
20
57
|
readonly alreadyPatched: boolean;
|
|
58
|
+
readonly unpatchArgs?: ReadonlyArray<string>;
|
|
21
59
|
readonly projectDir: string;
|
|
22
60
|
readonly executable: string;
|
|
23
61
|
readonly args: ReadonlyArray<string>;
|
|
@@ -26,7 +64,7 @@ export type EffectTsgoPatchPlan = {
|
|
|
26
64
|
readonly typescriptVersion: string;
|
|
27
65
|
};
|
|
28
66
|
|
|
29
|
-
export class EffectTsgoDependencyError extends Schema.
|
|
67
|
+
export class EffectTsgoDependencyError extends Schema.TaggedError<EffectTsgoDependencyError>()(
|
|
30
68
|
"EffectTsgoDependencyError",
|
|
31
69
|
{
|
|
32
70
|
packageName: Schema.String,
|
|
@@ -41,7 +79,7 @@ export class EffectTsgoDependencyError extends Schema.TaggedErrorClass<EffectTsg
|
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
|
|
44
|
-
export class EffectTsgoPatchCommandError extends Schema.
|
|
82
|
+
export class EffectTsgoPatchCommandError extends Schema.TaggedError<EffectTsgoPatchCommandError>()(
|
|
45
83
|
"EffectTsgoPatchCommandError",
|
|
46
84
|
{
|
|
47
85
|
command: Schema.String,
|
|
@@ -56,7 +94,7 @@ export class EffectTsgoPatchCommandError extends Schema.TaggedErrorClass<EffectT
|
|
|
56
94
|
}
|
|
57
95
|
}
|
|
58
96
|
|
|
59
|
-
export class InvalidEffectTsgoPackageNameError extends Schema.
|
|
97
|
+
export class InvalidEffectTsgoPackageNameError extends Schema.TaggedError<InvalidEffectTsgoPackageNameError>()(
|
|
60
98
|
"InvalidEffectTsgoPackageNameError",
|
|
61
99
|
{ packageName: Schema.String },
|
|
62
100
|
) {
|
|
@@ -82,15 +120,15 @@ const readExactPackageVersion = Effect.fn("readExactEffectTsgoPackageVersion")(f
|
|
|
82
120
|
.readFileString(manifestPath)
|
|
83
121
|
.pipe(
|
|
84
122
|
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
85
|
-
Effect.fail(
|
|
123
|
+
Effect.fail(EffectTsgoDependencyError.make({ packageName, expectedVersion })),
|
|
86
124
|
),
|
|
87
125
|
);
|
|
88
|
-
const manifest = yield* Schema.
|
|
89
|
-
Effect.mapError(() =>
|
|
126
|
+
const manifest = yield* Schema.decodeEffect(PackageVersionSchema)(contents).pipe(
|
|
127
|
+
Effect.mapError(() => EffectTsgoDependencyError.make({ packageName, expectedVersion })),
|
|
90
128
|
);
|
|
91
129
|
|
|
92
130
|
if (manifest.version !== expectedVersion) {
|
|
93
|
-
return yield*
|
|
131
|
+
return yield* EffectTsgoDependencyError.make({
|
|
94
132
|
packageName,
|
|
95
133
|
expectedVersion,
|
|
96
134
|
actualVersion: manifest.version,
|
|
@@ -115,7 +153,7 @@ const resolveEffectTsgoExecutable = Effect.fn("resolveEffectTsgoExecutable")(fun
|
|
|
115
153
|
if (yield* fs.exists(candidate)) return candidate;
|
|
116
154
|
}
|
|
117
155
|
|
|
118
|
-
return yield*
|
|
156
|
+
return yield* EffectTsgoDependencyError.make({
|
|
119
157
|
packageName: "@effect/tsgo",
|
|
120
158
|
expectedVersion: EFFECT_TSGO_VERSION,
|
|
121
159
|
});
|
|
@@ -140,7 +178,7 @@ const findNodeModulesRoot = (path: Path.Path, packageJsonPath: string): string |
|
|
|
140
178
|
}
|
|
141
179
|
};
|
|
142
180
|
|
|
143
|
-
const
|
|
181
|
+
const inspectEffectTsgoPatch = Effect.fn("inspectEffectTsgoPatch")(function* (
|
|
144
182
|
projectDir: string,
|
|
145
183
|
typescriptPackage: string,
|
|
146
184
|
) {
|
|
@@ -153,22 +191,28 @@ const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
|
|
|
153
191
|
const typescriptNodeModules = findNodeModulesRoot(path, typescriptPackageJson);
|
|
154
192
|
const effectNodeModules = findNodeModulesRoot(path, effectTsgoPackageJson);
|
|
155
193
|
|
|
156
|
-
if (typescriptNodeModules === undefined || effectNodeModules === undefined)
|
|
194
|
+
if (typescriptNodeModules === undefined || effectNodeModules === undefined) {
|
|
195
|
+
return { alreadyPatched: false, hasBackup: false };
|
|
196
|
+
}
|
|
157
197
|
|
|
158
198
|
const typescriptScope = path.join(typescriptNodeModules, "@typescript");
|
|
159
199
|
const effectScope = path.join(effectNodeModules, "@effect");
|
|
160
200
|
|
|
161
|
-
if (!(yield* fs.exists(typescriptScope)) || !(yield* fs.exists(effectScope)))
|
|
201
|
+
if (!(yield* fs.exists(typescriptScope)) || !(yield* fs.exists(effectScope))) {
|
|
202
|
+
return { alreadyPatched: false, hasBackup: false };
|
|
203
|
+
}
|
|
162
204
|
|
|
163
205
|
const executableName = path.sep === "\\" ? "tsc.exe" : "tsc";
|
|
164
206
|
const effectExecutableNames =
|
|
165
207
|
path.sep === "\\" ? ["tsc.exe", "tsc-next.exe"] : ["tsc", "tsc-next"];
|
|
208
|
+
let hasBackup = false;
|
|
166
209
|
|
|
167
210
|
for (const entry of yield* fs.readDirectory(typescriptScope)) {
|
|
168
211
|
if (!entry.startsWith("typescript-")) continue;
|
|
169
212
|
const platform = entry.slice("typescript-".length);
|
|
170
213
|
const installedPath = path.join(typescriptScope, entry, "lib", executableName);
|
|
171
214
|
|
|
215
|
+
if (yield* fs.exists(`${installedPath}.original`)) hasBackup = true;
|
|
172
216
|
if (!(yield* fs.exists(installedPath))) continue;
|
|
173
217
|
const installedDigest = yield* digestFileContents(installedPath);
|
|
174
218
|
|
|
@@ -184,12 +228,12 @@ const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
|
|
|
184
228
|
(yield* fs.exists(effectBinaryPath)) &&
|
|
185
229
|
installedDigest === (yield* digestFileContents(effectBinaryPath))
|
|
186
230
|
) {
|
|
187
|
-
return true;
|
|
231
|
+
return { alreadyPatched: true, hasBackup };
|
|
188
232
|
}
|
|
189
233
|
}
|
|
190
234
|
}
|
|
191
235
|
|
|
192
|
-
return false;
|
|
236
|
+
return { alreadyPatched: false, hasBackup };
|
|
193
237
|
});
|
|
194
238
|
|
|
195
239
|
export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
@@ -201,7 +245,7 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
|
201
245
|
const typescriptPackage = options.typescriptPackage ?? "typescript";
|
|
202
246
|
|
|
203
247
|
if (!isTypeScriptPackageName(typescriptPackage)) {
|
|
204
|
-
return yield*
|
|
248
|
+
return yield* InvalidEffectTsgoPackageNameError.make({ packageName: typescriptPackage });
|
|
205
249
|
}
|
|
206
250
|
const effectTsgoVersion = yield* readExactPackageVersion(
|
|
207
251
|
projectDir,
|
|
@@ -214,15 +258,22 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
|
214
258
|
EFFECT_TSGO_TYPESCRIPT_VERSION,
|
|
215
259
|
);
|
|
216
260
|
const executable = yield* resolveEffectTsgoExecutable(projectDir);
|
|
261
|
+
const patchState = yield* inspectEffectTsgoPatch(projectDir, typescriptPackage);
|
|
262
|
+
const typescriptPackageArgs =
|
|
263
|
+
typescriptPackage === "typescript" ? [] : ["--typescript-package", typescriptPackage];
|
|
217
264
|
|
|
218
265
|
return {
|
|
219
|
-
alreadyPatched:
|
|
266
|
+
alreadyPatched: patchState.alreadyPatched,
|
|
267
|
+
...(patchState.hasBackup && !patchState.alreadyPatched
|
|
268
|
+
? { unpatchArgs: ["unpatch", "--typescript", ...typescriptPackageArgs] }
|
|
269
|
+
: {}),
|
|
220
270
|
projectDir,
|
|
221
271
|
executable,
|
|
222
272
|
args: [
|
|
223
273
|
"patch",
|
|
274
|
+
"--typescript",
|
|
224
275
|
...(options.force ? ["--force"] : []),
|
|
225
|
-
...
|
|
276
|
+
...typescriptPackageArgs,
|
|
226
277
|
],
|
|
227
278
|
effectTsgoVersion,
|
|
228
279
|
typescriptPackage,
|
|
@@ -230,12 +281,13 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
|
230
281
|
} satisfies EffectTsgoPatchPlan;
|
|
231
282
|
});
|
|
232
283
|
|
|
233
|
-
|
|
234
|
-
|
|
284
|
+
const runEffectTsgoCommand = Effect.fn("runEffectTsgoCommand")(function* (
|
|
285
|
+
executable: string,
|
|
286
|
+
args: ReadonlyArray<string>,
|
|
287
|
+
projectDir: string,
|
|
235
288
|
) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
cwd: plan.projectDir,
|
|
289
|
+
const child = yield* ChildProcess.make(executable, args, {
|
|
290
|
+
cwd: projectDir,
|
|
239
291
|
stderr: "pipe",
|
|
240
292
|
stdout: "pipe",
|
|
241
293
|
});
|
|
@@ -246,14 +298,24 @@ export const applyEffectTsgoPatchPlan = Effect.fn("applyEffectTsgoPatchPlan")(fu
|
|
|
246
298
|
const trimmed = output.trim();
|
|
247
299
|
|
|
248
300
|
if (exitCode !== 0) {
|
|
249
|
-
return yield*
|
|
250
|
-
command: [
|
|
301
|
+
return yield* EffectTsgoPatchCommandError.make({
|
|
302
|
+
command: [executable, ...args].join(" "),
|
|
251
303
|
exitCode,
|
|
252
304
|
output: trimmed,
|
|
253
305
|
});
|
|
254
306
|
}
|
|
255
307
|
});
|
|
256
308
|
|
|
309
|
+
export const applyEffectTsgoPatchPlan = Effect.fn("applyEffectTsgoPatchPlan")(function* (
|
|
310
|
+
plan: EffectTsgoPatchPlan,
|
|
311
|
+
) {
|
|
312
|
+
if (plan.alreadyPatched) return;
|
|
313
|
+
if (plan.unpatchArgs !== undefined) {
|
|
314
|
+
yield* runEffectTsgoCommand(plan.executable, plan.unpatchArgs, plan.projectDir);
|
|
315
|
+
}
|
|
316
|
+
yield* runEffectTsgoCommand(plan.executable, plan.args, plan.projectDir);
|
|
317
|
+
});
|
|
318
|
+
|
|
257
319
|
export const patchEffectTsgo = Effect.fn("patchEffectTsgo")(function* (
|
|
258
320
|
options: EffectTsgoPatchOptions = {},
|
|
259
321
|
) {
|
package/src/gitignore.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type GitignorePatch = {
|
|
|
22
22
|
readonly added: ReadonlyArray<string>;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export class UnsafeGitignorePathError extends Schema.
|
|
25
|
+
export class UnsafeGitignorePathError extends Schema.TaggedError<UnsafeGitignorePathError>()(
|
|
26
26
|
"UnsafeGitignorePathError",
|
|
27
27
|
{
|
|
28
28
|
path: Schema.String,
|
|
@@ -34,7 +34,7 @@ export class UnsafeGitignorePathError extends Schema.TaggedErrorClass<UnsafeGiti
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export class GitignoreConflictError extends Schema.
|
|
37
|
+
export class GitignoreConflictError extends Schema.TaggedError<GitignoreConflictError>()(
|
|
38
38
|
"GitignoreConflictError",
|
|
39
39
|
{ path: Schema.String },
|
|
40
40
|
) {
|
|
@@ -94,7 +94,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
|
|
|
94
94
|
const observation = yield* observeSymbolicLink(gitignorePath);
|
|
95
95
|
|
|
96
96
|
if (observation.kind === "symlink") {
|
|
97
|
-
return yield*
|
|
97
|
+
return yield* UnsafeGitignorePathError.make({
|
|
98
98
|
path: gitignorePath,
|
|
99
99
|
reason: "the file is a symlink",
|
|
100
100
|
});
|
|
@@ -107,7 +107,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
|
|
|
107
107
|
const info = yield* fs.stat(gitignorePath);
|
|
108
108
|
|
|
109
109
|
if (info.type !== "File") {
|
|
110
|
-
return yield*
|
|
110
|
+
return yield* UnsafeGitignorePathError.make({
|
|
111
111
|
path: gitignorePath,
|
|
112
112
|
reason: "the path is not a regular file",
|
|
113
113
|
});
|
|
@@ -152,7 +152,7 @@ const applyGitignorePatch = Effect.fn("applyGitignorePatch")(function* (
|
|
|
152
152
|
: currentObservation.kind !== "missing";
|
|
153
153
|
|
|
154
154
|
if (changed) {
|
|
155
|
-
return yield*
|
|
155
|
+
return yield* GitignoreConflictError.make({ path: patch.path });
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
let backedUp = false;
|