@timmo001/oxlint-rules 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,13 @@ 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 calls that launch gh: `"gh"` followed by its arguments, such as
141
+ `executor.run("gh", ...)`, or an argument array starting with `"gh"`, such
142
+ as `Bun.spawn(["gh", ...])`. Use the
143
+ package's typed operations, and add one to effect-gh when a command has none.
144
+ effect-gh's own relative imports are not reported.
125
145
  - `no-try-catch-in-effect-generators`: diagnoses synchronous `try/catch` owned
126
146
  by generators passed directly to `Effect.gen` or the curried `Effect.fn`
127
147
  form. It leaves ordinary async polling, nested callback boundaries,
@@ -139,8 +159,7 @@ Initialise the upstream source and run the package checks:
139
159
 
140
160
  ```sh
141
161
  git submodule update --init --recursive
142
- mise run check
143
- mise run build
162
+ mise run check ::: build
144
163
  mise run test:package
145
164
  npm pack --dry-run
146
165
  bunx jsr@0.14.3 publish --dry-run --allow-dirty
package/dist/cli.js CHANGED
@@ -2985,8 +2985,72 @@ 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 launchesGh(args) {
3008
+ const [command, argv] = args;
3009
+ if (command?.type === "Literal")
3010
+ return command.value === "gh" && argv !== undefined;
3011
+ if (command?.type !== "ArrayExpression")
3012
+ return false;
3013
+ const first = command.elements[0];
3014
+ return first?.type === "Literal" && first.value === "gh";
3015
+ }
3016
+ var noRawGhRule = defineRule24({
3017
+ meta: {
3018
+ type: "problem",
3019
+ docs: {
3020
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
3021
+ },
3022
+ messages: {
3023
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
3024
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
3025
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
3026
+ }
3027
+ },
3028
+ create(context) {
3029
+ return {
3030
+ CallExpression(node) {
3031
+ const callee = node.callee;
3032
+ const method = staticMemberName(callee);
3033
+ if (callee.type === "MemberExpression" && method !== null) {
3034
+ const object = callee.object;
3035
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
3036
+ context.report({ node, messageId: "rawGh", data: { method } });
3037
+ return;
3038
+ }
3039
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
3040
+ context.report({ node, messageId: "rawApi" });
3041
+ return;
3042
+ }
3043
+ }
3044
+ if (launchesGh(node.arguments))
3045
+ context.report({ node, messageId: "spawnGh" });
3046
+ }
3047
+ };
3048
+ }
3049
+ });
3050
+
3051
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
3052
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
3053
+
2990
3054
  // src/shared/effect.ts
2991
3055
  function isEffectImport(sourceCode, identifier, namespace) {
2992
3056
  const variable = resolveVariable(sourceCode, identifier);
@@ -3033,7 +3097,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
3033
3097
  const factoryCall = parent.callee;
3034
3098
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
3035
3099
  }
3036
- var noTryCatchInEffectGeneratorsRule = defineRule24({
3100
+ var noTryCatchInEffectGeneratorsRule = defineRule25({
3037
3101
  meta: {
3038
3102
  type: "problem",
3039
3103
  docs: {
@@ -3059,7 +3123,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule24({
3059
3123
  });
3060
3124
 
3061
3125
  // src/effect/rules/prefer-platform-services.ts
3062
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3126
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
3063
3127
  var moduleServices = new Map([
3064
3128
  ["child_process", "childProcess"],
3065
3129
  ["fs", "fileSystem"],
@@ -3072,7 +3136,7 @@ var bunServices = new Map([
3072
3136
  ["spawnSync", "childProcess"],
3073
3137
  ["write", "fileSystem"]
3074
3138
  ]);
3075
- var preferPlatformServicesRule = defineRule25({
3139
+ var preferPlatformServicesRule = defineRule26({
3076
3140
  meta: {
3077
3141
  type: "suggestion",
3078
3142
  docs: {
@@ -3135,6 +3199,7 @@ var preferPlatformServicesRule = defineRule25({
3135
3199
  var timmoEffectPlugin = eslintCompatPlugin3({
3136
3200
  meta: { name: "timmo-effect" },
3137
3201
  rules: {
3202
+ "no-raw-gh": noRawGhRule,
3138
3203
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
3139
3204
  "prefer-platform-services": preferPlatformServicesRule
3140
3205
  }
@@ -3145,7 +3210,7 @@ var effect_default = timmoEffectPlugin;
3145
3210
  import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
3146
3211
 
3147
3212
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
3148
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3213
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
3149
3214
 
3150
3215
  // vendor/anti-slop/src/effect/shared/tagged-values.ts
3151
3216
  var equalityOperators = new Set(["==", "===", "!=", "!=="]);
@@ -3192,7 +3257,7 @@ var isMatchPatternObject = (node) => {
3192
3257
  };
3193
3258
 
3194
3259
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
3195
- var noManualEffectErrorTagRule = defineRule26({
3260
+ var noManualEffectErrorTagRule = defineRule27({
3196
3261
  meta: {
3197
3262
  type: "problem",
3198
3263
  docs: {
@@ -3229,8 +3294,8 @@ var noManualEffectErrorTagRule = defineRule26({
3229
3294
  });
3230
3295
 
3231
3296
  // vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
3232
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3233
- var noManualTagComparisonRule = defineRule27({
3297
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
3298
+ var noManualTagComparisonRule = defineRule28({
3234
3299
  meta: {
3235
3300
  type: "problem",
3236
3301
  docs: {
@@ -3260,8 +3325,8 @@ var noManualTagComparisonRule = defineRule27({
3260
3325
  });
3261
3326
 
3262
3327
  // vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
3263
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3264
- var noManualTaggedConstructionRule = defineRule28({
3328
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3329
+ var noManualTaggedConstructionRule = defineRule29({
3265
3330
  meta: {
3266
3331
  type: "problem",
3267
3332
  docs: {
@@ -3286,7 +3351,7 @@ var noManualTaggedConstructionRule = defineRule28({
3286
3351
  });
3287
3352
 
3288
3353
  // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
3289
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3354
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3290
3355
  var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
3291
3356
  var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
3292
3357
  function isProjectLocalImport(source) {
@@ -3297,7 +3362,7 @@ function getImportedName(specifier) {
3297
3362
  return specifier.imported.name;
3298
3363
  return specifier.imported.value;
3299
3364
  }
3300
- var noServiceConstructorImportsRule = defineRule29({
3365
+ var noServiceConstructorImportsRule = defineRule30({
3301
3366
  meta: {
3302
3367
  type: "problem",
3303
3368
  docs: {
@@ -3331,9 +3396,9 @@ var noServiceConstructorImportsRule = defineRule29({
3331
3396
  });
3332
3397
 
3333
3398
  // vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
3334
- import { defineRule as defineRule30 } from "@oxlint/plugins";
3399
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3335
3400
  var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
3336
- var preferEffectMatchRule = defineRule30({
3401
+ var preferEffectMatchRule = defineRule31({
3337
3402
  meta: {
3338
3403
  type: "problem",
3339
3404
  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,71 @@ 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 launchesGh(args) {
59
+ const [command, argv] = args;
60
+ if (command?.type === "Literal")
61
+ return command.value === "gh" && argv !== undefined;
62
+ if (command?.type !== "ArrayExpression")
63
+ return false;
64
+ const first = command.elements[0];
65
+ return first?.type === "Literal" && first.value === "gh";
66
+ }
67
+ var noRawGhRule = defineRule({
68
+ meta: {
69
+ type: "problem",
70
+ docs: {
71
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
72
+ },
73
+ messages: {
74
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
75
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
76
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
77
+ }
78
+ },
79
+ create(context) {
80
+ return {
81
+ CallExpression(node) {
82
+ const callee = node.callee;
83
+ const method = staticMemberName(callee);
84
+ if (callee.type === "MemberExpression" && method !== null) {
85
+ const object = callee.object;
86
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
87
+ context.report({ node, messageId: "rawGh", data: { method } });
88
+ return;
89
+ }
90
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
91
+ context.report({ node, messageId: "rawApi" });
92
+ return;
93
+ }
94
+ }
95
+ if (launchesGh(node.arguments))
96
+ context.report({ node, messageId: "spawnGh" });
97
+ }
98
+ };
99
+ }
100
+ });
101
+
102
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
103
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
104
+
40
105
  // src/shared/effect.ts
41
106
  function isEffectImport(sourceCode, identifier, namespace) {
42
107
  const variable = resolveVariable(sourceCode, identifier);
@@ -83,7 +148,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
83
148
  const factoryCall = parent.callee;
84
149
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
85
150
  }
86
- var noTryCatchInEffectGeneratorsRule = defineRule({
151
+ var noTryCatchInEffectGeneratorsRule = defineRule2({
87
152
  meta: {
88
153
  type: "problem",
89
154
  docs: {
@@ -109,7 +174,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule({
109
174
  });
110
175
 
111
176
  // src/effect/rules/prefer-platform-services.ts
112
- import { defineRule as defineRule2 } from "@oxlint/plugins";
177
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
113
178
  var moduleServices = new Map([
114
179
  ["child_process", "childProcess"],
115
180
  ["fs", "fileSystem"],
@@ -122,7 +187,7 @@ var bunServices = new Map([
122
187
  ["spawnSync", "childProcess"],
123
188
  ["write", "fileSystem"]
124
189
  ]);
125
- var preferPlatformServicesRule = defineRule2({
190
+ var preferPlatformServicesRule = defineRule3({
126
191
  meta: {
127
192
  type: "suggestion",
128
193
  docs: {
@@ -185,6 +250,7 @@ var preferPlatformServicesRule = defineRule2({
185
250
  var timmoEffectPlugin = eslintCompatPlugin({
186
251
  meta: { name: "timmo-effect" },
187
252
  rules: {
253
+ "no-raw-gh": noRawGhRule,
188
254
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
189
255
  "prefer-platform-services": preferPlatformServicesRule
190
256
  }
@@ -195,7 +261,7 @@ var effect_default = timmoEffectPlugin;
195
261
  import { eslintCompatPlugin as eslintCompatPlugin2 } from "@oxlint/plugins";
196
262
 
197
263
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
198
- import { defineRule as defineRule3 } from "@oxlint/plugins";
264
+ import { defineRule as defineRule4 } from "@oxlint/plugins";
199
265
 
200
266
  // vendor/anti-slop/src/effect/shared/tagged-values.ts
201
267
  var equalityOperators = new Set(["==", "===", "!=", "!=="]);
@@ -242,7 +308,7 @@ var isMatchPatternObject = (node) => {
242
308
  };
243
309
 
244
310
  // vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
245
- var noManualEffectErrorTagRule = defineRule3({
311
+ var noManualEffectErrorTagRule = defineRule4({
246
312
  meta: {
247
313
  type: "problem",
248
314
  docs: {
@@ -279,8 +345,8 @@ var noManualEffectErrorTagRule = defineRule3({
279
345
  });
280
346
 
281
347
  // vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
282
- import { defineRule as defineRule4 } from "@oxlint/plugins";
283
- var noManualTagComparisonRule = defineRule4({
348
+ import { defineRule as defineRule5 } from "@oxlint/plugins";
349
+ var noManualTagComparisonRule = defineRule5({
284
350
  meta: {
285
351
  type: "problem",
286
352
  docs: {
@@ -310,8 +376,8 @@ var noManualTagComparisonRule = defineRule4({
310
376
  });
311
377
 
312
378
  // vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
313
- import { defineRule as defineRule5 } from "@oxlint/plugins";
314
- var noManualTaggedConstructionRule = defineRule5({
379
+ import { defineRule as defineRule6 } from "@oxlint/plugins";
380
+ var noManualTaggedConstructionRule = defineRule6({
315
381
  meta: {
316
382
  type: "problem",
317
383
  docs: {
@@ -336,7 +402,7 @@ var noManualTaggedConstructionRule = defineRule5({
336
402
  });
337
403
 
338
404
  // vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
339
- import { defineRule as defineRule6 } from "@oxlint/plugins";
405
+ import { defineRule as defineRule7 } from "@oxlint/plugins";
340
406
  var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
341
407
  var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
342
408
  function isProjectLocalImport(source) {
@@ -347,7 +413,7 @@ function getImportedName(specifier) {
347
413
  return specifier.imported.name;
348
414
  return specifier.imported.value;
349
415
  }
350
- var noServiceConstructorImportsRule = defineRule6({
416
+ var noServiceConstructorImportsRule = defineRule7({
351
417
  meta: {
352
418
  type: "problem",
353
419
  docs: {
@@ -381,9 +447,9 @@ var noServiceConstructorImportsRule = defineRule6({
381
447
  });
382
448
 
383
449
  // vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
384
- import { defineRule as defineRule7 } from "@oxlint/plugins";
450
+ import { defineRule as defineRule8 } from "@oxlint/plugins";
385
451
  var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
386
- var preferEffectMatchRule = defineRule7({
452
+ var preferEffectMatchRule = defineRule8({
387
453
  meta: {
388
454
  type: "problem",
389
455
  docs: {
@@ -449,7 +515,7 @@ function enablePluginRules(namespace, plugin) {
449
515
  import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
450
516
 
451
517
  // vendor/anti-slop/src/rules/no-array-filter-map.ts
452
- import { defineRule as defineRule8 } from "@oxlint/plugins";
518
+ import { defineRule as defineRule9 } from "@oxlint/plugins";
453
519
 
454
520
  // vendor/anti-slop/src/shared/array-method.ts
455
521
  function unwrapArrayExpression(node) {
@@ -524,7 +590,7 @@ function isKnownArrayExpression(sourceCode, node, visited = new Set) {
524
590
  }
525
591
 
526
592
  // vendor/anti-slop/src/rules/no-array-filter-map.ts
527
- var noArrayFilterMapRule = defineRule8({
593
+ var noArrayFilterMapRule = defineRule9({
528
594
  meta: {
529
595
  type: "suggestion",
530
596
  docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
@@ -553,7 +619,7 @@ var noArrayFilterMapRule = defineRule8({
553
619
  });
554
620
 
555
621
  // vendor/anti-slop/src/rules/no-reduce-accumulator-copy.ts
556
- import { defineRule as defineRule9 } from "@oxlint/plugins";
622
+ import { defineRule as defineRule10 } from "@oxlint/plugins";
557
623
  function enclosingReducer(node) {
558
624
  let parent = node.parent;
559
625
  while (parent !== null) {
@@ -603,7 +669,7 @@ function isGlobalCopyOwner(sourceCode, node, name) {
603
669
  const variable = resolveArrayBinding(sourceCode, node);
604
670
  return variable === null || variable.defs.length === 0;
605
671
  }
606
- var noReduceAccumulatorCopyRule = defineRule9({
672
+ var noReduceAccumulatorCopyRule = defineRule10({
607
673
  meta: {
608
674
  type: "problem",
609
675
  docs: { description: "Disallow copying growing reducer accumulators with Object.assign, Array.from, or array copy methods." },
@@ -644,7 +710,7 @@ var noReduceAccumulatorCopyRule = defineRule9({
644
710
  });
645
711
 
646
712
  // vendor/anti-slop/src/rules/no-chained-type-assertions.ts
647
- import { defineRule as defineRule10 } from "@oxlint/plugins";
713
+ import { defineRule as defineRule11 } from "@oxlint/plugins";
648
714
  function isTypeAssertionExpression(node) {
649
715
  return node.type === "TSAsExpression" || node.type === "TSTypeAssertion";
650
716
  }
@@ -679,7 +745,7 @@ function isForbiddenAssertionChain(node) {
679
745
  }
680
746
  return assertionCount > 1 && hasNonConstAssertion;
681
747
  }
682
- var noChainedTypeAssertionsRule = defineRule10({
748
+ var noChainedTypeAssertionsRule = defineRule11({
683
749
  meta: {
684
750
  type: "problem",
685
751
  docs: {
@@ -703,7 +769,7 @@ var noChainedTypeAssertionsRule = defineRule10({
703
769
  });
704
770
 
705
771
  // vendor/anti-slop/src/rules/no-conditional-empty-object-spread.ts
706
- import { defineRule as defineRule11 } from "@oxlint/plugins";
772
+ import { defineRule as defineRule12 } from "@oxlint/plugins";
707
773
  function unwrapParentheses(node) {
708
774
  let current = node;
709
775
  while (current.type === "ParenthesizedExpression") {
@@ -718,7 +784,7 @@ function isConditionalEmptyObjectSpread(node) {
718
784
  const conditional = unwrapParentheses(node);
719
785
  return conditional.type === "ConditionalExpression" && (isEmptyObjectExpression(conditional.consequent) || isEmptyObjectExpression(conditional.alternate));
720
786
  }
721
- var noConditionalEmptyObjectSpreadRule = defineRule11({
787
+ var noConditionalEmptyObjectSpreadRule = defineRule12({
722
788
  meta: {
723
789
  type: "suggestion",
724
790
  docs: {
@@ -742,7 +808,7 @@ var noConditionalEmptyObjectSpreadRule = defineRule11({
742
808
  });
743
809
 
744
810
  // vendor/anti-slop/src/rules/no-known-value-widening.ts
745
- import { defineRule as defineRule12 } from "@oxlint/plugins";
811
+ import { defineRule as defineRule13 } from "@oxlint/plugins";
746
812
 
747
813
  // vendor/anti-slop/src/shared/lexical-type-parameters.ts
748
814
  function isNode(value) {
@@ -1431,7 +1497,7 @@ function isDictionaryAccumulatorTarget(destination) {
1431
1497
  function hasParentAssertion(node) {
1432
1498
  return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
1433
1499
  }
1434
- var noKnownValueWideningRule = defineRule12({
1500
+ var noKnownValueWideningRule = defineRule13({
1435
1501
  meta: {
1436
1502
  type: "problem",
1437
1503
  docs: {
@@ -1544,7 +1610,7 @@ var noKnownValueWideningRule = defineRule12({
1544
1610
  });
1545
1611
 
1546
1612
  // vendor/anti-slop/src/rules/no-module-mocking.ts
1547
- import { defineRule as defineRule13 } from "@oxlint/plugins";
1613
+ import { defineRule as defineRule14 } from "@oxlint/plugins";
1548
1614
  var moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
1549
1615
  function importedName(node) {
1550
1616
  if (node.type !== "ImportSpecifier")
@@ -1579,7 +1645,7 @@ function moduleMockCall(sourceCode, callee) {
1579
1645
  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
1646
  return method !== null && moduleMockMethods.has(method);
1581
1647
  }
1582
- var noModuleMockingRule = defineRule13({
1648
+ var noModuleMockingRule = defineRule14({
1583
1649
  meta: {
1584
1650
  type: "problem",
1585
1651
  docs: {
@@ -1603,8 +1669,8 @@ var noModuleMockingRule = defineRule13({
1603
1669
  });
1604
1670
 
1605
1671
  // vendor/anti-slop/src/rules/no-object-parameters.ts
1606
- import { defineRule as defineRule14 } from "@oxlint/plugins";
1607
- var noObjectParametersRule = defineRule14({
1672
+ import { defineRule as defineRule15 } from "@oxlint/plugins";
1673
+ var noObjectParametersRule = defineRule15({
1608
1674
  meta: {
1609
1675
  type: "problem",
1610
1676
  docs: {
@@ -1657,7 +1723,7 @@ var noObjectParametersRule = defineRule14({
1657
1723
  });
1658
1724
 
1659
1725
  // vendor/anti-slop/src/rules/no-reflect-apply.ts
1660
- import { defineRule as defineRule15 } from "@oxlint/plugins";
1726
+ import { defineRule as defineRule16 } from "@oxlint/plugins";
1661
1727
 
1662
1728
  // vendor/anti-slop/src/shared/reflect-method.ts
1663
1729
  function isGlobalReflect(sourceCode, expression) {
@@ -1678,7 +1744,7 @@ function isGlobalReflectMethodCall(sourceCode, callee, methodName) {
1678
1744
  }
1679
1745
 
1680
1746
  // vendor/anti-slop/src/rules/no-reflect-apply.ts
1681
- var noReflectApplyRule = defineRule15({
1747
+ var noReflectApplyRule = defineRule16({
1682
1748
  meta: {
1683
1749
  type: "problem",
1684
1750
  docs: {
@@ -1702,8 +1768,8 @@ var noReflectApplyRule = defineRule15({
1702
1768
  });
1703
1769
 
1704
1770
  // vendor/anti-slop/src/rules/no-reflect-get.ts
1705
- import { defineRule as defineRule16 } from "@oxlint/plugins";
1706
- var noReflectGetRule = defineRule16({
1771
+ import { defineRule as defineRule17 } from "@oxlint/plugins";
1772
+ var noReflectGetRule = defineRule17({
1707
1773
  meta: {
1708
1774
  type: "problem",
1709
1775
  docs: {
@@ -1727,7 +1793,7 @@ var noReflectGetRule = defineRule16({
1727
1793
  });
1728
1794
 
1729
1795
  // vendor/anti-slop/src/rules/no-runtime-typeof.ts
1730
- import { defineRule as defineRule17 } from "@oxlint/plugins";
1796
+ import { defineRule as defineRule18 } from "@oxlint/plugins";
1731
1797
  function isRuntimeFunction(node) {
1732
1798
  return node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
1733
1799
  }
@@ -1750,7 +1816,7 @@ function isExistenceProbe(node) {
1750
1816
  const other = parent.left === node ? parent.right : parent.left;
1751
1817
  return other.type === "Literal" && other.value === "undefined";
1752
1818
  }
1753
- var noRuntimeTypeofRule = defineRule17({
1819
+ var noRuntimeTypeofRule = defineRule18({
1754
1820
  meta: {
1755
1821
  type: "problem",
1756
1822
  docs: {
@@ -1784,7 +1850,7 @@ var noRuntimeTypeofRule = defineRule17({
1784
1850
  });
1785
1851
 
1786
1852
  // vendor/anti-slop/src/rules/no-shape-in-symbol-names.ts
1787
- import { defineRule as defineRule18 } from "@oxlint/plugins";
1853
+ import { defineRule as defineRule19 } from "@oxlint/plugins";
1788
1854
  var FORBIDDEN_SYMBOL_NAME = "shape";
1789
1855
  function containsForbiddenSymbolName(name) {
1790
1856
  return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
@@ -1795,7 +1861,7 @@ function isBorrowedMemberName(node) {
1795
1861
  return false;
1796
1862
  return parent.property === node && parent.computed === false;
1797
1863
  }
1798
- var noForbiddenTermInSymbolNamesRule = defineRule18({
1864
+ var noForbiddenTermInSymbolNamesRule = defineRule19({
1799
1865
  meta: {
1800
1866
  type: "problem",
1801
1867
  docs: {
@@ -1824,12 +1890,12 @@ var noForbiddenTermInSymbolNamesRule = defineRule18({
1824
1890
  });
1825
1891
 
1826
1892
  // vendor/anti-slop/src/rules/no-unknown-parameters.ts
1827
- import { defineRule as defineRule19 } from "@oxlint/plugins";
1893
+ import { defineRule as defineRule20 } from "@oxlint/plugins";
1828
1894
  function isTypePredicateSubject(owner, parameterName) {
1829
1895
  const predicate = owner.returnType?.typeAnnotation;
1830
1896
  return predicate?.type === "TSTypePredicate" && predicate.parameterName.type === "Identifier" && predicate.parameterName.name === parameterName;
1831
1897
  }
1832
- var noUnknownParametersRule = defineRule19({
1898
+ var noUnknownParametersRule = defineRule20({
1833
1899
  meta: {
1834
1900
  type: "problem",
1835
1901
  docs: {
@@ -1873,8 +1939,8 @@ var noUnknownParametersRule = defineRule19({
1873
1939
  });
1874
1940
 
1875
1941
  // vendor/anti-slop/src/rules/no-unknown-returns.ts
1876
- import { defineRule as defineRule20 } from "@oxlint/plugins";
1877
- var noUnknownReturnsRule = defineRule20({
1942
+ import { defineRule as defineRule21 } from "@oxlint/plugins";
1943
+ var noUnknownReturnsRule = defineRule21({
1878
1944
  meta: {
1879
1945
  type: "problem",
1880
1946
  docs: {
@@ -1927,8 +1993,8 @@ var noUnknownReturnsRule = defineRule20({
1927
1993
  });
1928
1994
 
1929
1995
  // vendor/anti-slop/src/rules/no-unknown-type-aliases.ts
1930
- import { defineRule as defineRule21 } from "@oxlint/plugins";
1931
- var noUnknownTypeAliasesRule = defineRule21({
1996
+ import { defineRule as defineRule22 } from "@oxlint/plugins";
1997
+ var noUnknownTypeAliasesRule = defineRule22({
1932
1998
  meta: {
1933
1999
  type: "problem",
1934
2000
  docs: {
@@ -1966,7 +2032,7 @@ var noUnknownTypeAliasesRule = defineRule21({
1966
2032
  });
1967
2033
 
1968
2034
  // vendor/anti-slop/src/rules/no-unsafe-dictionary-type.ts
1969
- import { defineRule as defineRule22 } from "@oxlint/plugins";
2035
+ import { defineRule as defineRule23 } from "@oxlint/plugins";
1970
2036
  var typeNodeKinds = new Set([
1971
2037
  "JSDocNonNullableType",
1972
2038
  "JSDocNullableType",
@@ -2053,7 +2119,7 @@ function shouldReportType(node, environment) {
2053
2119
  }
2054
2120
  return true;
2055
2121
  }
2056
- var noUnsafeDictionaryTypeRule = defineRule22({
2122
+ var noUnsafeDictionaryTypeRule = defineRule23({
2057
2123
  meta: {
2058
2124
  type: "problem",
2059
2125
  docs: {
@@ -2095,7 +2161,7 @@ var noUnsafeDictionaryTypeRule = defineRule22({
2095
2161
  });
2096
2162
 
2097
2163
  // vendor/anti-slop/src/rules/no-widen-then-assert.ts
2098
- import { defineRule as defineRule23 } from "@oxlint/plugins";
2164
+ import { defineRule as defineRule24 } from "@oxlint/plugins";
2099
2165
  var functionBoundaryTypes = new Set([
2100
2166
  "ArrowFunctionExpression",
2101
2167
  "FunctionDeclaration",
@@ -2291,7 +2357,7 @@ function assertionIsNarrower(sourceText, broadKind, evidence, assertedType) {
2291
2357
  return isDefinitelyObjectType(assertedType);
2292
2358
  return isDefinitelyNarrowerRecordType(assertedType);
2293
2359
  }
2294
- var noWidenThenAssertRule = defineRule23({
2360
+ var noWidenThenAssertRule = defineRule24({
2295
2361
  meta: {
2296
2362
  type: "problem",
2297
2363
  docs: {
@@ -2853,7 +2919,7 @@ var requireReadableSpacingRule = {
2853
2919
  };
2854
2920
 
2855
2921
  // vendor/anti-slop/src/rules/require-safety-comment-for-type-assertion.ts
2856
- import { defineRule as defineRule24 } from "@oxlint/plugins";
2922
+ import { defineRule as defineRule25 } from "@oxlint/plugins";
2857
2923
  var DEFAULT_SAFETY_MARKERS = ["SAFETY"];
2858
2924
  var commentOwnerKinds = new Set([
2859
2925
  "ExpressionStatement",
@@ -2896,7 +2962,7 @@ function hasSafetyComment(sourceCode, node, pattern) {
2896
2962
  current = current.parent;
2897
2963
  }
2898
2964
  }
2899
- var requireSafetyCommentForTypeAssertionRule = defineRule24({
2965
+ var requireSafetyCommentForTypeAssertionRule = defineRule25({
2900
2966
  meta: {
2901
2967
  type: "problem",
2902
2968
  docs: {
@@ -2974,7 +3040,7 @@ var src_default = antiSlopPlugin;
2974
3040
  import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
2975
3041
 
2976
3042
  // src/generic/rules/no-explicit-inferable-type-predicate.ts
2977
- import { defineRule as defineRule25 } from "@oxlint/plugins";
3043
+ import { defineRule as defineRule26 } from "@oxlint/plugins";
2978
3044
  function isNamed(node, name) {
2979
3045
  return node.type === "Identifier" && node.name === name;
2980
3046
  }
@@ -3014,7 +3080,7 @@ function isPublicSignature(owner) {
3014
3080
  }
3015
3081
  return parent.type === "VariableDeclarator" && parent.parent.parent?.type === "ExportNamedDeclaration";
3016
3082
  }
3017
- var noExplicitInferableTypePredicateRule = defineRule25({
3083
+ var noExplicitInferableTypePredicateRule = defineRule26({
3018
3084
  meta: {
3019
3085
  type: "suggestion",
3020
3086
  docs: {
@@ -3050,7 +3116,7 @@ var noExplicitInferableTypePredicateRule = defineRule25({
3050
3116
  });
3051
3117
 
3052
3118
  // src/generic/rules/no-unchecked-query-selector-type.ts
3053
- import { defineRule as defineRule26 } from "@oxlint/plugins";
3119
+ import { defineRule as defineRule27 } from "@oxlint/plugins";
3054
3120
 
3055
3121
  // src/shared/json.ts
3056
3122
  function jsonObject(value) {
@@ -3151,7 +3217,7 @@ function isLitRenderRootQuery(node, bases) {
3151
3217
 
3152
3218
  // src/generic/rules/no-unchecked-query-selector-type.ts
3153
3219
  var queryMethods = new Set(["closest", "querySelector", "querySelectorAll"]);
3154
- var noUncheckedQuerySelectorTypeRule = defineRule26({
3220
+ var noUncheckedQuerySelectorTypeRule = defineRule27({
3155
3221
  meta: {
3156
3222
  type: "problem",
3157
3223
  docs: {
@@ -3180,8 +3246,8 @@ var noUncheckedQuerySelectorTypeRule = defineRule26({
3180
3246
  });
3181
3247
 
3182
3248
  // src/generic/rules/no-untyped-custom-event.ts
3183
- import { defineRule as defineRule27 } from "@oxlint/plugins";
3184
- var noUntypedCustomEventRule = defineRule27({
3249
+ import { defineRule as defineRule28 } from "@oxlint/plugins";
3250
+ var noUntypedCustomEventRule = defineRule28({
3185
3251
  meta: {
3186
3252
  type: "problem",
3187
3253
  docs: {
@@ -3204,7 +3270,7 @@ var noUntypedCustomEventRule = defineRule27({
3204
3270
  });
3205
3271
 
3206
3272
  // src/generic/rules/prefer-event-parameter-type.ts
3207
- import { defineRule as defineRule28 } from "@oxlint/plugins";
3273
+ import { defineRule as defineRule29 } from "@oxlint/plugins";
3208
3274
  function staticMemberPath(node) {
3209
3275
  const path = [];
3210
3276
  let current = node.type === "ChainExpression" ? node.expression : node;
@@ -3236,7 +3302,7 @@ function assertedEventParameter(node) {
3236
3302
  property: member.path.join(".")
3237
3303
  };
3238
3304
  }
3239
- var preferEventParameterTypeRule = defineRule28({
3305
+ var preferEventParameterTypeRule = defineRule29({
3240
3306
  meta: {
3241
3307
  type: "suggestion",
3242
3308
  docs: {
@@ -3265,14 +3331,14 @@ var preferEventParameterTypeRule = defineRule28({
3265
3331
  });
3266
3332
 
3267
3333
  // src/generic/rules/prefer-lit-query-decorators.ts
3268
- import { defineRule as defineRule29 } from "@oxlint/plugins";
3334
+ import { defineRule as defineRule30 } from "@oxlint/plugins";
3269
3335
  var messageIds = {
3270
3336
  assignedElements: "preferQueryAssignedElements",
3271
3337
  assignedNodes: "preferQueryAssignedNodes",
3272
3338
  querySelector: "preferQuery",
3273
3339
  querySelectorAll: "preferQueryAll"
3274
3340
  };
3275
- var preferLitQueryDecoratorsRule = defineRule29({
3341
+ var preferLitQueryDecoratorsRule = defineRule30({
3276
3342
  meta: {
3277
3343
  type: "suggestion",
3278
3344
  docs: {
@@ -3325,8 +3391,8 @@ var preferLitQueryDecoratorsRule = defineRule29({
3325
3391
  });
3326
3392
 
3327
3393
  // src/generic/rules/prefer-types-over-assertions.ts
3328
- import { defineRule as defineRule30 } from "@oxlint/plugins";
3329
- var preferTypesOverAssertionsRule = defineRule30({
3394
+ import { defineRule as defineRule31 } from "@oxlint/plugins";
3395
+ var preferTypesOverAssertionsRule = defineRule31({
3330
3396
  meta: {
3331
3397
  type: "problem",
3332
3398
  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,71 @@ 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 launchesGh(args) {
54
+ const [command, argv] = args;
55
+ if (command?.type === "Literal")
56
+ return command.value === "gh" && argv !== undefined;
57
+ if (command?.type !== "ArrayExpression")
58
+ return false;
59
+ const first = command.elements[0];
60
+ return first?.type === "Literal" && first.value === "gh";
61
+ }
62
+ var noRawGhRule = defineRule({
63
+ meta: {
64
+ type: "problem",
65
+ docs: {
66
+ description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
67
+ },
68
+ messages: {
69
+ rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
70
+ rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
71
+ spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
72
+ }
73
+ },
74
+ create(context) {
75
+ return {
76
+ CallExpression(node) {
77
+ const callee = node.callee;
78
+ const method = staticMemberName(callee);
79
+ if (callee.type === "MemberExpression" && method !== null) {
80
+ const object = callee.object;
81
+ if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
82
+ context.report({ node, messageId: "rawGh", data: { method } });
83
+ return;
84
+ }
85
+ if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
86
+ context.report({ node, messageId: "rawApi" });
87
+ return;
88
+ }
89
+ }
90
+ if (launchesGh(node.arguments))
91
+ context.report({ node, messageId: "spawnGh" });
92
+ }
93
+ };
94
+ }
95
+ });
96
+
97
+ // src/effect/rules/no-try-catch-in-effect-generators.ts
98
+ import { defineRule as defineRule2 } from "@oxlint/plugins";
99
+
35
100
  // src/shared/effect.ts
36
101
  function isEffectImport(sourceCode, identifier, namespace) {
37
102
  const variable = resolveVariable(sourceCode, identifier);
@@ -78,7 +143,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
78
143
  const factoryCall = parent.callee;
79
144
  return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
80
145
  }
81
- var noTryCatchInEffectGeneratorsRule = defineRule({
146
+ var noTryCatchInEffectGeneratorsRule = defineRule2({
82
147
  meta: {
83
148
  type: "problem",
84
149
  docs: {
@@ -104,7 +169,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule({
104
169
  });
105
170
 
106
171
  // src/effect/rules/prefer-platform-services.ts
107
- import { defineRule as defineRule2 } from "@oxlint/plugins";
172
+ import { defineRule as defineRule3 } from "@oxlint/plugins";
108
173
  var moduleServices = new Map([
109
174
  ["child_process", "childProcess"],
110
175
  ["fs", "fileSystem"],
@@ -117,7 +182,7 @@ var bunServices = new Map([
117
182
  ["spawnSync", "childProcess"],
118
183
  ["write", "fileSystem"]
119
184
  ]);
120
- var preferPlatformServicesRule = defineRule2({
185
+ var preferPlatformServicesRule = defineRule3({
121
186
  meta: {
122
187
  type: "suggestion",
123
188
  docs: {
@@ -180,6 +245,7 @@ var preferPlatformServicesRule = defineRule2({
180
245
  var timmoEffectPlugin = eslintCompatPlugin({
181
246
  meta: { name: "timmo-effect" },
182
247
  rules: {
248
+ "no-raw-gh": noRawGhRule,
183
249
  "no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
184
250
  "prefer-platform-services": preferPlatformServicesRule
185
251
  }
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.1",
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,133 @@
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
+ /**
65
+ * Whether call arguments launch gh: `"gh"` followed by its argv, as in
66
+ * `run("gh", args)`, or an argv array starting with `"gh"`.
67
+ */
68
+ function launchesGh(args: ESTree.CallExpression["arguments"]): boolean {
69
+ const [command, argv] = args;
70
+
71
+ if (command?.type === "Literal")
72
+ return command.value === "gh" && argv !== undefined;
73
+
74
+ if (command?.type !== "ArrayExpression") return false;
75
+ const first = command.elements[0];
76
+
77
+ return first?.type === "Literal" && first.value === "gh";
78
+ }
79
+
80
+ /** Route gh through effect-gh's typed operations instead of raw argv or direct spawns. */
81
+ export const noRawGhRule = defineRule({
82
+ meta: {
83
+ type: "problem",
84
+ docs: {
85
+ description:
86
+ "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly.",
87
+ },
88
+ messages: {
89
+ rawGh:
90
+ "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
91
+ rawApi:
92
+ "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
93
+ spawnGh:
94
+ "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly.",
95
+ },
96
+ },
97
+ create(context) {
98
+ return {
99
+ CallExpression(node) {
100
+ const callee = node.callee;
101
+ const method = staticMemberName(callee);
102
+
103
+ if (callee.type === "MemberExpression" && method !== null) {
104
+ const object = callee.object;
105
+
106
+ if (
107
+ rawMethods.has(method) &&
108
+ (isYieldGh(context.sourceCode, object) ||
109
+ (object.type === "Identifier" &&
110
+ isGhBinding(context.sourceCode, object)))
111
+ ) {
112
+ context.report({ node, messageId: "rawGh", data: { method } });
113
+
114
+ return;
115
+ }
116
+
117
+ if (
118
+ method === "raw" &&
119
+ object.type === "Identifier" &&
120
+ isEffectGhImport(context.sourceCode, object, "Api")
121
+ ) {
122
+ context.report({ node, messageId: "rawApi" });
123
+
124
+ return;
125
+ }
126
+ }
127
+
128
+ if (launchesGh(node.arguments))
129
+ context.report({ node, messageId: "spawnGh" });
130
+ },
131
+ };
132
+ },
133
+ });