@danieljvdm/dev-kit 0.5.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 (56) hide show
  1. package/README.md +161 -68
  2. package/dev-kit.example.jsonc +8 -3
  3. package/package.json +19 -15
  4. package/schema/dev-kit.schema.json +52 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +74 -24
  8. package/skills/effect-atom-data-fetching/SKILL.md +40 -0
  9. package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
  10. package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
  11. package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
  12. package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
  13. package/skills/effect-atom-data-fetching/references/testing.md +63 -0
  14. package/skills/effect-ts/agents/openai.yaml +0 -1
  15. package/skills/effect-ts/references/audit-services.md +11 -11
  16. package/skills/effect-ts/references/guide-effect.md +56 -69
  17. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  18. package/skills/effect-ts/references/guide-layers.md +187 -215
  19. package/skills/effect-ts/references/guide-observability.md +91 -116
  20. package/skills/effect-ts/references/guide-retries.md +32 -44
  21. package/skills/effect-ts/references/guide-schedule.md +26 -40
  22. package/skills/effect-ts/references/guide-schema.md +50 -57
  23. package/skills/effect-ts/references/guide-sql.md +47 -50
  24. package/skills/effect-ts/references/guide-testing.md +96 -98
  25. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  26. package/skills/effect-ts/references/version-and-source.md +0 -1
  27. package/src/bin/dev-kit.ts +61 -28
  28. package/src/catalog-manager.ts +86 -34
  29. package/src/catalog.ts +72 -34
  30. package/src/cli-ui.ts +20 -16
  31. package/src/effect-source.ts +49 -19
  32. package/src/effect-tsgo.ts +66 -35
  33. package/src/gitignore.ts +19 -6
  34. package/src/index.ts +12 -0
  35. package/src/manifest.ts +51 -3
  36. package/src/node-symbolic-link.ts +3 -0
  37. package/src/oxlint-plugin-effect.js +3 -0
  38. package/src/oxlint-plugin-style.d.ts +8 -0
  39. package/src/oxlint-plugin-style.js +8 -0
  40. package/src/oxlint.js +14 -0
  41. package/src/oxlint.ts +14 -0
  42. package/src/package-skill-source.ts +190 -75
  43. package/src/path-digest.ts +37 -10
  44. package/src/project-package.ts +59 -0
  45. package/src/project-process-lock.ts +19 -12
  46. package/src/project-state.ts +29 -2
  47. package/src/skill-manager.ts +134 -55
  48. package/src/skill-selector.ts +8 -2
  49. package/src/source-manifest.ts +2 -6
  50. package/src/sync.ts +491 -121
  51. package/src/vendor.ts +112 -42
  52. package/src/vite-plus-hooks.ts +174 -0
  53. package/src/vite-plus-quality.ts +49 -0
  54. package/templates/AGENTS.md +9 -0
  55. package/templates/vite-plus/github-actions-check.yml +44 -0
  56. 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,40 +9,50 @@ 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 {
22
+ digestFileContent,
24
23
  digestSymlinkTarget,
25
24
  digestText,
26
25
  observePath,
27
26
  type ObservedPath,
28
27
  } from "./path-digest.ts";
29
- import { resolvePackageSkillSelector } from "./package-skill-source.ts";
30
- import { parseSkillSelector } from "./skill-selector.ts";
28
+ import { readDirectDependencyNames } from "./project-package.ts";
29
+ import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
31
30
  import {
32
31
  AppliedStateSchema,
33
32
  DevKitLockSchema,
34
33
  type AppliedState,
35
34
  type DevKitLock,
36
- type ManagedInstructionOutput,
35
+ type ManagedAgentInstructionsOutput,
36
+ type ManagedClaudeInstructionsOutput,
37
+ type ManagedGeneratedFileOutput,
37
38
  type ManagedOutput,
38
39
  type ManagedSkillOutput,
39
40
  type OwnershipReceipt,
40
41
  } from "./project-state.ts";
41
- import {
42
- acquireProjectProcessLock,
43
- PROJECT_PROCESS_LOCK_PATH,
44
- } from "./project-process-lock.ts";
45
- import { observeSymbolicLink } from "./node-symbolic-link.ts";
42
+ import { parseSkillSelector } from "./skill-selector.ts";
46
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";
47
56
 
48
57
  export type SyncOptions = {
49
58
  readonly manifestPath?: string;
@@ -76,12 +85,27 @@ type DesiredSkillOutput =
76
85
  readonly linkTarget: string;
77
86
  });
78
87
 
79
- type DesiredInstructionOutput = ManagedInstructionOutput & {
88
+ type DesiredAgentInstructionsOutput = ManagedAgentInstructionsOutput & {
89
+ readonly content: string;
90
+ readonly destination: string;
91
+ };
92
+
93
+ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
80
94
  readonly destination: string;
81
95
  readonly linkTarget: string;
82
96
  };
83
97
 
84
- type DesiredOutput = DesiredSkillOutput | DesiredInstructionOutput;
98
+ type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
99
+ readonly adoptIfExact: true;
100
+ readonly content: string;
101
+ readonly destination: string;
102
+ };
103
+
104
+ type DesiredOutput =
105
+ | DesiredSkillOutput
106
+ | DesiredAgentInstructionsOutput
107
+ | DesiredClaudeInstructionsOutput
108
+ | DesiredGeneratedFileOutput;
85
109
 
86
110
  type SkillPlanAction =
87
111
  | {
@@ -114,6 +138,7 @@ export type SkillPlan = {
114
138
  readonly actions: ReadonlyArray<SkillPlanAction>;
115
139
  readonly effectSource?: EffectSourcePlan;
116
140
  readonly effectTsgo?: EffectTsgoPatchPlan;
141
+ readonly vitePlusHooks?: VitePlusHooksPlan;
117
142
  readonly nextLock: DevKitLock;
118
143
  readonly nextState: AppliedState;
119
144
  readonly metadataChanged: boolean;
@@ -178,12 +203,12 @@ class LockedPlanMismatchError extends Schema.TaggedErrorClass<LockedPlanMismatch
178
203
  { message: Schema.String },
179
204
  ) {}
180
205
 
181
- class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()(
182
- "PlanConflictError",
183
- { conflicts: Schema.Array(Schema.String) },
184
- ) {
206
+ class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("PlanConflictError", {
207
+ conflicts: Schema.Array(Schema.String),
208
+ }) {
185
209
  override get message() {
186
210
  const heading = `plan has ${this.conflicts.length} conflict${this.conflicts.length === 1 ? "" : "s"}`;
211
+
187
212
  return `${heading}:\n${this.conflicts.map((conflict) => ` ${conflict}`).join("\n")}`;
188
213
  }
189
214
  }
@@ -196,14 +221,20 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
196
221
  }
197
222
  }
198
223
 
199
- const SKILL_FAMILIES: SkillCatalog = { effect: ["effect-ts"] };
224
+ const SKILL_FAMILIES: SkillCatalog = {
225
+ effect: ["effect-ts", "effect-atom-data-fetching"],
226
+ };
227
+
200
228
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
201
229
  const DEFAULT_LOCKFILE = "dev-kit.lock.json";
202
230
  const DEFAULT_STATE = ".dev-kit/state.json";
231
+ const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
232
+ const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
203
233
 
204
234
  const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
205
235
  const path = yield* Path.Path;
206
236
  const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url));
237
+
207
238
  return path.resolve(path.dirname(scriptPath), "..");
208
239
  });
209
240
 
@@ -219,9 +250,11 @@ const runCommand = Effect.fn("runCommand")(function* (
219
250
  child.exitCode,
220
251
  ]);
221
252
  const trimmed = output.trim();
253
+
222
254
  if (exitCode !== 0) {
223
255
  return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
224
256
  }
257
+
225
258
  return trimmed;
226
259
  });
227
260
 
@@ -237,12 +270,14 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
237
270
  const errors: Array<ParseError> = [];
238
271
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
239
272
  const first = errors[0];
273
+
240
274
  if (first !== undefined) {
241
275
  return yield* new StructuredFileError({
242
276
  path: filePath,
243
277
  message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
244
278
  });
245
279
  }
280
+
246
281
  return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
247
282
  Effect.mapError((cause) => new StructuredFileError({ path: filePath, message: cause.message })),
248
283
  );
@@ -250,10 +285,12 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
250
285
 
251
286
  const readManifest = Effect.fn("readManifest")(function* (manifestPath: string) {
252
287
  const fs = yield* FileSystem.FileSystem;
288
+
253
289
  if (!(yield* fs.exists(manifestPath))) {
254
290
  return yield* new ManifestNotFoundError({ path: manifestPath });
255
291
  }
256
292
  const raw = yield* fs.readFileString(manifestPath);
293
+
257
294
  return yield* parseStructuredFile(manifestPath, raw, DevKitManifestSchema);
258
295
  });
259
296
 
@@ -262,9 +299,11 @@ const readOptionalStructuredFile = Effect.fn("readOptionalStructuredFile")(funct
262
299
  schema: Schema.ConstraintDecoder<A>,
263
300
  ) {
264
301
  const fs = yield* FileSystem.FileSystem;
302
+
265
303
  if (!(yield* fs.exists(filePath))) {
266
304
  return undefined;
267
305
  }
306
+
268
307
  return yield* parseStructuredFile(filePath, yield* fs.readFileString(filePath), schema);
269
308
  });
270
309
 
@@ -276,6 +315,7 @@ const expandSelection = (
276
315
  ) => {
277
316
  const known = [...new Set([...Object.keys(skillFamilies), ...availableSkills])].sort();
278
317
  const selected = new Set<string>();
318
+
279
319
  for (const name of include) {
280
320
  if (skillFamilies[name]) {
281
321
  for (const skill of skillFamilies[name]) selected.add(skill);
@@ -287,9 +327,11 @@ const expandSelection = (
287
327
  }
288
328
  for (const name of exclude) {
289
329
  const family = skillFamilies[name];
330
+
290
331
  if (family) for (const skill of family) selected.delete(skill);
291
332
  else selected.delete(name);
292
333
  }
334
+
293
335
  return Effect.succeed([...selected].sort());
294
336
  };
295
337
 
@@ -301,20 +343,35 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
301
343
  candidate: string,
302
344
  ) {
303
345
  const path = yield* Path.Path;
346
+
304
347
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
305
- 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
+ });
306
352
  }
307
353
  const absolute = path.resolve(projectDir, candidate);
308
354
  const relative = path.relative(projectDir, absolute);
309
- if (relative.length === 0 || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
310
- 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
+ });
311
366
  }
312
367
 
313
368
  const segments = relative.split(path.sep);
314
369
  let ancestor = projectDir;
370
+
315
371
  for (const segment of segments.slice(0, -1)) {
316
372
  ancestor = path.join(ancestor, segment);
317
373
  const target = yield* observeSymbolicLink(ancestor);
374
+
318
375
  if (target.kind === "symlink") {
319
376
  return yield* new UnsafeManagedPathError({
320
377
  path: candidate,
@@ -322,6 +379,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
322
379
  });
323
380
  }
324
381
  }
382
+
325
383
  return { absolute, relative: portablePath(path, relative) } satisfies ManagedPath;
326
384
  });
327
385
 
@@ -334,12 +392,14 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
334
392
  outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
335
393
  ) {
336
394
  const outputPaths = new Set<string>();
395
+
337
396
  for (const output of outputs) {
338
397
  outputPaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
339
398
  }
340
399
 
341
400
  for (let index = 0; index < reserved.length; index += 1) {
342
401
  const current = reserved[index];
402
+
343
403
  if (current === undefined) continue;
344
404
  for (const other of reserved.slice(index + 1)) {
345
405
  if (pathsOverlap(current.path, other.path)) {
@@ -378,12 +438,17 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
378
438
  const ids = new Set<string>();
379
439
  const paths = new Set<string>();
380
440
  const sortedPaths: Array<string> = [];
441
+
381
442
  for (const output of outputs) {
382
443
  if (ids.has(output.resourceId)) {
383
- 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
+ });
384
447
  }
385
448
  if (paths.has(output.path)) {
386
- 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
+ });
387
452
  }
388
453
  ids.add(output.resourceId);
389
454
  paths.add(output.path);
@@ -393,9 +458,12 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
393
458
  for (let index = 1; index < sortedPaths.length; index += 1) {
394
459
  const previous = sortedPaths[index - 1];
395
460
  const current = sortedPaths[index];
461
+
396
462
  if (previous === undefined || current === undefined) continue;
397
463
  if (current.startsWith(`${previous}/`)) {
398
- 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
+ });
399
467
  }
400
468
  }
401
469
  });
@@ -405,13 +473,16 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
405
473
  outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
406
474
  ) {
407
475
  const uniquePaths = new Set<string>();
476
+
408
477
  for (const output of outputs) {
409
478
  uniquePaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
410
479
  }
411
480
  const sortedPaths = [...uniquePaths].sort();
481
+
412
482
  for (let index = 1; index < sortedPaths.length; index += 1) {
413
483
  const previous = sortedPaths[index - 1];
414
484
  const current = sortedPaths[index];
485
+
415
486
  if (previous === undefined || current === undefined) continue;
416
487
  if (current.startsWith(`${previous}/`)) {
417
488
  return yield* new InvalidProjectStateError({
@@ -421,7 +492,75 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
421
492
  }
422
493
  });
423
494
 
495
+ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
496
+ packageRoot: string,
497
+ projectDir: string,
498
+ sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
499
+ ) {
500
+ const fs = yield* FileSystem.FileSystem;
501
+ const path = yield* Path.Path;
502
+ const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
503
+
504
+ if ((yield* observePath(templatePath)).kind !== "file") {
505
+ return yield* new InvalidProjectStateError({
506
+ message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
507
+ });
508
+ }
509
+ const template = yield* fs.readFileString(templatePath);
510
+
511
+ if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
512
+ return yield* new InvalidProjectStateError({
513
+ message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
514
+ });
515
+ }
516
+
517
+ const devKitSkill = sourceBySkill.get("dev-kit");
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
+ );
528
+ const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
529
+
530
+ if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
531
+ const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
532
+
533
+ if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
534
+ return yield* new InvalidProjectStateError({
535
+ message:
536
+ "Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
537
+ });
538
+ }
539
+ sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
540
+ }
541
+
542
+ return `${sections.join("\n\n")}\n`;
543
+ });
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
+
424
562
  const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
563
+ packageRoot: string,
425
564
  projectDir: string,
426
565
  sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
427
566
  skills: ReadonlyArray<CatalogSkill>,
@@ -430,16 +569,36 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
430
569
  ) {
431
570
  const path = yield* Path.Path;
432
571
  const outputs: Array<DesiredOutput> = [];
572
+
573
+ if (setup.agentInstructions.enabled) {
574
+ const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
575
+ const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
576
+
577
+ outputs.push({
578
+ resourceId: "setup:agent-instructions",
579
+ path: managed.relative,
580
+ sourcePath: AGENT_INSTRUCTIONS_TEMPLATE,
581
+ mode: "copy",
582
+ kind: "file",
583
+ digest: yield* digestFileContent(content),
584
+ destination: managed.absolute,
585
+ content,
586
+ });
587
+ }
433
588
  if (setup.claudeInstructions.enabled) {
434
589
  const source = yield* resolveManagedPath(projectDir, "AGENTS.md");
435
- const sourceObservation = yield* observePath(source.absolute);
436
- if (sourceObservation.kind !== "file") {
590
+ const sourceObservation = setup.agentInstructions.enabled
591
+ ? undefined
592
+ : yield* observePath(source.absolute);
593
+
594
+ if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
437
595
  return yield* new InvalidProjectStateError({
438
596
  message: "Claude instructions source is not a regular file: AGENTS.md",
439
597
  });
440
598
  }
441
599
  const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
442
600
  const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
601
+
443
602
  outputs.push({
444
603
  resourceId: "setup:claude-instructions",
445
604
  path: managed.relative,
@@ -451,32 +610,71 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
451
610
  linkTarget,
452
611
  });
453
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
+ }
454
642
  const agentsTarget = targets.agents;
455
- const duplicateOutput = skills.find((skill, index) =>
456
- skills.findIndex((candidate) => candidate.name === skill.name) !== index
643
+ const duplicateOutput = skills.find(
644
+ (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
457
645
  );
646
+
458
647
  if (duplicateOutput !== undefined) {
459
648
  const selectors = skills
460
649
  .filter((skill) => skill.name === duplicateOutput.name)
461
650
  .map((skill) => skill.selector);
651
+
462
652
  return yield* new InvalidProjectStateError({
463
653
  message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
464
654
  });
465
655
  }
466
656
  for (const skill of skills) {
467
657
  const resolvedSource = sourceBySkill.get(skill.selector);
658
+
468
659
  if (resolvedSource === undefined) {
469
- 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
+ });
470
663
  }
471
664
  const source = resolvedSource.path;
472
665
  const sourceObservation = yield* observePath(source);
666
+
473
667
  if (sourceObservation.kind !== "directory") {
474
- 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
+ });
475
671
  }
476
672
  for (const targetName of ["agents", "claude", "opencode"] as const) {
477
673
  const target = targets[targetName];
674
+
478
675
  if (!target.enabled) continue;
479
676
  const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
677
+
480
678
  if (target.mode === "copy") {
481
679
  outputs.push({
482
680
  resourceId: `skill:${skill.selector}@${targetName}`,
@@ -494,10 +692,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
494
692
  }
495
693
  const linkSource =
496
694
  targetName === "agents" || !agentsTarget.enabled
497
- ? resolvedSource.linkPath ?? source
498
- : (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;
499
698
  const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
500
699
  const linkDigest = yield* digestSymlinkTarget(linkTarget);
700
+
501
701
  outputs.push({
502
702
  resourceId: `skill:${skill.selector}@${targetName}`,
503
703
  path: managed.relative,
@@ -514,6 +714,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
514
714
  }
515
715
  }
516
716
  yield* validateInventory(projectDir, outputs, "desired outputs");
717
+
517
718
  return outputs.sort((left, right) => left.path.localeCompare(right.path));
518
719
  });
519
720
 
@@ -531,7 +732,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
531
732
  if (currentState) yield* validateInventory(projectDir, currentState.outputs, "applied state");
532
733
  yield* validateCrossInventoryPaths(projectDir, [...desired, ...(currentState?.outputs ?? [])]);
533
734
  const lockById = new Map(currentLock?.outputs.map((output) => [output.resourceId, output]) ?? []);
534
- 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
+ );
535
738
  const desiredKeys = new Set(desired.map((output) => `${output.resourceId}\0${output.path}`));
536
739
  const actions: Array<SkillPlanAction> = [];
537
740
 
@@ -545,10 +748,14 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
545
748
  if (observed.kind === "missing") {
546
749
  actions.push({ action: "create", desired: output, observed });
547
750
  } else if (observed.kind === output.kind && observed.digest === output.digest) {
548
- if (sameReceipt || adoptable) {
751
+ if (sameReceipt || adoptable || ("adoptIfExact" in output && output.adoptIfExact)) {
549
752
  actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
550
753
  } else {
551
- 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
+ });
552
759
  }
553
760
  } else if (
554
761
  sameReceipt &&
@@ -560,7 +767,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
560
767
  actions.push({
561
768
  action: "conflict",
562
769
  path: output.path,
563
- 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",
564
773
  });
565
774
  }
566
775
  }
@@ -569,11 +778,21 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
569
778
  if (desiredKeys.has(`${receipt.resourceId}\0${receipt.path}`)) continue;
570
779
  const managed = yield* resolveManagedPath(projectDir, receipt.path);
571
780
  const observed = yield* observePath(managed.absolute);
781
+
572
782
  if (observed.kind === "missing") continue;
573
783
  if (observed.kind === receipt.kind && observed.digest === receipt.digest) {
574
- 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
+ });
575
790
  } else {
576
- 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
+ });
577
796
  }
578
797
  }
579
798
 
@@ -588,11 +807,26 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
588
807
  digest,
589
808
  })),
590
809
  };
591
- return { actions: actions.sort((left, right) => {
592
- const leftPath = left.action === "remove" ? left.previous.path : left.action === "conflict" ? left.path : left.desired.path;
593
- const rightPath = right.action === "remove" ? right.previous.path : right.action === "conflict" ? right.path : right.desired.path;
594
- return leftPath.localeCompare(rightPath);
595
- }), 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
+ };
596
830
  });
597
831
 
598
832
  const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
@@ -602,6 +836,7 @@ const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
602
836
  current.outputs.length === next.outputs.length &&
603
837
  current.outputs.every((output, index) => {
604
838
  const nextOutput = next.outputs[index];
839
+
605
840
  return nextOutput !== undefined && outputIdentity(output) === outputIdentity(nextOutput);
606
841
  });
607
842
 
@@ -617,12 +852,32 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
617
852
  ),
618
853
  );
619
854
  const projectDir = yield* fs.realPath(discoveredRoot);
620
- const manifestManaged = yield* resolveManagedPath(projectDir, options.manifestPath ?? DEFAULT_MANIFEST);
621
- 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
+ );
622
863
  const stateManaged = yield* resolveManagedPath(projectDir, options.statePath ?? DEFAULT_STATE);
623
864
  const processLockManaged = yield* resolveManagedPath(projectDir, PROJECT_PROCESS_LOCK_PATH);
624
865
  const packageRoot = yield* resolvePackageRoot();
625
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
+ }
626
881
  const effectSource = manifest.setup.effectSource.enabled
627
882
  ? yield* planEffectSource({
628
883
  packageName: manifest.setup.effectSource.packageName,
@@ -638,19 +893,35 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
638
893
  typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
639
894
  })
640
895
  : undefined;
896
+ const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
897
+ ? yield* planVitePlusHooks(projectDir)
898
+ : undefined;
641
899
  const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
642
900
  const availableSkills = catalog.skills.map((skill) => skill.selector);
643
901
  const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
902
+
644
903
  for (const [family, familySkills] of Object.entries(skillFamilies)) {
645
904
  if (availableSkills.includes(family)) {
646
- 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
+ });
647
909
  }
648
910
  const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
911
+
649
912
  if (missing.length > 0) {
650
- 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
+ });
651
917
  }
652
918
  }
653
- 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
+ );
654
925
  const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
655
926
  const sourceBySkill = yield* withSpinner(
656
927
  "Resolving selected skills",
@@ -663,8 +934,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
663
934
  ),
664
935
  );
665
936
  const selectedSkills: Array<CatalogSkill> = [];
937
+
666
938
  for (const selector of selectedSelectors) {
667
939
  const catalogSkill = catalogBySelector.get(selector);
940
+
668
941
  if (catalogSkill === undefined) {
669
942
  return yield* new InvalidProjectStateError({
670
943
  message: `selected skill is unavailable: ${selector}`,
@@ -673,6 +946,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
673
946
  selectedSkills.push(catalogSkill);
674
947
  }
675
948
  const desired = yield* buildDesiredOutputs(
949
+ packageRoot,
676
950
  projectDir,
677
951
  sourceBySkill,
678
952
  selectedSkills,
@@ -705,27 +979,49 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
705
979
  },
706
980
  }),
707
981
  },
708
- outputs: desired.map((output): ManagedOutput =>
709
- "skill" in output
710
- ? {
711
- resourceId: output.resourceId,
712
- path: output.path,
713
- skill: output.skill,
714
- target: output.target,
715
- mode: output.mode,
716
- kind: output.kind,
717
- digest: output.digest,
718
- ...(output.catalog ? { catalog: output.catalog } : {}),
719
- }
720
- : {
721
- resourceId: output.resourceId,
722
- path: output.path,
723
- sourcePath: output.sourcePath,
724
- mode: output.mode,
725
- kind: output.kind,
726
- digest: output.digest,
727
- },
728
- ),
982
+ outputs: desired.map((output): ManagedOutput => {
983
+ if ("skill" in output) {
984
+ return {
985
+ resourceId: output.resourceId,
986
+ path: output.path,
987
+ skill: output.skill,
988
+ target: output.target,
989
+ mode: output.mode,
990
+ kind: output.kind,
991
+ digest: output.digest,
992
+ ...(output.catalog ? { catalog: output.catalog } : {}),
993
+ };
994
+ }
995
+ if (output.resourceId === "setup:agent-instructions") {
996
+ return {
997
+ resourceId: output.resourceId,
998
+ path: output.path,
999
+ sourcePath: output.sourcePath,
1000
+ mode: output.mode,
1001
+ kind: output.kind,
1002
+ digest: output.digest,
1003
+ };
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
+
1016
+ return {
1017
+ resourceId: output.resourceId,
1018
+ path: output.path,
1019
+ sourcePath: output.sourcePath,
1020
+ mode: output.mode,
1021
+ kind: output.kind,
1022
+ digest: output.digest,
1023
+ };
1024
+ }),
729
1025
  };
730
1026
  const reservedPaths = [
731
1027
  { label: "manifest", path: manifestManaged.relative },
@@ -736,26 +1032,45 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
736
1032
  ? []
737
1033
  : [{ label: "Effect source checkout", path: effectSource.path }]),
738
1034
  ];
1035
+
739
1036
  yield* validateReservedPaths(projectDir, reservedPaths, desired);
740
1037
  const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
741
1038
  const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
742
- yield* validateReservedPaths(
743
- projectDir,
744
- reservedPaths,
745
- [
746
- ...(currentLock?.outputs ?? []),
747
- ...(currentState?.outputs ?? []),
748
- ],
749
- );
1039
+
1040
+ if (
1041
+ manifest.setup.claudeInstructions.enabled &&
1042
+ !manifest.setup.agentInstructions.enabled &&
1043
+ currentState?.outputs.some((output) => output.resourceId === "setup:agent-instructions")
1044
+ ) {
1045
+ return yield* new InvalidProjectStateError({
1046
+ message:
1047
+ "cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
1048
+ });
1049
+ }
1050
+ yield* validateReservedPaths(projectDir, reservedPaths, [
1051
+ ...(currentLock?.outputs ?? []),
1052
+ ...(currentState?.outputs ?? []),
1053
+ ]);
750
1054
  if (options.locked) {
751
1055
  if (!currentLock) {
752
- 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
+ });
753
1059
  }
754
1060
  if (!lockedPlanMatches(currentLock, nextLock)) {
755
- 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
+ });
756
1064
  }
757
1065
  }
758
- 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
+
759
1074
  return {
760
1075
  projectDir,
761
1076
  lockfilePath: lockManaged.absolute,
@@ -763,6 +1078,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
763
1078
  actions: planned.actions,
764
1079
  ...(effectSource === undefined ? {} : { effectSource }),
765
1080
  ...(effectTsgo === undefined ? {} : { effectTsgo }),
1081
+ ...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
766
1082
  nextLock,
767
1083
  nextState: planned.nextState,
768
1084
  metadataChanged:
@@ -773,46 +1089,51 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
773
1089
 
774
1090
  const formatAction = (action: SkillPlanAction): string => {
775
1091
  if (action.action === "conflict") return `! ${action.path}: ${action.reason}`;
776
- if (action.action === "remove") return `− ${action.previous.resourceId} → ${action.previous.path}`;
1092
+ if (action.action === "remove")
1093
+ return `− ${action.previous.resourceId} → ${action.previous.path}`;
777
1094
  const verb = action.desired.mode === "copy" ? "copy" : "link";
778
1095
  const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
779
1096
  const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
780
- const source = "skill" in action.desired
781
- ? action.desired.skill
782
- : action.desired.sourcePath;
1097
+ const source = "skill" in action.desired ? action.desired.skill : action.desired.sourcePath;
1098
+
783
1099
  return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
784
1100
  };
785
1101
 
786
1102
  const operationalChangeCount = (plan: SkillPlan): number =>
787
1103
  plan.actions.filter((action) => action.action !== "unchanged").length +
788
1104
  (plan.effectSource?.action === "sync" ? 1 : 0) +
789
- (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0);
1105
+ (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
1106
+ (plan.vitePlusHooks?.action === "configure" ? 1 : 0);
790
1107
 
791
1108
  const plannedChangeCount = (plan: SkillPlan): number => {
792
1109
  const operational = operationalChangeCount(plan);
1110
+
793
1111
  return operational === 0 && plan.metadataChanged ? 1 : operational;
794
1112
  };
795
1113
 
796
1114
  export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: SkillPlan) {
797
1115
  const changes = plannedChangeCount(plan);
1116
+
798
1117
  if (changes === 0) {
799
1118
  yield* printStatus("success", "Already up to date");
1119
+
800
1120
  return;
801
1121
  }
802
1122
  yield* printStatus("plan", `${changes} change${changes === 1 ? "" : "s"} planned`);
803
1123
  for (const action of plan.actions) {
804
- if (action.action !== "unchanged") yield* printDetail(formatAction(action));
1124
+ if (action.action !== "unchanged" || action.adopted) yield* printDetail(formatAction(action));
805
1125
  }
806
1126
  if (plan.effectSource?.action === "sync") {
807
- yield* printDetail(
808
- `+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`,
809
- );
1127
+ yield* printDetail(`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`);
810
1128
  }
811
1129
  if (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched) {
812
1130
  yield* printDetail(
813
1131
  `+ TypeScript patch @effect/tsgo@${plan.effectTsgo.effectTsgoVersion} → ${plan.effectTsgo.typescriptPackage}@${plan.effectTsgo.typescriptVersion}`,
814
1132
  );
815
1133
  }
1134
+ if (plan.vitePlusHooks?.action === "configure") {
1135
+ yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
1136
+ }
816
1137
  if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
817
1138
  yield* printDetail("+ Dev kit metadata");
818
1139
  }
@@ -822,22 +1143,24 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
822
1143
  left.kind === right.kind &&
823
1144
  (left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
824
1145
 
825
- const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
826
- root: string,
827
- ) {
1146
+ const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (root: string) {
828
1147
  const fs = yield* FileSystem.FileSystem;
829
1148
  const path = yield* Path.Path;
830
1149
  const pending = [root];
1150
+
831
1151
  while (pending.length > 0) {
832
1152
  const current = pending.pop();
1153
+
833
1154
  if (current === undefined) continue;
834
1155
  if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
835
1156
  const info = yield* fs.stat(current);
1157
+
836
1158
  if (info.type !== "Directory") continue;
837
1159
  for (const entry of yield* fs.readDirectory(current)) {
838
1160
  pending.push(path.join(current, entry));
839
1161
  }
840
1162
  }
1163
+
841
1164
  return undefined;
842
1165
  });
843
1166
 
@@ -845,18 +1168,26 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
845
1168
  plan: SkillPlan,
846
1169
  ) {
847
1170
  const verified = new Set<string>();
1171
+
848
1172
  for (const action of plan.actions) {
849
1173
  if (action.action === "remove" || action.action === "conflict") continue;
850
1174
  if (!("skill" in action.desired)) continue;
851
1175
  const catalog = action.desired.catalog;
1176
+
852
1177
  if (catalog === undefined || !("package" in catalog)) continue;
853
1178
  const selector = `${catalog.package}#${catalog.skill}`;
854
1179
  const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
1180
+
855
1181
  if (verified.has(key)) continue;
856
1182
  const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
857
1183
  const observation = yield* observePath(resolved.path);
858
- if (resolved.path !== action.desired.source || resolved.version !== catalog.version ||
859
- 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
+ ) {
860
1191
  return yield* new ApplyRaceError({ path: action.desired.source });
861
1192
  }
862
1193
  verified.add(key);
@@ -865,9 +1196,12 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
865
1196
 
866
1197
  const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
867
1198
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1199
+
868
1200
  if (conflicts.length > 0) {
869
1201
  return yield* new PlanConflictError({
870
- 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
+ ),
871
1205
  });
872
1206
  }
873
1207
 
@@ -877,10 +1211,15 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
877
1211
  (action): action is Exclude<SkillPlanAction, { readonly action: "unchanged" | "conflict" }> =>
878
1212
  action.action === "create" || action.action === "update" || action.action === "remove",
879
1213
  );
1214
+
880
1215
  for (const action of mutating) {
881
- const destination = action.action === "remove" ? action.destination : action.desired.destination;
1216
+ const destination =
1217
+ action.action === "remove" ? action.destination : action.desired.destination;
1218
+
882
1219
  if (!observationsEqual(yield* observePath(destination), action.observed)) {
883
- 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
+ });
884
1223
  }
885
1224
  }
886
1225
 
@@ -888,29 +1227,42 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
888
1227
  return;
889
1228
  }
890
1229
 
891
- 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
+ });
892
1234
  const stageDir = path.join(tempDir, "stage");
893
1235
  const backupDir = path.join(tempDir, "backup");
894
1236
  const stagedByResource = new Map<string, string>();
895
1237
  let stageIndex = 0;
1238
+
896
1239
  for (const action of mutating) {
897
1240
  if (action.action === "remove") continue;
898
1241
  const staged = path.join(stageDir, String(stageIndex++));
1242
+
899
1243
  yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
900
1244
  if (action.desired.mode === "copy") {
901
- yield* fs.copy(action.desired.source, staged, { overwrite: true });
902
- const symbolicLink = yield* findNestedSymbolicLink(staged);
903
- if (symbolicLink !== undefined) {
904
- return yield* new InvalidProjectStateError({
905
- message: `staged skill contains a symlink: ${action.desired.path}`,
906
- });
1245
+ if (action.desired.kind === "file") {
1246
+ yield* fs.writeFileString(staged, action.desired.content, { mode: 0o644 });
1247
+ } else {
1248
+ yield* fs.copy(action.desired.source, staged, { overwrite: true });
1249
+ const symbolicLink = yield* findNestedSymbolicLink(staged);
1250
+
1251
+ if (symbolicLink !== undefined) {
1252
+ return yield* new InvalidProjectStateError({
1253
+ message: `staged skill contains a symlink: ${action.desired.path}`,
1254
+ });
1255
+ }
907
1256
  }
908
1257
  } else {
909
1258
  yield* fs.symlink(action.desired.linkTarget, staged);
910
1259
  }
911
1260
  const observation = yield* observePath(staged);
1261
+
912
1262
  if (observation.kind !== action.desired.kind || observation.digest !== action.desired.digest) {
913
- 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
+ });
914
1266
  }
915
1267
  stagedByResource.set(action.desired.resourceId, staged);
916
1268
  }
@@ -919,6 +1271,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
919
1271
 
920
1272
  const stagedLock = path.join(tempDir, "next-lock.json");
921
1273
  const stagedState = path.join(tempDir, "next-state.json");
1274
+
922
1275
  yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
923
1276
  yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
924
1277
 
@@ -931,10 +1284,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
931
1284
  };
932
1285
  const replacements: Array<Replacement> = [];
933
1286
  let replacementIndex = 0;
1287
+
934
1288
  for (const action of mutating) {
935
- const staged = action.action === "remove"
936
- ? undefined
937
- : stagedByResource.get(action.desired.resourceId);
1289
+ const staged =
1290
+ action.action === "remove" ? undefined : stagedByResource.get(action.desired.resourceId);
1291
+
938
1292
  if (action.action !== "remove" && staged === undefined) {
939
1293
  return yield* new InvalidProjectStateError({
940
1294
  message: `missing staged output for ${action.desired.resourceId}`,
@@ -978,6 +1332,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
978
1332
  const apply = Effect.gen(function* () {
979
1333
  for (const replacement of replacements) {
980
1334
  const observed = yield* observePath(replacement.destination);
1335
+
981
1336
  if (
982
1337
  replacement.expected !== undefined &&
983
1338
  !observationsEqual(observed, replacement.expected)
@@ -997,25 +1352,33 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
997
1352
  }
998
1353
  });
999
1354
 
1000
- yield* Effect.uninterruptible(apply.pipe(
1001
- Effect.catchCause((applyCause) =>
1002
- rollback.pipe(
1003
- Effect.catchCause((rollbackCause) =>
1004
- 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)),
1005
1363
  ),
1006
- Effect.andThen(Effect.failCause(applyCause)),
1007
1364
  ),
1008
1365
  ),
1009
- ));
1366
+ );
1010
1367
  });
1011
1368
 
1012
- export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (options: SyncOptions) {
1369
+ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1370
+ options: SyncOptions,
1371
+ ) {
1013
1372
  const plan = yield* planProjectSkills(options);
1373
+
1014
1374
  if (options.dryRun) yield* printSkillPlan(plan);
1015
1375
  const conflicts = plan.actions.filter((action) => action.action === "conflict");
1376
+
1016
1377
  if (conflicts.length > 0) {
1017
1378
  return yield* new PlanConflictError({
1018
- 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
+ ),
1019
1382
  });
1020
1383
  }
1021
1384
  if (options.dryRun) return;
@@ -1026,6 +1389,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1026
1389
  actions: plan.actions,
1027
1390
  effectSource: plan.effectSource,
1028
1391
  effectTsgo: plan.effectTsgo,
1392
+ vitePlusHooks: plan.vitePlusHooks,
1029
1393
  nextLock: plan.nextLock,
1030
1394
  nextState: plan.nextState,
1031
1395
  });
@@ -1033,13 +1397,16 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1033
1397
  actions: replanned.actions,
1034
1398
  effectSource: replanned.effectSource,
1035
1399
  effectTsgo: replanned.effectTsgo,
1400
+ vitePlusHooks: replanned.vitePlusHooks,
1036
1401
  nextLock: replanned.nextLock,
1037
1402
  nextState: replanned.nextState,
1038
1403
  });
1404
+
1039
1405
  if (originalSignature !== nextSignature) {
1040
1406
  return yield* new ApplyRaceError({ path: "project state" });
1041
1407
  }
1042
1408
  const changes = plannedChangeCount(replanned);
1409
+
1043
1410
  yield* withSpinner(
1044
1411
  "Applying dev kit",
1045
1412
  Effect.gen(function* () {
@@ -1049,6 +1416,9 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
1049
1416
  if (replanned.effectTsgo !== undefined) {
1050
1417
  yield* applyEffectTsgoPatchPlan(replanned.effectTsgo);
1051
1418
  }
1419
+ if (replanned.vitePlusHooks !== undefined) {
1420
+ yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
1421
+ }
1052
1422
  yield* applyPlannedSkillChanges(replanned);
1053
1423
  }),
1054
1424
  );