@danieljvdm/dev-kit 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +18 -7
  2. package/package.json +20 -9
  3. package/scripts/sync-anti-slop-runtime.mjs +19 -0
  4. package/skill-sources.jsonc +17 -0
  5. package/skill-sources.lock.json +48 -0
  6. package/skills/dev-kit/SKILL.md +4 -5
  7. package/src/bin/dev-kit.ts +11 -7
  8. package/src/catalog-manager.ts +23 -18
  9. package/src/catalog.ts +8 -5
  10. package/src/cli-ui.ts +2 -2
  11. package/src/effect-tsgo.ts +10 -6
  12. package/src/gitignore.ts +6 -3
  13. package/src/manifest.ts +10 -2
  14. package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
  15. package/src/oxlint-plugin-anti-slop/index.ts +43 -0
  16. package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
  17. package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
  18. package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
  19. package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
  20. package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
  21. package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
  22. package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
  23. package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
  24. package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
  25. package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
  26. package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
  27. package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
  28. package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
  29. package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
  30. package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
  31. package/src/oxlint-plugin-anti-slop/runtime.d.ts +22 -0
  32. package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
  33. package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
  34. package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
  35. package/src/oxlint-plugin-effect.d.ts +4 -2
  36. package/src/oxlint.js +19 -0
  37. package/src/oxlint.ts +22 -5
  38. package/src/package-skill-source.ts +17 -25
  39. package/src/path-digest.ts +2 -1
  40. package/src/project-package.ts +14 -12
  41. package/src/skill-manager.ts +25 -13
  42. package/src/sync.ts +65 -49
  43. package/src/vendor.ts +35 -16
package/README.md CHANGED
@@ -493,7 +493,7 @@ Pin the compatible packages in the consuming project:
493
493
  {
494
494
  "devDependencies": {
495
495
  "@danieljvdm/dev-kit": "^0.2.0",
496
- "@effect/tsgo": "0.33.0",
496
+ "@effect/tsgo": "0.36.4",
497
497
  "typescript": "7.0.2",
498
498
  },
499
499
  }
@@ -701,14 +701,25 @@ export default defineConfig({
701
701
  });
702
702
  ```
703
703
 
704
- The Oxlint preset enables `stylistic/padding-line-between-statements`: adjacent
704
+ The Oxlint preset vendors and enables every rule from
705
+ [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) at upstream commit
706
+ `9b80d9a5c317d3af94d88a577bdbde4d9a45f7be`. These opinionated rules reject
707
+ low-evidence patterns such as broad `unknown` contracts, runtime `typeof`
708
+ narrowing, module mocking, chained assertions, and assertions without a
709
+ nearby `SAFETY:` justification. The package also exports the plugin from
710
+ `@danieljvdm/dev-kit/oxlint-plugin-anti-slop`. Its vendored MIT license is
711
+ retained beside the source.
712
+
713
+ The TypeScript files under `src/oxlint-plugin-anti-slop/` are canonical. After
714
+ updating them, run `vp run anti-slop:bundle` to regenerate the package-loadable
715
+ JavaScript entry point without cleaning canonical source.
716
+
717
+ The preset also enables `stylistic/padding-line-between-statements`: adjacent
705
718
  variable declarations remain grouped, while the next logical statement and
706
719
  all `return` statements require a separating blank line. The rule is fixable,
707
- so `vp lint --fix` repairs missing spacing automatically. Vite+ 0.2.6 forwards
708
- the preset's JavaScript-plugin declarations but its native Oxlint path does not
709
- execute their rules run standalone Oxlint when `effect/*` or the padding rule
710
- must be enforced, until a supported Vite+ release executes configured JS
711
- plugins.
720
+ so `vp lint --fix` repairs missing spacing automatically. With the matching
721
+ Oxlint 1.78 peer installed, Vite+ 0.2.6 executes the preset's JavaScript-plugin
722
+ rules through the normal `vp lint` command.
712
723
 
713
724
  The preset also registers the shared `effect` JavaScript plugin. Effect
714
725
  projects opt into its rules in path-specific overrides, for example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "private": false,
5
5
  "description": "Declarative project development toolkit with portable agent skills.",
6
6
  "license": "MIT",
@@ -19,6 +19,7 @@
19
19
  "skill-sources.jsonc",
20
20
  "skill-sources.lock.json",
21
21
  "src/",
22
+ "scripts/",
22
23
  "templates/"
23
24
  ],
24
25
  "type": "module",
@@ -46,6 +47,10 @@
46
47
  "types": "./src/oxlint-plugin-effect.d.ts",
47
48
  "default": "./src/oxlint-plugin-effect.js"
48
49
  },
50
+ "./oxlint-plugin-anti-slop": {
51
+ "types": "./src/oxlint-plugin-anti-slop/runtime.d.ts",
52
+ "default": "./src/oxlint-plugin-anti-slop/runtime.js"
53
+ },
49
54
  "./oxlint-plugin-style": {
50
55
  "types": "./src/oxlint-plugin-style.d.ts",
51
56
  "default": "./src/oxlint-plugin-style.js"
@@ -66,31 +71,34 @@
66
71
  "test:watch": "vitest --config vitest.config.ts",
67
72
  "tsgo:patch": "./bin/dev-kit.mjs tsgo patch",
68
73
  "catalog:refresh": "./bin/dev-kit.mjs catalog refresh",
69
- "catalog:check": "./bin/dev-kit.mjs catalog verify"
74
+ "catalog:check": "./bin/dev-kit.mjs catalog verify",
75
+ "anti-slop:bundle": "vp pack src/oxlint-plugin-anti-slop/index.ts --out-dir .dev-kit/anti-slop-build --format esm --no-dts && node scripts/sync-anti-slop-runtime.mjs",
76
+ "anti-slop:check": "vp pack src/oxlint-plugin-anti-slop/index.ts --out-dir .dev-kit/anti-slop-build --format esm --no-dts && node scripts/sync-anti-slop-runtime.mjs --check"
70
77
  },
71
78
  "dependencies": {
72
- "@effect/platform-bun": "4.0.0-beta.105",
79
+ "@effect/platform-bun": "4.0.0-beta.107",
80
+ "@oxlint/plugins": "1.78.0",
73
81
  "@stylistic/eslint-plugin": "5.10.0",
74
- "effect": "4.0.0-beta.105",
82
+ "effect": "4.0.0-beta.107",
75
83
  "jsonc-parser": "3.3.1",
76
84
  "semver": "7.8.5"
77
85
  },
78
86
  "devDependencies": {
79
87
  "@changesets/cli": "3.0.0-next.10",
80
- "@effect/platform-node": "4.0.0-beta.105",
81
- "@effect/tsgo": "0.33.0",
82
- "@effect/vitest": "4.0.0-beta.105",
88
+ "@effect/platform-node": "4.0.0-beta.107",
89
+ "@effect/tsgo": "0.36.4",
90
+ "@effect/vitest": "4.0.0-beta.107",
83
91
  "@types/node": "25.9.1",
84
92
  "@types/semver": "7.8.0",
85
93
  "oxfmt": "0.60.0",
86
- "oxlint": "1.75.0",
94
+ "oxlint": "1.78.0",
87
95
  "typescript": "7.0.2",
88
96
  "vite-plus": "0.2.6",
89
97
  "vitest": "4.1.10"
90
98
  },
91
99
  "peerDependencies": {
92
100
  "oxfmt": ">=0.60.0 <0.61.0",
93
- "oxlint": ">=1.75.0 <2.0.0",
101
+ "oxlint": ">=1.78.0 <2.0.0",
94
102
  "vite-plus": ">=0.2.6 <0.3.0"
95
103
  },
96
104
  "peerDependenciesMeta": {
@@ -104,6 +112,9 @@
104
112
  "optional": true
105
113
  }
106
114
  },
115
+ "overrides": {
116
+ "@effect/platform-node-shared": "4.0.0-beta.107"
117
+ },
107
118
  "engines": {
108
119
  "bun": ">=1.3.0"
109
120
  },
@@ -0,0 +1,19 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+
3
+ const generatedUrl = new URL("../.dev-kit/anti-slop-build/index.mjs", import.meta.url);
4
+ const runtimeUrl = new URL("../src/oxlint-plugin-anti-slop/runtime.js", import.meta.url);
5
+ const generated = await readFile(generatedUrl, "utf8");
6
+ const header =
7
+ "// Bundled from the vendored anti-slop source at commit 9b80d9a5c317d3af94d88a577bdbde4d9a45f7be.\n";
8
+ const runtime = generated.replace(/^\/\/[#](?:end)?region.*\n/gmu, "");
9
+ const expected = `${header}${runtime}`;
10
+
11
+ if (process.argv.includes("--check")) {
12
+ const current = await readFile(runtimeUrl, "utf8");
13
+
14
+ if (current !== expected) {
15
+ throw new Error("anti-slop runtime is stale; run `vp run anti-slop:bundle`");
16
+ }
17
+ } else {
18
+ await writeFile(runtimeUrl, expected);
19
+ }
@@ -85,5 +85,22 @@
85
85
  ],
86
86
  "licensePath": "LICENSE",
87
87
  },
88
+ {
89
+ "id": "getsentry-agent-plugin",
90
+ "repository": "https://github.com/getsentry/agent-plugin.git",
91
+ "ref": "main",
92
+ "skillsPath": "skills",
93
+ "include": [
94
+ "sentry-create-alert",
95
+ "sentry-debug-issue",
96
+ "sentry-fix-stack-traces",
97
+ "sentry-get-started",
98
+ "sentry-instrument",
99
+ "sentry-otel-exporter-setup",
100
+ "sentry-setup-releases",
101
+ "sentry-snapshots-cocoa",
102
+ ],
103
+ "licensePath": "LICENSE",
104
+ },
88
105
  ],
89
106
  }
@@ -243,6 +243,54 @@
243
243
  "expo-web-to-native": "sha256:360359742d72d0a8ed9926d53f19bae7603fb788f6e897a14208bbb35c2ed618"
244
244
  },
245
245
  "licensePath": "LICENSE"
246
+ },
247
+ {
248
+ "id": "getsentry-agent-plugin",
249
+ "repository": "https://github.com/getsentry/agent-plugin.git",
250
+ "ref": "main",
251
+ "resolved": "9f9156966be43f1bf4797bd9785a0644bf181d01",
252
+ "skillsPath": "skills",
253
+ "include": [
254
+ "sentry-create-alert",
255
+ "sentry-debug-issue",
256
+ "sentry-fix-stack-traces",
257
+ "sentry-get-started",
258
+ "sentry-instrument",
259
+ "sentry-otel-exporter-setup",
260
+ "sentry-setup-releases",
261
+ "sentry-snapshots-cocoa"
262
+ ],
263
+ "skills": [
264
+ "sentry-create-alert",
265
+ "sentry-debug-issue",
266
+ "sentry-fix-stack-traces",
267
+ "sentry-get-started",
268
+ "sentry-instrument",
269
+ "sentry-otel-exporter-setup",
270
+ "sentry-setup-releases",
271
+ "sentry-snapshots-cocoa"
272
+ ],
273
+ "descriptions": {
274
+ "sentry-create-alert": "Create Sentry alerts using the workflow engine API. Use when asked to create alerts, set up notifications, configure issue priority alerts, or build workflow automations. Supports email, Slack, PagerDuty, Discord, and other notification actions.",
275
+ "sentry-debug-issue": "Debug and fix a Sentry issue — find it (by link, ID, or search), pull full context (stack trace, breadcrumbs, trace, logs), optionally run Seer root-cause / autofix, apply the code fix, and resolve it via a `Fixes PROJECT-NAME-12A` commit/PR. Use when working a known error or hunting one down to fix.",
276
+ "sentry-fix-stack-traces": "Make Sentry stack traces readable — upload source maps for JavaScript/TypeScript, or debug files for native and mobile (dSYM, ProGuard/R8, NDK symbols, Dart obfuscation maps, .NET PDBs). Use when frames in Sentry show minified names, bundled paths, hex addresses, \"unknown\", or method names with no file/line, instead of your original source.",
277
+ "sentry-get-started": "Guided entry point for using Sentry through your agent. Orients you to your current setup and, for a new project, sets up Sentry end to end with sane defaults — provision a project, install the SDK (errors, tracing, and whatever it enables by default), and confirm real telemetry reaches Sentry. Routes other intents (adding more signals, fixing issues) to the right skill.",
278
+ "sentry-instrument": "Instrument an application with Sentry — detect the platform, install and initialize the SDK if needed, and wire up any signal — error monitoring, tracing/performance, logging, metrics, profiling, session replay, user feedback, cron check-ins, and AI/LLM monitoring (agent runs, token cost, and conversations for OpenAI, Anthropic, Vercel AI, LangChain, Google GenAI, Pydantic AI, and Laravel AI). Use to add Sentry to a project or to capture more than errors.",
279
+ "sentry-otel-exporter-setup": "Configure the OpenTelemetry Collector with Sentry Exporter for multi-project routing and automatic project creation. Use when setting up OTel with Sentry, configuring collector pipelines for traces and logs, or routing telemetry from multiple services to Sentry projects.",
280
+ "sentry-setup-releases": "Set up Sentry releases and deploy tracking — tag events with a version and environment, create the release in CI with its commits, and wire up suspect commits and code mappings, so Sentry can show which release introduced an issue, which commit is responsible, and release health. Use when asked to set up releases, track deploys, see what changed, or when issues show an unknown release or no suspect commit.",
281
+ "sentry-snapshots-cocoa": "Full Sentry Snapshots setup for Apple/Cocoa projects. Use when asked to \"setup SnapshotPreviews\", \"setup Apple snapshot testing\", \"upload Apple snapshots to Sentry\", \"setup Apple snapshot GitHub Actions\", or \"setup Apple selective snapshot testing\"."
282
+ },
283
+ "digests": {
284
+ "sentry-create-alert": "sha256:a2d4011d28593abd8148c95d3f3188bc68db02d8d9c8ae444723d5548f4e9f1f",
285
+ "sentry-debug-issue": "sha256:ee3b1bc93dddab007e54332bc39521e49221528f5350e7af5e0f0e962393c9f1",
286
+ "sentry-fix-stack-traces": "sha256:f66199cfe616abf2ff39b9e953cb30d1e5634f83fcffa8cce1da54b66ae4ad67",
287
+ "sentry-get-started": "sha256:68ae5780ab6713d2fbbcb553df6dbfa67db28589ee467f4e14524ab8c51046df",
288
+ "sentry-instrument": "sha256:2c97fe5dc8aae274ed0657de7bc8dd1c59bd454b44ed00b6bf98f1d010e1e885",
289
+ "sentry-otel-exporter-setup": "sha256:9c8ba54f7ec1b52beb24b0225b5836edcd61971619474d7eee3cd48d27a8d1a5",
290
+ "sentry-setup-releases": "sha256:3a13401662f32806d56b63765be846f6c4305190b2a4f184a1146b71eb373aa4",
291
+ "sentry-snapshots-cocoa": "sha256:9eeee7bc649475f2d8c00cb2725edcf07d9a6ce2784b4fbaf24ba0bd6de8f44b"
292
+ },
293
+ "licensePath": "LICENSE"
246
294
  }
247
295
  ]
248
296
  }
@@ -113,11 +113,10 @@ separate `check` and pure `typecheck` tasks; standalone Oxc projects import
113
113
  `recommendedOxlintConfig`/`recommendedOxfmtConfig` directly.
114
114
 
115
115
  Run the Effect-patched compiler separately with `vp run typecheck`; neither
116
- Oxlint's bundled `tsgolint` nor Vite+'s native lint path uses the Effect patch,
117
- and Vite+ 0.2.6 does not execute the preset's JavaScript-plugin rules use
118
- standalone Oxlint when `effect/*` or
119
- `stylistic/padding-line-between-statements` must be enforced. The `effect`
120
- plugin's scope-sensitive rules stay consumer-scoped: enable them per path in
116
+ Oxlint's bundled `tsgolint` nor Vite+'s native lint path uses the Effect patch.
117
+ Keep Oxlint and `@oxlint/plugins` on the matching version expected by the
118
+ preset so Vite+ can execute its JavaScript plugins. The `effect` plugin's
119
+ scope-sensitive rules stay consumer-scoped: enable them per path in
121
120
  Effect-owned code, with exceptions for tests and host boundaries.
122
121
 
123
122
  `setup.vitePlus.hooks` converges the Git-ignored `.vite-hooks/_` dispatcher by
@@ -283,8 +283,8 @@ const catalogAddCommand = CliCommand.make(
283
283
  lockfile,
284
284
  repoDir,
285
285
  sources,
286
- }) =>
287
- addCatalogSource({
286
+ }) => {
287
+ const options = {
288
288
  repository,
289
289
  all,
290
290
  dryRun,
@@ -293,11 +293,15 @@ const catalogAddCommand = CliCommand.make(
293
293
  lockfilePath: lockfile,
294
294
  repoDir,
295
295
  sourcesPath: sources,
296
- ...(id ? { id } : {}),
297
- ...(license ? { licensePath: license } : {}),
298
- ...(ref ? { ref } : {}),
299
- ...(skillsPath ? { skillsPath } : {}),
300
- }),
296
+ };
297
+
298
+ if (id) Object.assign(options, { id });
299
+ if (license) Object.assign(options, { licensePath: license });
300
+ if (ref) Object.assign(options, { ref });
301
+ if (skillsPath) Object.assign(options, { skillsPath });
302
+
303
+ return addCatalogSource(options);
304
+ },
301
305
  ).pipe(CliCommand.withDescription("Inspect a repository and approve selected skills."));
302
306
 
303
307
  const catalogListCommand = CliCommand.make(
@@ -122,11 +122,15 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
122
122
  }
123
123
  const selected = yield* Prompt.multiSelect({
124
124
  message: `Approve skills from ${inspection.id}`,
125
- choices: inspection.skills.map((skill) => ({
126
- title: skill.name,
127
- value: skill.name,
128
- ...(skill.description ? { description: compactDescription(skill.description) } : {}),
129
- })),
125
+ choices: inspection.skills.map((skill) => {
126
+ const choice = { title: skill.name, value: skill.name };
127
+
128
+ if (skill.description) {
129
+ Object.assign(choice, { description: compactDescription(skill.description) });
130
+ }
131
+
132
+ return choice;
133
+ }),
130
134
  min: 1,
131
135
  });
132
136
 
@@ -196,13 +200,16 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
196
200
  return yield* CatalogManagerError.make({ message: "use either --all or --skill, not both" });
197
201
  }
198
202
  const state = yield* readState(options);
199
- const inspection = yield* inspectCatalogRepository({
203
+ const inspectOptions = {
200
204
  repository: options.repository,
201
205
  repoDir: state.repoDir,
202
- ...(options.id ? { id: options.id } : {}),
203
- ...(options.ref ? { ref: options.ref } : {}),
204
- ...(options.skillsPath ? { skillsPath: options.skillsPath } : {}),
205
- });
206
+ };
207
+
208
+ if (options.id) Object.assign(inspectOptions, { id: options.id });
209
+ if (options.ref) Object.assign(inspectOptions, { ref: options.ref });
210
+ if (options.skillsPath) Object.assign(inspectOptions, { skillsPath: options.skillsPath });
211
+
212
+ const inspection = yield* inspectCatalogRepository(inspectOptions);
206
213
  const selection = yield* selectSkills(inspection, options.skills ?? [], options.all ?? false);
207
214
  const sources = state.sources.value.sources;
208
215
  const byId = sources.findIndex((source) => source.id === inspection.id);
@@ -256,15 +263,13 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
256
263
  ref: inspection.ref,
257
264
  skillsPath: inspection.skillsPath,
258
265
  include: selection.include,
259
- ...(options.licensePath
260
- ? { licensePath: options.licensePath }
261
- : inspection.licensePath
262
- ? { licensePath: inspection.licensePath }
263
- : {}),
264
- ...(options.stripFrontmatter?.length
265
- ? { stripFrontmatter: [...new Set(options.stripFrontmatter)] }
266
- : {}),
267
266
  };
267
+ const licensePath = options.licensePath || inspection.licensePath;
268
+
269
+ if (licensePath) Object.assign(source, { licensePath });
270
+ if (options.stripFrontmatter?.length) {
271
+ Object.assign(source, { stripFrontmatter: [...new Set(options.stripFrontmatter)] });
272
+ }
268
273
 
269
274
  next = applyEdits(
270
275
  next,
package/src/catalog.ts CHANGED
@@ -185,7 +185,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
185
185
  message: `duplicate catalog family: ${duplicateFamily[0]}`,
186
186
  });
187
187
  }
188
- const families: Readonly<Record<string, ReadonlyArray<string>>> = {
188
+ const families = {
189
189
  effect: [
190
190
  "effect-ts",
191
191
  "effect-architecture-audit",
@@ -194,13 +194,16 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
194
194
  "build-effect-clis",
195
195
  ],
196
196
  ...Object.fromEntries(externalFamilies),
197
- };
197
+ } satisfies Readonly<Record<string, ReadonlyArray<string>>>;
198
198
 
199
- return {
199
+ const catalog: SkillCatalog = {
200
200
  skills: skills.sort((left, right) => left.selector.localeCompare(right.selector)),
201
201
  families,
202
- ...(lock ? { lock } : {}),
203
- } satisfies SkillCatalog;
202
+ };
203
+
204
+ if (lock) Object.assign(catalog, { lock });
205
+
206
+ return catalog;
204
207
  });
205
208
 
206
209
  const stripFrontmatterKeys = (text: string, keys: ReadonlyArray<string>): string => {
package/src/cli-ui.ts CHANGED
@@ -12,12 +12,12 @@ const ANSI = {
12
12
 
13
13
  type StatusKind = "success" | "info" | "plan" | "error";
14
14
 
15
- const statusAppearance: Readonly<Record<StatusKind, readonly [string, string]>> = {
15
+ const statusAppearance = {
16
16
  success: ["✓", ANSI.green],
17
17
  info: ["•", ANSI.cyan],
18
18
  plan: ["→", ANSI.yellow],
19
19
  error: ["✗", ANSI.red],
20
- };
20
+ } satisfies Readonly<Record<StatusKind, readonly [string, string]>>;
21
21
 
22
22
  const terminalCapabilities = Effect.fn("terminalCapabilities")(function* () {
23
23
  const terminal = yield* Terminal.Terminal;
@@ -5,7 +5,7 @@ import { printStatus, withSpinner } from "./cli-ui.ts";
5
5
  import { acquireProjectProcessLock } from "./project-process-lock.ts";
6
6
  import { isTypeScriptPackageName } from "./typescript-package-name.ts";
7
7
 
8
- export const EFFECT_TSGO_VERSION = "0.33.0";
8
+ export const EFFECT_TSGO_VERSION = "0.36.4";
9
9
  export const EFFECT_TSGO_TYPESCRIPT_VERSION = "7.0.2";
10
10
  export const EFFECT_TSGO_PLUGIN_NAME = "@effect/language-service";
11
11
 
@@ -262,11 +262,15 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
262
262
  const typescriptPackageArgs =
263
263
  typescriptPackage === "typescript" ? [] : ["--typescript-package", typescriptPackage];
264
264
 
265
- return {
265
+ const plan = {
266
266
  alreadyPatched: patchState.alreadyPatched,
267
- ...(patchState.hasBackup && !patchState.alreadyPatched
268
- ? { unpatchArgs: ["unpatch", "--typescript", ...typescriptPackageArgs] }
269
- : {}),
267
+ };
268
+
269
+ if (patchState.hasBackup && !patchState.alreadyPatched) {
270
+ Object.assign(plan, { unpatchArgs: ["unpatch", "--typescript", ...typescriptPackageArgs] });
271
+ }
272
+
273
+ return Object.assign(plan, {
270
274
  projectDir,
271
275
  executable,
272
276
  args: [
@@ -278,7 +282,7 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
278
282
  effectTsgoVersion,
279
283
  typescriptPackage,
280
284
  typescriptVersion,
281
- } satisfies EffectTsgoPatchPlan;
285
+ }) satisfies EffectTsgoPatchPlan;
282
286
  });
283
287
 
284
288
  const runEffectTsgoCommand = Effect.fn("runEffectTsgoCommand")(function* (
package/src/gitignore.ts CHANGED
@@ -50,15 +50,18 @@ type PlannedGitignorePatch = GitignorePatch & {
50
50
  readonly previousContents: string;
51
51
  };
52
52
 
53
+ type GitignoreContentsPatch = {
54
+ readonly contents: string;
55
+ readonly added: ReadonlyArray<string>;
56
+ };
57
+
53
58
  const publicPatch = (patch: PlannedGitignorePatch): GitignorePatch => ({
54
59
  path: patch.path,
55
60
  changed: patch.changed,
56
61
  added: patch.added,
57
62
  });
58
63
 
59
- export const patchGitignoreContents = (
60
- current: string,
61
- ): { readonly contents: string; readonly added: ReadonlyArray<string> } => {
64
+ export const patchGitignoreContents = (current: string): GitignoreContentsPatch => {
62
65
  const lines = current.split(/\r?\n/);
63
66
  const added = DEV_KIT_GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
64
67
 
package/src/manifest.ts CHANGED
@@ -146,13 +146,21 @@ export type NormalizedManifest = {
146
146
  readonly targets: Readonly<Record<HarnessTarget, NormalizedTargetConfig>>;
147
147
  };
148
148
 
149
- const DEFAULT_TARGET_PATHS: Readonly<Record<HarnessTarget, string>> = {
149
+ type DefaultTargetPaths = {
150
+ readonly [Target in HarnessTarget]: string;
151
+ };
152
+
153
+ type DefaultTargets = {
154
+ readonly [Target in HarnessTarget]: NormalizedTargetConfig;
155
+ };
156
+
157
+ const DEFAULT_TARGET_PATHS: DefaultTargetPaths = {
150
158
  agents: ".agents/skills",
151
159
  claude: ".claude/skills",
152
160
  opencode: ".opencode/skills",
153
161
  };
154
162
 
155
- const DEFAULT_TARGETS: Readonly<Record<HarnessTarget, NormalizedTargetConfig>> = {
163
+ const DEFAULT_TARGETS: DefaultTargets = {
156
164
  agents: { enabled: true, mode: "copy", path: DEFAULT_TARGET_PATHS.agents },
157
165
  claude: { enabled: false, mode: "symlink", path: DEFAULT_TARGET_PATHS.claude },
158
166
  opencode: { enabled: false, mode: "symlink", path: DEFAULT_TARGET_PATHS.opencode },
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dillon Mulroy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,43 @@
1
+ // Vendored from dmmulroy/anti-slop commit 9b80d9a5c317d3af94d88a577bdbde4d9a45f7be.
2
+ // The upstream MIT notice is retained in LICENSE.
3
+ import { definePlugin } from "@oxlint/plugins";
4
+
5
+ import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
6
+ import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
7
+ import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
8
+ import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
9
+ import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
10
+ import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
11
+ import { noReflectGetRule } from "./rules/no-reflect-get.ts";
12
+ import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
13
+ import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
14
+ import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
15
+ import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
16
+ import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
17
+ import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
18
+ import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
19
+ import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
20
+
21
+ /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
22
+ const antiSlopPlugin = definePlugin({
23
+ meta: { name: "anti-slop" },
24
+ rules: {
25
+ "no-chained-type-assertions": noChainedTypeAssertionsRule,
26
+ "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
27
+ "no-known-value-widening": noKnownValueWideningRule,
28
+ "no-module-mocking": noModuleMockingRule,
29
+ "no-object-parameters": noObjectParametersRule,
30
+ "no-reflect-apply": noReflectApplyRule,
31
+ "no-reflect-get": noReflectGetRule,
32
+ "no-runtime-typeof": noRuntimeTypeofRule,
33
+ "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
34
+ "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
35
+ "no-unknown-parameters": noUnknownParametersRule,
36
+ "no-unknown-returns": noUnknownReturnsRule,
37
+ "no-unknown-type-aliases": noUnknownTypeAliasesRule,
38
+ "no-widen-then-assert": noWidenThenAssertRule,
39
+ "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
40
+ },
41
+ });
42
+
43
+ export default antiSlopPlugin;
@@ -0,0 +1,79 @@
1
+ import { defineRule, type ESTree } from "@oxlint/plugins";
2
+
3
+ type TypeAssertionExpression = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
4
+
5
+ function isTypeAssertionExpression(node: ESTree.Node): node is TypeAssertionExpression {
6
+ return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
7
+ }
8
+
9
+ function unwrapParenthesizedExpression(expression: ESTree.Expression): ESTree.Expression {
10
+ let current = expression;
11
+
12
+ while (current.type === "ParenthesizedExpression") {
13
+ current = current.expression;
14
+ }
15
+
16
+ return current;
17
+ }
18
+
19
+ function isConstAssertion(node: TypeAssertionExpression): boolean {
20
+ const { typeAnnotation } = node;
21
+
22
+ return (
23
+ typeAnnotation.type === "TSTypeReference" &&
24
+ typeAnnotation.typeName.type === "Identifier" &&
25
+ typeAnnotation.typeName.name === "const"
26
+ );
27
+ }
28
+
29
+ function isOutermostAssertionInChain(node: TypeAssertionExpression): boolean {
30
+ let current: ESTree.Expression = node;
31
+ let parent = node.parent;
32
+
33
+ while (parent.type === "ParenthesizedExpression" && parent.expression === current) {
34
+ current = parent;
35
+ parent = parent.parent;
36
+ }
37
+
38
+ return !isTypeAssertionExpression(parent) || parent.expression !== current;
39
+ }
40
+
41
+ function isForbiddenAssertionChain(node: TypeAssertionExpression): boolean {
42
+ let assertionCount = 0;
43
+ let hasNonConstAssertion = false;
44
+ let current: ESTree.Expression = node;
45
+
46
+ while (isTypeAssertionExpression(current)) {
47
+ assertionCount += 1;
48
+ hasNonConstAssertion ||= !isConstAssertion(current);
49
+ current = unwrapParenthesizedExpression(current.expression);
50
+ }
51
+
52
+ return assertionCount > 1 && hasNonConstAssertion;
53
+ }
54
+
55
+ /** Disallow nested TypeScript type assertions, while permitting chains made only of const assertions. */
56
+ export const noChainedTypeAssertionsRule = defineRule({
57
+ meta: {
58
+ type: "problem",
59
+ docs: {
60
+ description:
61
+ "Disallow chained TypeScript as and angle-bracket assertions, including parenthesized chains.",
62
+ },
63
+ messages: {
64
+ chained:
65
+ "This assertion chain discards type evidence. Keep the original precise type, or parse untrusted input at its boundary before narrowing it.",
66
+ },
67
+ },
68
+ create(context) {
69
+ const checkTypeAssertion = (node: TypeAssertionExpression) => {
70
+ if (!isOutermostAssertionInChain(node) || !isForbiddenAssertionChain(node)) return;
71
+ context.report({ node, messageId: "chained" });
72
+ };
73
+
74
+ return {
75
+ TSAsExpression: checkTypeAssertion,
76
+ TSTypeAssertion: checkTypeAssertion,
77
+ };
78
+ },
79
+ });