@danieljvdm/dev-kit 0.14.0 → 0.16.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 (35) hide show
  1. package/README.md +155 -99
  2. package/dev-kit.example.jsonc +4 -3
  3. package/package.json +1 -1
  4. package/schema/dev-kit.schema.json +19 -42
  5. package/skills/build-effect-apis/SKILL.md +13 -31
  6. package/skills/build-effect-apis/references/verification.md +3 -3
  7. package/skills/dev-kit/SKILL.md +115 -222
  8. package/skills/effect-atom-state/SKILL.md +97 -0
  9. package/skills/effect-atom-state/agents/openai.yaml +4 -0
  10. package/skills/effect-atom-state/references/effect-atom-workflows.md +180 -0
  11. package/skills/open-pull-request/SKILL.md +62 -23
  12. package/src/bin/dev-kit.ts +21 -0
  13. package/src/catalog.ts +39 -15
  14. package/src/effect-source.ts +70 -4
  15. package/src/global-cache.ts +304 -0
  16. package/src/index.ts +6 -6
  17. package/src/manifest.ts +28 -29
  18. package/src/oxlint.js +23 -0
  19. package/src/oxlint.ts +37 -1
  20. package/src/path-digest.ts +0 -13
  21. package/src/project-package.ts +127 -12
  22. package/src/project-state.ts +3 -0
  23. package/src/scaffold.ts +79 -0
  24. package/src/sync.ts +100 -173
  25. package/src/vite-plus-workflow.ts +82 -0
  26. package/src/vite-plus.js +8 -1
  27. package/src/vite-plus.ts +15 -1
  28. package/src/worktrunk-config.ts +88 -0
  29. package/templates/vite-plus/github-actions-check.yml +0 -2
  30. package/templates/worktrunk/wt.toml +27 -0
  31. package/src/vite-plus-quality.ts +0 -148
  32. /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-client.md +0 -0
  33. /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-lifecycle.md +0 -0
  34. /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-testing.md +0 -0
  35. /package/skills/{build-effect-apis → effect-atom-state}/references/tanstack-start.md +0 -0
@@ -0,0 +1,79 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+
3
+ import { observeSymbolicLink } from "./node-symbolic-link.ts";
4
+
5
+ export class ScaffoldTemplateError extends Schema.TaggedError<ScaffoldTemplateError>()(
6
+ "ScaffoldTemplateError",
7
+ { message: Schema.String },
8
+ ) {}
9
+
10
+ export type ScaffoldPlan = {
11
+ readonly action: "scaffold" | "unchanged";
12
+ readonly path: string;
13
+ readonly destination: string;
14
+ readonly content?: string;
15
+ };
16
+
17
+ export const replaceUniqueTemplateMarker = (
18
+ template: string,
19
+ marker: string,
20
+ replacement: string,
21
+ ): string => {
22
+ const parts = template.split(marker);
23
+
24
+ if (parts.length !== 2) {
25
+ throw new Error(`expected exactly one generated template marker: ${marker}`);
26
+ }
27
+
28
+ return `${parts[0]}${replacement}${parts[1]}`;
29
+ };
30
+
31
+ export const readScaffoldTemplate = Effect.fn("readScaffoldTemplate")(function* (
32
+ packageRoot: string,
33
+ templatePath: string,
34
+ ) {
35
+ const fs = yield* FileSystem.FileSystem;
36
+ const path = yield* Path.Path;
37
+ const absolute = path.join(packageRoot, templatePath);
38
+
39
+ if (!(yield* fs.exists(absolute))) {
40
+ return yield* ScaffoldTemplateError.make({
41
+ message: `dev-kit scaffold template is missing: ${templatePath}`,
42
+ });
43
+ }
44
+
45
+ return yield* fs.readFileString(absolute);
46
+ });
47
+
48
+ // Scaffolds are created once and repository-owned afterwards: an existing
49
+ // destination is never read, compared, updated, or removed, and `content` is
50
+ // only computed (and validated) when the file is actually being created.
51
+ export const planScaffold = Effect.fn("planScaffold")(function* <E, R>(options: {
52
+ readonly projectDir: string;
53
+ readonly path: string;
54
+ readonly content: Effect.Effect<string, E, R>;
55
+ }) {
56
+ const path = yield* Path.Path;
57
+ const destination = path.join(options.projectDir, ...options.path.split("/"));
58
+ const observed = yield* observeSymbolicLink(destination);
59
+
60
+ if (observed.kind !== "missing") {
61
+ return { action: "unchanged", path: options.path, destination } satisfies ScaffoldPlan;
62
+ }
63
+
64
+ return {
65
+ action: "scaffold",
66
+ path: options.path,
67
+ destination,
68
+ content: yield* options.content,
69
+ } satisfies ScaffoldPlan;
70
+ });
71
+
72
+ export const applyScaffoldPlan = Effect.fn("applyScaffoldPlan")(function* (plan: ScaffoldPlan) {
73
+ if (plan.action !== "scaffold" || plan.content === undefined) return;
74
+ const fs = yield* FileSystem.FileSystem;
75
+ const path = yield* Path.Path;
76
+
77
+ yield* fs.makeDirectory(path.dirname(plan.destination), { recursive: true });
78
+ yield* fs.writeFileString(plan.destination, plan.content);
79
+ });
package/src/sync.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  planEffectTsgoPatch,
16
16
  type EffectTsgoPatchPlan,
17
17
  } from "./effect-tsgo.ts";
18
+ import { maybePruneGlobalCache } from "./global-cache.ts";
18
19
  import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
19
20
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
20
21
  import { resolvePackageSkillSelector } from "./package-skill-source.ts";
@@ -23,10 +24,16 @@ import {
23
24
  digestSymlinkTarget,
24
25
  digestText,
25
26
  observePath,
26
- observePathWithRawModes,
27
27
  type ObservedPath,
28
28
  } from "./path-digest.ts";
29
- import { readDirectDependencyNames, readProjectPackage } from "./project-package.ts";
29
+ import {
30
+ detectPackageManager,
31
+ PACKAGE_MANAGER_COMMANDS,
32
+ readDirectDependencyNames,
33
+ readWorkspaceDependencyNames,
34
+ readProjectPackage,
35
+ type PackageManagerName,
36
+ } from "./project-package.ts";
30
37
  import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
31
38
  import {
32
39
  AppliedStateSchema,
@@ -38,11 +45,11 @@ import {
38
45
  type DevKitLock,
39
46
  type ManagedAgentInstructionsOutput,
40
47
  type ManagedClaudeInstructionsOutput,
41
- type ManagedGeneratedFileOutput,
42
48
  type ManagedOutput,
43
49
  type ManagedSkillOutput,
44
50
  type OwnershipReceipt,
45
51
  } from "./project-state.ts";
52
+ import { applyScaffoldPlan, type ScaffoldPlan } from "./scaffold.ts";
46
53
  import { parseSkillSelector } from "./skill-selector.ts";
47
54
  import { DEV_KIT_VERSION } from "./tool-metadata.ts";
48
55
  import {
@@ -50,12 +57,8 @@ import {
50
57
  planVitePlusHooks,
51
58
  type VitePlusHooksPlan,
52
59
  } from "./vite-plus-hooks.ts";
53
- import {
54
- renderVitePlusWorkflowTemplate,
55
- validateVitePlusQualitySupport,
56
- VITE_PLUS_GITHUB_ACTIONS_PATH,
57
- VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
58
- } from "./vite-plus-quality.ts";
60
+ import { planVitePlusWorkflow } from "./vite-plus-workflow.ts";
61
+ import { planWorktrunkConfig } from "./worktrunk-config.ts";
59
62
 
60
63
  export type SyncOptions = {
61
64
  readonly manifestPath?: string;
@@ -98,17 +101,10 @@ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
98
101
  readonly linkTarget: string;
99
102
  };
100
103
 
101
- type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
102
- readonly adoptIfExact: true;
103
- readonly content: string;
104
- readonly destination: string;
105
- };
106
-
107
104
  type DesiredOutput =
108
105
  | DesiredSkillOutput
109
106
  | DesiredAgentInstructionsOutput
110
- | DesiredClaudeInstructionsOutput
111
- | DesiredGeneratedFileOutput;
107
+ | DesiredClaudeInstructionsOutput;
112
108
 
113
109
  type SkillPlanAction =
114
110
  | {
@@ -144,6 +140,8 @@ export type SkillPlan = {
144
140
  readonly effectSource?: EffectSourcePlan;
145
141
  readonly effectTsgo?: EffectTsgoPatchPlan;
146
142
  readonly vitePlusHooks?: VitePlusHooksPlan;
143
+ readonly vitePlusWorkflow?: ScaffoldPlan;
144
+ readonly worktrunkConfig?: ScaffoldPlan;
147
145
  readonly nextLock: DevKitLock;
148
146
  readonly nextState: AppliedState;
149
147
  readonly metadataChanged: boolean;
@@ -270,7 +268,13 @@ const encodePlanSnapshotJson = Schema.encodeSync(Schema.fromJsonString(Schema.Un
270
268
  const encodeAppliedStatePrettyJson = Schema.encodeSync(fromJsonString(AppliedStateSchema, 2));
271
269
 
272
270
  const SKILL_FAMILIES: SkillCatalog = {
273
- effect: ["effect-ts", "effect-architecture-audit", "build-effect-apis", "build-effect-clis"],
271
+ effect: [
272
+ "effect-ts",
273
+ "effect-architecture-audit",
274
+ "build-effect-apis",
275
+ "effect-atom-state",
276
+ "build-effect-clis",
277
+ ],
274
278
  };
275
279
 
276
280
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
@@ -436,57 +440,6 @@ const renderVitePlusCommandPolicy = (
436
440
  ].join("\n");
437
441
  };
438
442
 
439
- const PACKAGE_MANAGER_COMMANDS = {
440
- bun: { install: "bun install", label: "Bun" },
441
- npm: { install: "npm install", label: "npm" },
442
- pnpm: { install: "pnpm install", label: "pnpm" },
443
- yarn: { install: "yarn install", label: "Yarn" },
444
- } as const;
445
-
446
- type PackageManagerName = keyof typeof PACKAGE_MANAGER_COMMANDS;
447
-
448
- const packageManagerName = (declaration: string | undefined): PackageManagerName | undefined => {
449
- const name = declaration?.split("@", 1)[0];
450
-
451
- return name !== undefined && name in PACKAGE_MANAGER_COMMANDS
452
- ? (name as PackageManagerName)
453
- : undefined;
454
- };
455
-
456
- const detectPackageManager = Effect.fn("detectPackageManager")(function* (
457
- projectDir: string,
458
- declaration: string | undefined,
459
- ) {
460
- const declared = packageManagerName(declaration);
461
-
462
- if (declared !== undefined || declaration !== undefined) return declared;
463
- const fs = yield* FileSystem.FileSystem;
464
- const path = yield* Path.Path;
465
- const lockfiles: ReadonlyArray<readonly [PackageManagerName, ReadonlyArray<string>]> = [
466
- ["bun", ["bun.lock", "bun.lockb"]],
467
- ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
468
- ["pnpm", ["pnpm-lock.yaml"]],
469
- ["yarn", ["yarn.lock"]],
470
- ];
471
- const detected: Array<PackageManagerName> = [];
472
-
473
- for (const [manager, files] of lockfiles) {
474
- let found = false;
475
-
476
- for (const file of files) {
477
- if (yield* fs.exists(path.join(projectDir, file))) {
478
- found = true;
479
- break;
480
- }
481
- }
482
- if (found) {
483
- detected.push(manager);
484
- }
485
- }
486
-
487
- return detected.length === 1 ? detected[0] : undefined;
488
- });
489
-
490
443
  const renderPackageScriptCommandPolicy = (
491
444
  manager: PackageManagerName | undefined,
492
445
  scripts: Readonly<Record<string, string>>,
@@ -746,16 +699,6 @@ const outputOwnershipIdentity = (output: ManagedOutput) =>
746
699
  },
747
700
  );
748
701
 
749
- const usesRawFileModeDigests = (toolVersion: string): boolean => {
750
- const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
751
-
752
- if (match === null) return false;
753
- const major = Number(match[1]);
754
- const minor = Number(match[2]);
755
-
756
- return major === 0 && minor <= 6;
757
- };
758
-
759
702
  const validateInventory = Effect.fn("validateManagedInventory")(function* (
760
703
  projectDir: string,
761
704
  outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
@@ -870,7 +813,7 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
870
813
  );
871
814
  const directDependencyNames = yield* readDirectDependencyNames(projectDir);
872
815
  const usesVitePlus = directDependencyNames.includes("vite-plus");
873
- const effectInstructions =
816
+ const effectGuideInstructions =
874
817
  directDependencyNames.includes("effect") &&
875
818
  (yield* observePath(path.join(projectDir, "node_modules", "effect", "AGENTS.md"))).kind ===
876
819
  "file"
@@ -886,6 +829,20 @@ guide doesn't cover, search through the source code in \`node_modules/effect/src
886
829
 
887
830
  `
888
831
  : "";
832
+ const atomBoundaryInstructions = (yield* readWorkspaceDependencyNames(projectDir)).includes(
833
+ "@effect/atom-react",
834
+ )
835
+ ? `# Effect Atom client boundary
836
+
837
+ This repository consumes APIs through Effect Atom clients (\`@effect/atom-react\`).
838
+ Keep business logic in Effect: compose multi-step client workflows as atoms,
839
+ declare cross-query invalidation as reactivity keys on mutations, and keep
840
+ promise-mode dispatches at the React boundary logic-free — no \`.then\` chains
841
+ in components or routes.
842
+
843
+ `
844
+ : "";
845
+ const effectInstructions = `${effectGuideInstructions}${atomBoundaryInstructions}`;
889
846
  const projectPackage = yield* readProjectPackage(projectDir).pipe(
890
847
  Effect.catchTag("ProjectPackageError", (error) =>
891
848
  error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
@@ -904,23 +861,6 @@ guide doesn't cover, search through the source code in \`node_modules/effect/src
904
861
  return `${devKitInstructions}\n`;
905
862
  });
906
863
 
907
- const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(function* (
908
- packageRoot: string,
909
- sourcePath: string,
910
- ) {
911
- const fs = yield* FileSystem.FileSystem;
912
- const path = yield* Path.Path;
913
- const templatePath = path.join(packageRoot, sourcePath);
914
-
915
- if ((yield* observePath(templatePath)).kind !== "file") {
916
- return yield* InvalidProjectStateError.make({
917
- message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
918
- });
919
- }
920
-
921
- return yield* fs.readFileString(templatePath);
922
- });
923
-
924
864
  const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
925
865
  packageRoot: string,
926
866
  projectDir: string,
@@ -938,9 +878,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
938
878
  packageRoot,
939
879
  projectDir,
940
880
  sourceBySkill,
941
- setup.vitePlus.quality.workflow.enabled &&
942
- setup.vitePlus.quality.workflow.typecheck.length === 1 &&
943
- setup.vitePlus.quality.workflow.typecheck[0] === "vp run typecheck",
881
+ setup.vitePlus.workflow.enabled,
944
882
  targets,
945
883
  );
946
884
 
@@ -980,32 +918,6 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
980
918
  linkTarget,
981
919
  });
982
920
  }
983
- if (setup.vitePlus.quality.workflow.enabled) {
984
- const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_GITHUB_ACTIONS_PATH);
985
- const template = yield* readGeneratedFileTemplate(
986
- packageRoot,
987
- VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
988
- );
989
- const content = renderVitePlusWorkflowTemplate(template, {
990
- devKitCommand:
991
- projectDir === packageRoot
992
- ? "./bin/dev-kit.mjs apply --locked"
993
- : "bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked",
994
- workflow: setup.vitePlus.quality.workflow,
995
- });
996
-
997
- outputs.push({
998
- resourceId: "setup:vite-plus-github-actions",
999
- path: managed.relative,
1000
- sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
1001
- mode: "copy",
1002
- kind: "file",
1003
- digest: yield* digestFileContent(content),
1004
- destination: managed.absolute,
1005
- content,
1006
- adoptIfExact: true,
1007
- });
1008
- }
1009
921
  const agentsTarget = targets.agents;
1010
922
  const duplicateOutput = skills.find(
1011
923
  (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
@@ -1116,20 +1028,10 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1116
1028
  observed.kind === locked.kind
1117
1029
  ? locked
1118
1030
  : undefined;
1119
- const rawModeObservation =
1120
- matchingLockedOutput !== undefined &&
1121
- observed.kind !== "missing" &&
1122
- observed.digest !== matchingLockedOutput.digest &&
1123
- currentLock !== undefined &&
1124
- usesRawFileModeDigests(currentLock.toolVersion)
1125
- ? yield* observePathWithRawModes(output.destination)
1126
- : undefined;
1127
1031
  const lockedOwnsObserved =
1128
1032
  matchingLockedOutput !== undefined &&
1129
1033
  observed.kind !== "missing" &&
1130
- (observed.digest === matchingLockedOutput.digest ||
1131
- (rawModeObservation?.kind === matchingLockedOutput.kind &&
1132
- rawModeObservation.digest === matchingLockedOutput.digest));
1034
+ observed.digest === matchingLockedOutput.digest;
1133
1035
 
1134
1036
  if (output.resourceId === "setup:agent-instructions" && "content" in output) {
1135
1037
  if (observed.kind === "missing") {
@@ -1217,7 +1119,7 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1217
1119
  if (observed.kind === "missing") {
1218
1120
  actions.push({ action: "create", desired: output, observed });
1219
1121
  } else if (observed.kind === output.kind && observed.digest === output.digest) {
1220
- if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
1122
+ if (sameReceipt || lockedOwnsObserved) {
1221
1123
  actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
1222
1124
  } else {
1223
1125
  actions.push({
@@ -1375,28 +1277,6 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1375
1277
  const packageRoot = yield* resolvePackageRoot();
1376
1278
  const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
1377
1279
 
1378
- const vitePlusQuality = manifest.setup.vitePlus.quality;
1379
- const vitePlusQualityEnabled = vitePlusQuality.workflow.enabled;
1380
-
1381
- if (vitePlusQualityEnabled) {
1382
- if (!manifest.setup.effectTsgo.enabled) {
1383
- return yield* InvalidProjectStateError.make({
1384
- message:
1385
- "setup.vitePlus.quality requires setup.effectTsgo.enabled so managed quality setup converges the Effect-patched compiler",
1386
- });
1387
- }
1388
- yield* validateVitePlusQualitySupport(
1389
- projectDir,
1390
- packageRoot,
1391
- manifest.setup.effectTsgo.typescriptPackage,
1392
- {
1393
- workflow: {
1394
- beforeChecks: vitePlusQuality.workflow.beforeChecks,
1395
- typecheck: vitePlusQuality.workflow.typecheck,
1396
- },
1397
- },
1398
- );
1399
- }
1400
1280
  const effectSource = manifest.setup.effectSource.enabled
1401
1281
  ? yield* planEffectSource({
1402
1282
  packageName: manifest.setup.effectSource.packageName,
@@ -1415,6 +1295,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1415
1295
  const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
1416
1296
  ? yield* planVitePlusHooks(projectDir)
1417
1297
  : undefined;
1298
+ const vitePlusWorkflow = manifest.setup.vitePlus.workflow.enabled
1299
+ ? yield* planVitePlusWorkflow({
1300
+ packageRoot,
1301
+ projectDir,
1302
+ effectTsgoEnabled: manifest.setup.effectTsgo.enabled,
1303
+ typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
1304
+ })
1305
+ : undefined;
1306
+ const worktrunkConfig = manifest.setup.worktrunk.config.enabled
1307
+ ? yield* planWorktrunkConfig(packageRoot, projectDir)
1308
+ : undefined;
1418
1309
  const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
1419
1310
  const availableSkills = catalog.skills.map((skill) => skill.selector);
1420
1311
  const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
@@ -1521,16 +1412,6 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1521
1412
  digest: output.digest,
1522
1413
  };
1523
1414
  }
1524
- if (output.resourceId === "setup:claude-instructions") {
1525
- return {
1526
- resourceId: output.resourceId,
1527
- path: output.path,
1528
- sourcePath: output.sourcePath,
1529
- mode: output.mode,
1530
- kind: output.kind,
1531
- digest: output.digest,
1532
- };
1533
- }
1534
1415
 
1535
1416
  return {
1536
1417
  resourceId: output.resourceId,
@@ -1553,8 +1434,22 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1553
1434
  ];
1554
1435
 
1555
1436
  yield* validateReservedPaths(projectDir, reservedPaths, desired);
1556
- const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
1557
- const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1437
+ const rawLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
1438
+ const rawState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1439
+ // Migration: dev-kit ≤0.14 owned the check workflow as a managed output. It
1440
+ // is a scaffold now, so stale lock entries and receipts are dropped on read —
1441
+ // releasing ownership to the repository instead of planning a removal.
1442
+ const dropRetiredOutputs = <O extends { readonly resourceId: string }>(
1443
+ outputs: ReadonlyArray<O>,
1444
+ ) => outputs.filter((output) => output.resourceId !== "setup:vite-plus-github-actions");
1445
+ const currentLock =
1446
+ rawLock === undefined
1447
+ ? undefined
1448
+ : { ...rawLock, outputs: dropRetiredOutputs(rawLock.outputs) };
1449
+ const currentState =
1450
+ rawState === undefined
1451
+ ? undefined
1452
+ : { ...rawState, outputs: dropRetiredOutputs(rawState.outputs) };
1558
1453
 
1559
1454
  yield* validateReservedPaths(projectDir, reservedPaths, [
1560
1455
  ...(currentLock?.outputs ?? []),
@@ -1601,6 +1496,8 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
1601
1496
  ...(effectSource === undefined ? {} : { effectSource }),
1602
1497
  ...(effectTsgo === undefined ? {} : { effectTsgo }),
1603
1498
  ...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
1499
+ ...(vitePlusWorkflow === undefined ? {} : { vitePlusWorkflow }),
1500
+ ...(worktrunkConfig === undefined ? {} : { worktrunkConfig }),
1604
1501
  nextLock,
1605
1502
  nextState: planned.nextState,
1606
1503
  metadataChanged:
@@ -1627,7 +1524,9 @@ const operationalChangeCount = (plan: SkillPlan): number =>
1627
1524
  plan.actions.filter((action) => action.action !== "unchanged").length +
1628
1525
  (plan.effectSource?.action === "sync" ? 1 : 0) +
1629
1526
  (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
1630
- (plan.vitePlusHooks?.action === "configure" ? 1 : 0);
1527
+ (plan.vitePlusHooks?.action === "configure" ? 1 : 0) +
1528
+ (plan.vitePlusWorkflow?.action === "scaffold" ? 1 : 0) +
1529
+ (plan.worktrunkConfig?.action === "scaffold" ? 1 : 0);
1631
1530
 
1632
1531
  const plannedChangeCount = (plan: SkillPlan): number => {
1633
1532
  const operational = operationalChangeCount(plan);
@@ -1658,6 +1557,12 @@ export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: Skill
1658
1557
  if (plan.vitePlusHooks?.action === "configure") {
1659
1558
  yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
1660
1559
  }
1560
+ if (plan.vitePlusWorkflow?.action === "scaffold") {
1561
+ yield* printDetail(`+ scaffold check workflow → ${plan.vitePlusWorkflow.path}`);
1562
+ }
1563
+ if (plan.worktrunkConfig?.action === "scaffold") {
1564
+ yield* printDetail(`+ scaffold Worktrunk config → ${plan.worktrunkConfig.path}`);
1565
+ }
1661
1566
  if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
1662
1567
  yield* printDetail("+ Dev kit metadata");
1663
1568
  }
@@ -1952,6 +1857,8 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1952
1857
  effectSource: plan.effectSource,
1953
1858
  effectTsgo: plan.effectTsgo,
1954
1859
  vitePlusHooks: plan.vitePlusHooks,
1860
+ vitePlusWorkflow: plan.vitePlusWorkflow,
1861
+ worktrunkConfig: plan.worktrunkConfig,
1955
1862
  nextLock: plan.nextLock,
1956
1863
  nextState: plan.nextState,
1957
1864
  });
@@ -1960,6 +1867,8 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1960
1867
  effectSource: replanned.effectSource,
1961
1868
  effectTsgo: replanned.effectTsgo,
1962
1869
  vitePlusHooks: replanned.vitePlusHooks,
1870
+ vitePlusWorkflow: replanned.vitePlusWorkflow,
1871
+ worktrunkConfig: replanned.worktrunkConfig,
1963
1872
  nextLock: replanned.nextLock,
1964
1873
  nextState: replanned.nextState,
1965
1874
  });
@@ -1981,9 +1890,27 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1981
1890
  if (replanned.vitePlusHooks !== undefined) {
1982
1891
  yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
1983
1892
  }
1893
+ if (replanned.vitePlusWorkflow !== undefined) {
1894
+ yield* applyScaffoldPlan(replanned.vitePlusWorkflow);
1895
+ }
1896
+ if (replanned.worktrunkConfig !== undefined) {
1897
+ yield* applyScaffoldPlan(replanned.worktrunkConfig);
1898
+ }
1984
1899
  yield* applyPlannedSkillChanges(replanned);
1985
1900
  }),
1986
1901
  );
1902
+ const fs = yield* FileSystem.FileSystem;
1903
+ const path = yield* Path.Path;
1904
+
1905
+ // Catalog checkouts moved to the machine-global cache; drop the regenerable
1906
+ // project-local copies left behind by earlier dev-kit versions.
1907
+ yield* fs
1908
+ .remove(path.join(plan.projectDir, ".dev-kit", "cache", "catalog"), {
1909
+ force: true,
1910
+ recursive: true,
1911
+ })
1912
+ .pipe(Effect.ignore);
1913
+ yield* maybePruneGlobalCache().pipe(Effect.ignore);
1987
1914
  yield* printStatus(
1988
1915
  "success",
1989
1916
  changes === 0 && !replanned.metadataChanged ? "Dev kit up to date" : "Dev kit ready",
@@ -0,0 +1,82 @@
1
+ import { Effect, Schema } from "effect";
2
+
3
+ import { readDirectDependencyNames } from "./project-package.ts";
4
+ import { planScaffold, readScaffoldTemplate, replaceUniqueTemplateMarker } from "./scaffold.ts";
5
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
6
+
7
+ export const VITE_PLUS_GITHUB_ACTIONS_PATH = ".github/workflows/check.yml";
8
+ export const VITE_PLUS_GITHUB_ACTIONS_TEMPLATE = "templates/vite-plus/github-actions-check.yml";
9
+
10
+ export class VitePlusWorkflowSupportError extends Schema.TaggedError<VitePlusWorkflowSupportError>()(
11
+ "VitePlusWorkflowSupportError",
12
+ { message: Schema.String },
13
+ ) {}
14
+
15
+ const LOCKED_DEV_KIT_COMMAND =
16
+ "bun ./node_modules/@danieljvdm/dev-kit/bin/dev-kit.mjs apply --locked";
17
+
18
+ export const renderVitePlusWorkflowTemplate = (
19
+ template: string,
20
+ options: { readonly devKitCommand?: string } = {},
21
+ ): string => {
22
+ const devKitCommand = options.devKitCommand;
23
+
24
+ return devKitCommand === undefined || devKitCommand === LOCKED_DEV_KIT_COMMAND
25
+ ? template
26
+ : replaceUniqueTemplateMarker(template, LOCKED_DEV_KIT_COMMAND, devKitCommand);
27
+ };
28
+
29
+ export const validateVitePlusWorkflowSupport = Effect.fn("validateVitePlusWorkflowSupport")(
30
+ function* (projectDir: string, packageRoot: string, typescriptPackage: string) {
31
+ const dependencies = yield* readDirectDependencyNames(projectDir);
32
+ const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
33
+
34
+ yield* validateInstalledVitePlus(projectDir).pipe(
35
+ Effect.mapError((error) => VitePlusWorkflowSupportError.make({ message: error.message })),
36
+ );
37
+
38
+ if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
39
+ const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
40
+
41
+ if (missing.length > 0) {
42
+ return yield* VitePlusWorkflowSupportError.make({
43
+ message: `setup.vitePlus.workflow requires direct dependencies: ${missing.join(", ")}`,
44
+ });
45
+ }
46
+ },
47
+ );
48
+
49
+ export const planVitePlusWorkflow = (options: {
50
+ readonly packageRoot: string;
51
+ readonly projectDir: string;
52
+ readonly effectTsgoEnabled: boolean;
53
+ readonly typescriptPackage: string;
54
+ }) =>
55
+ planScaffold({
56
+ projectDir: options.projectDir,
57
+ path: VITE_PLUS_GITHUB_ACTIONS_PATH,
58
+ content: Effect.gen(function* () {
59
+ if (!options.effectTsgoEnabled) {
60
+ return yield* VitePlusWorkflowSupportError.make({
61
+ message:
62
+ "setup.vitePlus.workflow requires setup.effectTsgo.enabled so the scaffolded workflow's typecheck uses the Effect-patched compiler",
63
+ });
64
+ }
65
+ yield* validateVitePlusWorkflowSupport(
66
+ options.projectDir,
67
+ options.packageRoot,
68
+ options.typescriptPackage,
69
+ );
70
+ const template = yield* readScaffoldTemplate(
71
+ options.packageRoot,
72
+ VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
73
+ );
74
+
75
+ return renderVitePlusWorkflowTemplate(
76
+ template,
77
+ options.projectDir === options.packageRoot
78
+ ? { devKitCommand: "./bin/dev-kit.mjs apply --locked" }
79
+ : {},
80
+ );
81
+ }),
82
+ });
package/src/vite-plus.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { recommendedOxfmtConfig } from "./oxfmt.js";
2
- import { recommendedOxlintConfig } from "./oxlint.js";
2
+ import { createAbsoluteImportsOxlintOverride, recommendedOxlintConfig } from "./oxlint.js";
3
3
  import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
4
4
 
5
5
  export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
@@ -58,6 +58,12 @@ const createTypecheckTask = (options) => {
58
58
  /** Build composable quality defaults for a project-owned Vite+ config. */
59
59
  export const createRecommendedVitePlusConfig = (options = {}) => {
60
60
  const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
61
+ const lintOverrides = options.absoluteImports
62
+ ? [
63
+ ...recommendedOxlintConfig.overrides,
64
+ createAbsoluteImportsOxlintOverride(options.absoluteImports),
65
+ ]
66
+ : recommendedOxlintConfig.overrides;
61
67
 
62
68
  return {
63
69
  staged: {
@@ -70,6 +76,7 @@ export const createRecommendedVitePlusConfig = (options = {}) => {
70
76
  lint: {
71
77
  ...recommendedOxlintConfig,
72
78
  ignorePatterns,
79
+ overrides: lintOverrides,
73
80
  },
74
81
  run: {
75
82
  tasks: {
package/src/vite-plus.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import { recommendedOxfmtConfig } from "./oxfmt.ts";
2
- import { recommendedOxlintConfig } from "./oxlint.ts";
2
+ import {
3
+ type AbsoluteImportsOptions,
4
+ createAbsoluteImportsOxlintOverride,
5
+ recommendedOxlintConfig,
6
+ } from "./oxlint.ts";
3
7
  import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
4
8
 
9
+ export type { AbsoluteImportsOptions } from "./oxlint.ts";
5
10
  export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
6
11
 
7
12
  export type VitePlusTypecheckOptions =
@@ -16,6 +21,8 @@ export type VitePlusTypecheckOptions =
16
21
  };
17
22
 
18
23
  export type RecommendedVitePlusConfigOptions = {
24
+ /** Enforce path-alias imports (no `../`) inside the given globs. */
25
+ readonly absoluteImports?: AbsoluteImportsOptions;
19
26
  /** Additional project-owned generated or vendored paths. */
20
27
  readonly ignorePatterns?: ReadonlyArray<string>;
21
28
  readonly typecheck?: VitePlusTypecheckOptions;
@@ -79,6 +86,12 @@ const createTypecheckTask = (options: VitePlusTypecheckOptions | undefined) => {
79
86
  */
80
87
  export const createRecommendedVitePlusConfig = (options: RecommendedVitePlusConfigOptions = {}) => {
81
88
  const ignorePatterns = [...devKitToolIgnorePatterns, ...(options.ignorePatterns ?? [])];
89
+ const lintOverrides = options.absoluteImports
90
+ ? [
91
+ ...recommendedOxlintConfig.overrides,
92
+ createAbsoluteImportsOxlintOverride(options.absoluteImports),
93
+ ]
94
+ : recommendedOxlintConfig.overrides;
82
95
 
83
96
  return {
84
97
  staged: {
@@ -91,6 +104,7 @@ export const createRecommendedVitePlusConfig = (options: RecommendedVitePlusConf
91
104
  lint: {
92
105
  ...recommendedOxlintConfig,
93
106
  ignorePatterns,
107
+ overrides: lintOverrides,
94
108
  },
95
109
  run: {
96
110
  tasks: {