@danieljvdm/dev-kit 0.17.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.
- package/README.md +18 -7
- package/package.json +20 -9
- package/scripts/sync-anti-slop-runtime.mjs +19 -0
- package/skills/dev-kit/SKILL.md +4 -5
- package/src/bin/dev-kit.ts +11 -7
- package/src/catalog-manager.ts +23 -18
- package/src/catalog.ts +8 -5
- package/src/cli-ui.ts +2 -2
- package/src/effect-tsgo.ts +10 -6
- package/src/gitignore.ts +6 -3
- package/src/manifest.ts +10 -2
- package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
- package/src/oxlint-plugin-anti-slop/index.ts +43 -0
- package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
- package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
- package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
- package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
- package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
- package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
- package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
- package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
- package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
- package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
- package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
- package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
- package/src/oxlint-plugin-anti-slop/runtime.d.ts +22 -0
- package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
- package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
- package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
- package/src/oxlint-plugin-effect.d.ts +4 -2
- package/src/oxlint.js +19 -0
- package/src/oxlint.ts +22 -5
- package/src/package-skill-source.ts +17 -25
- package/src/path-digest.ts +2 -1
- package/src/project-package.ts +14 -12
- package/src/skill-manager.ts +25 -13
- package/src/sync.ts +65 -49
- 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.
|
|
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
|
|
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.
|
|
708
|
-
the preset's JavaScript-plugin
|
|
709
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
-
"@effect/tsgo": "0.
|
|
82
|
-
"@effect/vitest": "4.0.0-beta.
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/skills/dev-kit/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
package/src/bin/dev-kit.ts
CHANGED
|
@@ -283,8 +283,8 @@ const catalogAddCommand = CliCommand.make(
|
|
|
283
283
|
lockfile,
|
|
284
284
|
repoDir,
|
|
285
285
|
sources,
|
|
286
|
-
}) =>
|
|
287
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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(
|
package/src/catalog-manager.ts
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
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
|
|
203
|
+
const inspectOptions = {
|
|
200
204
|
repository: options.repository,
|
|
201
205
|
repoDir: state.repoDir,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
199
|
+
const catalog: SkillCatalog = {
|
|
200
200
|
skills: skills.sort((left, right) => left.selector.localeCompare(right.selector)),
|
|
201
201
|
families,
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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;
|
package/src/effect-tsgo.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
265
|
+
const plan = {
|
|
266
266
|
alreadyPatched: patchState.alreadyPatched,
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineRule, type ESTree } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
function unwrapParentheses(node: ESTree.Expression): ESTree.Expression {
|
|
4
|
+
let current = node;
|
|
5
|
+
|
|
6
|
+
while (current.type === "ParenthesizedExpression") {
|
|
7
|
+
current = current.expression;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isEmptyObjectExpression(node: ESTree.Expression): boolean {
|
|
14
|
+
return node.type === "ObjectExpression" && node.properties.length === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isConditionalEmptyObjectSpread(node: ESTree.Expression): boolean {
|
|
18
|
+
const conditional = unwrapParentheses(node);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
conditional.type === "ConditionalExpression" &&
|
|
22
|
+
(isEmptyObjectExpression(conditional.consequent) ||
|
|
23
|
+
isEmptyObjectExpression(conditional.alternate))
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Ban conditional empty-object spreads without changing their omission semantics. */
|
|
28
|
+
export const noConditionalEmptyObjectSpreadRule = defineRule({
|
|
29
|
+
meta: {
|
|
30
|
+
type: "suggestion",
|
|
31
|
+
docs: {
|
|
32
|
+
description:
|
|
33
|
+
"Disallow object spreads that conditionally spread an empty object to omit fields.",
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
avoid:
|
|
37
|
+
"This conditional spread hides property omission behind an empty object. Build the object in separate statements and add the property only when present.",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
return {
|
|
42
|
+
SpreadElement(node) {
|
|
43
|
+
if (node.parent.type !== "ObjectExpression") return;
|
|
44
|
+
|
|
45
|
+
if (isConditionalEmptyObjectSpread(node.argument)) {
|
|
46
|
+
context.report({ node, messageId: "avoid" });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|