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