@danieljvdm/dev-kit 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +123 -56
  2. package/dev-kit.example.jsonc +7 -3
  3. package/package.json +19 -16
  4. package/schema/dev-kit.schema.json +38 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +52 -17
  8. package/skills/effect-ts/agents/openai.yaml +0 -1
  9. package/skills/effect-ts/references/audit-services.md +11 -11
  10. package/skills/effect-ts/references/guide-effect.md +56 -69
  11. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  12. package/skills/effect-ts/references/guide-layers.md +187 -215
  13. package/skills/effect-ts/references/guide-observability.md +91 -116
  14. package/skills/effect-ts/references/guide-retries.md +32 -44
  15. package/skills/effect-ts/references/guide-schedule.md +26 -40
  16. package/skills/effect-ts/references/guide-schema.md +50 -57
  17. package/skills/effect-ts/references/guide-sql.md +47 -50
  18. package/skills/effect-ts/references/guide-testing.md +96 -98
  19. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  20. package/skills/effect-ts/references/version-and-source.md +0 -1
  21. package/src/bin/dev-kit.ts +61 -28
  22. package/src/catalog-manager.ts +86 -34
  23. package/src/catalog.ts +71 -33
  24. package/src/cli-ui.ts +20 -16
  25. package/src/effect-source.ts +49 -19
  26. package/src/effect-tsgo.ts +66 -35
  27. package/src/gitignore.ts +19 -6
  28. package/src/index.ts +6 -0
  29. package/src/manifest.ts +38 -3
  30. package/src/node-symbolic-link.ts +3 -0
  31. package/src/oxlint-plugin-effect.js +3 -0
  32. package/src/oxlint-plugin-style.d.ts +8 -0
  33. package/src/oxlint-plugin-style.js +8 -0
  34. package/src/oxlint.js +14 -0
  35. package/src/oxlint.ts +14 -0
  36. package/src/package-skill-source.ts +189 -52
  37. package/src/path-digest.ts +47 -13
  38. package/src/project-package.ts +44 -19
  39. package/src/project-process-lock.ts +19 -12
  40. package/src/project-state.ts +11 -0
  41. package/src/skill-manager.ts +134 -55
  42. package/src/skill-selector.ts +8 -2
  43. package/src/source-manifest.ts +2 -6
  44. package/src/sync.ts +417 -107
  45. package/src/vendor.ts +112 -42
  46. package/src/vite-plus-hooks.ts +174 -0
  47. package/src/vite-plus-quality.ts +49 -0
  48. package/templates/vite-plus/github-actions-check.yml +44 -0
  49. package/templates/vite-plus/vite.config.ts +22 -0
package/src/sync.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
2
1
  import { Cause, Effect, FileSystem, Path, Schema, Stream } from "effect";
3
2
  import { ChildProcess } from "effect/unstable/process";
3
+ import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
4
4
 
5
- import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
6
5
  import {
7
6
  loadSkillCatalog,
8
7
  resolveSkillSources,
@@ -10,26 +9,25 @@ import {
10
9
  type ResolvedSkillSource,
11
10
  } from "./catalog.ts";
12
11
  import { printDetail, printStatus, withSpinner } from "./cli-ui.ts";
13
- import {
14
- applyEffectSourcePlan,
15
- planEffectSource,
16
- type EffectSourcePlan,
17
- } from "./effect-source.ts";
12
+ import { applyEffectSourcePlan, planEffectSource, type EffectSourcePlan } from "./effect-source.ts";
18
13
  import {
19
14
  applyEffectTsgoPatchPlan,
20
15
  planEffectTsgoPatch,
21
16
  type EffectTsgoPatchPlan,
22
17
  } from "./effect-tsgo.ts";
18
+ import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
19
+ import { observeSymbolicLink } from "./node-symbolic-link.ts";
20
+ import { resolvePackageSkillSelector } from "./package-skill-source.ts";
23
21
  import {
24
22
  digestFileContent,
25
23
  digestSymlinkTarget,
26
24
  digestText,
27
25
  observePath,
26
+ observePathWithRawModes,
28
27
  type ObservedPath,
29
28
  } from "./path-digest.ts";
30
- import { resolvePackageSkillSelector } from "./package-skill-source.ts";
31
29
  import { readDirectDependencyNames } from "./project-package.ts";
32
- import { parseSkillSelector } from "./skill-selector.ts";
30
+ import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
33
31
  import {
34
32
  AppliedStateSchema,
35
33
  DevKitLockSchema,
@@ -37,16 +35,25 @@ import {
37
35
  type DevKitLock,
38
36
  type ManagedAgentInstructionsOutput,
39
37
  type ManagedClaudeInstructionsOutput,
38
+ type ManagedGeneratedFileOutput,
40
39
  type ManagedOutput,
41
40
  type ManagedSkillOutput,
42
41
  type OwnershipReceipt,
43
42
  } from "./project-state.ts";
44
- import {
45
- acquireProjectProcessLock,
46
- PROJECT_PROCESS_LOCK_PATH,
47
- } from "./project-process-lock.ts";
48
- import { observeSymbolicLink } from "./node-symbolic-link.ts";
43
+ import { parseSkillSelector } from "./skill-selector.ts";
49
44
  import { DEV_KIT_VERSION } from "./tool-metadata.ts";
45
+ import {
46
+ applyVitePlusHooksPlan,
47
+ planVitePlusHooks,
48
+ type VitePlusHooksPlan,
49
+ } from "./vite-plus-hooks.ts";
50
+ import {
51
+ validateVitePlusQualitySupport,
52
+ VITE_PLUS_CONFIG_PATH,
53
+ VITE_PLUS_CONFIG_TEMPLATE,
54
+ VITE_PLUS_GITHUB_ACTIONS_PATH,
55
+ VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
56
+ } from "./vite-plus-quality.ts";
50
57
 
51
58
  export type SyncOptions = {
52
59
  readonly manifestPath?: string;
@@ -89,10 +96,17 @@ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
89
96
  readonly linkTarget: string;
90
97
  };
91
98
 
99
+ type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
100
+ readonly adoptIfExact: true;
101
+ readonly content: string;
102
+ readonly destination: string;
103
+ };
104
+
92
105
  type DesiredOutput =
93
106
  | DesiredSkillOutput
94
107
  | DesiredAgentInstructionsOutput
95
- | DesiredClaudeInstructionsOutput;
108
+ | DesiredClaudeInstructionsOutput
109
+ | DesiredGeneratedFileOutput;
96
110
 
97
111
  type SkillPlanAction =
98
112
  | {
@@ -125,6 +139,7 @@ export type SkillPlan = {
125
139
  readonly actions: ReadonlyArray<SkillPlanAction>;
126
140
  readonly effectSource?: EffectSourcePlan;
127
141
  readonly effectTsgo?: EffectTsgoPatchPlan;
142
+ readonly vitePlusHooks?: VitePlusHooksPlan;
128
143
  readonly nextLock: DevKitLock;
129
144
  readonly nextState: AppliedState;
130
145
  readonly metadataChanged: boolean;
@@ -189,12 +204,12 @@ class LockedPlanMismatchError extends Schema.TaggedErrorClass<LockedPlanMismatch
189
204
  { message: Schema.String },
190
205
  ) {}
191
206
 
192
- class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()(
193
- "PlanConflictError",
194
- { conflicts: Schema.Array(Schema.String) },
195
- ) {
207
+ class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("PlanConflictError", {
208
+ conflicts: Schema.Array(Schema.String),
209
+ }) {
196
210
  override get message() {
197
211
  const heading = `plan has ${this.conflicts.length} conflict${this.conflicts.length === 1 ? "" : "s"}`;
212
+
198
213
  return `${heading}:\n${this.conflicts.map((conflict) => ` ${conflict}`).join("\n")}`;
199
214
  }
200
215
  }
@@ -210,6 +225,7 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
210
225
  const SKILL_FAMILIES: SkillCatalog = {
211
226
  effect: ["effect-ts", "effect-atom-data-fetching"],
212
227
  };
228
+
213
229
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
214
230
  const DEFAULT_LOCKFILE = "dev-kit.lock.json";
215
231
  const DEFAULT_STATE = ".dev-kit/state.json";
@@ -219,6 +235,7 @@ const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
219
235
  const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
220
236
  const path = yield* Path.Path;
221
237
  const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url));
238
+
222
239
  return path.resolve(path.dirname(scriptPath), "..");
223
240
  });
224
241
 
@@ -234,9 +251,11 @@ const runCommand = Effect.fn("runCommand")(function* (
234
251
  child.exitCode,
235
252
  ]);
236
253
  const trimmed = output.trim();
254
+
237
255
  if (exitCode !== 0) {
238
256
  return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
239
257
  }
258
+
240
259
  return trimmed;
241
260
  });
242
261
 
@@ -252,12 +271,14 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
252
271
  const errors: Array<ParseError> = [];
253
272
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
254
273
  const first = errors[0];
274
+
255
275
  if (first !== undefined) {
256
276
  return yield* new StructuredFileError({
257
277
  path: filePath,
258
278
  message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
259
279
  });
260
280
  }
281
+
261
282
  return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
262
283
  Effect.mapError((cause) => new StructuredFileError({ path: filePath, message: cause.message })),
263
284
  );
@@ -265,10 +286,12 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
265
286
 
266
287
  const readManifest = Effect.fn("readManifest")(function* (manifestPath: string) {
267
288
  const fs = yield* FileSystem.FileSystem;
289
+
268
290
  if (!(yield* fs.exists(manifestPath))) {
269
291
  return yield* new ManifestNotFoundError({ path: manifestPath });
270
292
  }
271
293
  const raw = yield* fs.readFileString(manifestPath);
294
+
272
295
  return yield* parseStructuredFile(manifestPath, raw, DevKitManifestSchema);
273
296
  });
274
297
 
@@ -277,9 +300,11 @@ const readOptionalStructuredFile = Effect.fn("readOptionalStructuredFile")(funct
277
300
  schema: Schema.ConstraintDecoder<A>,
278
301
  ) {
279
302
  const fs = yield* FileSystem.FileSystem;
303
+
280
304
  if (!(yield* fs.exists(filePath))) {
281
305
  return undefined;
282
306
  }
307
+
283
308
  return yield* parseStructuredFile(filePath, yield* fs.readFileString(filePath), schema);
284
309
  });
285
310
 
@@ -291,6 +316,7 @@ const expandSelection = (
291
316
  ) => {
292
317
  const known = [...new Set([...Object.keys(skillFamilies), ...availableSkills])].sort();
293
318
  const selected = new Set<string>();
319
+
294
320
  for (const name of include) {
295
321
  if (skillFamilies[name]) {
296
322
  for (const skill of skillFamilies[name]) selected.add(skill);
@@ -302,9 +328,11 @@ const expandSelection = (
302
328
  }
303
329
  for (const name of exclude) {
304
330
  const family = skillFamilies[name];
331
+
305
332
  if (family) for (const skill of family) selected.delete(skill);
306
333
  else selected.delete(name);
307
334
  }
335
+
308
336
  return Effect.succeed([...selected].sort());
309
337
  };
310
338
 
@@ -316,20 +344,35 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
316
344
  candidate: string,
317
345
  ) {
318
346
  const path = yield* Path.Path;
347
+
319
348
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
320
- return yield* new UnsafeManagedPathError({ path: candidate, reason: "must be a non-empty project-relative path" });
349
+ return yield* new UnsafeManagedPathError({
350
+ path: candidate,
351
+ reason: "must be a non-empty project-relative path",
352
+ });
321
353
  }
322
354
  const absolute = path.resolve(projectDir, candidate);
323
355
  const relative = path.relative(projectDir, absolute);
324
- if (relative.length === 0 || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
325
- return yield* new UnsafeManagedPathError({ path: candidate, reason: "resolves outside the project" });
356
+
357
+ if (
358
+ relative.length === 0 ||
359
+ relative === ".." ||
360
+ relative.startsWith(`..${path.sep}`) ||
361
+ path.isAbsolute(relative)
362
+ ) {
363
+ return yield* new UnsafeManagedPathError({
364
+ path: candidate,
365
+ reason: "resolves outside the project",
366
+ });
326
367
  }
327
368
 
328
369
  const segments = relative.split(path.sep);
329
370
  let ancestor = projectDir;
371
+
330
372
  for (const segment of segments.slice(0, -1)) {
331
373
  ancestor = path.join(ancestor, segment);
332
374
  const target = yield* observeSymbolicLink(ancestor);
375
+
333
376
  if (target.kind === "symlink") {
334
377
  return yield* new UnsafeManagedPathError({
335
378
  path: candidate,
@@ -337,6 +380,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
337
380
  });
338
381
  }
339
382
  }
383
+
340
384
  return { absolute, relative: portablePath(path, relative) } satisfies ManagedPath;
341
385
  });
342
386
 
@@ -349,12 +393,14 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
349
393
  outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
350
394
  ) {
351
395
  const outputPaths = new Set<string>();
396
+
352
397
  for (const output of outputs) {
353
398
  outputPaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
354
399
  }
355
400
 
356
401
  for (let index = 0; index < reserved.length; index += 1) {
357
402
  const current = reserved[index];
403
+
358
404
  if (current === undefined) continue;
359
405
  for (const other of reserved.slice(index + 1)) {
360
406
  if (pathsOverlap(current.path, other.path)) {
@@ -385,6 +431,27 @@ const outputIdentity = (output: ManagedOutput) =>
385
431
  : { sourcePath: output.sourcePath }),
386
432
  });
387
433
 
434
+ const outputOwnershipIdentity = (output: ManagedOutput) =>
435
+ JSON.stringify({
436
+ resourceId: output.resourceId,
437
+ path: output.path,
438
+ mode: output.mode,
439
+ kind: output.kind,
440
+ ...("skill" in output
441
+ ? { skill: output.skill, target: output.target }
442
+ : { sourcePath: output.sourcePath }),
443
+ });
444
+
445
+ const usesRawFileModeDigests = (toolVersion: string): boolean => {
446
+ const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
447
+
448
+ if (match === null) return false;
449
+ const major = Number(match[1]);
450
+ const minor = Number(match[2]);
451
+
452
+ return major === 0 && minor <= 6;
453
+ };
454
+
388
455
  const validateInventory = Effect.fn("validateManagedInventory")(function* (
389
456
  projectDir: string,
390
457
  outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
@@ -393,12 +460,17 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
393
460
  const ids = new Set<string>();
394
461
  const paths = new Set<string>();
395
462
  const sortedPaths: Array<string> = [];
463
+
396
464
  for (const output of outputs) {
397
465
  if (ids.has(output.resourceId)) {
398
- return yield* new InvalidProjectStateError({ message: `${label} contains duplicate resource id ${output.resourceId}` });
466
+ return yield* new InvalidProjectStateError({
467
+ message: `${label} contains duplicate resource id ${output.resourceId}`,
468
+ });
399
469
  }
400
470
  if (paths.has(output.path)) {
401
- return yield* new InvalidProjectStateError({ message: `${label} contains duplicate path ${output.path}` });
471
+ return yield* new InvalidProjectStateError({
472
+ message: `${label} contains duplicate path ${output.path}`,
473
+ });
402
474
  }
403
475
  ids.add(output.resourceId);
404
476
  paths.add(output.path);
@@ -408,9 +480,12 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
408
480
  for (let index = 1; index < sortedPaths.length; index += 1) {
409
481
  const previous = sortedPaths[index - 1];
410
482
  const current = sortedPaths[index];
483
+
411
484
  if (previous === undefined || current === undefined) continue;
412
485
  if (current.startsWith(`${previous}/`)) {
413
- return yield* new InvalidProjectStateError({ message: `${label} contains overlapping paths ${previous} and ${current}` });
486
+ return yield* new InvalidProjectStateError({
487
+ message: `${label} contains overlapping paths ${previous} and ${current}`,
488
+ });
414
489
  }
415
490
  }
416
491
  });
@@ -420,13 +495,16 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
420
495
  outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
421
496
  ) {
422
497
  const uniquePaths = new Set<string>();
498
+
423
499
  for (const output of outputs) {
424
500
  uniquePaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
425
501
  }
426
502
  const sortedPaths = [...uniquePaths].sort();
503
+
427
504
  for (let index = 1; index < sortedPaths.length; index += 1) {
428
505
  const previous = sortedPaths[index - 1];
429
506
  const current = sortedPaths[index];
507
+
430
508
  if (previous === undefined || current === undefined) continue;
431
509
  if (current.startsWith(`${previous}/`)) {
432
510
  return yield* new InvalidProjectStateError({
@@ -444,12 +522,14 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
444
522
  const fs = yield* FileSystem.FileSystem;
445
523
  const path = yield* Path.Path;
446
524
  const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
525
+
447
526
  if ((yield* observePath(templatePath)).kind !== "file") {
448
527
  return yield* new InvalidProjectStateError({
449
528
  message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
450
529
  });
451
530
  }
452
531
  const template = yield* fs.readFileString(templatePath);
532
+
453
533
  if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
454
534
  return yield* new InvalidProjectStateError({
455
535
  message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
@@ -457,28 +537,50 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
457
537
  }
458
538
 
459
539
  const devKitSkill = sourceBySkill.get("dev-kit");
460
- const devKitSkillPath = devKitSkill === undefined
461
- ? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
462
- : portablePath(
463
- path,
464
- path.relative(
465
- projectDir,
466
- path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
467
- ),
468
- );
540
+ const devKitSkillPath =
541
+ devKitSkill === undefined
542
+ ? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
543
+ : portablePath(
544
+ path,
545
+ path.relative(
546
+ projectDir,
547
+ path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
548
+ ),
549
+ );
469
550
  const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
551
+
470
552
  if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
471
553
  const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
554
+
472
555
  if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
473
556
  return yield* new InvalidProjectStateError({
474
- message: "Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
557
+ message:
558
+ "Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
475
559
  });
476
560
  }
477
561
  sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
478
562
  }
563
+
479
564
  return `${sections.join("\n\n")}\n`;
480
565
  });
481
566
 
567
+ const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(function* (
568
+ packageRoot: string,
569
+ sourcePath: string,
570
+ ) {
571
+ const fs = yield* FileSystem.FileSystem;
572
+ const path = yield* Path.Path;
573
+ const templatePath = path.join(packageRoot, sourcePath);
574
+
575
+ if ((yield* observePath(templatePath)).kind !== "file") {
576
+ return yield* new InvalidProjectStateError({
577
+ message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
578
+ });
579
+ }
580
+
581
+ return yield* fs.readFileString(templatePath);
582
+ });
583
+
482
584
  const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
483
585
  packageRoot: string,
484
586
  projectDir: string,
@@ -489,9 +591,11 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
489
591
  ) {
490
592
  const path = yield* Path.Path;
491
593
  const outputs: Array<DesiredOutput> = [];
594
+
492
595
  if (setup.agentInstructions.enabled) {
493
596
  const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
494
597
  const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
598
+
495
599
  outputs.push({
496
600
  resourceId: "setup:agent-instructions",
497
601
  path: managed.relative,
@@ -508,6 +612,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
508
612
  const sourceObservation = setup.agentInstructions.enabled
509
613
  ? undefined
510
614
  : yield* observePath(source.absolute);
615
+
511
616
  if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
512
617
  return yield* new InvalidProjectStateError({
513
618
  message: "Claude instructions source is not a regular file: AGENTS.md",
@@ -515,6 +620,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
515
620
  }
516
621
  const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
517
622
  const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
623
+
518
624
  outputs.push({
519
625
  resourceId: "setup:claude-instructions",
520
626
  path: managed.relative,
@@ -526,32 +632,71 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
526
632
  linkTarget,
527
633
  });
528
634
  }
635
+ if (setup.vitePlus.quality.enabled) {
636
+ for (const generated of [
637
+ {
638
+ resourceId: "setup:vite-plus-config" as const,
639
+ path: VITE_PLUS_CONFIG_PATH,
640
+ sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
641
+ },
642
+ {
643
+ resourceId: "setup:vite-plus-github-actions" as const,
644
+ path: VITE_PLUS_GITHUB_ACTIONS_PATH,
645
+ sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
646
+ },
647
+ ]) {
648
+ const managed = yield* resolveManagedPath(projectDir, generated.path);
649
+ const content = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
650
+
651
+ outputs.push({
652
+ resourceId: generated.resourceId,
653
+ path: managed.relative,
654
+ sourcePath: generated.sourcePath,
655
+ mode: "copy",
656
+ kind: "file",
657
+ digest: yield* digestFileContent(content),
658
+ destination: managed.absolute,
659
+ content,
660
+ adoptIfExact: true,
661
+ });
662
+ }
663
+ }
529
664
  const agentsTarget = targets.agents;
530
- const duplicateOutput = skills.find((skill, index) =>
531
- skills.findIndex((candidate) => candidate.name === skill.name) !== index
665
+ const duplicateOutput = skills.find(
666
+ (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
532
667
  );
668
+
533
669
  if (duplicateOutput !== undefined) {
534
670
  const selectors = skills
535
671
  .filter((skill) => skill.name === duplicateOutput.name)
536
672
  .map((skill) => skill.selector);
673
+
537
674
  return yield* new InvalidProjectStateError({
538
675
  message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
539
676
  });
540
677
  }
541
678
  for (const skill of skills) {
542
679
  const resolvedSource = sourceBySkill.get(skill.selector);
680
+
543
681
  if (resolvedSource === undefined) {
544
- return yield* new InvalidProjectStateError({ message: `skill source is unavailable: ${skill.selector}` });
682
+ return yield* new InvalidProjectStateError({
683
+ message: `skill source is unavailable: ${skill.selector}`,
684
+ });
545
685
  }
546
686
  const source = resolvedSource.path;
547
687
  const sourceObservation = yield* observePath(source);
688
+
548
689
  if (sourceObservation.kind !== "directory") {
549
- return yield* new InvalidProjectStateError({ message: `skill source is not a directory: ${source}` });
690
+ return yield* new InvalidProjectStateError({
691
+ message: `skill source is not a directory: ${source}`,
692
+ });
550
693
  }
551
694
  for (const targetName of ["agents", "claude", "opencode"] as const) {
552
695
  const target = targets[targetName];
696
+
553
697
  if (!target.enabled) continue;
554
698
  const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
699
+
555
700
  if (target.mode === "copy") {
556
701
  outputs.push({
557
702
  resourceId: `skill:${skill.selector}@${targetName}`,
@@ -569,10 +714,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
569
714
  }
570
715
  const linkSource =
571
716
  targetName === "agents" || !agentsTarget.enabled
572
- ? resolvedSource.linkPath ?? source
573
- : (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name))).absolute;
717
+ ? (resolvedSource.linkPath ?? source)
718
+ : (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
719
+ .absolute;
574
720
  const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
575
721
  const linkDigest = yield* digestSymlinkTarget(linkTarget);
722
+
576
723
  outputs.push({
577
724
  resourceId: `skill:${skill.selector}@${targetName}`,
578
725
  path: managed.relative,
@@ -589,6 +736,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
589
736
  }
590
737
  }
591
738
  yield* validateInventory(projectDir, outputs, "desired outputs");
739
+
592
740
  return outputs.sort((left, right) => left.path.localeCompare(right.path));
593
741
  });
594
742
 
@@ -606,7 +754,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
606
754
  if (currentState) yield* validateInventory(projectDir, currentState.outputs, "applied state");
607
755
  yield* validateCrossInventoryPaths(projectDir, [...desired, ...(currentState?.outputs ?? [])]);
608
756
  const lockById = new Map(currentLock?.outputs.map((output) => [output.resourceId, output]) ?? []);
609
- const receiptsById = new Map(currentState?.outputs.map((output) => [output.resourceId, output]) ?? []);
757
+ const receiptsById = new Map(
758
+ currentState?.outputs.map((output) => [output.resourceId, output]) ?? [],
759
+ );
610
760
  const desiredKeys = new Set(desired.map((output) => `${output.resourceId}\0${output.path}`));
611
761
  const actions: Array<SkillPlanAction> = [];
612
762
 
@@ -615,27 +765,53 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
615
765
  const receipt = receiptsById.get(output.resourceId);
616
766
  const sameReceipt = receipt?.path === output.path ? receipt : undefined;
617
767
  const locked = lockById.get(output.resourceId);
618
- const adoptable = locked !== undefined && outputIdentity(locked) === outputIdentity(output);
768
+ const matchingLockedOutput =
769
+ locked !== undefined &&
770
+ outputOwnershipIdentity(locked) === outputOwnershipIdentity(output) &&
771
+ observed.kind === locked.kind
772
+ ? locked
773
+ : undefined;
774
+ const rawModeObservation =
775
+ matchingLockedOutput !== undefined &&
776
+ observed.kind !== "missing" &&
777
+ observed.digest !== matchingLockedOutput.digest &&
778
+ currentLock !== undefined &&
779
+ usesRawFileModeDigests(currentLock.toolVersion)
780
+ ? yield* observePathWithRawModes(output.destination)
781
+ : undefined;
782
+ const lockedOwnsObserved =
783
+ matchingLockedOutput !== undefined &&
784
+ observed.kind !== "missing" &&
785
+ (observed.digest === matchingLockedOutput.digest ||
786
+ (rawModeObservation?.kind === matchingLockedOutput.kind &&
787
+ rawModeObservation.digest === matchingLockedOutput.digest));
619
788
 
620
789
  if (observed.kind === "missing") {
621
790
  actions.push({ action: "create", desired: output, observed });
622
791
  } else if (observed.kind === output.kind && observed.digest === output.digest) {
623
- if (sameReceipt || adoptable) {
792
+ if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
624
793
  actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
625
794
  } else {
626
- actions.push({ action: "conflict", path: output.path, reason: "destination exists but is not owned" });
795
+ actions.push({
796
+ action: "conflict",
797
+ path: output.path,
798
+ reason: "destination exists but is not owned",
799
+ });
627
800
  }
628
801
  } else if (
629
- sameReceipt &&
630
- observed.kind === sameReceipt.kind &&
631
- observed.digest === sameReceipt.digest
802
+ (sameReceipt !== undefined &&
803
+ observed.kind === sameReceipt.kind &&
804
+ observed.digest === sameReceipt.digest) ||
805
+ lockedOwnsObserved
632
806
  ) {
633
807
  actions.push({ action: "update", desired: output, observed });
634
808
  } else {
635
809
  actions.push({
636
810
  action: "conflict",
637
811
  path: output.path,
638
- reason: sameReceipt ? "owned destination was modified" : "destination exists but is not owned",
812
+ reason: sameReceipt
813
+ ? "owned destination was modified"
814
+ : "destination exists but is not owned",
639
815
  });
640
816
  }
641
817
  }
@@ -644,11 +820,21 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
644
820
  if (desiredKeys.has(`${receipt.resourceId}\0${receipt.path}`)) continue;
645
821
  const managed = yield* resolveManagedPath(projectDir, receipt.path);
646
822
  const observed = yield* observePath(managed.absolute);
823
+
647
824
  if (observed.kind === "missing") continue;
648
825
  if (observed.kind === receipt.kind && observed.digest === receipt.digest) {
649
- actions.push({ action: "remove", previous: receipt, destination: managed.absolute, observed });
826
+ actions.push({
827
+ action: "remove",
828
+ previous: receipt,
829
+ destination: managed.absolute,
830
+ observed,
831
+ });
650
832
  } else {
651
- actions.push({ action: "conflict", path: receipt.path, reason: "stale owned destination was modified" });
833
+ actions.push({
834
+ action: "conflict",
835
+ path: receipt.path,
836
+ reason: "stale owned destination was modified",
837
+ });
652
838
  }
653
839
  }
654
840
 
@@ -663,11 +849,26 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
663
849
  digest,
664
850
  })),
665
851
  };
666
- return { actions: actions.sort((left, right) => {
667
- const leftPath = left.action === "remove" ? left.previous.path : left.action === "conflict" ? left.path : left.desired.path;
668
- const rightPath = right.action === "remove" ? right.previous.path : right.action === "conflict" ? right.path : right.desired.path;
669
- return leftPath.localeCompare(rightPath);
670
- }), nextState };
852
+
853
+ return {
854
+ actions: actions.sort((left, right) => {
855
+ const leftPath =
856
+ left.action === "remove"
857
+ ? left.previous.path
858
+ : left.action === "conflict"
859
+ ? left.path
860
+ : left.desired.path;
861
+ const rightPath =
862
+ right.action === "remove"
863
+ ? right.previous.path
864
+ : right.action === "conflict"
865
+ ? right.path
866
+ : right.desired.path;
867
+
868
+ return leftPath.localeCompare(rightPath);
869
+ }),
870
+ nextState,
871
+ };
671
872
  });
672
873
 
673
874
  const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
@@ -677,6 +878,7 @@ const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
677
878
  current.outputs.length === next.outputs.length &&
678
879
  current.outputs.every((output, index) => {
679
880
  const nextOutput = next.outputs[index];
881
+
680
882
  return nextOutput !== undefined && outputIdentity(output) === outputIdentity(nextOutput);
681
883
  });
682
884
 
@@ -692,12 +894,32 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
692
894
  ),
693
895
  );
694
896
  const projectDir = yield* fs.realPath(discoveredRoot);
695
- const manifestManaged = yield* resolveManagedPath(projectDir, options.manifestPath ?? DEFAULT_MANIFEST);
696
- const lockManaged = yield* resolveManagedPath(projectDir, options.lockfilePath ?? DEFAULT_LOCKFILE);
897
+ const manifestManaged = yield* resolveManagedPath(
898
+ projectDir,
899
+ options.manifestPath ?? DEFAULT_MANIFEST,
900
+ );
901
+ const lockManaged = yield* resolveManagedPath(
902
+ projectDir,
903
+ options.lockfilePath ?? DEFAULT_LOCKFILE,
904
+ );
697
905
  const stateManaged = yield* resolveManagedPath(projectDir, options.statePath ?? DEFAULT_STATE);
698
906
  const processLockManaged = yield* resolveManagedPath(projectDir, PROJECT_PROCESS_LOCK_PATH);
699
907
  const packageRoot = yield* resolvePackageRoot();
700
908
  const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
909
+
910
+ if (manifest.setup.vitePlus.quality.enabled) {
911
+ if (!manifest.setup.effectTsgo.enabled) {
912
+ return yield* new InvalidProjectStateError({
913
+ message:
914
+ "setup.vitePlus.quality requires setup.effectTsgo.enabled so vp run typecheck uses the Effect-patched compiler",
915
+ });
916
+ }
917
+ yield* validateVitePlusQualitySupport(
918
+ projectDir,
919
+ packageRoot,
920
+ manifest.setup.effectTsgo.typescriptPackage,
921
+ );
922
+ }
701
923
  const effectSource = manifest.setup.effectSource.enabled
702
924
  ? yield* planEffectSource({
703
925
  packageName: manifest.setup.effectSource.packageName,
@@ -713,19 +935,35 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
713
935
  typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
714
936
  })
715
937
  : undefined;
938
+ const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
939
+ ? yield* planVitePlusHooks(projectDir)
940
+ : undefined;
716
941
  const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
717
942
  const availableSkills = catalog.skills.map((skill) => skill.selector);
718
943
  const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
944
+
719
945
  for (const [family, familySkills] of Object.entries(skillFamilies)) {
720
946
  if (availableSkills.includes(family)) {
721
- return yield* new InvalidSkillCatalogError({ family, message: `family name conflicts with a skill name: ${family}` });
947
+ return yield* new InvalidSkillCatalogError({
948
+ family,
949
+ message: `family name conflicts with a skill name: ${family}`,
950
+ });
722
951
  }
723
952
  const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
953
+
724
954
  if (missing.length > 0) {
725
- return yield* new InvalidSkillCatalogError({ family, message: `family references missing skills: ${missing.join(", ")}` });
955
+ return yield* new InvalidSkillCatalogError({
956
+ family,
957
+ message: `family references missing skills: ${missing.join(", ")}`,
958
+ });
726
959
  }
727
960
  }
728
- const selectedSelectors = yield* expandSelection(manifest.include, manifest.exclude, availableSkills, skillFamilies);
961
+ const selectedSelectors = yield* expandSelection(
962
+ manifest.include,
963
+ manifest.exclude,
964
+ availableSkills,
965
+ skillFamilies,
966
+ );
729
967
  const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
730
968
  const sourceBySkill = yield* withSpinner(
731
969
  "Resolving selected skills",
@@ -738,8 +976,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
738
976
  ),
739
977
  );
740
978
  const selectedSkills: Array<CatalogSkill> = [];
979
+
741
980
  for (const selector of selectedSelectors) {
742
981
  const catalogSkill = catalogBySelector.get(selector);
982
+
743
983
  if (catalogSkill === undefined) {
744
984
  return yield* new InvalidProjectStateError({
745
985
  message: `selected skill is unavailable: ${selector}`,
@@ -804,6 +1044,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
804
1044
  digest: output.digest,
805
1045
  };
806
1046
  }
1047
+ if (output.resourceId === "setup:claude-instructions") {
1048
+ return {
1049
+ resourceId: output.resourceId,
1050
+ path: output.path,
1051
+ sourcePath: output.sourcePath,
1052
+ mode: output.mode,
1053
+ kind: output.kind,
1054
+ digest: output.digest,
1055
+ };
1056
+ }
1057
+
807
1058
  return {
808
1059
  resourceId: output.resourceId,
809
1060
  path: output.path,
@@ -823,37 +1074,45 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
823
1074
  ? []
824
1075
  : [{ label: "Effect source checkout", path: effectSource.path }]),
825
1076
  ];
1077
+
826
1078
  yield* validateReservedPaths(projectDir, reservedPaths, desired);
827
1079
  const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
828
1080
  const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1081
+
829
1082
  if (
830
1083
  manifest.setup.claudeInstructions.enabled &&
831
1084
  !manifest.setup.agentInstructions.enabled &&
832
- currentState?.outputs.some(
833
- (output) => output.resourceId === "setup:agent-instructions",
834
- )
1085
+ currentState?.outputs.some((output) => output.resourceId === "setup:agent-instructions")
835
1086
  ) {
836
1087
  return yield* new InvalidProjectStateError({
837
- message: "cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
1088
+ message:
1089
+ "cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
838
1090
  });
839
1091
  }
840
- yield* validateReservedPaths(
841
- projectDir,
842
- reservedPaths,
843
- [
844
- ...(currentLock?.outputs ?? []),
845
- ...(currentState?.outputs ?? []),
846
- ],
847
- );
1092
+ yield* validateReservedPaths(projectDir, reservedPaths, [
1093
+ ...(currentLock?.outputs ?? []),
1094
+ ...(currentState?.outputs ?? []),
1095
+ ]);
848
1096
  if (options.locked) {
849
1097
  if (!currentLock) {
850
- return yield* new LockedPlanMismatchError({ message: "dev-kit.lock.json is required with --locked" });
1098
+ return yield* new LockedPlanMismatchError({
1099
+ message: "dev-kit.lock.json is required with --locked",
1100
+ });
851
1101
  }
852
1102
  if (!lockedPlanMatches(currentLock, nextLock)) {
853
- return yield* new LockedPlanMismatchError({ message: "manifest or packaged skills differ from dev-kit.lock.json" });
1103
+ return yield* new LockedPlanMismatchError({
1104
+ message: "manifest or packaged skills differ from dev-kit.lock.json",
1105
+ });
854
1106
  }
855
1107
  }
856
- const planned = yield* planDesiredOutputs(projectDir, desired, currentLock, currentState, nextLock);
1108
+ const planned = yield* planDesiredOutputs(
1109
+ projectDir,
1110
+ desired,
1111
+ currentLock,
1112
+ currentState,
1113
+ nextLock,
1114
+ );
1115
+
857
1116
  return {
858
1117
  projectDir,
859
1118
  lockfilePath: lockManaged.absolute,
@@ -861,6 +1120,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
861
1120
  actions: planned.actions,
862
1121
  ...(effectSource === undefined ? {} : { effectSource }),
863
1122
  ...(effectTsgo === undefined ? {} : { effectTsgo }),
1123
+ ...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
864
1124
  nextLock,
865
1125
  nextState: planned.nextState,
866
1126
  metadataChanged:
@@ -871,46 +1131,51 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
871
1131
 
872
1132
  const formatAction = (action: SkillPlanAction): string => {
873
1133
  if (action.action === "conflict") return `! ${action.path}: ${action.reason}`;
874
- if (action.action === "remove") return `− ${action.previous.resourceId} → ${action.previous.path}`;
1134
+ if (action.action === "remove")
1135
+ return `− ${action.previous.resourceId} → ${action.previous.path}`;
875
1136
  const verb = action.desired.mode === "copy" ? "copy" : "link";
876
1137
  const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
877
1138
  const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
878
- const source = "skill" in action.desired
879
- ? action.desired.skill
880
- : action.desired.sourcePath;
1139
+ const source = "skill" in action.desired ? action.desired.skill : action.desired.sourcePath;
1140
+
881
1141
  return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
882
1142
  };
883
1143
 
884
1144
  const operationalChangeCount = (plan: SkillPlan): number =>
885
1145
  plan.actions.filter((action) => action.action !== "unchanged").length +
886
1146
  (plan.effectSource?.action === "sync" ? 1 : 0) +
887
- (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0);
1147
+ (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
1148
+ (plan.vitePlusHooks?.action === "configure" ? 1 : 0);
888
1149
 
889
1150
  const plannedChangeCount = (plan: SkillPlan): number => {
890
1151
  const operational = operationalChangeCount(plan);
1152
+
891
1153
  return operational === 0 && plan.metadataChanged ? 1 : operational;
892
1154
  };
893
1155
 
894
1156
  export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: SkillPlan) {
895
1157
  const changes = plannedChangeCount(plan);
1158
+
896
1159
  if (changes === 0) {
897
1160
  yield* printStatus("success", "Already up to date");
1161
+
898
1162
  return;
899
1163
  }
900
1164
  yield* printStatus("plan", `${changes} change${changes === 1 ? "" : "s"} planned`);
901
1165
  for (const action of plan.actions) {
902
- if (action.action !== "unchanged") yield* printDetail(formatAction(action));
1166
+ if (action.action !== "unchanged" || action.adopted) yield* printDetail(formatAction(action));
903
1167
  }
904
1168
  if (plan.effectSource?.action === "sync") {
905
- yield* printDetail(
906
- `+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`,
907
- );
1169
+ yield* printDetail(`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`);
908
1170
  }
909
1171
  if (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched) {
910
1172
  yield* printDetail(
911
1173
  `+ TypeScript patch @effect/tsgo@${plan.effectTsgo.effectTsgoVersion} → ${plan.effectTsgo.typescriptPackage}@${plan.effectTsgo.typescriptVersion}`,
912
1174
  );
913
1175
  }
1176
+ if (plan.vitePlusHooks?.action === "configure") {
1177
+ yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
1178
+ }
914
1179
  if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
915
1180
  yield* printDetail("+ Dev kit metadata");
916
1181
  }
@@ -920,22 +1185,24 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
920
1185
  left.kind === right.kind &&
921
1186
  (left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
922
1187
 
923
- const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
924
- root: string,
925
- ) {
1188
+ const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (root: string) {
926
1189
  const fs = yield* FileSystem.FileSystem;
927
1190
  const path = yield* Path.Path;
928
1191
  const pending = [root];
1192
+
929
1193
  while (pending.length > 0) {
930
1194
  const current = pending.pop();
1195
+
931
1196
  if (current === undefined) continue;
932
1197
  if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
933
1198
  const info = yield* fs.stat(current);
1199
+
934
1200
  if (info.type !== "Directory") continue;
935
1201
  for (const entry of yield* fs.readDirectory(current)) {
936
1202
  pending.push(path.join(current, entry));
937
1203
  }
938
1204
  }
1205
+
939
1206
  return undefined;
940
1207
  });
941
1208
 
@@ -943,18 +1210,26 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
943
1210
  plan: SkillPlan,
944
1211
  ) {
945
1212
  const verified = new Set<string>();
1213
+
946
1214
  for (const action of plan.actions) {
947
1215
  if (action.action === "remove" || action.action === "conflict") continue;
948
1216
  if (!("skill" in action.desired)) continue;
949
1217
  const catalog = action.desired.catalog;
1218
+
950
1219
  if (catalog === undefined || !("package" in catalog)) continue;
951
1220
  const selector = `${catalog.package}#${catalog.skill}`;
952
1221
  const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
1222
+
953
1223
  if (verified.has(key)) continue;
954
1224
  const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
955
1225
  const observation = yield* observePath(resolved.path);
956
- if (resolved.path !== action.desired.source || resolved.version !== catalog.version ||
957
- observation.kind !== "directory" || observation.digest !== catalog.digest) {
1226
+
1227
+ if (
1228
+ resolved.path !== action.desired.source ||
1229
+ resolved.version !== catalog.version ||
1230
+ observation.kind !== "directory" ||
1231
+ observation.digest !== catalog.digest
1232
+ ) {
958
1233
  return yield* new ApplyRaceError({ path: action.desired.source });
959
1234
  }
960
1235
  verified.add(key);
@@ -963,9 +1238,12 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
963
1238
 
964
1239
  const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
965
1240
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1241
+
966
1242
  if (conflicts.length > 0) {
967
1243
  return yield* new PlanConflictError({
968
- conflicts: conflicts.map((action) => action.action === "conflict" ? `${action.path}: ${action.reason}` : ""),
1244
+ conflicts: conflicts.map((action) =>
1245
+ action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1246
+ ),
969
1247
  });
970
1248
  }
971
1249
 
@@ -975,10 +1253,15 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
975
1253
  (action): action is Exclude<SkillPlanAction, { readonly action: "unchanged" | "conflict" }> =>
976
1254
  action.action === "create" || action.action === "update" || action.action === "remove",
977
1255
  );
1256
+
978
1257
  for (const action of mutating) {
979
- const destination = action.action === "remove" ? action.destination : action.desired.destination;
1258
+ const destination =
1259
+ action.action === "remove" ? action.destination : action.desired.destination;
1260
+
980
1261
  if (!observationsEqual(yield* observePath(destination), action.observed)) {
981
- return yield* new ApplyRaceError({ path: action.action === "remove" ? action.previous.path : action.desired.path });
1262
+ return yield* new ApplyRaceError({
1263
+ path: action.action === "remove" ? action.previous.path : action.desired.path,
1264
+ });
982
1265
  }
983
1266
  }
984
1267
 
@@ -986,14 +1269,19 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
986
1269
  return;
987
1270
  }
988
1271
 
989
- const tempDir = yield* fs.makeTempDirectoryScoped({ directory: plan.projectDir, prefix: ".dev-kit-apply-" });
1272
+ const tempDir = yield* fs.makeTempDirectoryScoped({
1273
+ directory: plan.projectDir,
1274
+ prefix: ".dev-kit-apply-",
1275
+ });
990
1276
  const stageDir = path.join(tempDir, "stage");
991
1277
  const backupDir = path.join(tempDir, "backup");
992
1278
  const stagedByResource = new Map<string, string>();
993
1279
  let stageIndex = 0;
1280
+
994
1281
  for (const action of mutating) {
995
1282
  if (action.action === "remove") continue;
996
1283
  const staged = path.join(stageDir, String(stageIndex++));
1284
+
997
1285
  yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
998
1286
  if (action.desired.mode === "copy") {
999
1287
  if (action.desired.kind === "file") {
@@ -1001,6 +1289,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1001
1289
  } else {
1002
1290
  yield* fs.copy(action.desired.source, staged, { overwrite: true });
1003
1291
  const symbolicLink = yield* findNestedSymbolicLink(staged);
1292
+
1004
1293
  if (symbolicLink !== undefined) {
1005
1294
  return yield* new InvalidProjectStateError({
1006
1295
  message: `staged skill contains a symlink: ${action.desired.path}`,
@@ -1011,8 +1300,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1011
1300
  yield* fs.symlink(action.desired.linkTarget, staged);
1012
1301
  }
1013
1302
  const observation = yield* observePath(staged);
1303
+
1014
1304
  if (observation.kind !== action.desired.kind || observation.digest !== action.desired.digest) {
1015
- return yield* new InvalidProjectStateError({ message: `staged output digest mismatch for ${action.desired.path}` });
1305
+ return yield* new InvalidProjectStateError({
1306
+ message: `staged output digest mismatch for ${action.desired.path}`,
1307
+ });
1016
1308
  }
1017
1309
  stagedByResource.set(action.desired.resourceId, staged);
1018
1310
  }
@@ -1021,6 +1313,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1021
1313
 
1022
1314
  const stagedLock = path.join(tempDir, "next-lock.json");
1023
1315
  const stagedState = path.join(tempDir, "next-state.json");
1316
+
1024
1317
  yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
1025
1318
  yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
1026
1319
 
@@ -1033,10 +1326,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1033
1326
  };
1034
1327
  const replacements: Array<Replacement> = [];
1035
1328
  let replacementIndex = 0;
1329
+
1036
1330
  for (const action of mutating) {
1037
- const staged = action.action === "remove"
1038
- ? undefined
1039
- : stagedByResource.get(action.desired.resourceId);
1331
+ const staged =
1332
+ action.action === "remove" ? undefined : stagedByResource.get(action.desired.resourceId);
1333
+
1040
1334
  if (action.action !== "remove" && staged === undefined) {
1041
1335
  return yield* new InvalidProjectStateError({
1042
1336
  message: `missing staged output for ${action.desired.resourceId}`,
@@ -1080,6 +1374,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1080
1374
  const apply = Effect.gen(function* () {
1081
1375
  for (const replacement of replacements) {
1082
1376
  const observed = yield* observePath(replacement.destination);
1377
+
1083
1378
  if (
1084
1379
  replacement.expected !== undefined &&
1085
1380
  !observationsEqual(observed, replacement.expected)
@@ -1099,25 +1394,33 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
1099
1394
  }
1100
1395
  });
1101
1396
 
1102
- yield* Effect.uninterruptible(apply.pipe(
1103
- Effect.catchCause((applyCause) =>
1104
- rollback.pipe(
1105
- Effect.catchCause((rollbackCause) =>
1106
- Effect.failCause(Cause.combine(applyCause, rollbackCause)),
1397
+ yield* Effect.uninterruptible(
1398
+ apply.pipe(
1399
+ Effect.catchCause((applyCause) =>
1400
+ rollback.pipe(
1401
+ Effect.catchCause((rollbackCause) =>
1402
+ Effect.failCause(Cause.combine(applyCause, rollbackCause)),
1403
+ ),
1404
+ Effect.andThen(Effect.failCause(applyCause)),
1107
1405
  ),
1108
- Effect.andThen(Effect.failCause(applyCause)),
1109
1406
  ),
1110
1407
  ),
1111
- ));
1408
+ );
1112
1409
  });
1113
1410
 
1114
- export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (options: SyncOptions) {
1411
+ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1412
+ options: SyncOptions,
1413
+ ) {
1115
1414
  const plan = yield* planProjectSkills(options);
1415
+
1116
1416
  if (options.dryRun) yield* printSkillPlan(plan);
1117
1417
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1418
+
1118
1419
  if (conflicts.length > 0) {
1119
1420
  return yield* new PlanConflictError({
1120
- conflicts: conflicts.map((action) => action.action === "conflict" ? `${action.path}: ${action.reason}` : ""),
1421
+ conflicts: conflicts.map((action) =>
1422
+ action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1423
+ ),
1121
1424
  });
1122
1425
  }
1123
1426
  if (options.dryRun) return;
@@ -1128,6 +1431,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1128
1431
  actions: plan.actions,
1129
1432
  effectSource: plan.effectSource,
1130
1433
  effectTsgo: plan.effectTsgo,
1434
+ vitePlusHooks: plan.vitePlusHooks,
1131
1435
  nextLock: plan.nextLock,
1132
1436
  nextState: plan.nextState,
1133
1437
  });
@@ -1135,13 +1439,16 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1135
1439
  actions: replanned.actions,
1136
1440
  effectSource: replanned.effectSource,
1137
1441
  effectTsgo: replanned.effectTsgo,
1442
+ vitePlusHooks: replanned.vitePlusHooks,
1138
1443
  nextLock: replanned.nextLock,
1139
1444
  nextState: replanned.nextState,
1140
1445
  });
1446
+
1141
1447
  if (originalSignature !== nextSignature) {
1142
1448
  return yield* new ApplyRaceError({ path: "project state" });
1143
1449
  }
1144
1450
  const changes = plannedChangeCount(replanned);
1451
+
1145
1452
  yield* withSpinner(
1146
1453
  "Applying dev kit",
1147
1454
  Effect.gen(function* () {
@@ -1151,6 +1458,9 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1151
1458
  if (replanned.effectTsgo !== undefined) {
1152
1459
  yield* applyEffectTsgoPatchPlan(replanned.effectTsgo);
1153
1460
  }
1461
+ if (replanned.vitePlusHooks !== undefined) {
1462
+ yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
1463
+ }
1154
1464
  yield* applyPlannedSkillChanges(replanned);
1155
1465
  }),
1156
1466
  );