@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
@@ -0,0 +1,9 @@
1
+ /** Tool-owned paths excluded by Dev Kit's lint and format presets. */
2
+ export const devKitToolIgnorePatterns = [
3
+ ".agents/**",
4
+ ".claude/**",
5
+ ".dev-kit/**",
6
+ ".opencode/**",
7
+ ".repos/**",
8
+ ".vite-hooks/_/**",
9
+ ];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tool-owned paths that should not be linted or formatted.
3
+ *
4
+ * Skill copies remain tracked project inputs, while symlinked harness targets
5
+ * and local Dev Kit state may duplicate or contain third-party source. Keep
6
+ * these exclusions in tool configuration rather than `.gitignore`.
7
+ */
8
+ export const devKitToolIgnorePatterns = [
9
+ ".agents/**",
10
+ ".claude/**",
11
+ ".dev-kit/**",
12
+ ".opencode/**",
13
+ ".repos/**",
14
+ ".vite-hooks/_/**",
15
+ ] as const;
package/src/vendor.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, Path, Schema, Stream } from "effect";
1
+ import { Effect, FileSystem, Path, Schema, SchemaGetter, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
  import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
4
4
 
@@ -51,39 +51,30 @@ type PreparedSource = {
51
51
  readonly licenseSource?: string;
52
52
  };
53
53
 
54
- class SourceManifestError extends Schema.TaggedErrorClass<SourceManifestError>()(
55
- "SourceManifestError",
56
- {
57
- path: Schema.String,
58
- message: Schema.String,
59
- },
60
- ) {}
54
+ class SourceManifestError extends Schema.TaggedError<SourceManifestError>()("SourceManifestError", {
55
+ path: Schema.String,
56
+ message: Schema.String,
57
+ }) {}
61
58
 
62
- class InvalidSourceError extends Schema.TaggedErrorClass<InvalidSourceError>()(
63
- "InvalidSourceError",
64
- {
65
- source: Schema.String,
66
- reason: Schema.String,
67
- },
68
- ) {
59
+ class InvalidSourceError extends Schema.TaggedError<InvalidSourceError>()("InvalidSourceError", {
60
+ source: Schema.String,
61
+ reason: Schema.String,
62
+ }) {
69
63
  override get message(): string {
70
64
  return `invalid source "${this.source}": ${this.reason}`;
71
65
  }
72
66
  }
73
67
 
74
- class SkillCollisionError extends Schema.TaggedErrorClass<SkillCollisionError>()(
75
- "SkillCollisionError",
76
- {
77
- skill: Schema.String,
78
- owners: Schema.Array(Schema.String),
79
- },
80
- ) {
68
+ class SkillCollisionError extends Schema.TaggedError<SkillCollisionError>()("SkillCollisionError", {
69
+ skill: Schema.String,
70
+ owners: Schema.Array(Schema.String),
71
+ }) {
81
72
  override get message() {
82
73
  return `skill "${this.skill}" is owned by more than one source: ${this.owners.join(", ")}`;
83
74
  }
84
75
  }
85
76
 
86
- class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError", {
77
+ class CommandError extends Schema.TaggedError<CommandError>()("CommandError", {
87
78
  command: Schema.String,
88
79
  exitCode: Schema.Int,
89
80
  output: Schema.String,
@@ -95,6 +86,16 @@ class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError
95
86
  }
96
87
  }
97
88
 
89
+ const SkillSourcesLockJsonSchema = Schema.fromJsonString(SkillSourcesLockSchema);
90
+ const SkillSourcesLockPrettyJsonSchema = Schema.String.pipe(
91
+ Schema.decodeTo(Schema.toCodecJson(SkillSourcesLockSchema), {
92
+ decode: SchemaGetter.parseJson(),
93
+ encode: SchemaGetter.stringifyJson({ space: 2 }),
94
+ }),
95
+ );
96
+ const encodeSkillSourcesLockJson = Schema.encodeSync(SkillSourcesLockJsonSchema);
97
+ const encodeSkillSourcesLockPrettyJson = Schema.encodeSync(SkillSourcesLockPrettyJsonSchema);
98
+
98
99
  const DEFAULT_SOURCES_PATH = "skill-sources.jsonc";
99
100
  const DEFAULT_LOCKFILE_PATH = "skill-sources.lock.json";
100
101
  const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -131,7 +132,7 @@ const normalizeRepositoryLocator = (
131
132
  > => {
132
133
  if (containsControlCharacter(repository)) {
133
134
  return Effect.fail(
134
- new InvalidSourceError({
135
+ InvalidSourceError.make({
135
136
  source: repository,
136
137
  reason: "repository contains control characters",
137
138
  }),
@@ -142,7 +143,7 @@ const normalizeRepositoryLocator = (
142
143
 
143
144
  if ((url.protocol === "http:" || url.protocol === "https:") && (url.username || url.password)) {
144
145
  return Effect.fail(
145
- new InvalidSourceError({
146
+ InvalidSourceError.make({
146
147
  source: repository,
147
148
  reason: "repository URLs must not contain credentials",
148
149
  }),
@@ -184,7 +185,7 @@ const runCommand = Effect.fn("runVendorCommand")(function* (
184
185
  const trimmed = output.trim();
185
186
 
186
187
  if (exitCode !== 0) {
187
- return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
188
+ return yield* CommandError.make({ command: formatted, exitCode, output: trimmed });
188
189
  }
189
190
 
190
191
  return trimmed;
@@ -201,7 +202,7 @@ const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
201
202
  const fs = yield* FileSystem.FileSystem;
202
203
 
203
204
  if (!(yield* fs.exists(filePath))) {
204
- return yield* new SourceManifestError({ path: filePath, message: "file not found" });
205
+ return yield* SourceManifestError.make({ path: filePath, message: "file not found" });
205
206
  }
206
207
 
207
208
  const raw = yield* fs.readFileString(filePath);
@@ -211,14 +212,16 @@ const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
211
212
  const first = errors[0];
212
213
 
213
214
  if (first !== undefined) {
214
- return yield* new SourceManifestError({
215
+ return yield* SourceManifestError.make({
215
216
  path: filePath,
216
217
  message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
217
218
  });
218
219
  }
219
220
 
220
221
  return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
221
- Effect.mapError((cause) => new SourceManifestError({ path: filePath, message: cause.message })),
222
+ Effect.mapError((cause) =>
223
+ SourceManifestError.make({ path: filePath, message: cause.message }),
224
+ ),
222
225
  );
223
226
  });
224
227
 
@@ -239,7 +242,7 @@ const resolveInside = (
239
242
  path.isAbsolute(relative)
240
243
  ) {
241
244
  return Effect.fail(
242
- new InvalidSourceError({
245
+ InvalidSourceError.make({
243
246
  source: sourceId,
244
247
  reason: `${field} must be a relative path inside the source repository`,
245
248
  }),
@@ -264,7 +267,7 @@ const ensureCanonicalPathInside = Effect.fn("ensureCanonicalSourcePathInside")(f
264
267
  const relative = path.relative(canonicalRoot, canonicalTarget);
265
268
 
266
269
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
267
- return yield* new InvalidSourceError({
270
+ return yield* InvalidSourceError.make({
268
271
  source: sourceId,
269
272
  reason: `${field} resolves outside the source repository`,
270
273
  });
@@ -287,7 +290,7 @@ const rejectGitSymlinks = Effect.fn("rejectGitSymlinks")(function* (
287
290
  const symlink = entries.split(/\r?\n/).find((line) => line.startsWith("120000 "));
288
291
 
289
292
  if (symlink) {
290
- return yield* new InvalidSourceError({
293
+ return yield* InvalidSourceError.make({
291
294
  source: sourceId,
292
295
  reason: `symlinks are not allowed in catalog paths: ${symlink.slice(symlink.indexOf("\t") + 1)}`,
293
296
  });
@@ -302,7 +305,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
302
305
  const path = yield* Path.Path;
303
306
 
304
307
  if (!(yield* fs.exists(skillsDir))) {
305
- return yield* new InvalidSourceError({
308
+ return yield* InvalidSourceError.make({
306
309
  source: source.id,
307
310
  reason: `skillsPath does not exist: ${source.skillsPath}`,
308
311
  });
@@ -327,7 +330,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
327
330
  .replace(/^(['"])(.*)\1$/, "$2");
328
331
 
329
332
  if (declaredName !== entry) {
330
- return yield* new InvalidSourceError({
333
+ return yield* InvalidSourceError.make({
331
334
  source: source.id,
332
335
  reason: `${source.skillsPath}/${entry}/SKILL.md must declare name: ${entry}`,
333
336
  });
@@ -340,7 +343,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
340
343
  const includeAll = source.include.length === 1 && source.include[0] === "*";
341
344
 
342
345
  if (source.include.includes("*") && !includeAll) {
343
- return yield* new InvalidSourceError({
346
+ return yield* InvalidSourceError.make({
344
347
  source: source.id,
345
348
  reason: 'include must contain either "*" or explicit skill names, not both',
346
349
  });
@@ -351,7 +354,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
351
354
  );
352
355
 
353
356
  if (selected.length === 0) {
354
- return yield* new InvalidSourceError({
357
+ return yield* InvalidSourceError.make({
355
358
  source: source.id,
356
359
  reason: "include must select at least one skill",
357
360
  });
@@ -359,13 +362,13 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
359
362
 
360
363
  for (const skill of selected) {
361
364
  if (!SKILL_NAME_PATTERN.test(skill)) {
362
- return yield* new InvalidSourceError({
365
+ return yield* InvalidSourceError.make({
363
366
  source: source.id,
364
367
  reason: `invalid skill name "${skill}"`,
365
368
  });
366
369
  }
367
370
  if (!discovered.includes(skill)) {
368
- return yield* new InvalidSourceError({
371
+ return yield* InvalidSourceError.make({
369
372
  source: source.id,
370
373
  reason: `skill not found under ${source.skillsPath}: ${skill}`,
371
374
  });
@@ -386,19 +389,19 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
386
389
  const path = yield* Path.Path;
387
390
 
388
391
  if (!SOURCE_ID_PATTERN.test(source.id)) {
389
- return yield* new InvalidSourceError({
392
+ return yield* InvalidSourceError.make({
390
393
  source: source.id,
391
394
  reason: "id must use lowercase letters, numbers, and hyphens",
392
395
  });
393
396
  }
394
397
  if (RESERVED_SOURCE_IDS.has(source.id)) {
395
- return yield* new InvalidSourceError({
398
+ return yield* InvalidSourceError.make({
396
399
  source: source.id,
397
400
  reason: "id conflicts with a built-in skill family",
398
401
  });
399
402
  }
400
403
  if (source.repository.length === 0 || source.ref.length === 0) {
401
- return yield* new InvalidSourceError({
404
+ return yield* InvalidSourceError.make({
402
405
  source: source.id,
403
406
  reason: "repository and ref must not be empty",
404
407
  });
@@ -417,7 +420,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
417
420
  [...(lockedSource.stripFrontmatter ?? [])].sort().join("\0") !==
418
421
  [...(source.stripFrontmatter ?? [])].sort().join("\0"))
419
422
  ) {
420
- return yield* new InvalidSourceError({
423
+ return yield* InvalidSourceError.make({
421
424
  source: source.id,
422
425
  reason: "no matching lockfile entry; run catalog refresh without --locked first",
423
426
  });
@@ -431,7 +434,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
431
434
  const fetchRef = useLock ? lockedSource?.resolved : source.ref;
432
435
 
433
436
  if (fetchRef === undefined) {
434
- return yield* new InvalidSourceError({
437
+ return yield* InvalidSourceError.make({
435
438
  source: source.id,
436
439
  reason: "no matching lockfile entry; run catalog refresh without --locked first",
437
440
  });
@@ -469,7 +472,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
469
472
  );
470
473
  yield* rejectGitSymlinks(checkoutDir, source.licensePath, source.id);
471
474
  if (!(yield* fs.exists(licenseSource))) {
472
- return yield* new InvalidSourceError({
475
+ return yield* InvalidSourceError.make({
473
476
  source: source.id,
474
477
  reason: `licensePath does not exist: ${source.licensePath}`,
475
478
  });
@@ -483,7 +486,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
483
486
  const licenseInfo = yield* fs.stat(licenseSource);
484
487
 
485
488
  if (licenseInfo.type !== "File") {
486
- return yield* new InvalidSourceError({
489
+ return yield* InvalidSourceError.make({
487
490
  source: source.id,
488
491
  reason: `licensePath must be a file: ${source.licensePath}`,
489
492
  });
@@ -520,33 +523,33 @@ const validateCurrentLock = Effect.fn("validateCurrentSkillSourcesLock")(functio
520
523
 
521
524
  for (const source of lock.sources) {
522
525
  if (!SOURCE_ID_PATTERN.test(source.id) || RESERVED_SOURCE_IDS.has(source.id)) {
523
- return yield* new InvalidSourceError({
526
+ return yield* InvalidSourceError.make({
524
527
  source: source.id,
525
528
  reason: "lockfile contains an invalid source id",
526
529
  });
527
530
  }
528
531
  if (sourceIds.has(source.id)) {
529
- return yield* new InvalidSourceError({
532
+ return yield* InvalidSourceError.make({
530
533
  source: source.id,
531
534
  reason: "lockfile source ids must be unique",
532
535
  });
533
536
  }
534
537
  sourceIds.add(source.id);
535
538
  if (!/^[0-9a-f]{40,64}$/.test(source.resolved)) {
536
- return yield* new InvalidSourceError({
539
+ return yield* InvalidSourceError.make({
537
540
  source: source.id,
538
541
  reason: "lockfile resolved commit must be a full hexadecimal object id",
539
542
  });
540
543
  }
541
544
  for (const skill of source.skills) {
542
545
  if (!SKILL_NAME_PATTERN.test(skill)) {
543
- return yield* new InvalidSourceError({
546
+ return yield* InvalidSourceError.make({
544
547
  source: source.id,
545
548
  reason: `lockfile contains an invalid skill name: ${skill}`,
546
549
  });
547
550
  }
548
551
  if (skills.has(skill)) {
549
- return yield* new SkillCollisionError({
552
+ return yield* SkillCollisionError.make({
550
553
  skill,
551
554
  owners: ["multiple lockfile sources"],
552
555
  });
@@ -590,12 +593,12 @@ const validateOwnership = Effect.fn("validateSkillOwnership")(function* (
590
593
  }
591
594
  for (const [skill, skillOwners] of owners) {
592
595
  if (skillOwners.length > 1) {
593
- return yield* new SkillCollisionError({ skill, owners: skillOwners });
596
+ return yield* SkillCollisionError.make({ skill, owners: skillOwners });
594
597
  }
595
598
  }
596
599
  for (const preparedSource of prepared) {
597
600
  if (owners.has(preparedSource.source.id)) {
598
- return yield* new InvalidSourceError({
601
+ return yield* InvalidSourceError.make({
599
602
  source: preparedSource.source.id,
600
603
  reason: "id conflicts with a skill name",
601
604
  });
@@ -603,7 +606,7 @@ const validateOwnership = Effect.fn("validateSkillOwnership")(function* (
603
606
  }
604
607
  for (const reservedFamily of RESERVED_SOURCE_IDS) {
605
608
  if (owners.has(reservedFamily)) {
606
- return yield* new InvalidSourceError({
609
+ return yield* InvalidSourceError.make({
607
610
  source: reservedFamily,
608
611
  reason: "skill name conflicts with a built-in skill family",
609
612
  });
@@ -703,7 +706,7 @@ const buildLock = Effect.fn("buildSkillCatalogLock")(function* (
703
706
  const observation = yield* observePath(stagedSkill);
704
707
 
705
708
  if (observation.kind !== "directory") {
706
- return yield* new InvalidSourceError({
709
+ return yield* InvalidSourceError.make({
707
710
  source: source.id,
708
711
  reason: `could not digest ${skill}`,
709
712
  });
@@ -740,7 +743,7 @@ export const inspectCatalogRepository = Effect.fn("inspectCatalogRepository")(fu
740
743
  const id = options.id ?? inferSourceId(repository);
741
744
 
742
745
  if (id.length === 0) {
743
- return yield* new InvalidSourceError({
746
+ return yield* InvalidSourceError.make({
744
747
  source: repository,
745
748
  reason: "could not infer a source id; pass --id",
746
749
  });
@@ -844,7 +847,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
844
847
 
845
848
  for (const source of manifest.sources) {
846
849
  if (sourceIds.has(source.id)) {
847
- return yield* new InvalidSourceError({
850
+ return yield* InvalidSourceError.make({
848
851
  source: source.id,
849
852
  reason: "source ids must be unique",
850
853
  });
@@ -853,7 +856,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
853
856
  }
854
857
  if (options.locked) {
855
858
  if (!currentLock) {
856
- return yield* new SourceManifestError({
859
+ return yield* SourceManifestError.make({
857
860
  path: lockfilePath,
858
861
  message: "lockfile is required with --locked",
859
862
  });
@@ -863,7 +866,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
863
866
  const missingFromLock = manifest.sources.find((source) => !lockedIds.has(source.id));
864
867
 
865
868
  if (missingFromManifest || missingFromLock) {
866
- return yield* new SourceManifestError({
869
+ return yield* SourceManifestError.make({
867
870
  path: lockfilePath,
868
871
  message: "source ids differ from skill-sources.jsonc; run catalog refresh without --locked",
869
872
  });
@@ -906,8 +909,11 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
906
909
  const summary = `${skillCount} skill${skillCount === 1 ? "" : "s"} from ${nextLock.sources.length} source${nextLock.sources.length === 1 ? "" : "s"}`;
907
910
 
908
911
  if (options.locked) {
909
- if (JSON.stringify(currentLock) !== JSON.stringify(nextLock)) {
910
- return yield* new SourceManifestError({
912
+ if (
913
+ currentLock === undefined ||
914
+ encodeSkillSourcesLockJson(currentLock) !== encodeSkillSourcesLockJson(nextLock)
915
+ ) {
916
+ return yield* SourceManifestError.make({
911
917
  path: lockfilePath,
912
918
  message:
913
919
  "approved catalog metadata differs from the lock; run catalog refresh and review it",
@@ -926,7 +932,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
926
932
 
927
933
  const nextLockPath = path.join(tempDir, "next-catalog-lock.json");
928
934
 
929
- yield* fs.writeFileString(nextLockPath, `${JSON.stringify(nextLock, null, 2)}\n`);
935
+ yield* fs.writeFileString(nextLockPath, `${encodeSkillSourcesLockPrettyJson(nextLock)}\n`);
930
936
  yield* fs.rename(nextLockPath, lockfilePath);
931
937
  yield* printStatus("success", "Catalog refreshed", summary);
932
938
  });
@@ -8,7 +8,7 @@ const InstalledVitePlusPackageSchema = Schema.fromJsonString(
8
8
  Schema.Struct({ version: Schema.String }),
9
9
  );
10
10
 
11
- export class VitePlusDependencyError extends Schema.TaggedErrorClass<VitePlusDependencyError>()(
11
+ export class VitePlusDependencyError extends Schema.TaggedError<VitePlusDependencyError>()(
12
12
  "VitePlusDependencyError",
13
13
  { message: Schema.String },
14
14
  ) {}
@@ -26,42 +26,41 @@ export const validateInstalledVitePlus = Effect.fn("validateInstalledVitePlus")(
26
26
  const dependencies = yield* readDirectDependencyNames(projectDir);
27
27
 
28
28
  if (!dependencies.includes("vite-plus")) {
29
- return yield* new VitePlusDependencyError({
29
+ return yield* VitePlusDependencyError.make({
30
30
  message: "vite-plus must be a direct project dependency",
31
31
  });
32
32
  }
33
33
  const packagePath = path.join(projectDir, "node_modules", "vite-plus", "package.json");
34
34
 
35
35
  if (!(yield* fs.exists(packagePath))) {
36
- return yield* new VitePlusDependencyError({
36
+ return yield* VitePlusDependencyError.make({
37
37
  message:
38
38
  "vite-plus must be installed before enabling this setup: node_modules/vite-plus/package.json is missing",
39
39
  });
40
40
  }
41
41
  const installed = yield* fs.readFileString(packagePath).pipe(
42
42
  Effect.flatMap(Schema.decodeUnknownEffect(InstalledVitePlusPackageSchema)),
43
- Effect.mapError(
44
- () =>
45
- new VitePlusDependencyError({
46
- message: "installed vite-plus package metadata has no valid version",
47
- }),
43
+ Effect.mapError(() =>
44
+ VitePlusDependencyError.make({
45
+ message: "installed vite-plus package metadata has no valid version",
46
+ }),
48
47
  ),
49
48
  );
50
49
 
51
50
  if (!semver.valid(installed.version)) {
52
- return yield* new VitePlusDependencyError({
51
+ return yield* VitePlusDependencyError.make({
53
52
  message: `installed vite-plus package metadata has invalid version: ${installed.version}`,
54
53
  });
55
54
  }
56
55
  if (!semver.satisfies(installed.version, VITE_PLUS_SUPPORTED_RANGE)) {
57
- return yield* new VitePlusDependencyError({
56
+ return yield* VitePlusDependencyError.make({
58
57
  message: `installed vite-plus ${installed.version} is incompatible with @danieljvdm/dev-kit; supported range: ${VITE_PLUS_SUPPORTED_RANGE}`,
59
58
  });
60
59
  }
61
60
  const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
62
61
 
63
62
  if (!(yield* fs.exists(vpBin))) {
64
- return yield* new VitePlusDependencyError({
63
+ return yield* VitePlusDependencyError.make({
65
64
  message: "vite-plus is installed but node_modules/.bin/vp is missing",
66
65
  });
67
66
  }
@@ -14,12 +14,12 @@ export type VitePlusHooksPlan = {
14
14
  readonly vpBin: string;
15
15
  };
16
16
 
17
- export class VitePlusHooksDependencyError extends Schema.TaggedErrorClass<VitePlusHooksDependencyError>()(
17
+ export class VitePlusHooksDependencyError extends Schema.TaggedError<VitePlusHooksDependencyError>()(
18
18
  "VitePlusHooksDependencyError",
19
19
  { message: Schema.String },
20
20
  ) {}
21
21
 
22
- export class VitePlusHooksConflictError extends Schema.TaggedErrorClass<VitePlusHooksConflictError>()(
22
+ export class VitePlusHooksConflictError extends Schema.TaggedError<VitePlusHooksConflictError>()(
23
23
  "VitePlusHooksConflictError",
24
24
  { hooksPath: Schema.String },
25
25
  ) {
@@ -28,7 +28,7 @@ export class VitePlusHooksConflictError extends Schema.TaggedErrorClass<VitePlus
28
28
  }
29
29
  }
30
30
 
31
- class VitePlusHooksCommandError extends Schema.TaggedErrorClass<VitePlusHooksCommandError>()(
31
+ class VitePlusHooksCommandError extends Schema.TaggedError<VitePlusHooksCommandError>()(
32
32
  "VitePlusHooksCommandError",
33
33
  { command: Schema.String, exitCode: Schema.Int, output: Schema.String },
34
34
  ) {
@@ -39,7 +39,7 @@ class VitePlusHooksCommandError extends Schema.TaggedErrorClass<VitePlusHooksCom
39
39
  }
40
40
  }
41
41
 
42
- class VitePlusHooksConvergenceError extends Schema.TaggedErrorClass<VitePlusHooksConvergenceError>()(
42
+ class VitePlusHooksConvergenceError extends Schema.TaggedError<VitePlusHooksConvergenceError>()(
43
43
  "VitePlusHooksConvergenceError",
44
44
  { message: Schema.String },
45
45
  ) {}
@@ -62,7 +62,7 @@ const runCommand = Effect.fn("runVitePlusHooksCommand")(function* (
62
62
  const trimmed = output.trim();
63
63
 
64
64
  if (!allowedExitCodes.includes(exitCode)) {
65
- return yield* new VitePlusHooksCommandError({
65
+ return yield* VitePlusHooksCommandError.make({
66
66
  command: [command, ...args].join(" "),
67
67
  exitCode,
68
68
  output: trimmed,
@@ -108,25 +108,35 @@ const inspectVitePlusHooks = Effect.fn("inspectVitePlusHooks")(function* (projec
108
108
  hooksPath !== ".husky" &&
109
109
  !hooksPath.startsWith(".husky/")
110
110
  ) {
111
- return yield* new VitePlusHooksConflictError({ hooksPath });
111
+ return yield* VitePlusHooksConflictError.make({ hooksPath });
112
112
  }
113
113
  const internalDir = path.join(projectDir, VITE_PLUS_HOOKS_DIR, "_");
114
- const [hasLauncher, hasDispatcher, hasPreCommit] = yield* Effect.all([
114
+ const [hasLauncher, hasDispatcher, hasPreCommit, hasInternalIgnore] = yield* Effect.all([
115
115
  hasExecutableFile(path.join(internalDir, "h")),
116
116
  hasExecutableFile(path.join(internalDir, "pre-commit")),
117
117
  hasVitePlusPreCommitHook(path.join(projectDir, VITE_PLUS_HOOKS_DIR, "pre-commit")),
118
+ FileSystem.FileSystem.pipe(
119
+ Effect.flatMap((fs) => fs.readFileString(path.join(internalDir, ".gitignore"))),
120
+ Effect.map((contents) => contents.split(/\r?\n/).includes("*")),
121
+ Effect.orElseSucceed(() => false),
122
+ ),
118
123
  ]);
119
124
 
120
- return hooksPath === VITE_PLUS_HOOKS_PATH && hasLauncher && hasDispatcher && hasPreCommit;
125
+ return (
126
+ hooksPath === VITE_PLUS_HOOKS_PATH &&
127
+ hasLauncher &&
128
+ hasDispatcher &&
129
+ hasPreCommit &&
130
+ hasInternalIgnore
131
+ );
121
132
  });
122
133
 
123
134
  export const planVitePlusHooks = Effect.fn("planVitePlusHooks")(function* (projectDir: string) {
124
135
  const { vpBin } = yield* validateInstalledVitePlus(projectDir).pipe(
125
- Effect.mapError(
126
- (error) =>
127
- new VitePlusHooksDependencyError({
128
- message: `${error.message} before enabling setup.vitePlus.hooks`,
129
- }),
136
+ Effect.mapError((error) =>
137
+ VitePlusHooksDependencyError.make({
138
+ message: `${error.message} before enabling setup.vitePlus.hooks`,
139
+ }),
130
140
  ),
131
141
  );
132
142
  const viteGitHooks = yield* Config.string("VITE_GIT_HOOKS").pipe(Config.withDefault(""));
@@ -158,7 +168,7 @@ export const applyVitePlusHooksPlan = Effect.fn("applyVitePlusHooksPlan")(functi
158
168
  plan.hooksDir,
159
169
  ]);
160
170
  if (!(yield* inspectVitePlusHooks(plan.projectDir))) {
161
- return yield* new VitePlusHooksConvergenceError({
171
+ return yield* VitePlusHooksConvergenceError.make({
162
172
  message: `Vite+ hook setup did not converge at ${plan.hooksPath}`,
163
173
  });
164
174
  }