@timmo001/oxlint-rules 0.5.0 → 0.6.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
@@ -69,6 +69,19 @@ the package configs to merge into the target Oxlint config. Effect settings
69
69
  remain opt-in. It excludes tests and repository metadata, and refuses to
70
70
  replace an existing destination unless `--force` is passed.
71
71
 
72
+ ## Skills
73
+
74
+ [`install-timmo-oxlint-rules`](skills/install-timmo-oxlint-rules/SKILL.md)
75
+ guides agents through installing or copying these rules:
76
+
77
+ ```sh
78
+ npx skills add timmo001/oxlint-rules --skill install-timmo-oxlint-rules
79
+ ```
80
+
81
+ [`add-oxlint-rule`](skills/add-oxlint-rule/SKILL.md) and
82
+ [`release-oxlint-rules`](skills/release-oxlint-rules/SKILL.md) are for
83
+ maintaining this package.
84
+
72
85
  ## Rules
73
86
 
74
87
  ### `anti-slop` and `anti-slop-effect`
@@ -122,6 +135,12 @@ they only run when the consumer enables `typeAware`.
122
135
 
123
136
  ### `timmo-effect`
124
137
 
138
+ - `no-raw-gh`: reports the raw `execute`, `json` and `stream` methods called on
139
+ a `yield* Gh` binding from `@timmo001/effect-gh`, `Api.raw` from that package,
140
+ and any call whose first argument is `"gh"` or an array starting with `"gh"`,
141
+ such as `executor.run("gh", ...)` or `Bun.spawn(["gh", ...])`. Use the
142
+ package's typed operations, and add one to effect-gh when a command has none.
143
+ effect-gh's own relative imports are not reported.
125
144
  - `no-try-catch-in-effect-generators`: diagnoses synchronous `try/catch` owned
126
145
  by generators passed directly to `Effect.gen` or the curried `Effect.fn`
127
146
  form. It leaves ordinary async polling, nested callback boundaries,
@@ -139,8 +158,7 @@ Initialise the upstream source and run the package checks:
139
158
 
140
159
  ```sh
141
160
  git submodule update --init --recursive
142
- mise run check
143
- mise run build
161
+ mise run check ::: build
144
162
  mise run test:package
145
163
  npm pack --dry-run
146
164
  bunx jsr@0.14.3 publish --dry-run --allow-dirty
package/dist/cli.js CHANGED
@@ -2985,8 +2985,71 @@ var recommended_default = recommended;
2985
2985
  // src/effect/index.ts
2986
2986
  import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
2987
2987
 
2988
- // src/effect/rules/no-try-catch-in-effect-generators.ts
2988
+ // src/effect/rules/no-raw-gh.ts
2989
2989
  import { defineRule as defineRule24 } from "@oxlint/plugins";
2990
+ var PACKAGE = "@timmo001/effect-gh";
2991
+ var rawMethods = new Set(["execute", "json", "stream"]);
2992
+ function isEffectGhImport(sourceCode, identifier, name) {
2993
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
2994
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== PACKAGE || definition.node.type !== "ImportSpecifier") {
2995
+ return false;
2996
+ }
2997
+ const imported = definition.node.imported;
2998
+ return (imported.type === "Identifier" ? imported.name : imported.value) === name;
2999
+ }) ?? false;
3000
+ }
3001
+ function isYieldGh(sourceCode, node) {
3002
+ return node?.type === "YieldExpression" && node.delegate && node.argument?.type === "Identifier" && isEffectGhImport(sourceCode, node.argument, "Gh");
3003
+ }
3004
+ function isGhBinding(sourceCode, identifier) {
3005
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
3006
+ }
3007
+ function isGhArgument(node) {
3008
+ if (node?.type === "Literal")
3009
+ return node.value === "gh";
3010
+ if (node?.type !== "ArrayExpression")
3011
+ return false;
3012
+ const first = node.elements[0];
3013
+ return first?.type === "Literal" && first.value === "gh";
3014
+ }
3015
+ var noRawGhRule = defineRule24({
3016
+ meta: {
3017
+ type: "problem",
3018
+ docs: {
3019
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
3020
+ },
3021
+ messages: {
3022
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
3023
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
3024
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
3025
+ }
3026
+ },
3027
+ create(context) {
3028
+ return {
3029
+ CallExpression(node) {
3030
+ const callee = node.callee;
3031
+ const method = staticMemberName(callee);
3032
+ if (callee.type === "MemberExpression" && method !== null) {
3033
+ const object = callee.object;
3034
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
3035
+ context.report({ node, messageId: "rawGh", data: { method } });
3036
+ return;
3037
+ }
3038
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
3039
+ context.report({ node, messageId: "rawApi" });
3040
+ return;
3041
+ }
3042
+ }
3043
+ if (isGhArgument(node.arguments[0]))
3044
+ context.report({ node, messageId: "spawnGh" });
3045
+ }
3046
+ };
3047
+ }
3048
+ });
3049
+
3050
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
3051
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
3052
+
2990
3053
  // src/shared/effect.ts
2991
3054
  function isEffectImport(sourceCode, identifier, namespace) {
2992
3055
  const variable = resolveVariable(sourceCode, identifier);
@@ -3033,7 +3096,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
3033
3096
  const factoryCall = parent.callee;
3034
3097
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
3035
3098
  }
3036
- var noTryCatchInEffectGeneratorsRule = defineRule24({
3099
+ var noTryCatchInEffectGeneratorsRule = defineRule25({
3037
3100
  meta: {
3038
3101
  type: "problem",
3039
3102
  docs: {
@@ -3059,7 +3122,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule24({
3059
3122
  });
3060
3123
 
3061
3124
  // src/effect/rules/prefer-platform-services.ts
3062
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3125
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
3063
3126
  var moduleServices = new Map([
3064
3127
  ["child_process", "childProcess"],
3065
3128
  ["fs", "fileSystem"],
@@ -3072,7 +3135,7 @@ var bunServices = new Map([
3072
3135
  ["spawnSync", "childProcess"],
3073
3136
  ["write", "fileSystem"]
3074
3137
  ]);
3075
- var preferPlatformServicesRule = defineRule25({
3138
+ var preferPlatformServicesRule = defineRule26({
3076
3139
  meta: {
3077
3140
  type: "suggestion",
3078
3141
  docs: {
@@ -3135,6 +3198,7 @@ var preferPlatformServicesRule = defineRule25({
3135
3198
  var timmoEffectPlugin = eslintCompatPlugin3({
3136
3199
  meta: { name: "timmo-effect" },
3137
3200
  rules: {
3201
+ "no-raw-gh": noRawGhRule,
3138
3202
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
3139
3203
  "prefer-platform-services": preferPlatformServicesRule
3140
3204
  }
@@ -3145,7 +3209,7 @@ var effect_default = timmoEffectPlugin;
3145
3209
  import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
3146
3210
 
3147
3211
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
3148
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3212
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
3149
3213
 
3150
3214
  // vendor/anti-slop/src/effect/shared/tagged-values.ts
3151
3215
  var equalityOperators = new Set(["==", "===", "!=", "!=="]);
@@ -3192,7 +3256,7 @@ var isMatchPatternObject = (node) => {
3192
3256
  };
3193
3257
 
3194
3258
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
3195
- var noManualEffectErrorTagRule = defineRule26({
3259
+ var noManualEffectErrorTagRule = defineRule27({
3196
3260
  meta: {
3197
3261
  type: "problem",
3198
3262
  docs: {
@@ -3229,8 +3293,8 @@ var noManualEffectErrorTagRule = defineRule26({
3229
3293
  });
3230
3294
 
3231
3295
  // vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
3232
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3233
- var noManualTagComparisonRule = defineRule27({
3296
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
3297
+ var noManualTagComparisonRule = defineRule28({
3234
3298
  meta: {
3235
3299
  type: "problem",
3236
3300
  docs: {
@@ -3260,8 +3324,8 @@ var noManualTagComparisonRule = defineRule27({
3260
3324
  });
3261
3325
 
3262
3326
  // vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
3263
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3264
- var noManualTaggedConstructionRule = defineRule28({
3327
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3328
+ var noManualTaggedConstructionRule = defineRule29({
3265
3329
  meta: {
3266
3330
  type: "problem",
3267
3331
  docs: {
@@ -3286,7 +3350,7 @@ var noManualTaggedConstructionRule = defineRule28({
3286
3350
  });
3287
3351
 
3288
3352
  // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
3289
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3353
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3290
3354
  var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
3291
3355
  var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
3292
3356
  function isProjectLocalImport(source) {
@@ -3297,7 +3361,7 @@ function getImportedName(specifier) {
3297
3361
  return specifier.imported.name;
3298
3362
  return specifier.imported.value;
3299
3363
  }
3300
- var noServiceConstructorImportsRule = defineRule29({
3364
+ var noServiceConstructorImportsRule = defineRule30({
3301
3365
  meta: {
3302
3366
  type: "problem",
3303
3367
  docs: {
@@ -3331,9 +3395,9 @@ var noServiceConstructorImportsRule = defineRule29({
3331
3395
  });
3332
3396
 
3333
3397
  // vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
3334
- import { defineRule as defineRule30 } from "@oxlint/plugins";
3398
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3335
3399
  var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
3336
- var preferEffectMatchRule = defineRule30({
3400
+ var preferEffectMatchRule = defineRule31({
3337
3401
  meta: {
3338
3402
  type: "problem",
3339
3403
  docs: {
@@ -6,7 +6,7 @@ function defineConfig(config) {
6
6
  // src/effect/index.ts
7
7
  import { eslintCompatPlugin } from "@oxlint/plugins";
8
8
 
9
- // src/effect/rules/no-try-catch-in-effect-generators.ts
9
+ // src/effect/rules/no-raw-gh.ts
10
10
  import { defineRule } from "@oxlint/plugins";
11
11
 
12
12
  // src/shared/ast.ts
@@ -37,6 +37,70 @@ function resolveVariable(sourceCode, identifier) {
37
37
  }
38
38
  return null;
39
39
  }
40
+ // src/effect/rules/no-raw-gh.ts
41
+ var PACKAGE = "@timmo001/effect-gh";
42
+ var rawMethods = new Set(["execute", "json", "stream"]);
43
+ function isEffectGhImport(sourceCode, identifier, name) {
44
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
45
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== PACKAGE || definition.node.type !== "ImportSpecifier") {
46
+ return false;
47
+ }
48
+ const imported = definition.node.imported;
49
+ return (imported.type === "Identifier" ? imported.name : imported.value) === name;
50
+ }) ?? false;
51
+ }
52
+ function isYieldGh(sourceCode, node) {
53
+ return node?.type === "YieldExpression" && node.delegate && node.argument?.type === "Identifier" && isEffectGhImport(sourceCode, node.argument, "Gh");
54
+ }
55
+ function isGhBinding(sourceCode, identifier) {
56
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
57
+ }
58
+ function isGhArgument(node) {
59
+ if (node?.type === "Literal")
60
+ return node.value === "gh";
61
+ if (node?.type !== "ArrayExpression")
62
+ return false;
63
+ const first = node.elements[0];
64
+ return first?.type === "Literal" && first.value === "gh";
65
+ }
66
+ var noRawGhRule = defineRule({
67
+ meta: {
68
+ type: "problem",
69
+ docs: {
70
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
71
+ },
72
+ messages: {
73
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
74
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
75
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
76
+ }
77
+ },
78
+ create(context) {
79
+ return {
80
+ CallExpression(node) {
81
+ const callee = node.callee;
82
+ const method = staticMemberName(callee);
83
+ if (callee.type === "MemberExpression" && method !== null) {
84
+ const object = callee.object;
85
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
86
+ context.report({ node, messageId: "rawGh", data: { method } });
87
+ return;
88
+ }
89
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
90
+ context.report({ node, messageId: "rawApi" });
91
+ return;
92
+ }
93
+ }
94
+ if (isGhArgument(node.arguments[0]))
95
+ context.report({ node, messageId: "spawnGh" });
96
+ }
97
+ };
98
+ }
99
+ });
100
+
101
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
102
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
103
+
40
104
  // src/shared/effect.ts
41
105
  function isEffectImport(sourceCode, identifier, namespace) {
42
106
  const variable = resolveVariable(sourceCode, identifier);
@@ -83,7 +147,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
83
147
  const factoryCall = parent.callee;
84
148
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
85
149
  }
86
- var noTryCatchInEffectGeneratorsRule = defineRule({
150
+ var noTryCatchInEffectGeneratorsRule = defineRule2({
87
151
  meta: {
88
152
  type: "problem",
89
153
  docs: {
@@ -109,7 +173,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule({
109
173
  });
110
174
 
111
175
  // src/effect/rules/prefer-platform-services.ts
112
- import { defineRule as defineRule2 } from "@oxlint/plugins";
176
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
113
177
  var moduleServices = new Map([
114
178
  ["child_process", "childProcess"],
115
179
  ["fs", "fileSystem"],
@@ -122,7 +186,7 @@ var bunServices = new Map([
122
186
  ["spawnSync", "childProcess"],
123
187
  ["write", "fileSystem"]
124
188
  ]);
125
- var preferPlatformServicesRule = defineRule2({
189
+ var preferPlatformServicesRule = defineRule3({
126
190
  meta: {
127
191
  type: "suggestion",
128
192
  docs: {
@@ -185,6 +249,7 @@ var preferPlatformServicesRule = defineRule2({
185
249
  var timmoEffectPlugin = eslintCompatPlugin({
186
250
  meta: { name: "timmo-effect" },
187
251
  rules: {
252
+ "no-raw-gh": noRawGhRule,
188
253
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
189
254
  "prefer-platform-services": preferPlatformServicesRule
190
255
  }
@@ -195,7 +260,7 @@ var effect_default = timmoEffectPlugin;
195
260
  import { eslintCompatPlugin as eslintCompatPlugin2 } from "@oxlint/plugins";
196
261
 
197
262
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
198
- import { defineRule as defineRule3 } from "@oxlint/plugins";
263
+ import { defineRule as defineRule4 } from "@oxlint/plugins";
199
264
 
200
265
  // vendor/anti-slop/src/effect/shared/tagged-values.ts
201
266
  var equalityOperators = new Set(["==", "===", "!=", "!=="]);
@@ -242,7 +307,7 @@ var isMatchPatternObject = (node) => {
242
307
  };
243
308
 
244
309
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
245
- var noManualEffectErrorTagRule = defineRule3({
310
+ var noManualEffectErrorTagRule = defineRule4({
246
311
  meta: {
247
312
  type: "problem",
248
313
  docs: {
@@ -279,8 +344,8 @@ var noManualEffectErrorTagRule = defineRule3({
279
344
  });
280
345
 
281
346
  // vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
282
- import { defineRule as defineRule4 } from "@oxlint/plugins";
283
- var noManualTagComparisonRule = defineRule4({
347
+ import { defineRule as defineRule5 } from "@oxlint/plugins";
348
+ var noManualTagComparisonRule = defineRule5({
284
349
  meta: {
285
350
  type: "problem",
286
351
  docs: {
@@ -310,8 +375,8 @@ var noManualTagComparisonRule = defineRule4({
310
375
  });
311
376
 
312
377
  // vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
313
- import { defineRule as defineRule5 } from "@oxlint/plugins";
314
- var noManualTaggedConstructionRule = defineRule5({
378
+ import { defineRule as defineRule6 } from "@oxlint/plugins";
379
+ var noManualTaggedConstructionRule = defineRule6({
315
380
  meta: {
316
381
  type: "problem",
317
382
  docs: {
@@ -336,7 +401,7 @@ var noManualTaggedConstructionRule = defineRule5({
336
401
  });
337
402
 
338
403
  // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
339
- import { defineRule as defineRule6 } from "@oxlint/plugins";
404
+ import { defineRule as defineRule7 } from "@oxlint/plugins";
340
405
  var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
341
406
  var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
342
407
  function isProjectLocalImport(source) {
@@ -347,7 +412,7 @@ function getImportedName(specifier) {
347
412
  return specifier.imported.name;
348
413
  return specifier.imported.value;
349
414
  }
350
- var noServiceConstructorImportsRule = defineRule6({
415
+ var noServiceConstructorImportsRule = defineRule7({
351
416
  meta: {
352
417
  type: "problem",
353
418
  docs: {
@@ -381,9 +446,9 @@ var noServiceConstructorImportsRule = defineRule6({
381
446
  });
382
447
 
383
448
  // vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
384
- import { defineRule as defineRule7 } from "@oxlint/plugins";
449
+ import { defineRule as defineRule8 } from "@oxlint/plugins";
385
450
  var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
386
- var preferEffectMatchRule = defineRule7({
451
+ var preferEffectMatchRule = defineRule8({
387
452
  meta: {
388
453
  type: "problem",
389
454
  docs: {
@@ -449,7 +514,7 @@ function enablePluginRules(namespace, plugin) {
449
514
  import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
450
515
 
451
516
  // vendor/anti-slop/src/rules/no-array-filter-map.ts
452
- import { defineRule as defineRule8 } from "@oxlint/plugins";
517
+ import { defineRule as defineRule9 } from "@oxlint/plugins";
453
518
 
454
519
  // vendor/anti-slop/src/shared/array-method.ts
455
520
  function unwrapArrayExpression(node) {
@@ -524,7 +589,7 @@ function isKnownArrayExpression(sourceCode, node, visited = new Set) {
524
589
  }
525
590
 
526
591
  // vendor/anti-slop/src/rules/no-array-filter-map.ts
527
- var noArrayFilterMapRule = defineRule8({
592
+ var noArrayFilterMapRule = defineRule9({
528
593
  meta: {
529
594
  type: "suggestion",
530
595
  docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
@@ -553,7 +618,7 @@ var noArrayFilterMapRule = defineRule8({
553
618
  });
554
619
 
555
620
  // vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts
556
- import { defineRule as defineRule9 } from "@oxlint/plugins";
621
+ import { defineRule as defineRule10 } from "@oxlint/plugins";
557
622
  function enclosingReducer(node) {
558
623
  let parent = node.parent;
559
624
  while (parent !== null) {
@@ -603,7 +668,7 @@ function isGlobalCopyOwner(sourceCode, node, name) {
603
668
  const variable = resolveArrayBinding(sourceCode, node);
604
669
  return variable === null || variable.defs.length === 0;
605
670
  }
606
- var noReduceAccumulatorCopyRule = defineRule9({
671
+ var noReduceAccumulatorCopyRule = defineRule10({
607
672
  meta: {
608
673
  type: "problem",
609
674
  docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
@@ -644,7 +709,7 @@ var noReduceAccumulatorCopyRule = defineRule9({
644
709
  });
645
710
 
646
711
  // vendor/anti-slop/src/rules/no-chained-type-assertions.ts
647
- import { defineRule as defineRule10 } from "@oxlint/plugins";
712
+ import { defineRule as defineRule11 } from "@oxlint/plugins";
648
713
  function isTypeAssertionExpression(node) {
649
714
  return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
650
715
  }
@@ -679,7 +744,7 @@ function isForbiddenAssertionChain(node) {
679
744
  }
680
745
  return assertionCount > 1 && hasNonConstAssertion;
681
746
  }
682
- var noChainedTypeAssertionsRule = defineRule10({
747
+ var noChainedTypeAssertionsRule = defineRule11({
683
748
  meta: {
684
749
  type: "problem",
685
750
  docs: {
@@ -703,7 +768,7 @@ var noChainedTypeAssertionsRule = defineRule10({
703
768
  });
704
769
 
705
770
  // vendor/anti-slop/src/rules/no-conditional-empty-object-spread.ts
706
- import { defineRule as defineRule11 } from "@oxlint/plugins";
771
+ import { defineRule as defineRule12 } from "@oxlint/plugins";
707
772
  function unwrapParentheses(node) {
708
773
  let current = node;
709
774
  while (current.type === "ParenthesizedExpression") {
@@ -718,7 +783,7 @@ function isConditionalEmptyObjectSpread(node) {
718
783
  const conditional = unwrapParentheses(node);
719
784
  return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression(conditional.consequent) || isEmptyObjectExpression(conditional.alternate));
720
785
  }
721
- var noConditionalEmptyObjectSpreadRule = defineRule11({
786
+ var noConditionalEmptyObjectSpreadRule = defineRule12({
722
787
  meta: {
723
788
  type: "suggestion",
724
789
  docs: {
@@ -742,7 +807,7 @@ var noConditionalEmptyObjectSpreadRule = defineRule11({
742
807
  });
743
808
 
744
809
  // vendor/anti-slop/src/rules/no-known-value-widening.ts
745
- import { defineRule as defineRule12 } from "@oxlint/plugins";
810
+ import { defineRule as defineRule13 } from "@oxlint/plugins";
746
811
 
747
812
  // vendor/anti-slop/src/shared/lexical-type-parameters.ts
748
813
  function isNode(value) {
@@ -1431,7 +1496,7 @@ function isDictionaryAccumulatorTarget(destination) {
1431
1496
  function hasParentAssertion(node) {
1432
1497
  return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
1433
1498
  }
1434
- var noKnownValueWideningRule = defineRule12({
1499
+ var noKnownValueWideningRule = defineRule13({
1435
1500
  meta: {
1436
1501
  type: "problem",
1437
1502
  docs: {
@@ -1544,7 +1609,7 @@ var noKnownValueWideningRule = defineRule12({
1544
1609
  });
1545
1610
 
1546
1611
  // vendor/anti-slop/src/rules/no-module-mocking.ts
1547
- import { defineRule as defineRule13 } from "@oxlint/plugins";
1612
+ import { defineRule as defineRule14 } from "@oxlint/plugins";
1548
1613
  var moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
1549
1614
  function importedName(node) {
1550
1615
  if (node.type !== "ImportSpecifier")
@@ -1579,7 +1644,7 @@ function moduleMockCall(sourceCode, callee) {
1579
1644
  const method = callee.computed ? property.type === "Literal" && (property.value === "doMock" || property.value === "mock" || property.value === "unstable_mockModule") ? property.value : null : property.type === "Identifier" ? property.name : null;
1580
1645
  return method !== null && moduleMockMethods.has(method);
1581
1646
  }
1582
- var noModuleMockingRule = defineRule13({
1647
+ var noModuleMockingRule = defineRule14({
1583
1648
  meta: {
1584
1649
  type: "problem",
1585
1650
  docs: {
@@ -1603,8 +1668,8 @@ var noModuleMockingRule = defineRule13({
1603
1668
  });
1604
1669
 
1605
1670
  // vendor/anti-slop/src/rules/no-object-parameters.ts
1606
- import { defineRule as defineRule14 } from "@oxlint/plugins";
1607
- var noObjectParametersRule = defineRule14({
1671
+ import { defineRule as defineRule15 } from "@oxlint/plugins";
1672
+ var noObjectParametersRule = defineRule15({
1608
1673
  meta: {
1609
1674
  type: "problem",
1610
1675
  docs: {
@@ -1657,7 +1722,7 @@ var noObjectParametersRule = defineRule14({
1657
1722
  });
1658
1723
 
1659
1724
  // vendor/anti-slop/src/rules/no-reflect-apply.ts
1660
- import { defineRule as defineRule15 } from "@oxlint/plugins";
1725
+ import { defineRule as defineRule16 } from "@oxlint/plugins";
1661
1726
 
1662
1727
  // vendor/anti-slop/src/shared/reflect-method.ts
1663
1728
  function isGlobalReflect(sourceCode, expression) {
@@ -1678,7 +1743,7 @@ function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
1678
1743
  }
1679
1744
 
1680
1745
  // vendor/anti-slop/src/rules/no-reflect-apply.ts
1681
- var noReflectApplyRule = defineRule15({
1746
+ var noReflectApplyRule = defineRule16({
1682
1747
  meta: {
1683
1748
  type: "problem",
1684
1749
  docs: {
@@ -1702,8 +1767,8 @@ var noReflectApplyRule = defineRule15({
1702
1767
  });
1703
1768
 
1704
1769
  // vendor/anti-slop/src/rules/no-reflect-get.ts
1705
- import { defineRule as defineRule16 } from "@oxlint/plugins";
1706
- var noReflectGetRule = defineRule16({
1770
+ import { defineRule as defineRule17 } from "@oxlint/plugins";
1771
+ var noReflectGetRule = defineRule17({
1707
1772
  meta: {
1708
1773
  type: "problem",
1709
1774
  docs: {
@@ -1727,7 +1792,7 @@ var noReflectGetRule = defineRule16({
1727
1792
  });
1728
1793
 
1729
1794
  // vendor/anti-slop/src/rules/no-runtime-typeof.ts
1730
- import { defineRule as defineRule17 } from "@oxlint/plugins";
1795
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
1731
1796
  function isRuntimeFunction(node) {
1732
1797
  return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
1733
1798
  }
@@ -1750,7 +1815,7 @@ function isExistenceProbe(node) {
1750
1815
  const other = parent.left === node ? parent.right : parent.left;
1751
1816
  return other.type === "Literal" && other.value === "undefined";
1752
1817
  }
1753
- var noRuntimeTypeofRule = defineRule17({
1818
+ var noRuntimeTypeofRule = defineRule18({
1754
1819
  meta: {
1755
1820
  type: "problem",
1756
1821
  docs: {
@@ -1784,7 +1849,7 @@ var noRuntimeTypeofRule = defineRule17({
1784
1849
  });
1785
1850
 
1786
1851
  // vendor/anti-slop/src/rules/no-shape-in-symbol-names.ts
1787
- import { defineRule as defineRule18 } from "@oxlint/plugins";
1852
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
1788
1853
  var FORBIDDEN_SYMBOL_NAME = "shape";
1789
1854
  function containsForbiddenSymbolName(name) {
1790
1855
  return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
@@ -1795,7 +1860,7 @@ function isBorrowedMemberName(node) {
1795
1860
  return false;
1796
1861
  return parent.property === node && parent.computed === false;
1797
1862
  }
1798
- var noForbiddenTermInSymbolNamesRule = defineRule18({
1863
+ var noForbiddenTermInSymbolNamesRule = defineRule19({
1799
1864
  meta: {
1800
1865
  type: "problem",
1801
1866
  docs: {
@@ -1824,12 +1889,12 @@ var noForbiddenTermInSymbolNamesRule = defineRule18({
1824
1889
  });
1825
1890
 
1826
1891
  // vendor/anti-slop/src/rules/no-unknown-parameters.ts
1827
- import { defineRule as defineRule19 } from "@oxlint/plugins";
1892
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
1828
1893
  function isTypePredicateSubject(owner, parameterName) {
1829
1894
  const predicate = owner.returnType?.typeAnnotation;
1830
1895
  return predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier" && predicate.parameterName.name === parameterName;
1831
1896
  }
1832
- var noUnknownParametersRule = defineRule19({
1897
+ var noUnknownParametersRule = defineRule20({
1833
1898
  meta: {
1834
1899
  type: "problem",
1835
1900
  docs: {
@@ -1873,8 +1938,8 @@ var noUnknownParametersRule = defineRule19({
1873
1938
  });
1874
1939
 
1875
1940
  // vendor/anti-slop/src/rules/no-unknown-returns.ts
1876
- import { defineRule as defineRule20 } from "@oxlint/plugins";
1877
- var noUnknownReturnsRule = defineRule20({
1941
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
1942
+ var noUnknownReturnsRule = defineRule21({
1878
1943
  meta: {
1879
1944
  type: "problem",
1880
1945
  docs: {
@@ -1927,8 +1992,8 @@ var noUnknownReturnsRule = defineRule20({
1927
1992
  });
1928
1993
 
1929
1994
  // vendor/anti-slop/src/rules/no-unknown-type-aliases.ts
1930
- import { defineRule as defineRule21 } from "@oxlint/plugins";
1931
- var noUnknownTypeAliasesRule = defineRule21({
1995
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
1996
+ var noUnknownTypeAliasesRule = defineRule22({
1932
1997
  meta: {
1933
1998
  type: "problem",
1934
1999
  docs: {
@@ -1966,7 +2031,7 @@ var noUnknownTypeAliasesRule = defineRule21({
1966
2031
  });
1967
2032
 
1968
2033
  // vendor/anti-slop/src/rules/no-unsafe-dictionary-type.ts
1969
- import { defineRule as defineRule22 } from "@oxlint/plugins";
2034
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
1970
2035
  var typeNodeKinds = new Set([
1971
2036
  "JSDocNonNullableType",
1972
2037
  "JSDocNullableType",
@@ -2053,7 +2118,7 @@ function shouldReportType(node, environment) {
2053
2118
  }
2054
2119
  return true;
2055
2120
  }
2056
- var noUnsafeDictionaryTypeRule = defineRule22({
2121
+ var noUnsafeDictionaryTypeRule = defineRule23({
2057
2122
  meta: {
2058
2123
  type: "problem",
2059
2124
  docs: {
@@ -2095,7 +2160,7 @@ var noUnsafeDictionaryTypeRule = defineRule22({
2095
2160
  });
2096
2161
 
2097
2162
  // vendor/anti-slop/src/rules/no-widen-then-assert.ts
2098
- import { defineRule as defineRule23 } from "@oxlint/plugins";
2163
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
2099
2164
  var functionBoundaryTypes = new Set([
2100
2165
  "ArrowFunctionExpression",
2101
2166
  "FunctionDeclaration",
@@ -2291,7 +2356,7 @@ function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
2291
2356
  return isDefinitelyObjectType(assertedType);
2292
2357
  return isDefinitelyNarrowerRecordType(assertedType);
2293
2358
  }
2294
- var noWidenThenAssertRule = defineRule23({
2359
+ var noWidenThenAssertRule = defineRule24({
2295
2360
  meta: {
2296
2361
  type: "problem",
2297
2362
  docs: {
@@ -2853,7 +2918,7 @@ var requireReadableSpacingRule = {
2853
2918
  };
2854
2919
 
2855
2920
  // vendor/anti-slop/src/rules/require-safety-comment-for-type-assertion.ts
2856
- import { defineRule as defineRule24 } from "@oxlint/plugins";
2921
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2857
2922
  var DEFAULT_SAFETY_MARKERS = ["SAFETY"];
2858
2923
  var commentOwnerKinds = new Set([
2859
2924
  "ExpressionStatement",
@@ -2896,7 +2961,7 @@ function hasSafetyComment(sourceCode, node, pattern) {
2896
2961
  current = current.parent;
2897
2962
  }
2898
2963
  }
2899
- var requireSafetyCommentForTypeAssertionRule = defineRule24({
2964
+ var requireSafetyCommentForTypeAssertionRule = defineRule25({
2900
2965
  meta: {
2901
2966
  type: "problem",
2902
2967
  docs: {
@@ -2974,7 +3039,7 @@ var src_default = antiSlopPlugin;
2974
3039
  import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
2975
3040
 
2976
3041
  // src/generic/rules/no-explicit-inferable-type-predicate.ts
2977
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3042
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
2978
3043
  function isNamed(node, name) {
2979
3044
  return node.type === "Identifier" && node.name === name;
2980
3045
  }
@@ -3014,7 +3079,7 @@ function isPublicSignature(owner) {
3014
3079
  }
3015
3080
  return parent.type === "VariableDeclarator" && parent.parent.parent?.type === "ExportNamedDeclaration";
3016
3081
  }
3017
- var noExplicitInferableTypePredicateRule = defineRule25({
3082
+ var noExplicitInferableTypePredicateRule = defineRule26({
3018
3083
  meta: {
3019
3084
  type: "suggestion",
3020
3085
  docs: {
@@ -3050,7 +3115,7 @@ var noExplicitInferableTypePredicateRule = defineRule25({
3050
3115
  });
3051
3116
 
3052
3117
  // src/generic/rules/no-unchecked-query-selector-type.ts
3053
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3118
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
3054
3119
 
3055
3120
  // src/shared/json.ts
3056
3121
  function jsonObject(value) {
@@ -3151,7 +3216,7 @@ function isLitRenderRootQuery(node, bases) {
3151
3216
 
3152
3217
  // src/generic/rules/no-unchecked-query-selector-type.ts
3153
3218
  var queryMethods = new Set(["closest", "querySelector", "querySelectorAll"]);
3154
- var noUncheckedQuerySelectorTypeRule = defineRule26({
3219
+ var noUncheckedQuerySelectorTypeRule = defineRule27({
3155
3220
  meta: {
3156
3221
  type: "problem",
3157
3222
  docs: {
@@ -3180,8 +3245,8 @@ var noUncheckedQuerySelectorTypeRule = defineRule26({
3180
3245
  });
3181
3246
 
3182
3247
  // src/generic/rules/no-untyped-custom-event.ts
3183
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3184
- var noUntypedCustomEventRule = defineRule27({
3248
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
3249
+ var noUntypedCustomEventRule = defineRule28({
3185
3250
  meta: {
3186
3251
  type: "problem",
3187
3252
  docs: {
@@ -3204,7 +3269,7 @@ var noUntypedCustomEventRule = defineRule27({
3204
3269
  });
3205
3270
 
3206
3271
  // src/generic/rules/prefer-event-parameter-type.ts
3207
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3272
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3208
3273
  function staticMemberPath(node) {
3209
3274
  const path = [];
3210
3275
  let current = node.type === "ChainExpression" ? node.expression : node;
@@ -3236,7 +3301,7 @@ function assertedEventParameter(node) {
3236
3301
  property: member.path.join(".")
3237
3302
  };
3238
3303
  }
3239
- var preferEventParameterTypeRule = defineRule28({
3304
+ var preferEventParameterTypeRule = defineRule29({
3240
3305
  meta: {
3241
3306
  type: "suggestion",
3242
3307
  docs: {
@@ -3265,14 +3330,14 @@ var preferEventParameterTypeRule = defineRule28({
3265
3330
  });
3266
3331
 
3267
3332
  // src/generic/rules/prefer-lit-query-decorators.ts
3268
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3333
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3269
3334
  var messageIds = {
3270
3335
  assignedElements: "preferQueryAssignedElements",
3271
3336
  assignedNodes: "preferQueryAssignedNodes",
3272
3337
  querySelector: "preferQuery",
3273
3338
  querySelectorAll: "preferQueryAll"
3274
3339
  };
3275
- var preferLitQueryDecoratorsRule = defineRule29({
3340
+ var preferLitQueryDecoratorsRule = defineRule30({
3276
3341
  meta: {
3277
3342
  type: "suggestion",
3278
3343
  docs: {
@@ -3325,8 +3390,8 @@ var preferLitQueryDecoratorsRule = defineRule29({
3325
3390
  });
3326
3391
 
3327
3392
  // src/generic/rules/prefer-types-over-assertions.ts
3328
- import { defineRule as defineRule30 } from "@oxlint/plugins";
3329
- var preferTypesOverAssertionsRule = defineRule30({
3393
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3394
+ var preferTypesOverAssertionsRule = defineRule31({
3330
3395
  meta: {
3331
3396
  type: "problem",
3332
3397
  docs: {
@@ -1,7 +1,7 @@
1
1
  // src/effect/index.ts
2
2
  import { eslintCompatPlugin } from "@oxlint/plugins";
3
3
 
4
- // src/effect/rules/no-try-catch-in-effect-generators.ts
4
+ // src/effect/rules/no-raw-gh.ts
5
5
  import { defineRule } from "@oxlint/plugins";
6
6
 
7
7
  // src/shared/ast.ts
@@ -32,6 +32,70 @@ function resolveVariable(sourceCode, identifier) {
32
32
  }
33
33
  return null;
34
34
  }
35
+ // src/effect/rules/no-raw-gh.ts
36
+ var PACKAGE = "@timmo001/effect-gh";
37
+ var rawMethods = new Set(["execute", "json", "stream"]);
38
+ function isEffectGhImport(sourceCode, identifier, name) {
39
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
40
+ if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== PACKAGE || definition.node.type !== "ImportSpecifier") {
41
+ return false;
42
+ }
43
+ const imported = definition.node.imported;
44
+ return (imported.type === "Identifier" ? imported.name : imported.value) === name;
45
+ }) ?? false;
46
+ }
47
+ function isYieldGh(sourceCode, node) {
48
+ return node?.type === "YieldExpression" && node.delegate && node.argument?.type === "Identifier" && isEffectGhImport(sourceCode, node.argument, "Gh");
49
+ }
50
+ function isGhBinding(sourceCode, identifier) {
51
+ return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
52
+ }
53
+ function isGhArgument(node) {
54
+ if (node?.type === "Literal")
55
+ return node.value === "gh";
56
+ if (node?.type !== "ArrayExpression")
57
+ return false;
58
+ const first = node.elements[0];
59
+ return first?.type === "Literal" && first.value === "gh";
60
+ }
61
+ var noRawGhRule = defineRule({
62
+ meta: {
63
+ type: "problem",
64
+ docs: {
65
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
66
+ },
67
+ messages: {
68
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
69
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
70
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
71
+ }
72
+ },
73
+ create(context) {
74
+ return {
75
+ CallExpression(node) {
76
+ const callee = node.callee;
77
+ const method = staticMemberName(callee);
78
+ if (callee.type === "MemberExpression" && method !== null) {
79
+ const object = callee.object;
80
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
81
+ context.report({ node, messageId: "rawGh", data: { method } });
82
+ return;
83
+ }
84
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
85
+ context.report({ node, messageId: "rawApi" });
86
+ return;
87
+ }
88
+ }
89
+ if (isGhArgument(node.arguments[0]))
90
+ context.report({ node, messageId: "spawnGh" });
91
+ }
92
+ };
93
+ }
94
+ });
95
+
96
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
97
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
98
+
35
99
  // src/shared/effect.ts
36
100
  function isEffectImport(sourceCode, identifier, namespace) {
37
101
  const variable = resolveVariable(sourceCode, identifier);
@@ -78,7 +142,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
78
142
  const factoryCall = parent.callee;
79
143
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
80
144
  }
81
- var noTryCatchInEffectGeneratorsRule = defineRule({
145
+ var noTryCatchInEffectGeneratorsRule = defineRule2({
82
146
  meta: {
83
147
  type: "problem",
84
148
  docs: {
@@ -104,7 +168,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule({
104
168
  });
105
169
 
106
170
  // src/effect/rules/prefer-platform-services.ts
107
- import { defineRule as defineRule2 } from "@oxlint/plugins";
171
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
108
172
  var moduleServices = new Map([
109
173
  ["child_process", "childProcess"],
110
174
  ["fs", "fileSystem"],
@@ -117,7 +181,7 @@ var bunServices = new Map([
117
181
  ["spawnSync", "childProcess"],
118
182
  ["write", "fileSystem"]
119
183
  ]);
120
- var preferPlatformServicesRule = defineRule2({
184
+ var preferPlatformServicesRule = defineRule3({
121
185
  meta: {
122
186
  type: "suggestion",
123
187
  docs: {
@@ -180,6 +244,7 @@ var preferPlatformServicesRule = defineRule2({
180
244
  var timmoEffectPlugin = eslintCompatPlugin({
181
245
  meta: { name: "timmo-effect" },
182
246
  rules: {
247
+ "no-raw-gh": noRawGhRule,
183
248
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
184
249
  "prefer-platform-services": preferPlatformServicesRule
185
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/oxlint-rules",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared Oxlint plugins and configs with optional Effect rules.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -38,24 +38,25 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "bun run scripts/build.ts",
41
- "check": "bun run lint && bun run test && bun run typecheck && bun run format:check && bun run skills:validate",
41
+ "check": "bun run --parallel --no-exit-on-error lint test typecheck format:check skills:validate",
42
42
  "format": "prettier --write \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.ts\" \"*.md\" \"*.json\"",
43
43
  "format:check": "prettier --check \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.ts\" \"*.md\" \"*.json\"",
44
- "lint": "bun run build && oxlint --disable-nested-config .",
45
- "skills:validate": "bunx skills-ref validate skills/install-timmo-oxlint-rules && bunx skills-ref validate skills/add-oxlint-rule && bunx skills-ref validate skills/release-oxlint-rules",
44
+ "lint": "bun run build && bun run lint:oxlint",
45
+ "lint:oxlint": "oxlint --disable-nested-config .",
46
+ "skills:validate": "printf '%s\\n' skills/*/ | xargs -P 0 -n 1 skills-ref validate",
46
47
  "test": "node --experimental-strip-types scripts/test.ts",
47
48
  "test:package": "node --experimental-strip-types scripts/test-package.ts",
48
49
  "typecheck": "tsc --noEmit"
49
50
  },
50
51
  "peerDependencies": {
51
- "@oxlint/plugins": "1.86.0",
52
- "oxlint": "1.86.0"
52
+ "@oxlint/plugins": "1.87.0",
53
+ "oxlint": "1.87.0"
53
54
  },
54
55
  "devDependencies": {
55
- "@oxlint/plugins": "1.86.0",
56
+ "@oxlint/plugins": "1.87.0",
56
57
  "@types/bun": "^1.3.14",
57
- "oxlint": "1.86.0",
58
- "oxlint-tsgolint": "7.0.2003",
58
+ "oxlint": "1.87.0",
59
+ "oxlint-tsgolint": "7.0.2004",
59
60
  "prettier": "^3.9.5",
60
61
  "skills-ref": "0.1.5",
61
62
  "typescript": "^7.0.0"
@@ -32,10 +32,8 @@ description: >-
32
32
  instead only when `effect` is a direct dependency or the user explicitly
33
33
  requests it.
34
34
  4. Keep dependency and config edits visible. Do not delegate them to a script.
35
- 5. For type-aware linting, explicitly pin the latest `oxlint-tsgolint` that
36
- satisfies Oxlint's peer requirement and was validated with the rules release.
37
- Enable `options.typeAware` in the target config and verify type-aware linting
38
- alongside the shared rules.
35
+ 5. Explicitly pin the latest `oxlint-tsgolint` that satisfies Oxlint's peer
36
+ requirement and was validated with the rules release.
39
37
 
40
38
  ## Copy rules
41
39
 
@@ -51,8 +49,23 @@ description: >-
51
49
  output as authoritative rather than maintaining plugin or rule inventories
52
50
  in this skill.
53
51
 
52
+ ## Strictness
53
+
54
+ 1. Start with every strictness option in the target config's `options`:
55
+ `typeAware: true`, `typeCheck: true`, and `maxWarnings: 0`. Keep them in the
56
+ config rather than as lint script flags, and remove any existing
57
+ `--type-aware`, `--type-check`, `--deny-warnings`, or `--max-warnings` flags
58
+ so the config is the single source.
59
+ 2. Run the target's lint command. If it passes, keep every option.
60
+ 3. If it fails, group the failures by cause and the option that raises them,
61
+ then ask one structured question: fix the failures, or drop the smallest set
62
+ of options that makes lint pass (`typeCheck` first, then `maxWarnings`, then
63
+ `typeAware`). Recommend the reduction that keeps the most strictness, and name
64
+ any shared cause, such as a missing declaration in an upstream package, that
65
+ a follow-up could fix for every consumer.
66
+
54
67
  Run the target repository's lint command to verify the configuration loads, plus
55
68
  any checks explicitly required by that repository. Run typechecks, tests, or a
56
69
  build only when code or dependencies changed in a way those checks cover. Report
57
70
  package-manager changes, preserved local configuration, enabled rule groups,
58
- and checks.
71
+ strictness options kept or dropped, and checks.
@@ -1,11 +1,13 @@
1
1
  import { eslintCompatPlugin } from "@oxlint/plugins";
2
2
 
3
+ import { noRawGhRule } from "./rules/no-raw-gh.ts";
3
4
  import { noTryCatchInEffectGeneratorsRule } from "./rules/no-try-catch-in-effect-generators.ts";
4
5
  import { preferPlatformServicesRule } from "./rules/prefer-platform-services.ts";
5
6
 
6
7
  const timmoEffectPlugin = eslintCompatPlugin({
7
8
  meta: { name: "timmo-effect" },
8
9
  rules: {
10
+ "no-raw-gh": noRawGhRule,
9
11
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
10
12
  "prefer-platform-services": preferPlatformServicesRule,
11
13
  },
@@ -0,0 +1,126 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import { staticMemberName } from "../../shared/ast.ts";
4
+ import { resolveVariable } from "../../shared/scope.ts";
5
+
6
+ import type { ESTree, SourceCode } from "@oxlint/plugins";
7
+
8
+ const PACKAGE = "@timmo001/effect-gh";
9
+
10
+ const rawMethods = new Set(["execute", "json", "stream"]);
11
+
12
+ /** Whether an identifier is bound by a named import of `name` from effect-gh. */
13
+ function isEffectGhImport(
14
+ sourceCode: SourceCode,
15
+ identifier: ESTree.IdentifierReference,
16
+ name: string,
17
+ ): boolean {
18
+ return (
19
+ resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
20
+ if (
21
+ definition.type !== "ImportBinding" ||
22
+ definition.parent?.type !== "ImportDeclaration" ||
23
+ definition.parent.source.value !== PACKAGE ||
24
+ definition.node.type !== "ImportSpecifier"
25
+ ) {
26
+ return false;
27
+ }
28
+
29
+ const imported = definition.node.imported;
30
+
31
+ return (
32
+ (imported.type === "Identifier" ? imported.name : imported.value) ===
33
+ name
34
+ );
35
+ }) ?? false
36
+ );
37
+ }
38
+
39
+ /** Whether an expression is `yield* Gh` with Gh from effect-gh. */
40
+ function isYieldGh(sourceCode: SourceCode, node: ESTree.Node | null): boolean {
41
+ return (
42
+ node?.type === "YieldExpression" &&
43
+ node.delegate &&
44
+ node.argument?.type === "Identifier" &&
45
+ isEffectGhImport(sourceCode, node.argument, "Gh")
46
+ );
47
+ }
48
+
49
+ /** Whether an identifier is a variable initialised with `yield* Gh`. */
50
+ function isGhBinding(
51
+ sourceCode: SourceCode,
52
+ identifier: ESTree.IdentifierReference,
53
+ ): boolean {
54
+ return (
55
+ resolveVariable(sourceCode, identifier)?.defs.some(
56
+ (definition) =>
57
+ definition.type === "Variable" &&
58
+ definition.node.type === "VariableDeclarator" &&
59
+ isYieldGh(sourceCode, definition.node.init),
60
+ ) ?? false
61
+ );
62
+ }
63
+
64
+ function isGhArgument(node: ESTree.Node | undefined): boolean {
65
+ if (node?.type === "Literal") return node.value === "gh";
66
+
67
+ if (node?.type !== "ArrayExpression") return false;
68
+ const first = node.elements[0];
69
+
70
+ return first?.type === "Literal" && first.value === "gh";
71
+ }
72
+
73
+ /** Route gh through effect-gh's typed operations instead of raw argv or direct spawns. */
74
+ export const noRawGhRule = defineRule({
75
+ meta: {
76
+ type: "problem",
77
+ docs: {
78
+ description:
79
+ "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly.",
80
+ },
81
+ messages: {
82
+ rawGh:
83
+ "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
84
+ rawApi:
85
+ "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
86
+ spawnGh:
87
+ "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly.",
88
+ },
89
+ },
90
+ create(context) {
91
+ return {
92
+ CallExpression(node) {
93
+ const callee = node.callee;
94
+ const method = staticMemberName(callee);
95
+
96
+ if (callee.type === "MemberExpression" && method !== null) {
97
+ const object = callee.object;
98
+
99
+ if (
100
+ rawMethods.has(method) &&
101
+ (isYieldGh(context.sourceCode, object) ||
102
+ (object.type === "Identifier" &&
103
+ isGhBinding(context.sourceCode, object)))
104
+ ) {
105
+ context.report({ node, messageId: "rawGh", data: { method } });
106
+
107
+ return;
108
+ }
109
+
110
+ if (
111
+ method === "raw" &&
112
+ object.type === "Identifier" &&
113
+ isEffectGhImport(context.sourceCode, object, "Api")
114
+ ) {
115
+ context.report({ node, messageId: "rawApi" });
116
+
117
+ return;
118
+ }
119
+ }
120
+
121
+ if (isGhArgument(node.arguments[0]))
122
+ context.report({ node, messageId: "spawnGh" });
123
+ },
124
+ };
125
+ },
126
+ });