@danieljvdm/dev-kit 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +123 -56
  2. package/dev-kit.example.jsonc +7 -3
  3. package/package.json +19 -16
  4. package/schema/dev-kit.schema.json +38 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +52 -17
  8. package/skills/effect-ts/agents/openai.yaml +0 -1
  9. package/skills/effect-ts/references/audit-services.md +11 -11
  10. package/skills/effect-ts/references/guide-effect.md +56 -69
  11. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  12. package/skills/effect-ts/references/guide-layers.md +187 -215
  13. package/skills/effect-ts/references/guide-observability.md +91 -116
  14. package/skills/effect-ts/references/guide-retries.md +32 -44
  15. package/skills/effect-ts/references/guide-schedule.md +26 -40
  16. package/skills/effect-ts/references/guide-schema.md +50 -57
  17. package/skills/effect-ts/references/guide-sql.md +47 -50
  18. package/skills/effect-ts/references/guide-testing.md +96 -98
  19. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  20. package/skills/effect-ts/references/version-and-source.md +0 -1
  21. package/src/bin/dev-kit.ts +61 -28
  22. package/src/catalog-manager.ts +86 -34
  23. package/src/catalog.ts +71 -33
  24. package/src/cli-ui.ts +20 -16
  25. package/src/effect-source.ts +49 -19
  26. package/src/effect-tsgo.ts +66 -35
  27. package/src/gitignore.ts +19 -6
  28. package/src/index.ts +6 -0
  29. package/src/manifest.ts +38 -3
  30. package/src/node-symbolic-link.ts +3 -0
  31. package/src/oxlint-plugin-effect.js +3 -0
  32. package/src/oxlint-plugin-style.d.ts +8 -0
  33. package/src/oxlint-plugin-style.js +8 -0
  34. package/src/oxlint.js +14 -0
  35. package/src/oxlint.ts +14 -0
  36. package/src/package-skill-source.ts +189 -52
  37. package/src/path-digest.ts +47 -13
  38. package/src/project-package.ts +44 -19
  39. package/src/project-process-lock.ts +19 -12
  40. package/src/project-state.ts +11 -0
  41. package/src/skill-manager.ts +134 -55
  42. package/src/skill-selector.ts +8 -2
  43. package/src/source-manifest.ts +2 -6
  44. package/src/sync.ts +417 -107
  45. package/src/vendor.ts +112 -42
  46. package/src/vite-plus-hooks.ts +174 -0
  47. package/src/vite-plus-quality.ts +49 -0
  48. package/templates/vite-plus/github-actions-check.yml +44 -0
  49. package/templates/vite-plus/vite.config.ts +22 -0
@@ -1,9 +1,9 @@
1
1
  import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
 
4
- import { acquireProjectProcessLock } from "./project-process-lock.ts";
5
4
  import { printStatus, withSpinner } from "./cli-ui.ts";
6
5
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
6
+ import { acquireProjectProcessLock } from "./project-process-lock.ts";
7
7
  import { isTypeScriptPackageName } from "./typescript-package-name.ts";
8
8
 
9
9
  export const DEFAULT_EFFECT_REPOSITORY = "https://github.com/Effect-TS/effect.git";
@@ -55,9 +55,7 @@ class EffectSourceCommandError extends Schema.TaggedErrorClass<EffectSourceComma
55
55
 
56
56
  const PackageVersionSchema = Schema.fromJsonString(
57
57
  Schema.Struct({
58
- version: Schema.String.check(
59
- Schema.isPattern(/^[0-9A-Za-z][0-9A-Za-z.+-]*$/),
60
- ),
58
+ version: Schema.String.check(Schema.isPattern(/^[0-9A-Za-z][0-9A-Za-z.+-]*$/)),
61
59
  }),
62
60
  );
63
61
 
@@ -76,6 +74,7 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
76
74
  child.exitCode,
77
75
  ]);
78
76
  const trimmed = output.trim();
77
+
79
78
  if (exitCode !== 0) {
80
79
  return yield* new EffectSourceCommandError({
81
80
  command: [command, ...args].join(" "),
@@ -83,11 +82,11 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
83
82
  output: trimmed,
84
83
  });
85
84
  }
85
+
86
86
  return trimmed;
87
87
  });
88
88
 
89
- const runGit = (cwd: string, args: ReadonlyArray<string>) =>
90
- runCommand(cwd, "git", args);
89
+ const runGit = (cwd: string, args: ReadonlyArray<string>) => runCommand(cwd, "git", args);
91
90
 
92
91
  const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function* (
93
92
  projectDir: string,
@@ -101,11 +100,14 @@ const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function*
101
100
  ...packageName.split("/"),
102
101
  "package.json",
103
102
  );
104
- const contents = yield* fs.readFileString(manifestPath).pipe(
105
- Effect.catchReason("PlatformError", "NotFound", () =>
106
- Effect.fail(new EffectSourceDependencyError({ packageName })),
107
- ),
108
- );
103
+ const contents = yield* fs
104
+ .readFileString(manifestPath)
105
+ .pipe(
106
+ Effect.catchReason("PlatformError", "NotFound", () =>
107
+ Effect.fail(new EffectSourceDependencyError({ packageName })),
108
+ ),
109
+ );
110
+
109
111
  return yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
110
112
  Effect.mapError(() => new EffectSourceDependencyError({ packageName })),
111
113
  Effect.map((manifest) => manifest.version),
@@ -117,6 +119,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
117
119
  candidate: string,
118
120
  ) {
119
121
  const path = yield* Path.Path;
122
+
120
123
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
121
124
  return yield* new EffectSourceCheckoutError({
122
125
  message: `Effect source path must be a non-empty project-relative path: ${candidate}`,
@@ -124,6 +127,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
124
127
  }
125
128
  const checkoutDir = path.resolve(projectDir, candidate);
126
129
  const relative = path.relative(projectDir, checkoutDir);
130
+
127
131
  if (
128
132
  relative.length === 0 ||
129
133
  relative === ".." ||
@@ -135,6 +139,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
135
139
  });
136
140
  }
137
141
  let ancestor = projectDir;
142
+
138
143
  for (const segment of relative.split(path.sep).slice(0, -1)) {
139
144
  ancestor = path.join(ancestor, segment);
140
145
  if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
@@ -143,6 +148,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
143
148
  });
144
149
  }
145
150
  }
151
+
146
152
  return {
147
153
  checkoutDir,
148
154
  path: path.sep === "/" ? relative : relative.split(path.sep).join("/"),
@@ -155,6 +161,7 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
155
161
  tag: string,
156
162
  ) {
157
163
  const fs = yield* FileSystem.FileSystem;
164
+
158
165
  if ((yield* observeSymbolicLink(checkoutDir)).kind === "symlink") {
159
166
  return yield* new EffectSourceCheckoutError({
160
167
  message: `Effect source destination is a symlink: ${checkoutDir}`,
@@ -163,19 +170,22 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
163
170
  if (!(yield* fs.exists(checkoutDir))) return "sync" as const;
164
171
 
165
172
  const actualRoot = yield* runGit(checkoutDir, ["rev-parse", "--show-toplevel"]).pipe(
166
- Effect.mapError(() =>
167
- new EffectSourceCheckoutError({
168
- message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
169
- }),
173
+ Effect.mapError(
174
+ () =>
175
+ new EffectSourceCheckoutError({
176
+ message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
177
+ }),
170
178
  ),
171
179
  );
172
180
  const expectedRoot = yield* fs.realPath(checkoutDir);
181
+
173
182
  if ((yield* fs.realPath(actualRoot)) !== expectedRoot) {
174
183
  return yield* new EffectSourceCheckoutError({
175
184
  message: `Effect source destination is nested inside another Git checkout: ${checkoutDir}`,
176
185
  });
177
186
  }
178
187
  const remote = yield* runGit(checkoutDir, ["remote", "get-url", "origin"]);
188
+
179
189
  if (remote !== repository) {
180
190
  return yield* new EffectSourceCheckoutError({
181
191
  message: `Effect source origin is ${remote}; expected ${repository}`,
@@ -188,17 +198,21 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
188
198
  "--verify",
189
199
  `${tag}^{commit}`,
190
200
  ]).pipe(Effect.catchTag("EffectSourceCommandError", () => Effect.void));
201
+
191
202
  if (target !== undefined) {
192
203
  const current = yield* runGit(checkoutDir, ["rev-parse", "HEAD"]);
204
+
193
205
  if (current === target) return "unchanged" as const;
194
206
  }
195
207
 
196
208
  const dirty = yield* runGit(checkoutDir, ["status", "--porcelain", "--untracked-files=all"]);
209
+
197
210
  if (dirty.length > 0) {
198
211
  return yield* new EffectSourceCheckoutError({
199
212
  message: `Effect source checkout has local changes; refusing to switch ${checkoutDir} to ${tag}`,
200
213
  });
201
214
  }
215
+
202
216
  return "sync" as const;
203
217
  });
204
218
 
@@ -209,14 +223,18 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
209
223
  const path = yield* Path.Path;
210
224
  const projectDir = yield* fs.realPath(path.resolve(options.projectDir ?? "."));
211
225
  const packageName = options.packageName ?? "effect";
226
+
212
227
  if (!isTypeScriptPackageName(packageName)) {
213
228
  return yield* new EffectSourceCheckoutError({
214
229
  message: `invalid Effect source package name: ${packageName}`,
215
230
  });
216
231
  }
217
232
  const repository = options.repository ?? DEFAULT_EFFECT_REPOSITORY;
233
+
218
234
  if (repository.length === 0) {
219
- return yield* new EffectSourceCheckoutError({ message: "Effect source repository cannot be empty" });
235
+ return yield* new EffectSourceCheckoutError({
236
+ message: "Effect source repository cannot be empty",
237
+ });
220
238
  }
221
239
  const resolved = yield* resolveCheckoutPath(
222
240
  projectDir,
@@ -225,9 +243,11 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
225
243
  const packageVersion = yield* readPackageVersion(projectDir, packageName);
226
244
  const tag = `effect@${packageVersion}`;
227
245
  const ci = yield* Config.string("CI").pipe(Config.withDefault(""));
228
- const action = ci === "true" || ci === "1"
229
- ? "skipped" as const
230
- : yield* inspectExistingCheckout(resolved.checkoutDir, repository, tag);
246
+ const action =
247
+ ci === "true" || ci === "1"
248
+ ? ("skipped" as const)
249
+ : yield* inspectExistingCheckout(resolved.checkoutDir, repository, tag);
250
+
231
251
  return {
232
252
  action,
233
253
  checkoutDir: resolved.checkoutDir,
@@ -246,14 +266,17 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
246
266
  if (plan.action !== "sync") return;
247
267
  const fs = yield* FileSystem.FileSystem;
248
268
  const path = yield* Path.Path;
269
+
249
270
  if (!(yield* fs.exists(plan.checkoutDir))) {
250
271
  const parent = path.dirname(plan.checkoutDir);
272
+
251
273
  yield* fs.makeDirectory(parent, { recursive: true });
252
274
  const tempDir = yield* fs.makeTempDirectoryScoped({
253
275
  directory: parent,
254
276
  prefix: ".dev-kit-effect-source-",
255
277
  });
256
278
  const staged = path.join(tempDir, "checkout");
279
+
257
280
  yield* runGit(plan.projectDir, [
258
281
  "clone",
259
282
  "--depth",
@@ -271,6 +294,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
271
294
  });
272
295
  }
273
296
  yield* fs.rename(staged, plan.checkoutDir);
297
+
274
298
  return;
275
299
  }
276
300
 
@@ -289,6 +313,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
289
313
  "--verify",
290
314
  `${plan.tag}^{commit}`,
291
315
  ]);
316
+
292
317
  yield* runGit(plan.checkoutDir, ["checkout", "--detach", target]);
293
318
  });
294
319
 
@@ -297,8 +322,10 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
297
322
  ) {
298
323
  const plan = yield* planEffectSource(options);
299
324
  const detail = `${plan.tag} → ${plan.path}`;
325
+
300
326
  if (plan.action === "skipped") {
301
327
  yield* printStatus("plan", "Effect source skipped", "CI");
328
+
302
329
  return;
303
330
  }
304
331
  if (options.dryRun) {
@@ -307,14 +334,17 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
307
334
  plan.action === "sync" ? "Would sync Effect source" : "Effect source up to date",
308
335
  detail,
309
336
  );
337
+
310
338
  return;
311
339
  }
312
340
  if (plan.action === "unchanged") {
313
341
  yield* printStatus("success", "Effect source up to date", detail);
342
+
314
343
  return;
315
344
  }
316
345
  yield* acquireProjectProcessLock(plan.projectDir);
317
346
  const replanned = yield* planEffectSource(options);
347
+
318
348
  if (JSON.stringify(plan) !== JSON.stringify(replanned)) {
319
349
  return yield* new EffectSourceCheckoutError({
320
350
  message: "Effect source checkout changed after planning; rerun the command",
@@ -1,8 +1,8 @@
1
1
  import { Crypto, Effect, Encoding, FileSystem, Path, Schema, Stream } from "effect";
2
2
  import { ChildProcess } from "effect/unstable/process";
3
3
 
4
- import { acquireProjectProcessLock } from "./project-process-lock.ts";
5
4
  import { printStatus, withSpinner } from "./cli-ui.ts";
5
+ import { acquireProjectProcessLock } from "./project-process-lock.ts";
6
6
  import { isTypeScriptPackageName } from "./typescript-package-name.ts";
7
7
 
8
8
  export const EFFECT_TSGO_VERSION = "0.24.3";
@@ -65,9 +65,7 @@ export class InvalidEffectTsgoPackageNameError extends Schema.TaggedErrorClass<I
65
65
  }
66
66
  }
67
67
 
68
- const PackageVersionSchema = Schema.fromJsonString(
69
- Schema.Struct({ version: Schema.String }),
70
- );
68
+ const PackageVersionSchema = Schema.fromJsonString(Schema.Struct({ version: Schema.String }));
71
69
 
72
70
  const packagePath = (path: Path.Path, projectDir: string, packageName: string): string =>
73
71
  path.join(projectDir, "node_modules", ...packageName.split("/"), "package.json");
@@ -80,18 +78,17 @@ const readExactPackageVersion = Effect.fn("readExactEffectTsgoPackageVersion")(f
80
78
  const fs = yield* FileSystem.FileSystem;
81
79
  const path = yield* Path.Path;
82
80
  const manifestPath = packagePath(path, projectDir, packageName);
83
- const contents = yield* fs.readFileString(manifestPath).pipe(
84
- Effect.catchReason(
85
- "PlatformError",
86
- "NotFound",
87
- () => Effect.fail(new EffectTsgoDependencyError({ packageName, expectedVersion })),
88
- ),
89
- );
81
+ const contents = yield* fs
82
+ .readFileString(manifestPath)
83
+ .pipe(
84
+ Effect.catchReason("PlatformError", "NotFound", () =>
85
+ Effect.fail(new EffectTsgoDependencyError({ packageName, expectedVersion })),
86
+ ),
87
+ );
90
88
  const manifest = yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
91
- Effect.mapError(() =>
92
- new EffectTsgoDependencyError({ packageName, expectedVersion }),
93
- ),
89
+ Effect.mapError(() => new EffectTsgoDependencyError({ packageName, expectedVersion })),
94
90
  );
91
+
95
92
  if (manifest.version !== expectedVersion) {
96
93
  return yield* new EffectTsgoDependencyError({
97
94
  packageName,
@@ -99,6 +96,7 @@ const readExactPackageVersion = Effect.fn("readExactEffectTsgoPackageVersion")(f
99
96
  actualVersion: manifest.version,
100
97
  });
101
98
  }
99
+
102
100
  return manifest.version;
103
101
  });
104
102
 
@@ -108,53 +106,80 @@ const resolveEffectTsgoExecutable = Effect.fn("resolveEffectTsgoExecutable")(fun
108
106
  const fs = yield* FileSystem.FileSystem;
109
107
  const path = yield* Path.Path;
110
108
  const binDir = path.join(projectDir, "node_modules", ".bin");
111
- const candidates = path.sep === "\\"
112
- ? [path.join(binDir, "effect-tsgo.cmd"), path.join(binDir, "effect-tsgo")]
113
- : [path.join(binDir, "effect-tsgo"), path.join(binDir, "effect-tsgo.cmd")];
109
+ const candidates =
110
+ path.sep === "\\"
111
+ ? [path.join(binDir, "effect-tsgo.cmd"), path.join(binDir, "effect-tsgo")]
112
+ : [path.join(binDir, "effect-tsgo"), path.join(binDir, "effect-tsgo.cmd")];
113
+
114
114
  for (const candidate of candidates) {
115
115
  if (yield* fs.exists(candidate)) return candidate;
116
116
  }
117
+
117
118
  return yield* new EffectTsgoDependencyError({
118
119
  packageName: "@effect/tsgo",
119
120
  expectedVersion: EFFECT_TSGO_VERSION,
120
121
  });
121
122
  });
122
123
 
123
- const digestFileContents = Effect.fn("digestEffectTsgoFileContents")(function* (
124
- filePath: string,
125
- ) {
124
+ const digestFileContents = Effect.fn("digestEffectTsgoFileContents")(function* (filePath: string) {
126
125
  const crypto = yield* Crypto.Crypto;
127
126
  const fs = yield* FileSystem.FileSystem;
127
+
128
128
  return Encoding.encodeHex(yield* crypto.digest("SHA-256", yield* fs.readFile(filePath)));
129
129
  });
130
130
 
131
+ const findNodeModulesRoot = (path: Path.Path, packageJsonPath: string): string | undefined => {
132
+ let current = path.dirname(packageJsonPath);
133
+
134
+ while (true) {
135
+ if (path.basename(current) === "node_modules") return current;
136
+ const parent = path.dirname(current);
137
+
138
+ if (parent === current) return undefined;
139
+ current = parent;
140
+ }
141
+ };
142
+
131
143
  const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
132
144
  projectDir: string,
145
+ typescriptPackage: string,
133
146
  ) {
134
147
  const fs = yield* FileSystem.FileSystem;
135
148
  const path = yield* Path.Path;
136
- const scopeDir = path.join(projectDir, "node_modules", "@typescript");
137
- if (!(yield* fs.exists(scopeDir))) return false;
138
- const entries = yield* fs.readDirectory(scopeDir);
149
+ const typescriptPackageJson = yield* fs.realPath(
150
+ packagePath(path, projectDir, typescriptPackage),
151
+ );
152
+ const effectTsgoPackageJson = yield* fs.realPath(packagePath(path, projectDir, "@effect/tsgo"));
153
+ const typescriptNodeModules = findNodeModulesRoot(path, typescriptPackageJson);
154
+ const effectNodeModules = findNodeModulesRoot(path, effectTsgoPackageJson);
155
+
156
+ if (typescriptNodeModules === undefined || effectNodeModules === undefined) return false;
157
+
158
+ const typescriptScope = path.join(typescriptNodeModules, "@typescript");
159
+ const effectScope = path.join(effectNodeModules, "@effect");
160
+
161
+ if (!(yield* fs.exists(typescriptScope)) || !(yield* fs.exists(effectScope))) return false;
162
+
139
163
  const executableName = path.sep === "\\" ? "tsc.exe" : "tsc";
140
- const effectExecutableNames = path.sep === "\\"
141
- ? ["tsc.exe", "tsc-next.exe"]
142
- : ["tsc", "tsc-next"];
143
- for (const entry of entries) {
164
+ const effectExecutableNames =
165
+ path.sep === "\\" ? ["tsc.exe", "tsc-next.exe"] : ["tsc", "tsc-next"];
166
+
167
+ for (const entry of yield* fs.readDirectory(typescriptScope)) {
144
168
  if (!entry.startsWith("typescript-")) continue;
145
169
  const platform = entry.slice("typescript-".length);
146
- const installedPath = path.join(scopeDir, entry, "lib", executableName);
170
+ const installedPath = path.join(typescriptScope, entry, "lib", executableName);
171
+
147
172
  if (!(yield* fs.exists(installedPath))) continue;
148
173
  const installedDigest = yield* digestFileContents(installedPath);
174
+
149
175
  for (const effectExecutableName of effectExecutableNames) {
150
176
  const effectBinaryPath = path.join(
151
- projectDir,
152
- "node_modules",
153
- "@effect",
177
+ effectScope,
154
178
  `tsgo-${platform}`,
155
179
  "lib",
156
180
  effectExecutableName,
157
181
  );
182
+
158
183
  if (
159
184
  (yield* fs.exists(effectBinaryPath)) &&
160
185
  installedDigest === (yield* digestFileContents(effectBinaryPath))
@@ -163,6 +188,7 @@ const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
163
188
  }
164
189
  }
165
190
  }
191
+
166
192
  return false;
167
193
  });
168
194
 
@@ -173,6 +199,7 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
173
199
  const path = yield* Path.Path;
174
200
  const projectDir = yield* fs.realPath(path.resolve(options.projectDir ?? "."));
175
201
  const typescriptPackage = options.typescriptPackage ?? "typescript";
202
+
176
203
  if (!isTypeScriptPackageName(typescriptPackage)) {
177
204
  return yield* new InvalidEffectTsgoPackageNameError({ packageName: typescriptPackage });
178
205
  }
@@ -187,16 +214,15 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
187
214
  EFFECT_TSGO_TYPESCRIPT_VERSION,
188
215
  );
189
216
  const executable = yield* resolveEffectTsgoExecutable(projectDir);
217
+
190
218
  return {
191
- alreadyPatched: yield* isEffectTsgoPatched(projectDir),
219
+ alreadyPatched: yield* isEffectTsgoPatched(projectDir, typescriptPackage),
192
220
  projectDir,
193
221
  executable,
194
222
  args: [
195
223
  "patch",
196
224
  ...(options.force ? ["--force"] : []),
197
- ...(typescriptPackage === "typescript"
198
- ? []
199
- : ["--typescript-package", typescriptPackage]),
225
+ ...(typescriptPackage === "typescript" ? [] : ["--typescript-package", typescriptPackage]),
200
226
  ],
201
227
  effectTsgoVersion,
202
228
  typescriptPackage,
@@ -218,6 +244,7 @@ export const applyEffectTsgoPatchPlan = Effect.fn("applyEffectTsgoPatchPlan")(fu
218
244
  child.exitCode,
219
245
  ]);
220
246
  const trimmed = output.trim();
247
+
221
248
  if (exitCode !== 0) {
222
249
  return yield* new EffectTsgoPatchCommandError({
223
250
  command: [plan.executable, ...plan.args].join(" "),
@@ -232,12 +259,14 @@ export const patchEffectTsgo = Effect.fn("patchEffectTsgo")(function* (
232
259
  ) {
233
260
  const plan = yield* planEffectTsgoPatch(options);
234
261
  const detail = `@effect/tsgo@${plan.effectTsgoVersion} → ${plan.typescriptPackage}@${plan.typescriptVersion}`;
262
+
235
263
  if (options.dryRun) {
236
264
  yield* printStatus(
237
265
  plan.alreadyPatched ? "success" : "plan",
238
266
  plan.alreadyPatched ? "TypeScript patch up to date" : "Would patch TypeScript",
239
267
  detail,
240
268
  );
269
+
241
270
  return plan;
242
271
  }
243
272
 
@@ -246,10 +275,12 @@ export const patchEffectTsgo = Effect.fn("patchEffectTsgo")(function* (
246
275
  yield* acquireProjectProcessLock(plan.projectDir);
247
276
  if (plan.alreadyPatched) {
248
277
  yield* printStatus("success", "TypeScript patch up to date", detail);
278
+
249
279
  return plan;
250
280
  }
251
281
  yield* withSpinner("Patching TypeScript", applyEffectTsgoPatchPlan(plan));
252
282
  yield* printStatus("success", "TypeScript patched", detail);
283
+
253
284
  return plan;
254
285
  }),
255
286
  );
package/src/gitignore.ts CHANGED
@@ -61,17 +61,20 @@ export const patchGitignoreContents = (
61
61
  ): { readonly contents: string; readonly added: ReadonlyArray<string> } => {
62
62
  const lines = current.split(/\r?\n/);
63
63
  const added = DEV_KIT_GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
64
+
64
65
  if (added.length === 0) return { contents: current, added };
65
66
 
66
67
  const newline = current.includes("\r\n") ? "\r\n" : "\n";
67
- const separator = current.length === 0
68
- ? ""
69
- : current.endsWith(`${newline}${newline}`)
68
+ const separator =
69
+ current.length === 0
70
70
  ? ""
71
- : current.endsWith(newline)
72
- ? newline
73
- : `${newline}${newline}`;
71
+ : current.endsWith(`${newline}${newline}`)
72
+ ? ""
73
+ : current.endsWith(newline)
74
+ ? newline
75
+ : `${newline}${newline}`;
74
76
  const block = ["# dev-kit managed paths", ...added].join(newline);
77
+
75
78
  return {
76
79
  contents: `${current}${separator}${block}${newline}`,
77
80
  added,
@@ -89,6 +92,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
89
92
  const path = yield* Path.Path;
90
93
  const gitignorePath = path.join(projectDir, ".gitignore");
91
94
  const observation = yield* observeSymbolicLink(gitignorePath);
95
+
92
96
  if (observation.kind === "symlink") {
93
97
  return yield* new UnsafeGitignorePathError({
94
98
  path: gitignorePath,
@@ -98,8 +102,10 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
98
102
 
99
103
  let current = "";
100
104
  let mode: number | undefined;
105
+
101
106
  if (observation.kind !== "missing") {
102
107
  const info = yield* fs.stat(gitignorePath);
108
+
103
109
  if (info.type !== "File") {
104
110
  return yield* new UnsafeGitignorePathError({
105
111
  path: gitignorePath,
@@ -111,6 +117,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
111
117
  }
112
118
 
113
119
  const patch = patchGitignoreContents(current);
120
+
114
121
  return {
115
122
  path: gitignorePath,
116
123
  changed: patch.added.length > 0,
@@ -135,6 +142,7 @@ const applyGitignorePatch = Effect.fn("applyGitignorePatch")(function* (
135
142
  });
136
143
  const staged = path.join(tempDir, "next.gitignore");
137
144
  const backup = path.join(tempDir, "previous.gitignore");
145
+
138
146
  yield* fs.writeFileString(staged, patch.contents, { mode: patch.mode ?? 0o666 });
139
147
 
140
148
  const currentObservation = yield* observeSymbolicLink(patch.path);
@@ -142,6 +150,7 @@ const applyGitignorePatch = Effect.fn("applyGitignorePatch")(function* (
142
150
  ? currentObservation.kind !== "not-symlink" ||
143
151
  (yield* fs.readFileString(patch.path)) !== patch.previousContents
144
152
  : currentObservation.kind !== "missing";
153
+
145
154
  if (changed) {
146
155
  return yield* new GitignoreConflictError({ path: patch.path });
147
156
  }
@@ -188,11 +197,13 @@ export const patchProjectGitignore = Effect.fn("patchProjectGitignore")(function
188
197
 
189
198
  if (options.dryRun) {
190
199
  const patch = yield* planGitignorePatch(projectDir);
200
+
191
201
  yield* printStatus(
192
202
  patch.changed ? "plan" : "success",
193
203
  patch.changed ? "Would update .gitignore" : ".gitignore up to date",
194
204
  patch.changed ? `add ${patch.added.join(", ")}` : undefined,
195
205
  );
206
+
196
207
  return publicPatch(patch);
197
208
  }
198
209
 
@@ -200,12 +211,14 @@ export const patchProjectGitignore = Effect.fn("patchProjectGitignore")(function
200
211
  Effect.gen(function* () {
201
212
  yield* acquireProjectProcessLock(projectDir);
202
213
  const patch = yield* planGitignorePatch(projectDir);
214
+
203
215
  yield* applyGitignorePatch(projectDir, patch);
204
216
  yield* printStatus(
205
217
  "success",
206
218
  patch.changed ? "Updated .gitignore" : ".gitignore up to date",
207
219
  patch.changed ? `added ${patch.added.join(", ")}` : undefined,
208
220
  );
221
+
209
222
  return publicPatch(patch);
210
223
  }),
211
224
  );
package/src/index.ts CHANGED
@@ -11,6 +11,10 @@ export {
11
11
  EffectTsgoSetupSchema,
12
12
  type HarnessTarget,
13
13
  TargetConfigSchema,
14
+ type VitePlusQualitySetup,
15
+ VitePlusQualitySetupSchema,
16
+ type VitePlusSetup,
17
+ VitePlusSetupSchema,
14
18
  } from "./manifest.ts";
15
19
  export {
16
20
  applyEffectSourcePlan,
@@ -68,6 +72,7 @@ export {
68
72
  EffectTsgoLockSchema,
69
73
  ManagedAgentInstructionsOutputSchema,
70
74
  ManagedClaudeInstructionsOutputSchema,
75
+ ManagedGeneratedFileOutputSchema,
71
76
  ManagedInstructionOutputSchema,
72
77
  ManagedOutputSchema,
73
78
  ManagedSkillOutputSchema,
@@ -79,6 +84,7 @@ export {
79
84
  type EffectTsgoLock,
80
85
  type ManagedAgentInstructionsOutput,
81
86
  type ManagedClaudeInstructionsOutput,
87
+ type ManagedGeneratedFileOutput,
82
88
  type ManagedInstructionOutput,
83
89
  type ManagedOutput,
84
90
  type ManagedSkillOutput,
package/src/manifest.ts CHANGED
@@ -49,16 +49,35 @@ export const ClaudeInstructionsSetupSchema = Schema.Struct({
49
49
 
50
50
  export type ClaudeInstructionsSetup = typeof ClaudeInstructionsSetupSchema.Type;
51
51
 
52
+ export const VitePlusHooksSetupSchema = Schema.Struct({
53
+ enabled: Schema.optional(Schema.Boolean),
54
+ });
55
+
56
+ export const VitePlusQualitySetupSchema = Schema.Struct({
57
+ enabled: Schema.optional(Schema.Boolean),
58
+ });
59
+ export type VitePlusQualitySetup = typeof VitePlusQualitySetupSchema.Type;
60
+
61
+ export const VitePlusSetupSchema = Schema.Struct({
62
+ hooks: Schema.optional(VitePlusHooksSetupSchema),
63
+ quality: Schema.optional(VitePlusQualitySetupSchema),
64
+ });
65
+
66
+ export type VitePlusSetup = typeof VitePlusSetupSchema.Type;
67
+
52
68
  export const DevKitManifestSchema = Schema.Struct({
53
69
  $schema: Schema.optional(Schema.String),
54
70
  include: Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
55
- exclude: Schema.optional(Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN)))),
71
+ exclude: Schema.optional(
72
+ Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
73
+ ),
56
74
  setup: Schema.optional(
57
75
  Schema.Struct({
58
76
  agentInstructions: Schema.optional(AgentInstructionsSetupSchema),
59
77
  claudeInstructions: Schema.optional(ClaudeInstructionsSetupSchema),
60
78
  effectSource: Schema.optional(EffectSourceSetupSchema),
61
79
  effectTsgo: Schema.optional(EffectTsgoSetupSchema),
80
+ vitePlus: Schema.optional(VitePlusSetupSchema),
62
81
  }),
63
82
  ),
64
83
  targets: Schema.optional(
@@ -99,6 +118,14 @@ export type NormalizedManifest = {
99
118
  readonly force: boolean;
100
119
  readonly typescriptPackage: string;
101
120
  };
121
+ readonly vitePlus: {
122
+ readonly hooks: {
123
+ readonly enabled: boolean;
124
+ };
125
+ readonly quality: {
126
+ readonly enabled: boolean;
127
+ };
128
+ };
102
129
  };
103
130
  readonly targets: Readonly<Record<HarnessTarget, NormalizedTargetConfig>>;
104
131
  };
@@ -122,6 +149,7 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
122
149
 
123
150
  for (const key of ["agents", "claude", "opencode"] as const) {
124
151
  const override = manifest.targets?.[key];
152
+
125
153
  if (override) {
126
154
  targets[key] = {
127
155
  enabled: override.enabled ?? DEFAULT_TARGETS[key].enabled,
@@ -146,14 +174,21 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
146
174
  packageName: manifest.setup?.effectSource?.packageName ?? "effect",
147
175
  path: manifest.setup?.effectSource?.path ?? ".repos/effect",
148
176
  repository:
149
- manifest.setup?.effectSource?.repository ??
150
- "https://github.com/Effect-TS/effect.git",
177
+ manifest.setup?.effectSource?.repository ?? "https://github.com/Effect-TS/effect.git",
151
178
  },
152
179
  effectTsgo: {
153
180
  enabled: manifest.setup?.effectTsgo?.enabled ?? false,
154
181
  force: manifest.setup?.effectTsgo?.force ?? false,
155
182
  typescriptPackage: manifest.setup?.effectTsgo?.typescriptPackage ?? "typescript",
156
183
  },
184
+ vitePlus: {
185
+ hooks: {
186
+ enabled: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
187
+ },
188
+ quality: {
189
+ enabled: manifest.setup?.vitePlus?.quality?.enabled ?? false,
190
+ },
191
+ },
157
192
  },
158
193
  targets,
159
194
  };