@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.
Files changed (79) hide show
  1. package/README.md +111 -78
  2. package/dev-kit.example.jsonc +0 -4
  3. package/package.json +10 -6
  4. package/schema/dev-kit.schema.json +1 -46
  5. package/skills/build-effect-apis/SKILL.md +77 -0
  6. package/skills/build-effect-apis/agents/openai.yaml +4 -0
  7. package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
  8. package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
  9. package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
  10. package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
  11. package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
  12. package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
  13. package/skills/build-effect-apis/references/shared-contracts.md +108 -0
  14. package/skills/build-effect-apis/references/tanstack-start.md +86 -0
  15. package/skills/build-effect-apis/references/verification.md +50 -0
  16. package/skills/build-effect-clis/SKILL.md +61 -0
  17. package/skills/build-effect-clis/agents/openai.yaml +4 -0
  18. package/skills/build-effect-clis/references/command-design.md +93 -0
  19. package/skills/build-effect-clis/references/entrypoints-and-testing.md +59 -0
  20. package/skills/build-effect-clis/references/processes-and-platform.md +73 -0
  21. package/skills/dev-kit/SKILL.md +77 -48
  22. package/skills/effect-architecture-audit/SKILL.md +26 -0
  23. package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
  24. package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
  25. package/skills/effect-ts/SKILL.md +21 -256
  26. package/skills/effect-ts/agents/openai.yaml +3 -3
  27. package/skills/testing/SKILL.md +5 -0
  28. package/src/catalog-manager.ts +16 -17
  29. package/src/catalog.ts +71 -16
  30. package/src/effect-source.ts +46 -24
  31. package/src/effect-tsgo.ts +86 -24
  32. package/src/gitignore.ts +5 -5
  33. package/src/index.ts +3 -6
  34. package/src/manifest.ts +0 -34
  35. package/src/node-symbolic-link.ts +2 -2
  36. package/src/oxfmt.js +5 -0
  37. package/src/oxfmt.ts +5 -0
  38. package/src/oxlint.js +5 -0
  39. package/src/oxlint.ts +5 -0
  40. package/src/package-skill-source.ts +51 -59
  41. package/src/path-digest.ts +7 -7
  42. package/src/project-package.ts +8 -7
  43. package/src/project-process-lock.ts +17 -12
  44. package/src/project-state.ts +1 -1
  45. package/src/skill-manager.ts +18 -16
  46. package/src/skill-selector.ts +12 -0
  47. package/src/sync.ts +181 -131
  48. package/src/tool-ignore-patterns.js +9 -0
  49. package/src/tool-ignore-patterns.ts +15 -0
  50. package/src/vendor.ts +67 -61
  51. package/src/vite-plus-dependency.ts +10 -11
  52. package/src/vite-plus-hooks.ts +24 -14
  53. package/src/vite-plus-quality.ts +21 -172
  54. package/src/vite-plus.js +81 -0
  55. package/src/vite-plus.ts +102 -0
  56. package/templates/AGENTS.md +1 -1
  57. package/skills/effect-ts/UPSTREAM.md +0 -28
  58. package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
  59. package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
  60. package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
  61. package/skills/effect-ts/references/atom-testing.md +0 -67
  62. package/skills/effect-ts/references/audit-services.md +0 -144
  63. package/skills/effect-ts/references/features.md +0 -525
  64. package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
  65. package/skills/effect-ts/references/guide-cli.md +0 -107
  66. package/skills/effect-ts/references/guide-datetime.md +0 -72
  67. package/skills/effect-ts/references/guide-effect.md +0 -440
  68. package/skills/effect-ts/references/guide-error-handling.md +0 -565
  69. package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
  70. package/skills/effect-ts/references/guide-layers.md +0 -989
  71. package/skills/effect-ts/references/guide-observability.md +0 -746
  72. package/skills/effect-ts/references/guide-retries.md +0 -434
  73. package/skills/effect-ts/references/guide-schedule.md +0 -343
  74. package/skills/effect-ts/references/guide-schema.md +0 -664
  75. package/skills/effect-ts/references/guide-sql.md +0 -536
  76. package/skills/effect-ts/references/guide-testing.md +0 -532
  77. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
  78. package/skills/effect-ts/references/version-and-source.md +0 -86
  79. 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 { discoverPackageSkills, resolvePackageSkillSelector } from "./package-skill-source.ts";
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.TaggedErrorClass<CatalogError>()("CatalogError", {
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* new CatalogError({
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* new CatalogError({ message: `invalid skill catalog lock: ${lockPath}` });
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) => new CatalogError({ message: error.message })),
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* new CatalogError({
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* new CatalogError({
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* new CatalogError({
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* new CatalogError({
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* new CatalogError({
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* new CatalogError({
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* new CatalogError({ message: `package skill is missing: ${selector}` });
382
+ return yield* CatalogError.make({ message: `package skill is missing: ${selector}` });
328
383
  }
329
384
  sources.set(selector, {
330
- path: resolved.path,
385
+ path: yield* materializePackageSkill(projectDir, resolved, observation.digest, cache),
331
386
  linkPath: resolved.linkPath,
332
387
  catalog: {
333
388
  package: resolved.package,
@@ -28,7 +28,20 @@ export type EffectSourcePlan = {
28
28
  readonly tag: string;
29
29
  };
30
30
 
31
- export class EffectSourceDependencyError extends Schema.TaggedErrorClass<EffectSourceDependencyError>()(
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.TaggedErrorClass<EffectSourceCheckoutError>()(
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.TaggedErrorClass<EffectSourceCommandError>()(
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* new EffectSourceCommandError({
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(new EffectSourceDependencyError({ packageName })),
120
+ Effect.fail(EffectSourceDependencyError.make({ packageName })),
108
121
  ),
109
122
  );
110
123
 
111
- return yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
112
- Effect.mapError(() => new EffectSourceDependencyError({ packageName })),
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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
- new EffectSourceCheckoutError({
176
- message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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* new EffectSourceCheckoutError({
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 (JSON.stringify(plan) !== JSON.stringify(replanned)) {
349
- return yield* new EffectSourceCheckoutError({
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
  }
@@ -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.24.3";
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.TaggedErrorClass<EffectTsgoDependencyError>()(
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.TaggedErrorClass<EffectTsgoPatchCommandError>()(
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.TaggedErrorClass<InvalidEffectTsgoPackageNameError>()(
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(new EffectTsgoDependencyError({ packageName, expectedVersion })),
123
+ Effect.fail(EffectTsgoDependencyError.make({ packageName, expectedVersion })),
86
124
  ),
87
125
  );
88
- const manifest = yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
89
- Effect.mapError(() => new EffectTsgoDependencyError({ packageName, expectedVersion })),
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* new EffectTsgoDependencyError({
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* new EffectTsgoDependencyError({
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 isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
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) return false;
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))) return false;
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* new InvalidEffectTsgoPackageNameError({ packageName: typescriptPackage });
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: yield* isEffectTsgoPatched(projectDir, typescriptPackage),
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
- ...(typescriptPackage === "typescript" ? [] : ["--typescript-package", typescriptPackage]),
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
- export const applyEffectTsgoPatchPlan = Effect.fn("applyEffectTsgoPatchPlan")(function* (
234
- plan: EffectTsgoPatchPlan,
284
+ const runEffectTsgoCommand = Effect.fn("runEffectTsgoCommand")(function* (
285
+ executable: string,
286
+ args: ReadonlyArray<string>,
287
+ projectDir: string,
235
288
  ) {
236
- if (plan.alreadyPatched) return;
237
- const child = yield* ChildProcess.make(plan.executable, plan.args, {
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* new EffectTsgoPatchCommandError({
250
- command: [plan.executable, ...plan.args].join(" "),
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.TaggedErrorClass<UnsafeGitignorePathError>()(
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.TaggedErrorClass<GitignoreConflictError>()(
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* new UnsafeGitignorePathError({
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* new UnsafeGitignorePathError({
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* new GitignoreConflictError({ path: patch.path });
155
+ return yield* GitignoreConflictError.make({ path: patch.path });
156
156
  }
157
157
 
158
158
  let backedUp = false;