@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.
- package/README.md +155 -99
- package/dev-kit.example.jsonc +4 -3
- package/package.json +1 -1
- package/schema/dev-kit.schema.json +19 -42
- package/skills/build-effect-apis/SKILL.md +13 -31
- package/skills/build-effect-apis/references/verification.md +3 -3
- package/skills/dev-kit/SKILL.md +115 -222
- package/skills/effect-atom-state/SKILL.md +97 -0
- package/skills/effect-atom-state/agents/openai.yaml +4 -0
- package/skills/effect-atom-state/references/effect-atom-workflows.md +180 -0
- package/skills/open-pull-request/SKILL.md +62 -23
- package/src/bin/dev-kit.ts +21 -0
- package/src/catalog.ts +39 -15
- package/src/effect-source.ts +70 -4
- package/src/global-cache.ts +304 -0
- package/src/index.ts +6 -6
- package/src/manifest.ts +28 -29
- package/src/oxlint.js +23 -0
- package/src/oxlint.ts +37 -1
- package/src/path-digest.ts +0 -13
- package/src/project-package.ts +127 -12
- package/src/project-state.ts +3 -0
- package/src/scaffold.ts +79 -0
- package/src/sync.ts +100 -173
- package/src/vite-plus-workflow.ts +82 -0
- package/src/vite-plus.js +8 -1
- package/src/vite-plus.ts +15 -1
- package/src/worktrunk-config.ts +88 -0
- package/templates/vite-plus/github-actions-check.yml +0 -2
- package/templates/worktrunk/wt.toml +27 -0
- package/src/vite-plus-quality.ts +0 -148
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-client.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-lifecycle.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-testing.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/tanstack-start.md +0 -0
package/src/scaffold.ts
ADDED
|
@@ -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 {
|
|
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
|
-
|
|
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: [
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
1557
|
-
const
|
|
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 {
|
|
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: {
|