@danieljvdm/dev-kit 0.2.3 → 0.3.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
@@ -309,6 +309,28 @@ transforms, and installs the result through the ownership-safe sync path. Normal
309
309
  installs never float to a newer upstream commit; only a reviewed catalog refresh
310
310
  changes what is approved.
311
311
 
312
+ ## Oxlint preset for Vite+
313
+
314
+ Dev Kit exports a typed, composable set of high-signal Oxlint rules for Vite+
315
+ projects:
316
+
317
+ ```ts
318
+ import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
319
+ import { defineConfig } from "vite-plus";
320
+
321
+ export default defineConfig({
322
+ lint: {
323
+ extends: [recommendedOxlintConfig],
324
+ rules: {
325
+ // Add repository-specific rules here.
326
+ },
327
+ },
328
+ });
329
+ ```
330
+
331
+ Use `lint.extends` rather than a shallow object spread so Vite+ composes the
332
+ nested plugin and rule configuration correctly.
333
+
312
334
  ## Development
313
335
 
314
336
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
@@ -19,7 +19,11 @@
19
19
  },
20
20
  "./schema": "./schema/dev-kit.schema.json",
21
21
  "./schema/dev-kit.schema.json": "./schema/dev-kit.schema.json",
22
- "./schema/skill-sources.schema.json": "./schema/skill-sources.schema.json"
22
+ "./schema/skill-sources.schema.json": "./schema/skill-sources.schema.json",
23
+ "./oxlint": {
24
+ "types": "./src/oxlint.ts",
25
+ "default": "./src/oxlint.ts"
26
+ }
23
27
  },
24
28
  "files": [
25
29
  "dev-kit.example.jsonc",
@@ -36,7 +40,8 @@
36
40
  "changeset": "changeset",
37
41
  "version-packages": "changeset version && ./bin/dev-kit.mjs apply",
38
42
  "release": "changeset publish",
39
- "check": "tsc --noEmit",
43
+ "check": "tsc --noEmit && vp lint",
44
+ "lint": "vp lint",
40
45
  "plan": "./bin/dev-kit.mjs plan",
41
46
  "apply": "./bin/dev-kit.mjs apply",
42
47
  "gitignore": "./bin/dev-kit.mjs gitignore",
@@ -60,8 +65,17 @@
60
65
  "@effect/vitest": "4.0.0-beta.102",
61
66
  "@types/node": "25.9.1",
62
67
  "typescript": "7.0.2",
68
+ "vite-plus": "0.2.6",
63
69
  "vitest": "4.1.10"
64
70
  },
71
+ "peerDependencies": {
72
+ "vite-plus": ">=0.2.6 <0.3.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "vite-plus": {
76
+ "optional": true
77
+ }
78
+ },
65
79
  "engines": {
66
80
  "node": ">=22.12.0"
67
81
  },
@@ -156,10 +156,34 @@ dependencies; `dev-kit apply` patches once and then converges.
156
156
  Use `dev-kit tsgo patch --dry-run` for focused diagnosis. Use `--force` only
157
157
  after the user accepts a potentially commit-incompatible TypeScript binary.
158
158
 
159
+ ## Oxlint preset for Vite+
160
+
161
+ For Vite+ projects, compose the package's typed recommended Oxlint preset with
162
+ project-specific lint configuration:
163
+
164
+ ```ts
165
+ import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
166
+ import { defineConfig } from "vite-plus";
167
+
168
+ export default defineConfig({
169
+ lint: {
170
+ extends: [recommendedOxlintConfig],
171
+ rules: {
172
+ // Project-specific rules apply after the shared preset.
173
+ },
174
+ },
175
+ });
176
+ ```
177
+
178
+ Use `lint.extends` instead of spreading the object so Vite+ composes nested
179
+ rule maps correctly. Keep repository-specific path conventions and
180
+ platform-specific accessibility rules in the consuming project.
181
+
159
182
  ## Current boundary
160
183
 
161
184
  Manage skill outputs, the `setup.effectSource` checkout, and the explicit
162
185
  `setup.effectTsgo` task. Edit shared `package.json` and `tsconfig.json`
163
- contributions deliberately. Treat named bundles, Oxlint/Oxfmt presets, and
164
- broader setup tasks as future manifest capabilities until the installed CLI
165
- exposes them.
186
+ contributions deliberately. The Oxlint preset is a composable package export,
187
+ not a manifest-managed output. Treat named bundles, Oxfmt presets, and broader
188
+ setup tasks as future manifest capabilities until the installed CLI exposes
189
+ them.
@@ -216,7 +216,10 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
216
216
  const existingIndex = byId >= 0 ? byId : byRepository;
217
217
  let next = state.sources.raw;
218
218
  if (existingIndex >= 0) {
219
- const existing = sources[existingIndex]!;
219
+ const existing = sources[existingIndex];
220
+ if (existing === undefined) {
221
+ return yield* new CatalogManagerError({ message: "catalog source index is out of bounds" });
222
+ }
220
223
  const approved = state.lock?.value.sources.find((source) => source.id === existing.id)?.skills ?? [];
221
224
  const currentInclude = existing.include.includes("*") ? approved : existing.include;
222
225
  const include = [...new Set([...currentInclude, ...selection.include])].sort();
package/src/gitignore.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Cause, Effect, FileSystem, Path, PlatformError, Schema } from "effect";
1
+ import { Cause, Effect, FileSystem, Path, type PlatformError, Schema } from "effect";
2
2
 
3
3
  import { printStatus } from "./cli-ui.ts";
4
4
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, PlatformError } from "effect";
1
+ import { Effect, FileSystem, type PlatformError } from "effect";
2
2
 
3
3
  export type SymbolicLinkObservation =
4
4
  | { readonly kind: "missing" | "not-symlink" }
package/src/oxlint.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { OxlintConfig } from "vite-plus/lint";
2
+
3
+ /**
4
+ * High-signal Oxlint defaults for TypeScript projects using Vite+.
5
+ *
6
+ * Extend this object from `lint.extends` so project-local plugins, rules, and
7
+ * overrides compose without losing nested configuration.
8
+ */
9
+ export const recommendedOxlintConfig = {
10
+ options: {
11
+ typeAware: true,
12
+ },
13
+ plugins: ["import", "react", "vitest"],
14
+ rules: {
15
+ eqeqeq: "error",
16
+ "import/default": "off",
17
+ "import/namespace": "off",
18
+ "import/no-cycle": "error",
19
+ "import/no-duplicates": "error",
20
+ "import/no-self-import": "error",
21
+ "react/exhaustive-deps": "error",
22
+ "react/rules-of-hooks": "error",
23
+ "typescript/consistent-type-imports": "error",
24
+ "typescript/no-explicit-any": "error",
25
+ "typescript/no-non-null-assertion": "error",
26
+ "typescript/switch-exhaustiveness-check": "error",
27
+ "unicorn/prefer-node-protocol": "error",
28
+ "vitest/no-focused-tests": "error",
29
+ "vitest/no-identical-title": "error",
30
+ "vitest/no-standalone-expect": "off",
31
+ "vitest/valid-expect": "error",
32
+ },
33
+ } satisfies OxlintConfig;
34
+
35
+ export type RecommendedOxlintConfig = typeof recommendedOxlintConfig;
@@ -1,4 +1,4 @@
1
- import { Crypto, Effect, Encoding, FileSystem, Path, PlatformError, Schema } from "effect";
1
+ import { Crypto, Effect, Encoding, FileSystem, Path, type PlatformError, Schema } from "effect";
2
2
 
3
3
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
4
4
 
@@ -154,7 +154,8 @@ const writeArray = Effect.fn("writeManifestArray")(function* (
154
154
  let next = raw;
155
155
  const retained = [...current];
156
156
  for (let index = current.length - 1; index >= 0; index -= 1) {
157
- if (!values.includes(current[index]!)) {
157
+ const currentValue = current[index];
158
+ if (currentValue !== undefined && !values.includes(currentValue)) {
158
159
  next = applyEdits(next, modify(next, [property, index], undefined, {
159
160
  formattingOptions: { insertSpaces: true, tabSize: 2 },
160
161
  }));
package/src/sync.ts CHANGED
@@ -284,7 +284,6 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
284
284
  projectDir: string,
285
285
  candidate: string,
286
286
  ) {
287
- const fs = yield* FileSystem.FileSystem;
288
287
  const path = yield* Path.Path;
289
288
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
290
289
  return yield* new UnsafeManagedPathError({ path: candidate, reason: "must be a non-empty project-relative path" });
package/src/vendor.ts CHANGED
@@ -109,6 +109,14 @@ const inferSourceId = (repository: string): string => {
109
109
  return tail.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
110
110
  };
111
111
 
112
+ const containsControlCharacter = (value: string): boolean => {
113
+ for (const character of value) {
114
+ const code = character.charCodeAt(0);
115
+ if (code <= 0x1f || code === 0x7f) return true;
116
+ }
117
+ return false;
118
+ };
119
+
112
120
  const normalizeRepositoryLocator = (
113
121
  repository: string,
114
122
  ): Effect.Effect<{
@@ -116,7 +124,7 @@ const normalizeRepositoryLocator = (
116
124
  readonly ref?: string;
117
125
  readonly skillsPath?: string;
118
126
  }, InvalidSourceError> => {
119
- if (/[\u0000-\u001f\u007f]/.test(repository)) {
127
+ if (containsControlCharacter(repository)) {
120
128
  return Effect.fail(new InvalidSourceError({ source: repository, reason: "repository contains control characters" }));
121
129
  }
122
130
  try {
@@ -126,9 +134,9 @@ const normalizeRepositoryLocator = (
126
134
  }
127
135
  if (url.hostname.toLowerCase() !== "github.com") return Effect.succeed({ repository });
128
136
  const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
129
- if (segments.length < 2) return Effect.succeed({ repository });
130
- const owner = segments[0]!;
131
- const name = segments[1]!.replace(/\.git$/i, "");
137
+ const [owner, rawName] = segments;
138
+ if (owner === undefined || rawName === undefined) return Effect.succeed({ repository });
139
+ const name = rawName.replace(/\.git$/i, "");
132
140
  const normalized = `https://github.com/${owner}/${name}.git`;
133
141
  if (segments[2] === "tree" && segments[3]) {
134
142
  return Effect.succeed({
@@ -389,13 +397,20 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
389
397
  yield* fs.makeDirectory(checkoutDir, { recursive: true });
390
398
  yield* runCommand(checkoutDir, "git", ["init", "--quiet"]);
391
399
  yield* runCommand(checkoutDir, "git", ["remote", "add", "origin", source.repository]);
400
+ const fetchRef = useLock ? lockedSource?.resolved : source.ref;
401
+ if (fetchRef === undefined) {
402
+ return yield* new InvalidSourceError({
403
+ source: source.id,
404
+ reason: "no matching lockfile entry; run catalog refresh without --locked first",
405
+ });
406
+ }
392
407
  yield* runCommand(checkoutDir, "git", [
393
408
  "fetch",
394
409
  "--quiet",
395
410
  "--depth",
396
411
  "1",
397
412
  "origin",
398
- useLock ? lockedSource!.resolved : source.ref,
413
+ fetchRef,
399
414
  ]);
400
415
  yield* runCommand(checkoutDir, "git", ["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
401
416
  const resolved = yield* runCommand(checkoutDir, "git", ["rev-parse", "HEAD"]);