@danieljvdm/dev-kit 0.11.3 → 0.12.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 (74) hide show
  1. package/README.md +70 -76
  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/dev-kit/SKILL.md +58 -46
  17. package/skills/effect-architecture-audit/SKILL.md +26 -0
  18. package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
  19. package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
  20. package/skills/effect-ts/SKILL.md +21 -256
  21. package/skills/effect-ts/agents/openai.yaml +3 -3
  22. package/skills/testing/SKILL.md +5 -0
  23. package/src/catalog-manager.ts +16 -17
  24. package/src/catalog.ts +71 -16
  25. package/src/effect-source.ts +46 -24
  26. package/src/effect-tsgo.ts +49 -24
  27. package/src/gitignore.ts +5 -5
  28. package/src/index.ts +0 -6
  29. package/src/manifest.ts +0 -34
  30. package/src/node-symbolic-link.ts +2 -2
  31. package/src/oxfmt.js +5 -0
  32. package/src/oxfmt.ts +5 -0
  33. package/src/oxlint.js +5 -0
  34. package/src/oxlint.ts +5 -0
  35. package/src/package-skill-source.ts +51 -59
  36. package/src/path-digest.ts +7 -7
  37. package/src/project-package.ts +8 -7
  38. package/src/project-process-lock.ts +17 -12
  39. package/src/project-state.ts +1 -1
  40. package/src/skill-manager.ts +16 -14
  41. package/src/skill-selector.ts +12 -0
  42. package/src/sync.ts +170 -120
  43. package/src/tool-ignore-patterns.js +9 -0
  44. package/src/tool-ignore-patterns.ts +15 -0
  45. package/src/vendor.ts +67 -61
  46. package/src/vite-plus-dependency.ts +10 -11
  47. package/src/vite-plus-hooks.ts +24 -14
  48. package/src/vite-plus-quality.ts +21 -172
  49. package/src/vite-plus.js +81 -0
  50. package/src/vite-plus.ts +102 -0
  51. package/templates/AGENTS.md +1 -1
  52. package/skills/effect-ts/UPSTREAM.md +0 -28
  53. package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
  54. package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
  55. package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
  56. package/skills/effect-ts/references/atom-testing.md +0 -67
  57. package/skills/effect-ts/references/audit-services.md +0 -144
  58. package/skills/effect-ts/references/features.md +0 -525
  59. package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
  60. package/skills/effect-ts/references/guide-cli.md +0 -107
  61. package/skills/effect-ts/references/guide-datetime.md +0 -72
  62. package/skills/effect-ts/references/guide-effect.md +0 -440
  63. package/skills/effect-ts/references/guide-error-handling.md +0 -565
  64. package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
  65. package/skills/effect-ts/references/guide-layers.md +0 -989
  66. package/skills/effect-ts/references/guide-observability.md +0 -746
  67. package/skills/effect-ts/references/guide-retries.md +0 -434
  68. package/skills/effect-ts/references/guide-schedule.md +0 -343
  69. package/skills/effect-ts/references/guide-schema.md +0 -664
  70. package/skills/effect-ts/references/guide-sql.md +0 -536
  71. package/skills/effect-ts/references/guide-testing.md +0 -532
  72. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
  73. package/skills/effect-ts/references/version-and-source.md +0 -86
  74. package/templates/vite-plus/vite.config.ts +0 -22
@@ -36,10 +36,9 @@ export type CatalogAddOptions = CatalogCommandOptions & {
36
36
  readonly dryRun?: boolean;
37
37
  };
38
38
 
39
- class CatalogManagerError extends Schema.TaggedErrorClass<CatalogManagerError>()(
40
- "CatalogManagerError",
41
- { message: Schema.String },
42
- ) {}
39
+ class CatalogManagerError extends Schema.TaggedError<CatalogManagerError>()("CatalogManagerError", {
40
+ message: Schema.String,
41
+ }) {}
43
42
 
44
43
  const formattingOptions = { insertSpaces: true, tabSize: 2 } as const;
45
44
 
@@ -63,17 +62,17 @@ const readJsonc = Effect.fn("readCatalogManagerJsonc")(function* <A>(
63
62
  const fs = yield* FileSystem.FileSystem;
64
63
 
65
64
  if (!(yield* fs.exists(filePath))) {
66
- return yield* new CatalogManagerError({ message: `file not found: ${filePath}` });
65
+ return yield* CatalogManagerError.make({ message: `file not found: ${filePath}` });
67
66
  }
68
67
  const raw = yield* fs.readFileString(filePath);
69
68
  const errors: Array<ParseError> = [];
70
69
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
71
70
 
72
71
  if (errors.length > 0) {
73
- return yield* new CatalogManagerError({ message: `could not parse ${filePath}` });
72
+ return yield* CatalogManagerError.make({ message: `could not parse ${filePath}` });
74
73
  }
75
74
  const value = yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
76
- Effect.mapError((error) => new CatalogManagerError({ message: error.message })),
75
+ Effect.mapError((error) => CatalogManagerError.make({ message: error.message })),
77
76
  );
78
77
 
79
78
  return { raw, value };
@@ -105,7 +104,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
105
104
  const unknown = requested.filter((skill) => !available.has(skill));
106
105
 
107
106
  if (unknown.length > 0) {
108
- return yield* new CatalogManagerError({
107
+ return yield* CatalogManagerError.make({
109
108
  message: `repository does not contain: ${unknown.join(", ")}`,
110
109
  });
111
110
  }
@@ -117,7 +116,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
117
116
  if (requested.length > 0)
118
117
  return { include: [...new Set(requested)], selected: [...new Set(requested)] };
119
118
  if (!(yield* isInteractiveTerminal)) {
120
- return yield* new CatalogManagerError({
119
+ return yield* CatalogManagerError.make({
121
120
  message: "choose skills with --skill <name>, or pass --all",
122
121
  });
123
122
  }
@@ -194,7 +193,7 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
194
193
  options: CatalogAddOptions,
195
194
  ) {
196
195
  if (options.all && (options.skills?.length ?? 0) > 0) {
197
- return yield* new CatalogManagerError({ message: "use either --all or --skill, not both" });
196
+ return yield* CatalogManagerError.make({ message: "use either --all or --skill, not both" });
198
197
  }
199
198
  const state = yield* readState(options);
200
199
  const inspection = yield* inspectCatalogRepository({
@@ -210,12 +209,12 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
210
209
  const byRepository = sources.findIndex((source) => source.repository === inspection.repository);
211
210
 
212
211
  if (byId >= 0 && sources[byId]?.repository !== inspection.repository) {
213
- return yield* new CatalogManagerError({
212
+ return yield* CatalogManagerError.make({
214
213
  message: `source id ${inspection.id} is already used by ${sources[byId]?.repository}`,
215
214
  });
216
215
  }
217
216
  if (byRepository >= 0 && sources[byRepository]?.id !== inspection.id) {
218
- return yield* new CatalogManagerError({
217
+ return yield* CatalogManagerError.make({
219
218
  message: `repository is already cataloged as ${sources[byRepository]?.id}`,
220
219
  });
221
220
  }
@@ -226,7 +225,7 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
226
225
  const existing = sources[existingIndex];
227
226
 
228
227
  if (existing === undefined) {
229
- return yield* new CatalogManagerError({ message: "catalog source index is out of bounds" });
228
+ return yield* CatalogManagerError.make({ message: "catalog source index is out of bounds" });
230
229
  }
231
230
  const approved =
232
231
  state.lock?.value.sources.find((source) => source.id === existing.id)?.skills ?? [];
@@ -308,12 +307,12 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
308
307
  const owner = state.lock?.value.sources.find((source) => source.skills.includes(name));
309
308
 
310
309
  if (!owner)
311
- return yield* new CatalogManagerError({ message: `catalog entry not found: ${name}` });
310
+ return yield* CatalogManagerError.make({ message: `catalog entry not found: ${name}` });
312
311
  const index = sources.findIndex((source) => source.id === owner.id);
313
312
  const source = sources[index];
314
313
 
315
314
  if (!source)
316
- return yield* new CatalogManagerError({ message: `source not found: ${owner.id}` });
315
+ return yield* CatalogManagerError.make({ message: `source not found: ${owner.id}` });
317
316
  if (source.include.includes("*")) {
318
317
  const exclude = [...new Set([...(source.exclude ?? []), name])];
319
318
 
@@ -342,7 +341,7 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
342
341
  }
343
342
  if (!options.yes) {
344
343
  if (!(yield* isInteractiveTerminal)) {
345
- return yield* new CatalogManagerError({
344
+ return yield* CatalogManagerError.make({
346
345
  message: "catalog removal requires --yes outside a terminal",
347
346
  });
348
347
  }
@@ -388,7 +387,7 @@ export const showCatalogSource = Effect.fn("showCatalogSource")(function* (
388
387
  const source = state.sources.value.sources.find((candidate) => candidate.id === id);
389
388
 
390
389
  if (!source)
391
- return yield* new CatalogManagerError({ message: `catalog source not found: ${id}` });
390
+ return yield* CatalogManagerError.make({ message: `catalog source not found: ${id}` });
392
391
  const locked = state.lock?.value.sources.find((candidate) => candidate.id === id);
393
392
 
394
393
  yield* printLine(source.id);
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"],
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
  }