@danieljvdm/dev-kit 0.7.1 → 0.8.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 CHANGED
@@ -193,8 +193,9 @@ tool versions. A project-local process lock also prevents concurrent applies.
193
193
  ```
194
194
 
195
195
  - `dev-kit` installs guidance for operating the toolkit itself.
196
- - `effect` expands to `effect-ts` plus focused Effect Atom HTTP data-fetching
197
- guidance.
196
+ - `effect` expands to the `effect-ts` umbrella, whose lazy references include
197
+ Effect Atom HTTP data fetching and Effect DateTime guidance. The former
198
+ `effect-atom-data-fetching` selector remains an alias for compatibility.
198
199
  - Prefer individual external skills such as `workers-best-practices` and
199
200
  `wrangler`, selected after scanning the project for relevant technologies.
200
201
  - `serve-sim` selects the approved Evan Bacon simulator skill directly.
@@ -259,8 +260,8 @@ worktree converges its own copy during install while the project-owned
259
260
 
260
261
  ## Vite+ quality setup
261
262
 
262
- Supported Vite+/Effect repositories can explicitly opt into Dev Kit's canonical
263
- quality configuration and GitHub Actions workflow:
263
+ Vite+/Effect repositories opt into the canonical Vite config and GitHub Actions
264
+ workflow independently:
264
265
 
265
266
  ```jsonc
266
267
  {
@@ -269,24 +270,95 @@ quality configuration and GitHub Actions workflow:
269
270
  "effectTsgo": { "enabled": true },
270
271
  "vitePlus": {
271
272
  "hooks": { "enabled": true },
272
- "quality": { "enabled": true },
273
+ "quality": {
274
+ "config": { "enabled": true },
275
+ "workflow": { "enabled": true },
276
+ },
273
277
  },
274
278
  },
275
279
  }
276
280
  ```
277
281
 
278
- The quality task requires direct `@danieljvdm/dev-kit`, `vite-plus`, `effect`,
279
- `@effect/tsgo`, and native TypeScript dependencies, plus the enabled Effect
280
- TypeScript-Go patch. It manages `vite.config.ts` and
281
- `.github/workflows/check.yml` as digest-owned files. Existing custom files or
282
- conflicting `check`/`typecheck` package scripts are rejected instead of merged
283
- or overwritten. Exact canonical files can be adopted; disabling the task
284
- removes only unchanged owned files.
282
+ `quality.config.enabled` owns `vite.config.ts`; `quality.workflow.enabled` owns
283
+ `.github/workflows/check.yml`. Selecting one does not impose the other resource's
284
+ config, scripts, or TypeScript topology. Both require direct
285
+ `@danieljvdm/dev-kit`, `vite-plus`, `effect`, `@effect/tsgo`, and native
286
+ TypeScript dependencies with `setup.effectTsgo.enabled`. The installed Vite+
287
+ must satisfy Dev Kit's peer range.
285
288
 
286
289
  The managed Vite config composes the shared Oxfmt and Oxlint presets, configures
287
- `vp staged`, and defines `vp run check` and `vp run typecheck`. The check task
288
- and CI run `vp fmt --check`, `vp lint`, `vp test`, then the Effect-patched
289
- compiler through `vp run typecheck`.
290
+ `vp staged`, and defines separate `vp run check` and pure `vp run typecheck`
291
+ tasks. It rejects conflicting root scripts. The default `single-project`
292
+ strategy requires a root `tsconfig.json` without project references. Workspaces
293
+ can instead select Vite Task orchestration explicitly:
294
+
295
+ ```jsonc
296
+ {
297
+ "setup": {
298
+ "effectTsgo": { "enabled": true },
299
+ "vitePlus": {
300
+ "quality": {
301
+ "config": {
302
+ "enabled": true,
303
+ "typecheck": {
304
+ "strategy": "workspace",
305
+ "concurrency": 4,
306
+ "packages": ["apps/web", "packages/core"],
307
+ },
308
+ },
309
+ },
310
+ },
311
+ },
312
+ }
313
+ ```
314
+
315
+ Workspace mode requires `package.json#workspaces` and an explicit list of
316
+ package directories with pure `typecheck` scripts. The generated task uses
317
+ dependency order, caching, `--fail-if-no-match`, and bounded concurrency.
318
+ Project-reference builds remain custom because their `.tsbuildinfo` inputs and
319
+ outputs are repository-specific.
320
+
321
+ Repositories with a custom Vite config can still adopt only the hardened CI
322
+ workflow and declare repository-specific preparation and typecheck commands:
323
+
324
+ ```jsonc
325
+ {
326
+ "setup": {
327
+ "effectTsgo": { "enabled": true },
328
+ "vitePlus": {
329
+ "quality": {
330
+ "workflow": {
331
+ "enabled": true,
332
+ "beforeChecks": [
333
+ {
334
+ "name": "Install media tools",
335
+ "run": ["sudo apt-get update", "sudo apt-get install --yes ffmpeg"],
336
+ },
337
+ ],
338
+ "typecheck": [
339
+ "vp run -F './apps/*' -F './packages/*' check",
340
+ "vp exec tsc --noEmit -p scripts/tsconfig.json",
341
+ ],
342
+ },
343
+ },
344
+ },
345
+ },
346
+ }
347
+ ```
348
+
349
+ Workflow-only setup does not validate or take ownership of the Vite config,
350
+ scripts, or TypeScript topology. It performs one frozen, script-suppressed
351
+ install, runs `dev-kit apply --locked`, and only then runs custom preparation,
352
+ formatting, linting, tests, and typechecking. Its default typecheck command is
353
+ `vp run typecheck`; `workflow.typecheck` replaces it. Vite+ maps install flags
354
+ to the detected package manager. The template pins an exact `setup-vp` release
355
+ commit because its `v1` tag is frozen; keep it current with Renovate or
356
+ Dependabot. Existing workflows remain user-owned until their rendered content
357
+ matches exactly—Dev Kit never merges YAML. See the primary
358
+ [`setup-vp` versioning guidance](https://github.com/voidzero-dev/setup-vp#versioning),
359
+ [Vite+ install guide](https://viteplus.dev/guide/install), and
360
+ [Vite Task run guide](https://viteplus.dev/guide/run) when maintaining the
361
+ templates.
290
362
 
291
363
  ## Effect source checkout
292
364
 
@@ -350,8 +422,7 @@ native TypeScript compiler. It does not download dependencies and skips an
350
422
  installation that is already patched. Use `dev-kit tsgo patch --dry-run` when
351
423
  troubleshooting the task directly.
352
424
 
353
- Dependency and `tsconfig.json` edits remain explicit. Enable the Vite+ quality
354
- task only when Dev Kit can own the canonical root config and GitHub workflow.
425
+ Dependency and `tsconfig.json` edits remain explicit.
355
426
 
356
427
  ## Installed package skills
357
428
 
@@ -484,9 +555,8 @@ Use `lint.extends` rather than a shallow object spread so Vite+ composes the
484
555
  nested plugin and rule configuration correctly. Oxfmt has no `extends`, so
485
556
  spread its configuration before project-local formatter options. The shared
486
557
  lint preset enables `typeAware` for semantic lint rules but leaves `typeCheck`
487
- disabled. The opt-in `setup.vitePlus.quality` task manages this composition for
488
- repositories that use the canonical config; custom configs keep the task
489
- disabled and compose the exports manually. Effect TypeScript-Go projects run:
558
+ disabled. Managed quality config uses this composition; custom Vite configs
559
+ compose the exports manually. Effect TypeScript-Go projects run:
490
560
 
491
561
  ```sh
492
562
  vp fmt --check
@@ -8,7 +8,13 @@
8
8
  "effectTsgo": { "enabled": true },
9
9
  "vitePlus": {
10
10
  "hooks": { "enabled": true },
11
- "quality": { "enabled": true },
11
+ "quality": {
12
+ "config": {
13
+ "enabled": true,
14
+ "typecheck": { "strategy": "single-project" },
15
+ },
16
+ "workflow": { "enabled": true },
17
+ },
12
18
  },
13
19
  },
14
20
  "targets": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "Declarative project development toolkit with portable agent skills.",
6
6
  "license": "MIT",
@@ -69,6 +69,7 @@
69
69
  "@stylistic/eslint-plugin": "5.10.0",
70
70
  "effect": "4.0.0-beta.102",
71
71
  "jsonc-parser": "3.3.1",
72
+ "semver": "7.8.5",
72
73
  "tsx": "4.22.4"
73
74
  },
74
75
  "devDependencies": {
@@ -76,6 +77,7 @@
76
77
  "@effect/tsgo": "0.24.3",
77
78
  "@effect/vitest": "4.0.0-beta.102",
78
79
  "@types/node": "25.9.1",
80
+ "@types/semver": "7.8.0",
79
81
  "oxfmt": "0.60.0",
80
82
  "oxlint": "1.75.0",
81
83
  "typescript": "7.0.2",
@@ -161,13 +161,105 @@
161
161
  }
162
162
  },
163
163
  "vitePlusQualitySetup": {
164
- "description": "Manage the canonical Vite+ quality config and GitHub Actions workflow for supported Effect TypeScript-Go repositories.",
164
+ "description": "Opt into independently managed Vite+ quality resources for supported Effect TypeScript-Go repositories.",
165
+ "type": "object",
166
+ "additionalProperties": false,
167
+ "properties": {
168
+ "config": {
169
+ "$ref": "#/$defs/vitePlusQualityConfigSetup"
170
+ },
171
+ "workflow": {
172
+ "$ref": "#/$defs/vitePlusQualityWorkflowSetup"
173
+ }
174
+ }
175
+ },
176
+ "vitePlusQualityConfigSetup": {
177
+ "description": "Manage the canonical root Vite config without requiring Dev Kit to own CI.",
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "properties": {
181
+ "enabled": {
182
+ "type": "boolean",
183
+ "default": false
184
+ },
185
+ "typecheck": {
186
+ "$ref": "#/$defs/vitePlusQualityTypecheckSetup"
187
+ }
188
+ }
189
+ },
190
+ "vitePlusQualityWorkflowSetup": {
191
+ "description": "Manage the canonical GitHub Actions check workflow without requiring Dev Kit to own the Vite config.",
165
192
  "type": "object",
166
193
  "additionalProperties": false,
167
194
  "properties": {
168
195
  "enabled": {
169
196
  "type": "boolean",
170
197
  "default": false
198
+ },
199
+ "beforeChecks": {
200
+ "description": "Repository-specific preparation steps inserted after locked setup verification and before quality checks.",
201
+ "type": "array",
202
+ "items": {
203
+ "$ref": "#/$defs/vitePlusQualityWorkflowStep"
204
+ },
205
+ "default": []
206
+ },
207
+ "typecheck": {
208
+ "description": "Shell commands for the workflow's pure typecheck step.",
209
+ "type": "array",
210
+ "minItems": 1,
211
+ "items": {
212
+ "type": "string",
213
+ "minLength": 1
214
+ },
215
+ "default": ["vp run typecheck"]
216
+ }
217
+ }
218
+ },
219
+ "vitePlusQualityWorkflowStep": {
220
+ "type": "object",
221
+ "additionalProperties": false,
222
+ "required": ["name", "run"],
223
+ "properties": {
224
+ "name": {
225
+ "type": "string",
226
+ "minLength": 1
227
+ },
228
+ "run": {
229
+ "type": "array",
230
+ "minItems": 1,
231
+ "items": {
232
+ "type": "string",
233
+ "minLength": 1
234
+ }
235
+ }
236
+ }
237
+ },
238
+ "vitePlusQualityTypecheckSetup": {
239
+ "description": "Select the managed typecheck topology. Single-project is the default; workspace mode runs explicitly scoped package scripts through Vite Task.",
240
+ "type": "object",
241
+ "additionalProperties": false,
242
+ "properties": {
243
+ "strategy": {
244
+ "enum": ["single-project", "workspace"],
245
+ "default": "single-project"
246
+ },
247
+ "concurrency": {
248
+ "description": "Maximum concurrent workspace typecheck tasks.",
249
+ "type": "integer",
250
+ "minimum": 1,
251
+ "maximum": 32,
252
+ "default": 4
253
+ },
254
+ "packages": {
255
+ "description": "Project-relative workspace package directories whose typecheck scripts should run.",
256
+ "type": "array",
257
+ "items": {
258
+ "type": "string",
259
+ "minLength": 1
260
+ },
261
+ "uniqueItems": true,
262
+ "default": []
171
263
  }
172
264
  }
173
265
  },
@@ -76,7 +76,10 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
76
76
  "claudeInstructions": { "enabled": true },
77
77
  "vitePlus": {
78
78
  "hooks": { "enabled": true },
79
- "quality": { "enabled": true },
79
+ "quality": {
80
+ "config": { "enabled": true },
81
+ "workflow": { "enabled": true },
82
+ },
80
83
  },
81
84
  },
82
85
  "targets": {
@@ -109,19 +112,26 @@ ignored dispatchers in linked worktrees. Preserve other hook managers; Dev Kit
109
112
  refuses to replace an unrelated `core.hooksPath`. Use `VITE_GIT_HOOKS=0` or
110
113
  `HUSKY=0` to skip hook setup for an invocation.
111
114
 
112
- Enable `setup.vitePlus.quality` only in supported Vite+/Effect repositories
113
- that explicitly want Dev Kit to own the canonical root `vite.config.ts` and
114
- `.github/workflows/check.yml`. It requires direct Dev Kit, Vite+, Effect,
115
- Effect TypeScript-Go, and native TypeScript dependencies, as well as
116
- `setup.effectTsgo.enabled`. It refuses custom destination files and conflicting
117
- `check` or `typecheck` package scripts. Exact canonical files can be adopted;
118
- later updates and cleanup occur only while the owned files remain unchanged.
119
-
120
- The managed Vite config composes the shared formatter and linter presets,
121
- configures `vp staged`, and defines cached `check` and `typecheck` Vite tasks.
122
- The check task and GitHub Actions workflow run `vp fmt --check`, `vp lint`,
123
- `vp test`, and finally `vp run typecheck` so Effect diagnostics come from the
124
- patched native compiler.
115
+ Enable `setup.vitePlus.quality.config` and
116
+ `setup.vitePlus.quality.workflow` independently in supported Vite+/Effect
117
+ repositories. Config owns canonical root `vite.config.ts`; workflow owns
118
+ `.github/workflows/check.yml`; selecting one must not constrain the other's
119
+ config, scripts, or TypeScript topology. Both require direct Dev Kit, compatible
120
+ Vite+, Effect, Effect TypeScript-Go, and native TypeScript dependencies with
121
+ `setup.effectTsgo.enabled`. Preserve unowned destinations and adopt only exact
122
+ rendered matches.
123
+
124
+ Managed config defines separate `check` and pure `typecheck` Vite tasks. Its
125
+ default single-project strategy rejects project references; workspace mode
126
+ requires explicit package directories with `typecheck` scripts and generates
127
+ cached, dependency-ordered, bounded-concurrency filters. Keep project-reference
128
+ builds custom. Workflow-only consumers may configure `workflow.beforeChecks`
129
+ and `workflow.typecheck`; treat these commands as trusted manifest input.
130
+
131
+ The workflow must use one frozen, script-suppressed install, then locked Dev Kit
132
+ convergence before preparation or checks. Keep `setup-vp` pinned to a reviewed
133
+ release commit—the `v1` tag is frozen—and let Vite+ resolve the consumer's
134
+ compatible locked version.
125
135
 
126
136
  ## Ownership and conflicts
127
137
 
@@ -255,8 +265,8 @@ in the consuming project.
255
265
 
256
266
  Manage skill outputs, the `setup.agentInstructions` wrapper, the
257
267
  `setup.claudeInstructions` link, the `setup.vitePlus.hooks` dispatcher, the
258
- opt-in `setup.vitePlus.quality` config and GitHub workflow, the
268
+ independently opt-in `setup.vitePlus.quality` config and GitHub workflow, the
259
269
  `setup.effectSource` checkout, and the explicit `setup.effectTsgo` task.
260
270
  Dependency and `tsconfig.json` contributions remain deliberate user-owned
261
- edits. Custom Vite configs compose the Oxlint and Oxfmt package exports manually
262
- and leave the canonical quality task disabled.
271
+ edits. Custom Vite configs compose the Oxlint and Oxfmt package exports
272
+ manually, leave managed config disabled, and may still use the managed workflow.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: effect-ts
3
- description: Use this skill whenever working in a repository that uses Effect, even if the current task is in a new file or the user does not explicitly ask for Effect help. Apply it to Effect patterns, services, layers, schemas, streams, runtimes, typed errors, observability, testing, HTTP, SQL, command-line scripts, project automation, and supporting tooling.
3
+ description: Use this skill whenever working in a repository that uses Effect, even if the current task is in a new file or the user does not explicitly ask for Effect help. Apply it to Effect patterns, services, layers, schemas, streams, runtimes, typed errors, DateTime, Effect Atom, observability, testing, HTTP, SQL, command-line scripts, project automation, and supporting tooling.
4
4
  ---
5
5
 
6
6
  # Effect Expert
@@ -106,6 +106,19 @@ When the task touches one of these areas, consult the matching guide before impl
106
106
  - `./references/guide-schedule.md` for retries, repeats, backoff, polling, cron, and schedule composition
107
107
  - `./references/guide-schema.md` for schema-first application modeling,
108
108
  service contracts, transformations, unions, recursion, and branded types
109
+ - `./references/guide-datetime.md` for current time, parsing, UTC and zoned
110
+ values, time zones, DST-safe arithmetic, formatting, Date interoperability,
111
+ and deterministic `TestClock` tests
112
+ - `./references/guide-atom-data-fetching.md` for the core Effect Atom HTTP
113
+ data-fetching workflow and ownership rules
114
+ - `./references/atom-cache-lifecycle.md` for Effect Atom registry scope,
115
+ runtime memoization, families, TTL, SWR, polling, and aggregation resets
116
+ - `./references/atom-http-and-invalidation.md` for `AtomHttpApi.Service`,
117
+ queries, mutations, reactivity keys, and invalidation
118
+ - `./references/atom-tanstack-start.md` only for TanStack Start provider
119
+ placement, SSR isolation, hydration, and focus guidance
120
+ - `./references/atom-testing.md` when adding or diagnosing deterministic Effect
121
+ Atom lifecycle tests
109
122
  - `./references/guide-sql.md` for Effect SQL usage, transactions, resolvers, schema-aware SQL, and migrations
110
123
  - `./references/guide-testing.md` for `@effect/vitest`, deterministic testing,
111
124
  honest test Layers, property tests, and protocol round trips
@@ -208,6 +221,15 @@ Install additional `@effect/*` packages only when the user task actually needs t
208
221
  - If a type is hard to express, simplify the design or introduce a properly typed helper instead of using unsafe TypeScript.
209
222
  - For layers, do not hide them inside `namespace` blocks. Prefer either `static` members on the service class or plain exported layer constants.
210
223
 
224
+ ### Date and Time
225
+
226
+ - Prefer Effect `DateTime` over vanilla JavaScript `Date` for application
227
+ logic. Keep `Date` as an interoperability type at external boundaries.
228
+ - Use `DateTime.now` inside Effect programs so current time remains driven by
229
+ the `Clock` service and deterministic under `TestClock`.
230
+ - Preserve the distinction between instants, zoned wall-clock values, and
231
+ date-only domain values.
232
+
211
233
  ### Code Quality
212
234
 
213
235
  - Write type-safe code that leverages Effect's type system.
@@ -233,6 +255,12 @@ rely on installed declarations and version-matched canonical Effect source.
233
255
  - `./references/guide-retries.md`
234
256
  - `./references/guide-schedule.md`
235
257
  - `./references/guide-schema.md`
258
+ - `./references/guide-datetime.md`
259
+ - `./references/guide-atom-data-fetching.md`
260
+ - `./references/atom-cache-lifecycle.md`
261
+ - `./references/atom-http-and-invalidation.md`
262
+ - `./references/atom-tanstack-start.md`
263
+ - `./references/atom-testing.md`
236
264
  - `./references/guide-sql.md`
237
265
  - `./references/guide-testing.md`
238
266
  - `./references/guide-cli.md`
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Effect Expert"
3
3
  short_description: "Apply current Effect v4 patterns and tooling"
4
- default_prompt: "Use $effect-ts to apply the relevant Effect architecture, schema, service, error, testing, HTTP, SQL, observability, or CLI guidance."
4
+ default_prompt: "Use $effect-ts to apply the relevant Effect architecture, DateTime, Atom, schema, service, error, testing, HTTP, SQL, observability, or CLI guidance."
@@ -1,4 +1,4 @@
1
- # Cache lifecycle
1
+ # Effect Atom cache lifecycle
2
2
 
3
3
  ## Registry and runtime scope
4
4
 
@@ -1,4 +1,4 @@
1
- # HTTP queries and invalidation
1
+ # Effect Atom HTTP queries and invalidation
2
2
 
3
3
  ## Build one API service
4
4
 
@@ -1,4 +1,4 @@
1
- # TanStack Start integration
1
+ # Effect Atom TanStack Start integration
2
2
 
3
3
  TanStack Start code is isomorphic by default, including route loaders. Treat every module used by a route as server-capable unless an explicit boundary says otherwise.
4
4
 
@@ -1,4 +1,4 @@
1
- # Deterministic lifecycle testing
1
+ # Deterministic Effect Atom lifecycle testing
2
2
 
3
3
  Test cache policy below React first with `AtomRegistry.make()`. Add a React integration test only for provider placement, hook behavior, a browser-only SSR boundary, or hydration.
4
4
 
@@ -1,8 +1,3 @@
1
- ---
2
- name: effect-atom-data-fetching
3
- description: Design, implement, debug, or review HTTP data fetching with Effect Atom in React applications. Use for AtomHttpApi.Service queries and mutations, parameterized Atom.family caches, RegistryProvider and runtime placement, TTL/SWR/polling behavior, reactivity-key invalidation, framework-appropriate SSR and focus handling, AsyncResult aggregation resets, and deterministic cache lifecycle tests.
4
- ---
5
-
6
1
  # Effect Atom Data Fetching
7
2
 
8
3
  Model server data as stable atoms owned outside React renders. Give the application one intentional registry boundary and one compatible runtime factory; then choose cache retention, freshness, polling, and invalidation independently.
@@ -28,13 +23,6 @@ Model server data as stable atoms owned outside React renders. Give the applicat
28
23
  - Ensure every input to a route-level `AsyncResult.all` has stable atom identity and compatible retention. One newly allocated or immediately evicted input returns to `Initial` and makes the whole aggregate appear reset even when the other inputs remain cached.
29
24
  - Do not describe manual refresh or polling as freshness caching. Refresh is forceful; `staleTime` only gates SWR's automatic mount/focus decisions.
30
25
 
31
- ## References
32
-
33
- - Read `./references/cache-lifecycle.md` for registry scope, runtime memoization, families, TTL, SWR, polling, and the aggregation trap.
34
- - Read `./references/http-and-invalidation.md` for `AtomHttpApi.Service`, query/mutation ownership, reactivity-key vocabulary, and invalidation.
35
- - Read `./references/tanstack-start.md` only for its framework-specific provider placement, SSR isolation, hydration, and focus guidance.
36
- - Read `./references/testing.md` when adding or diagnosing lifecycle tests.
37
-
38
26
  ## Completion check
39
27
 
40
28
  Confirm one registry boundary, stable atom identity, deliberate TTL/staleness/polling values, matching query and mutation keys, SSR-safe browser access, and tests for every lifecycle behavior changed.
@@ -0,0 +1,72 @@
1
+ # Effect DateTime
2
+
3
+ Prefer Effect `DateTime` over vanilla JavaScript `Date` for application logic. Keep `Date` as an interoperability type at external boundaries, not the domain model.
4
+
5
+ ## Workflow
6
+
7
+ 1. Inspect the installed `effect` version and `DateTime` source before relying on exact signatures. DateTime APIs are version-sensitive.
8
+ 2. Identify whether each value represents an absolute instant, a wall-clock time in a named zone, or only a calendar date. Do not silently collapse these meanings.
9
+ 3. Decode external strings, numbers, and `Date` objects at the boundary. Use `Schema.DateTimeUtc`, `Schema.DateTimeUtcFromString`, `Schema.DateTimeUtcFromMillis`, `Schema.DateTimeUtcFromDate`, or `Schema.DateTimeZoned` as appropriate.
10
+ 4. Keep domain values as `DateTime.Utc` by default. Use `DateTime.Zoned` when calendar operations or presentation must retain an IANA time zone.
11
+ 5. Use `DateTime.now` inside Effect programs so the current time comes from the `Clock` service. Reserve `DateTime.nowUnsafe()` and `Date.now()` for explicit synchronous host boundaries.
12
+ 6. Perform comparisons, arithmetic, rounding, and formatting with `DateTime` operations, then convert to `Date` or epoch milliseconds only when an external API requires them.
13
+ 7. Test time-dependent behavior with `it.effect` and `TestClock`; advance virtual time instead of sleeping or consulting the live clock.
14
+
15
+ ## Construction and boundaries
16
+
17
+ - Prefer safe decoding for untrusted input. `DateTime.make` and zoned constructors return `Option`; Schema decoders produce structured parse failures.
18
+ - Use `DateTime.makeUnsafe` only for literals and values already validated by the program. Do not turn user input into defects.
19
+ - Prefer explicit ISO 8601 strings with offsets at wire boundaries. Avoid implementation-dependent or locale-formatted date strings.
20
+ - Do not invent a midnight instant for a genuinely date-only value such as a birthday. Preserve a validated `YYYY-MM-DD` or a domain-specific year/month/day structure until an actual time and zone are chosen.
21
+ - Use `DateTime.makeZonedFromString` or `Schema.DateTimeZoned` when a serialized value must preserve its zone.
22
+ - Use `DateTime.toDateUtc`, `DateTime.toEpochMillis`, and `DateTime.fromDateUnsafe` only at interop boundaries.
23
+
24
+ ```ts
25
+ import { DateTime, Effect, Schema } from "effect";
26
+
27
+ const Timestamp = Schema.DateTimeUtcFromString;
28
+
29
+ const expiresAt = DateTime.makeUnsafe("2030-01-01T00:00:00Z");
30
+
31
+ const isExpired = Effect.gen(function* () {
32
+ const now = yield* DateTime.now;
33
+ return DateTime.isLessThanOrEqualTo(expiresAt, now);
34
+ });
35
+ ```
36
+
37
+ ## Instants, zones, and arithmetic
38
+
39
+ - Treat `DateTime.Utc` as an instant without retained zone information. Treat `DateTime.Zoned` as the same kind of instant plus a zone used for wall-clock parts, formatting, and zone-aware transformations.
40
+ - Use `DateTime.setZone` or `setZoneNamed` to view the same instant in another zone. When constructing from local wall-clock parts, use `makeZoned` with `adjustForTimeZone: true` and choose a deliberate `disambiguation` policy for DST gaps and repeated times.
41
+ - Use `DateTime.addDuration` and `subtractDuration` for elapsed time. Use `DateTime.add` and `subtract` for calendar arithmetic such as days, months, and years; these operations account for a zoned value's calendar rules.
42
+ - Use `DateTime.startOf`, `endOf`, or `nearest` rather than hand-editing fields. State `weekStartsOn` when week boundaries are domain-sensitive.
43
+ - Use `DateTime.Order`, `Equivalence`, `min`, `max`, `between`, and comparison helpers rather than comparing formatted strings or mutable `Date` objects.
44
+ - Use `formatIso` for UTC interchange, `formatIsoZoned` when preserving a zone, and `format` / `formatIntl` for presentation. Do not persist locale-formatted output.
45
+
46
+ ## Current time and tests
47
+
48
+ Code that asks what time it is must remain clock-driven:
49
+
50
+ ```ts
51
+ import { assert, it } from "@effect/vitest";
52
+ import { DateTime, Duration, Effect } from "effect";
53
+ import { TestClock } from "effect/testing";
54
+
55
+ it.effect("expires after an hour", () =>
56
+ Effect.gen(function* () {
57
+ yield* TestClock.setTime(DateTime.toEpochMillis(DateTime.makeUnsafe("2030-01-01T00:00:00Z")));
58
+ const startedAt = yield* DateTime.now;
59
+
60
+ yield* TestClock.adjust("1 hour");
61
+ const now = yield* DateTime.now;
62
+
63
+ assert.strictEqual(Duration.toMillis(DateTime.distance(startedAt, now)), 60 * 60 * 1000);
64
+ }),
65
+ );
66
+ ```
67
+
68
+ `@effect/vitest` supplies `TestClock` to `it.effect`. Fork work that sleeps, times out, retries, or follows a schedule before advancing the clock. Use `it.live` only when a test intentionally needs real time.
69
+
70
+ ## Completion check
71
+
72
+ Confirm that domain code uses `DateTime`, untrusted inputs are decoded, UTC versus zoned intent is explicit, DST behavior is deliberate, elapsed and calendar arithmetic are not confused, serialization is stable, and time-dependent tests use `TestClock` without wall-clock sleeps.
package/src/catalog.ts CHANGED
@@ -176,7 +176,8 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
176
176
  });
177
177
  }
178
178
  const families: Readonly<Record<string, ReadonlyArray<string>>> = {
179
- effect: ["effect-ts", "effect-atom-data-fetching"],
179
+ effect: ["effect-ts"],
180
+ "effect-atom-data-fetching": ["effect-ts"],
180
181
  ...Object.fromEntries(externalFamilies),
181
182
  };
182
183
 
package/src/index.ts CHANGED
@@ -11,10 +11,20 @@ export {
11
11
  EffectTsgoSetupSchema,
12
12
  type HarnessTarget,
13
13
  TargetConfigSchema,
14
+ type VitePlusQualityConfigSetup,
15
+ VitePlusQualityConfigSetupSchema,
14
16
  type VitePlusQualitySetup,
15
17
  VitePlusQualitySetupSchema,
18
+ type VitePlusQualityTypecheckSetup,
19
+ VitePlusQualityTypecheckSetupSchema,
20
+ type VitePlusQualityWorkflowSetup,
21
+ VitePlusQualityWorkflowSetupSchema,
22
+ type VitePlusQualityWorkflowStep,
23
+ VitePlusQualityWorkflowStepSchema,
16
24
  type VitePlusSetup,
17
25
  VitePlusSetupSchema,
26
+ type VitePlusTypecheckStrategy,
27
+ VitePlusTypecheckStrategySchema,
18
28
  } from "./manifest.ts";
19
29
  export {
20
30
  applyEffectSourcePlan,
package/src/manifest.ts CHANGED
@@ -53,8 +53,38 @@ export const VitePlusHooksSetupSchema = Schema.Struct({
53
53
  enabled: Schema.optional(Schema.Boolean),
54
54
  });
55
55
 
56
- export const VitePlusQualitySetupSchema = Schema.Struct({
56
+ export const VitePlusTypecheckStrategySchema = Schema.Literals(["single-project", "workspace"]);
57
+ export type VitePlusTypecheckStrategy = typeof VitePlusTypecheckStrategySchema.Type;
58
+
59
+ export const VitePlusQualityTypecheckSetupSchema = Schema.Struct({
60
+ strategy: Schema.optional(VitePlusTypecheckStrategySchema),
61
+ concurrency: Schema.optional(Schema.Int),
62
+ packages: Schema.optional(Schema.Array(Schema.String)),
63
+ });
64
+ export type VitePlusQualityTypecheckSetup = typeof VitePlusQualityTypecheckSetupSchema.Type;
65
+
66
+ export const VitePlusQualityConfigSetupSchema = Schema.Struct({
57
67
  enabled: Schema.optional(Schema.Boolean),
68
+ typecheck: Schema.optional(VitePlusQualityTypecheckSetupSchema),
69
+ });
70
+ export type VitePlusQualityConfigSetup = typeof VitePlusQualityConfigSetupSchema.Type;
71
+
72
+ export const VitePlusQualityWorkflowStepSchema = Schema.Struct({
73
+ name: Schema.String,
74
+ run: Schema.Array(Schema.String),
75
+ });
76
+ export type VitePlusQualityWorkflowStep = typeof VitePlusQualityWorkflowStepSchema.Type;
77
+
78
+ export const VitePlusQualityWorkflowSetupSchema = Schema.Struct({
79
+ enabled: Schema.optional(Schema.Boolean),
80
+ beforeChecks: Schema.optional(Schema.Array(VitePlusQualityWorkflowStepSchema)),
81
+ typecheck: Schema.optional(Schema.Array(Schema.String)),
82
+ });
83
+ export type VitePlusQualityWorkflowSetup = typeof VitePlusQualityWorkflowSetupSchema.Type;
84
+
85
+ export const VitePlusQualitySetupSchema = Schema.Struct({
86
+ config: Schema.optional(VitePlusQualityConfigSetupSchema),
87
+ workflow: Schema.optional(VitePlusQualityWorkflowSetupSchema),
58
88
  });
59
89
  export type VitePlusQualitySetup = typeof VitePlusQualitySetupSchema.Type;
60
90
 
@@ -123,7 +153,19 @@ export type NormalizedManifest = {
123
153
  readonly enabled: boolean;
124
154
  };
125
155
  readonly quality: {
126
- readonly enabled: boolean;
156
+ readonly config: {
157
+ readonly enabled: boolean;
158
+ readonly typecheck: {
159
+ readonly strategy: VitePlusTypecheckStrategy;
160
+ readonly concurrency: number;
161
+ readonly packages: ReadonlyArray<string>;
162
+ };
163
+ };
164
+ readonly workflow: {
165
+ readonly enabled: boolean;
166
+ readonly beforeChecks: ReadonlyArray<VitePlusQualityWorkflowStep>;
167
+ readonly typecheck: ReadonlyArray<string>;
168
+ };
127
169
  };
128
170
  };
129
171
  };
@@ -158,6 +200,8 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
158
200
  };
159
201
  }
160
202
  }
203
+ const quality = manifest.setup?.vitePlus?.quality;
204
+ const typecheck = quality?.config?.typecheck;
161
205
 
162
206
  return {
163
207
  exclude: manifest.exclude ?? [],
@@ -186,7 +230,19 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
186
230
  enabled: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
187
231
  },
188
232
  quality: {
189
- enabled: manifest.setup?.vitePlus?.quality?.enabled ?? false,
233
+ config: {
234
+ enabled: quality?.config?.enabled ?? false,
235
+ typecheck: {
236
+ strategy: typecheck?.strategy ?? "single-project",
237
+ concurrency: typecheck?.concurrency ?? 4,
238
+ packages: typecheck?.packages ?? [],
239
+ },
240
+ },
241
+ workflow: {
242
+ enabled: quality?.workflow?.enabled ?? false,
243
+ beforeChecks: quality?.workflow?.beforeChecks ?? [],
244
+ typecheck: quality?.workflow?.typecheck ?? ["vp run typecheck"],
245
+ },
190
246
  },
191
247
  },
192
248
  },
@@ -13,6 +13,7 @@ const ProjectPackageSchema = Schema.fromJsonString(
13
13
  devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
14
14
  optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
15
15
  peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
16
+ workspaces: Schema.optional(Schema.Unknown),
16
17
  }),
17
18
  );
18
19
 
package/src/sync.ts CHANGED
@@ -48,6 +48,8 @@ import {
48
48
  type VitePlusHooksPlan,
49
49
  } from "./vite-plus-hooks.ts";
50
50
  import {
51
+ renderVitePlusConfigTemplate,
52
+ renderVitePlusWorkflowTemplate,
51
53
  validateVitePlusQualitySupport,
52
54
  VITE_PLUS_CONFIG_PATH,
53
55
  VITE_PLUS_CONFIG_TEMPLATE,
@@ -223,7 +225,8 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
223
225
  }
224
226
 
225
227
  const SKILL_FAMILIES: SkillCatalog = {
226
- effect: ["effect-ts", "effect-atom-data-fetching"],
228
+ effect: ["effect-ts"],
229
+ "effect-atom-data-fetching": ["effect-ts"],
227
230
  };
228
231
 
229
232
  export const DEFAULT_MANIFEST = "dev-kit.jsonc";
@@ -632,34 +635,48 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
632
635
  linkTarget,
633
636
  });
634
637
  }
635
- if (setup.vitePlus.quality.enabled) {
636
- for (const generated of [
637
- {
638
- resourceId: "setup:vite-plus-config" as const,
639
- path: VITE_PLUS_CONFIG_PATH,
640
- sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
641
- },
642
- {
643
- resourceId: "setup:vite-plus-github-actions" as const,
644
- path: VITE_PLUS_GITHUB_ACTIONS_PATH,
645
- sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
646
- },
647
- ]) {
648
- const managed = yield* resolveManagedPath(projectDir, generated.path);
649
- const content = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
638
+ if (setup.vitePlus.quality.config.enabled) {
639
+ const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_CONFIG_PATH);
640
+ const template = yield* readGeneratedFileTemplate(packageRoot, VITE_PLUS_CONFIG_TEMPLATE);
641
+ const content = renderVitePlusConfigTemplate(template, setup.vitePlus.quality.config.typecheck);
650
642
 
651
- outputs.push({
652
- resourceId: generated.resourceId,
653
- path: managed.relative,
654
- sourcePath: generated.sourcePath,
655
- mode: "copy",
656
- kind: "file",
657
- digest: yield* digestFileContent(content),
658
- destination: managed.absolute,
659
- content,
660
- adoptIfExact: true,
661
- });
662
- }
643
+ outputs.push({
644
+ resourceId: "setup:vite-plus-config",
645
+ path: managed.relative,
646
+ sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
647
+ mode: "copy",
648
+ kind: "file",
649
+ digest: yield* digestFileContent(content),
650
+ destination: managed.absolute,
651
+ content,
652
+ adoptIfExact: true,
653
+ });
654
+ }
655
+ if (setup.vitePlus.quality.workflow.enabled) {
656
+ const managed = yield* resolveManagedPath(projectDir, VITE_PLUS_GITHUB_ACTIONS_PATH);
657
+ const template = yield* readGeneratedFileTemplate(
658
+ packageRoot,
659
+ VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
660
+ );
661
+ const content = renderVitePlusWorkflowTemplate(template, {
662
+ devKitCommand:
663
+ projectDir === packageRoot
664
+ ? "./bin/dev-kit.mjs apply --locked"
665
+ : "vp exec dev-kit apply --locked",
666
+ workflow: setup.vitePlus.quality.workflow,
667
+ });
668
+
669
+ outputs.push({
670
+ resourceId: "setup:vite-plus-github-actions",
671
+ path: managed.relative,
672
+ sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
673
+ mode: "copy",
674
+ kind: "file",
675
+ digest: yield* digestFileContent(content),
676
+ destination: managed.absolute,
677
+ content,
678
+ adoptIfExact: true,
679
+ });
663
680
  }
664
681
  const agentsTarget = targets.agents;
665
682
  const duplicateOutput = skills.find(
@@ -907,17 +924,31 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
907
924
  const packageRoot = yield* resolvePackageRoot();
908
925
  const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
909
926
 
910
- if (manifest.setup.vitePlus.quality.enabled) {
927
+ const vitePlusQuality = manifest.setup.vitePlus.quality;
928
+ const vitePlusQualityEnabled = vitePlusQuality.config.enabled || vitePlusQuality.workflow.enabled;
929
+
930
+ if (vitePlusQualityEnabled) {
911
931
  if (!manifest.setup.effectTsgo.enabled) {
912
932
  return yield* new InvalidProjectStateError({
913
933
  message:
914
- "setup.vitePlus.quality requires setup.effectTsgo.enabled so vp run typecheck uses the Effect-patched compiler",
934
+ "setup.vitePlus.quality requires setup.effectTsgo.enabled so managed quality setup converges the Effect-patched compiler",
915
935
  });
916
936
  }
917
937
  yield* validateVitePlusQualitySupport(
918
938
  projectDir,
919
939
  packageRoot,
920
940
  manifest.setup.effectTsgo.typescriptPackage,
941
+ {
942
+ ...(vitePlusQuality.config.enabled ? { config: vitePlusQuality.config.typecheck } : {}),
943
+ ...(vitePlusQuality.workflow.enabled
944
+ ? {
945
+ workflow: {
946
+ beforeChecks: vitePlusQuality.workflow.beforeChecks,
947
+ typecheck: vitePlusQuality.workflow.typecheck,
948
+ },
949
+ }
950
+ : {}),
951
+ },
921
952
  );
922
953
  }
923
954
  const effectSource = manifest.setup.effectSource.enabled
@@ -1,3 +1,5 @@
1
1
  import packageMetadata from "../package.json" with { type: "json" };
2
2
 
3
3
  export const DEV_KIT_VERSION = packageMetadata.version;
4
+ export const VITE_PLUS_TESTED_VERSION = packageMetadata.devDependencies["vite-plus"];
5
+ export const VITE_PLUS_SUPPORTED_RANGE = packageMetadata.peerDependencies["vite-plus"];
@@ -0,0 +1,70 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+ import semver from "semver";
3
+
4
+ import { readDirectDependencyNames } from "./project-package.ts";
5
+ import { VITE_PLUS_SUPPORTED_RANGE } from "./tool-metadata.ts";
6
+
7
+ const InstalledVitePlusPackageSchema = Schema.fromJsonString(
8
+ Schema.Struct({ version: Schema.String }),
9
+ );
10
+
11
+ export class VitePlusDependencyError extends Schema.TaggedErrorClass<VitePlusDependencyError>()(
12
+ "VitePlusDependencyError",
13
+ { message: Schema.String },
14
+ ) {}
15
+
16
+ export type InstalledVitePlus = {
17
+ readonly version: string;
18
+ readonly vpBin: string;
19
+ };
20
+
21
+ export const validateInstalledVitePlus = Effect.fn("validateInstalledVitePlus")(function* (
22
+ projectDir: string,
23
+ ) {
24
+ const fs = yield* FileSystem.FileSystem;
25
+ const path = yield* Path.Path;
26
+ const dependencies = yield* readDirectDependencyNames(projectDir);
27
+
28
+ if (!dependencies.includes("vite-plus")) {
29
+ return yield* new VitePlusDependencyError({
30
+ message: "vite-plus must be a direct project dependency",
31
+ });
32
+ }
33
+ const packagePath = path.join(projectDir, "node_modules", "vite-plus", "package.json");
34
+
35
+ if (!(yield* fs.exists(packagePath))) {
36
+ return yield* new VitePlusDependencyError({
37
+ message:
38
+ "vite-plus must be installed before enabling this setup: node_modules/vite-plus/package.json is missing",
39
+ });
40
+ }
41
+ const installed = yield* fs.readFileString(packagePath).pipe(
42
+ Effect.flatMap(Schema.decodeUnknownEffect(InstalledVitePlusPackageSchema)),
43
+ Effect.mapError(
44
+ () =>
45
+ new VitePlusDependencyError({
46
+ message: "installed vite-plus package metadata has no valid version",
47
+ }),
48
+ ),
49
+ );
50
+
51
+ if (!semver.valid(installed.version)) {
52
+ return yield* new VitePlusDependencyError({
53
+ message: `installed vite-plus package metadata has invalid version: ${installed.version}`,
54
+ });
55
+ }
56
+ if (!semver.satisfies(installed.version, VITE_PLUS_SUPPORTED_RANGE)) {
57
+ return yield* new VitePlusDependencyError({
58
+ message: `installed vite-plus ${installed.version} is incompatible with @danieljvdm/dev-kit; supported range: ${VITE_PLUS_SUPPORTED_RANGE}`,
59
+ });
60
+ }
61
+ const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
62
+
63
+ if (!(yield* fs.exists(vpBin))) {
64
+ return yield* new VitePlusDependencyError({
65
+ message: "vite-plus is installed but node_modules/.bin/vp is missing",
66
+ });
67
+ }
68
+
69
+ return { version: installed.version, vpBin } satisfies InstalledVitePlus;
70
+ });
@@ -1,7 +1,7 @@
1
1
  import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
 
4
- import { readDirectDependencyNames } from "./project-package.ts";
4
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
5
5
 
6
6
  export const VITE_PLUS_HOOKS_DIR = ".vite-hooks";
7
7
  export const VITE_PLUS_HOOKS_PATH = `${VITE_PLUS_HOOKS_DIR}/_`;
@@ -121,23 +121,14 @@ const inspectVitePlusHooks = Effect.fn("inspectVitePlusHooks")(function* (projec
121
121
  });
122
122
 
123
123
  export const planVitePlusHooks = Effect.fn("planVitePlusHooks")(function* (projectDir: string) {
124
- const fs = yield* FileSystem.FileSystem;
125
- const path = yield* Path.Path;
126
- const dependencies = yield* readDirectDependencyNames(projectDir);
127
-
128
- if (!dependencies.includes("vite-plus")) {
129
- return yield* new VitePlusHooksDependencyError({
130
- message: "vite-plus must be a direct project dependency before enabling setup.vitePlus.hooks",
131
- });
132
- }
133
- const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
134
-
135
- if (!(yield* fs.exists(vpBin))) {
136
- return yield* new VitePlusHooksDependencyError({
137
- message:
138
- "vite-plus must be installed before enabling setup.vitePlus.hooks: node_modules/.bin/vp is missing",
139
- });
140
- }
124
+ const { vpBin } = yield* validateInstalledVitePlus(projectDir).pipe(
125
+ Effect.mapError(
126
+ (error) =>
127
+ new VitePlusHooksDependencyError({
128
+ message: `${error.message} before enabling setup.vitePlus.hooks`,
129
+ }),
130
+ ),
131
+ );
141
132
  const viteGitHooks = yield* Config.string("VITE_GIT_HOOKS").pipe(Config.withDefault(""));
142
133
  const husky = yield* Config.string("HUSKY").pipe(Config.withDefault(""));
143
134
  const action =
@@ -1,6 +1,9 @@
1
1
  import { Effect, FileSystem, Path, Schema } from "effect";
2
+ import { parse as parseJsonc, type ParseError } from "jsonc-parser";
2
3
 
4
+ import type { VitePlusQualityWorkflowStep, VitePlusTypecheckStrategy } from "./manifest.ts";
3
5
  import { readDirectDependencyNames, readProjectPackage } from "./project-package.ts";
6
+ import { validateInstalledVitePlus } from "./vite-plus-dependency.ts";
4
7
 
5
8
  export const VITE_PLUS_CONFIG_PATH = "vite.config.ts";
6
9
  export const VITE_PLUS_CONFIG_TEMPLATE = "templates/vite-plus/vite.config.ts";
@@ -12,13 +15,136 @@ export class VitePlusQualitySupportError extends Schema.TaggedErrorClass<VitePlu
12
15
  { message: Schema.String },
13
16
  ) {}
14
17
 
18
+ export type VitePlusQualityTypecheck = {
19
+ readonly strategy: VitePlusTypecheckStrategy;
20
+ readonly concurrency: number;
21
+ readonly packages: ReadonlyArray<string>;
22
+ };
23
+
24
+ export type VitePlusQualityWorkflow = {
25
+ readonly beforeChecks: ReadonlyArray<VitePlusQualityWorkflowStep>;
26
+ readonly typecheck: ReadonlyArray<string>;
27
+ };
28
+
29
+ export type VitePlusQualitySelection = {
30
+ readonly config?: VitePlusQualityTypecheck;
31
+ readonly workflow?: VitePlusQualityWorkflow;
32
+ };
33
+
34
+ const SINGLE_PROJECT_TYPECHECK_TASK = ' typecheck: "tsc --noEmit",';
35
+ const LOCKED_DEV_KIT_COMMAND = "vp exec dev-kit apply --locked";
36
+ const BEFORE_CHECKS_MARKER =
37
+ " # Dev Kit inserts configured quality.workflow.beforeChecks steps here.\n\n";
38
+ const DEFAULT_WORKFLOW_TYPECHECK = ` - name: Type check with Effect TypeScript-Go
39
+ run: vp run typecheck`;
40
+
41
+ const replaceUniqueTemplateMarker = (
42
+ template: string,
43
+ marker: string,
44
+ replacement: string,
45
+ ): string => {
46
+ const parts = template.split(marker);
47
+
48
+ if (parts.length !== 2) {
49
+ throw new Error(`expected exactly one generated template marker: ${marker}`);
50
+ }
51
+
52
+ return `${parts[0]}${replacement}${parts[1]}`;
53
+ };
54
+
55
+ const shellQuote = (value: string): string => `'${value.replaceAll("'", `'"'"'`)}'`;
56
+
57
+ export const renderVitePlusConfigTemplate = (
58
+ template: string,
59
+ typecheck: VitePlusQualityTypecheck,
60
+ ): string => {
61
+ if (typecheck.strategy === "single-project") return template;
62
+ const filters = typecheck.packages
63
+ .map((packageDir) => `--filter ${shellQuote(`./${packageDir}`)}`)
64
+ .join(" ");
65
+ const command = `vp run --cache --concurrency-limit ${typecheck.concurrency} ${filters} --fail-if-no-match typecheck`;
66
+
67
+ return replaceUniqueTemplateMarker(
68
+ template,
69
+ SINGLE_PROJECT_TYPECHECK_TASK,
70
+ ` typecheck: {
71
+ command: ${JSON.stringify(command)},
72
+ cache: false,
73
+ },`,
74
+ );
75
+ };
76
+
77
+ export const renderVitePlusWorkflowTemplate = (
78
+ template: string,
79
+ options: {
80
+ readonly devKitCommand?: string;
81
+ readonly workflow?: VitePlusQualityWorkflow;
82
+ } = {},
83
+ ): string => {
84
+ const devKitCommand = options.devKitCommand;
85
+ const workflow = options.workflow;
86
+ let rendered =
87
+ devKitCommand === undefined || devKitCommand === LOCKED_DEV_KIT_COMMAND
88
+ ? template
89
+ : replaceUniqueTemplateMarker(template, LOCKED_DEV_KIT_COMMAND, devKitCommand);
90
+
91
+ if (workflow !== undefined) {
92
+ const steps = workflow.beforeChecks
93
+ .map((step) => {
94
+ const commands = step.run
95
+ .flatMap((command) => command.split("\n"))
96
+ .map((line) => ` ${line}`)
97
+ .join("\n");
98
+
99
+ return ` - name: ${JSON.stringify(step.name)}
100
+ run: |
101
+ ${commands}`;
102
+ })
103
+ .join("\n\n");
104
+
105
+ rendered = replaceUniqueTemplateMarker(
106
+ rendered,
107
+ BEFORE_CHECKS_MARKER,
108
+ steps.length === 0 ? "" : `${steps}\n\n`,
109
+ );
110
+ }
111
+ if (
112
+ workflow !== undefined &&
113
+ (workflow.typecheck.length !== 1 || workflow.typecheck[0] !== "vp run typecheck")
114
+ ) {
115
+ const commands = workflow.typecheck
116
+ .flatMap((command) => command.split("\n"))
117
+ .map((line) => ` ${line}`)
118
+ .join("\n");
119
+
120
+ rendered = replaceUniqueTemplateMarker(
121
+ rendered,
122
+ DEFAULT_WORKFLOW_TYPECHECK,
123
+ ` - name: Type check with Effect TypeScript-Go
124
+ run: |
125
+ ${commands}`,
126
+ );
127
+ }
128
+
129
+ return rendered;
130
+ };
131
+
15
132
  export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQualitySupport")(
16
- function* (projectDir: string, packageRoot: string, typescriptPackage: string) {
133
+ function* (
134
+ projectDir: string,
135
+ packageRoot: string,
136
+ typescriptPackage: string,
137
+ selection: VitePlusQualitySelection,
138
+ ) {
17
139
  const fs = yield* FileSystem.FileSystem;
18
140
  const path = yield* Path.Path;
19
141
  const packageJson = yield* readProjectPackage(projectDir);
20
142
  const dependencies = yield* readDirectDependencyNames(projectDir);
21
- const required = new Set(["vite-plus", "effect", "@effect/tsgo", typescriptPackage]);
143
+ const required = new Set(["effect", "@effect/tsgo", typescriptPackage]);
144
+
145
+ yield* validateInstalledVitePlus(projectDir).pipe(
146
+ Effect.mapError((error) => new VitePlusQualitySupportError({ message: error.message })),
147
+ );
22
148
 
23
149
  if (projectDir !== packageRoot) required.add("@danieljvdm/dev-kit");
24
150
  const missing = [...required].filter((dependency) => !dependencies.includes(dependency));
@@ -28,22 +154,145 @@ export const validateVitePlusQualitySupport = Effect.fn("validateVitePlusQuality
28
154
  message: `setup.vitePlus.quality requires direct dependencies: ${missing.join(", ")}`,
29
155
  });
30
156
  }
157
+ if (selection.workflow !== undefined) {
158
+ if (selection.workflow.typecheck.length === 0) {
159
+ return yield* new VitePlusQualitySupportError({
160
+ message: "setup.vitePlus.quality.workflow.typecheck requires at least one command",
161
+ });
162
+ }
163
+ for (const command of selection.workflow.typecheck) {
164
+ if (command.trim().length === 0) {
165
+ return yield* new VitePlusQualitySupportError({
166
+ message: "setup.vitePlus.quality.workflow.typecheck commands must not be empty",
167
+ });
168
+ }
169
+ }
170
+ for (const step of selection.workflow.beforeChecks) {
171
+ if (step.name.trim().length === 0 || step.run.length === 0) {
172
+ return yield* new VitePlusQualitySupportError({
173
+ message:
174
+ "setup.vitePlus.quality.workflow.beforeChecks steps require a name and at least one command",
175
+ });
176
+ }
177
+ if (step.run.some((command) => command.trim().length === 0)) {
178
+ return yield* new VitePlusQualitySupportError({
179
+ message: "setup.vitePlus.quality.workflow.beforeChecks commands must not be empty",
180
+ });
181
+ }
182
+ }
183
+ }
184
+ const typecheck = selection.config;
185
+
186
+ if (typecheck === undefined) return;
31
187
  const conflictingScripts = ["check", "typecheck"].filter(
32
188
  (script) => packageJson.scripts?.[script] !== undefined,
33
189
  );
34
190
 
35
191
  if (conflictingScripts.length > 0) {
36
192
  return yield* new VitePlusQualitySupportError({
37
- message: `setup.vitePlus.quality defines Vite tasks that conflict with package scripts: ${conflictingScripts.join(", ")}`,
193
+ message: `setup.vitePlus.quality.config defines Vite tasks that conflict with package scripts: ${conflictingScripts.join(", ")}`,
194
+ });
195
+ }
196
+ if (typecheck.concurrency < 1 || typecheck.concurrency > 32) {
197
+ return yield* new VitePlusQualitySupportError({
198
+ message: "setup.vitePlus.quality.config typecheck concurrency must be between 1 and 32",
199
+ });
200
+ }
201
+ if (typecheck.strategy === "single-project" && typecheck.packages.length > 0) {
202
+ return yield* new VitePlusQualitySupportError({
203
+ message:
204
+ "setup.vitePlus.quality.config single-project typechecking does not accept workspace packages",
38
205
  });
39
206
  }
40
- const vpBin = path.join(projectDir, "node_modules", ".bin", "vp");
207
+ const workspacePatterns = Array.isArray(packageJson.workspaces)
208
+ ? packageJson.workspaces
209
+ : typeof packageJson.workspaces === "object" && packageJson.workspaces !== null
210
+ ? (packageJson.workspaces as { readonly packages?: unknown }).packages
211
+ : undefined;
41
212
 
42
- if (!(yield* fs.exists(vpBin))) {
213
+ if (
214
+ typecheck.strategy === "workspace" &&
215
+ (!Array.isArray(workspacePatterns) ||
216
+ workspacePatterns.length === 0 ||
217
+ !workspacePatterns.every((pattern) => typeof pattern === "string"))
218
+ ) {
43
219
  return yield* new VitePlusQualitySupportError({
44
220
  message:
45
- "vite-plus must be installed before enabling setup.vitePlus.quality: node_modules/.bin/vp is missing",
221
+ "setup.vitePlus.quality.config workspace typechecking requires package.json workspaces",
46
222
  });
47
223
  }
224
+ if (typecheck.strategy === "workspace") {
225
+ if (typecheck.packages.length === 0) {
226
+ return yield* new VitePlusQualitySupportError({
227
+ message:
228
+ "setup.vitePlus.quality.config workspace typechecking requires explicit package directories",
229
+ });
230
+ }
231
+ const uniquePackages = new Set(typecheck.packages);
232
+
233
+ if (uniquePackages.size !== typecheck.packages.length) {
234
+ return yield* new VitePlusQualitySupportError({
235
+ message: "setup.vitePlus.quality.config workspace typecheck packages must be unique",
236
+ });
237
+ }
238
+ for (const packageDir of typecheck.packages) {
239
+ const absolute = path.resolve(projectDir, packageDir);
240
+ const relative = path.relative(projectDir, absolute);
241
+
242
+ if (
243
+ packageDir.length === 0 ||
244
+ path.isAbsolute(packageDir) ||
245
+ relative.length === 0 ||
246
+ relative === ".." ||
247
+ relative.startsWith(`..${path.sep}`)
248
+ ) {
249
+ return yield* new VitePlusQualitySupportError({
250
+ message: `setup.vitePlus.quality.config workspace package must be a project-relative subdirectory: ${packageDir}`,
251
+ });
252
+ }
253
+ const workspacePackage = yield* readProjectPackage(absolute).pipe(
254
+ Effect.mapError(
255
+ () =>
256
+ new VitePlusQualitySupportError({
257
+ message: `setup.vitePlus.quality.config workspace package is missing a valid package.json: ${packageDir}`,
258
+ }),
259
+ ),
260
+ );
261
+
262
+ if (workspacePackage.scripts?.typecheck === undefined) {
263
+ return yield* new VitePlusQualitySupportError({
264
+ message: `setup.vitePlus.quality.config workspace package requires a typecheck script: ${packageDir}`,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ if (typecheck.strategy === "single-project") {
270
+ const tsconfigPath = path.join(projectDir, "tsconfig.json");
271
+
272
+ if (!(yield* fs.exists(tsconfigPath))) {
273
+ return yield* new VitePlusQualitySupportError({
274
+ message:
275
+ "setup.vitePlus.quality.config single-project typechecking requires tsconfig.json",
276
+ });
277
+ }
278
+ const errors: Array<ParseError> = [];
279
+ const tsconfig = parseJsonc(yield* fs.readFileString(tsconfigPath), errors, {
280
+ allowTrailingComma: true,
281
+ });
282
+
283
+ if (errors.length > 0 || typeof tsconfig !== "object" || tsconfig === null) {
284
+ return yield* new VitePlusQualitySupportError({
285
+ message: "setup.vitePlus.quality.config requires a valid root tsconfig.json",
286
+ });
287
+ }
288
+ const references = (tsconfig as { readonly references?: unknown }).references;
289
+
290
+ if (Array.isArray(references) && references.length > 0) {
291
+ return yield* new VitePlusQualitySupportError({
292
+ message:
293
+ "setup.vitePlus.quality.config single-project typechecking does not support tsconfig project references; select the workspace strategy or use a custom Vite config",
294
+ });
295
+ }
296
+ }
48
297
  },
49
298
  );
@@ -22,14 +22,21 @@ jobs:
22
22
  with:
23
23
  persist-credentials: false
24
24
 
25
- - name: Set up Vite+
26
- uses: voidzero-dev/setup-vp@v1
25
+ # setup-vp's v1 tag is frozen. Keep this v1.16.1 commit current with
26
+ # Renovate or Dependabot so fixes arrive through reviewed pull requests.
27
+ - name: Set up Vite+ and install dependencies
28
+ uses: voidzero-dev/setup-vp@143f5f385f39b1b753ffed1a01ad443811855c8b # v1.16.1
27
29
  with:
30
+ # Vite+ version intentionally resolves from the consumer manifest/lock.
28
31
  node-version: "24"
29
32
  cache: true
33
+ run-install: |
34
+ args: ["--frozen-lockfile", "--ignore-scripts"]
30
35
 
31
- - name: Install dependencies
32
- run: vp install
36
+ - name: Verify locked Dev Kit setup
37
+ run: vp exec dev-kit apply --locked
38
+
39
+ # Dev Kit inserts configured quality.workflow.beforeChecks steps here.
33
40
 
34
41
  - name: Check formatting
35
42
  run: vp fmt --check
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Effect Atom Data Fetching"
3
- short_description: "Cache HTTP data safely with Effect Atom"
4
- default_prompt: "Use $effect-atom-data-fetching to design or debug Effect Atom HTTP queries, caching, polling, invalidation, and SSR integration in a React app."