@agjs/tsforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. package/strict.web.eslint.config.mjs +185 -0
@@ -0,0 +1,107 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import {
6
+ findObjectProperty,
7
+ getChainRoot,
8
+ isNewElysiaExpression,
9
+ } from "../utils/elysiaChain";
10
+
11
+ export const RULE_NAME = "require-plugin-name";
12
+
13
+ export interface RequirePluginNameOptions {
14
+ readonly allowAnonymousDefault?: boolean;
15
+ }
16
+
17
+ type RuleOptions = [RequirePluginNameOptions];
18
+ type MessageIds = "missingPluginName";
19
+
20
+ const optionSchema: JSONSchema4 = {
21
+ type: "object",
22
+ additionalProperties: false,
23
+ properties: {
24
+ allowAnonymousDefault: { type: "boolean" },
25
+ },
26
+ };
27
+
28
+ export const requirePluginNameRule = createRule<RuleOptions, MessageIds>({
29
+ name: RULE_NAME,
30
+ meta: {
31
+ type: "problem",
32
+ docs: {
33
+ description:
34
+ "Exported Elysia plugin instances must declare `new Elysia({ name: '...' })` so the runtime can deduplicate plugin re-imports.",
35
+ },
36
+ schema: [optionSchema],
37
+ messages: {
38
+ missingPluginName:
39
+ 'Exported Elysia plugin is anonymous — pass `{ name: "..." }` to `new Elysia(...)` so the runtime can deduplicate this plugin across imports.',
40
+ },
41
+ },
42
+ defaultOptions: [{ allowAnonymousDefault: false }],
43
+ create(context, [options]) {
44
+ const allowAnonymousDefault = options.allowAnonymousDefault === true;
45
+
46
+ return {
47
+ ExportNamedDeclaration(node) {
48
+ if (node.declaration?.type !== AST_NODE_TYPES.VariableDeclaration) {
49
+ return;
50
+ }
51
+
52
+ for (const declarator of node.declaration.declarations) {
53
+ if (declarator.init) {
54
+ checkExpression(declarator.init);
55
+ }
56
+ }
57
+ },
58
+ ExportDefaultDeclaration(node) {
59
+ if (allowAnonymousDefault) {
60
+ return;
61
+ }
62
+
63
+ const expr = node.declaration;
64
+
65
+ if (
66
+ expr.type === AST_NODE_TYPES.NewExpression ||
67
+ expr.type === AST_NODE_TYPES.CallExpression ||
68
+ expr.type === AST_NODE_TYPES.MemberExpression
69
+ ) {
70
+ checkExpression(expr);
71
+ }
72
+ },
73
+ };
74
+
75
+ function checkExpression(expression: TSESTree.Expression): void {
76
+ let root: TSESTree.Node = expression;
77
+
78
+ if (
79
+ expression.type === AST_NODE_TYPES.CallExpression ||
80
+ expression.type === AST_NODE_TYPES.MemberExpression
81
+ ) {
82
+ root = getChainRoot(expression);
83
+ }
84
+
85
+ if (!isNewElysiaExpression(root)) {
86
+ return;
87
+ }
88
+
89
+ const newExpr = root;
90
+ const arg = newExpr.arguments[0];
91
+
92
+ if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
93
+ const nameProperty = findObjectProperty(arg, "name");
94
+
95
+ if (
96
+ nameProperty?.value.type === AST_NODE_TYPES.Literal &&
97
+ typeof nameProperty.value.value === "string" &&
98
+ nameProperty.value.value.length > 0
99
+ ) {
100
+ return;
101
+ }
102
+ }
103
+
104
+ context.report({ node: newExpr, messageId: "missingPluginName" });
105
+ }
106
+ },
107
+ });
@@ -0,0 +1,306 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+
3
+ export const ROUTE_METHODS = new Set([
4
+ "get",
5
+ "post",
6
+ "put",
7
+ "patch",
8
+ "delete",
9
+ "options",
10
+ "head",
11
+ "trace",
12
+ "all",
13
+ "ws",
14
+ ]);
15
+
16
+ export const HOOK_METHODS = new Set([
17
+ "onRequest",
18
+ "onParse",
19
+ "onTransform",
20
+ "onBeforeHandle",
21
+ "resolve",
22
+ "onAfterHandle",
23
+ "mapResponse",
24
+ "onError",
25
+ "onAfterResponse",
26
+ "trace",
27
+ ]);
28
+
29
+ type FunctionLike =
30
+ | TSESTree.ArrowFunctionExpression
31
+ | TSESTree.FunctionExpression
32
+ | TSESTree.FunctionDeclaration;
33
+
34
+ export function isNewElysiaExpression(
35
+ node: TSESTree.Node | null | undefined
36
+ ): node is TSESTree.NewExpression {
37
+ return (
38
+ !!node &&
39
+ node.type === AST_NODE_TYPES.NewExpression &&
40
+ node.callee.type === AST_NODE_TYPES.Identifier &&
41
+ node.callee.name === "Elysia"
42
+ );
43
+ }
44
+
45
+ export function getChainRoot(
46
+ node: TSESTree.CallExpression | TSESTree.MemberExpression
47
+ ): TSESTree.Expression {
48
+ let current: TSESTree.Expression = node;
49
+
50
+ while (
51
+ current.type === AST_NODE_TYPES.CallExpression ||
52
+ current.type === AST_NODE_TYPES.MemberExpression
53
+ ) {
54
+ if (current.type === AST_NODE_TYPES.CallExpression) {
55
+ current = current.callee;
56
+ continue;
57
+ }
58
+
59
+ current = current.object;
60
+ }
61
+
62
+ return current;
63
+ }
64
+
65
+ export function collectElysiaVariables(program: TSESTree.Program): Set<string> {
66
+ const elysiaVars = new Set<string>();
67
+
68
+ for (const stmt of program.body) {
69
+ visitDeclarations(stmt, elysiaVars);
70
+ }
71
+
72
+ return elysiaVars;
73
+ }
74
+
75
+ function visitDeclarations(stmt: TSESTree.Node, elysiaVars: Set<string>): void {
76
+ if (stmt.type === AST_NODE_TYPES.VariableDeclaration) {
77
+ for (const declarator of stmt.declarations) {
78
+ if (
79
+ declarator.id.type === AST_NODE_TYPES.Identifier &&
80
+ declarator.init &&
81
+ chainRootIsNewElysia(declarator.init)
82
+ ) {
83
+ elysiaVars.add(declarator.id.name);
84
+ }
85
+ }
86
+
87
+ return;
88
+ }
89
+
90
+ if (stmt.type === AST_NODE_TYPES.ExportNamedDeclaration && stmt.declaration) {
91
+ visitDeclarations(stmt.declaration, elysiaVars);
92
+
93
+ return;
94
+ }
95
+
96
+ if (stmt.type === AST_NODE_TYPES.ExportDefaultDeclaration) {
97
+ return;
98
+ }
99
+ }
100
+
101
+ function chainRootIsNewElysia(expression: TSESTree.Expression): boolean {
102
+ if (expression.type === AST_NODE_TYPES.NewExpression) {
103
+ return isNewElysiaExpression(expression);
104
+ }
105
+
106
+ if (
107
+ expression.type === AST_NODE_TYPES.CallExpression ||
108
+ expression.type === AST_NODE_TYPES.MemberExpression
109
+ ) {
110
+ const root = getChainRoot(expression);
111
+
112
+ return isNewElysiaExpression(root);
113
+ }
114
+
115
+ return false;
116
+ }
117
+
118
+ export function isElysiaRouted(
119
+ call: TSESTree.CallExpression,
120
+ elysiaVars: Set<string>
121
+ ): boolean {
122
+ if (call.callee.type !== AST_NODE_TYPES.MemberExpression) {
123
+ return false;
124
+ }
125
+
126
+ const root = getChainRoot(call);
127
+
128
+ if (isNewElysiaExpression(root)) {
129
+ return true;
130
+ }
131
+
132
+ if (root.type === AST_NODE_TYPES.Identifier && elysiaVars.has(root.name)) {
133
+ return true;
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ export function getMemberMethodName(
140
+ call: TSESTree.CallExpression
141
+ ): string | null {
142
+ if (
143
+ call.callee.type === AST_NODE_TYPES.MemberExpression &&
144
+ call.callee.property.type === AST_NODE_TYPES.Identifier
145
+ ) {
146
+ return call.callee.property.name;
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ export function isElysiaRouteCall(
153
+ call: TSESTree.CallExpression,
154
+ elysiaVars: Set<string>
155
+ ): boolean {
156
+ const method = getMemberMethodName(call);
157
+
158
+ return (
159
+ method !== null &&
160
+ ROUTE_METHODS.has(method) &&
161
+ isElysiaRouted(call, elysiaVars)
162
+ );
163
+ }
164
+
165
+ export function isElysiaHookCall(
166
+ call: TSESTree.CallExpression,
167
+ elysiaVars: Set<string>
168
+ ): boolean {
169
+ const method = getMemberMethodName(call);
170
+
171
+ return (
172
+ method !== null &&
173
+ HOOK_METHODS.has(method) &&
174
+ isElysiaRouted(call, elysiaVars)
175
+ );
176
+ }
177
+
178
+ export function getRouteMethod(call: TSESTree.CallExpression): string | null {
179
+ const method = getMemberMethodName(call);
180
+
181
+ return method && ROUTE_METHODS.has(method) ? method : null;
182
+ }
183
+
184
+ export function getRoutePathLiteral(
185
+ call: TSESTree.CallExpression
186
+ ): string | null {
187
+ const arg = call.arguments[0];
188
+
189
+ if (arg?.type === AST_NODE_TYPES.Literal && typeof arg.value === "string") {
190
+ return arg.value;
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ export function getRouteHandlerFunction(
197
+ call: TSESTree.CallExpression
198
+ ): FunctionLike | null {
199
+ for (let i = call.arguments.length - 1; i >= 0; i--) {
200
+ const arg = call.arguments[i];
201
+
202
+ if (
203
+ arg &&
204
+ (arg.type === AST_NODE_TYPES.ArrowFunctionExpression ||
205
+ arg.type === AST_NODE_TYPES.FunctionExpression)
206
+ ) {
207
+ return arg;
208
+ }
209
+ }
210
+
211
+ return null;
212
+ }
213
+
214
+ export function getRouteOptionsObject(
215
+ call: TSESTree.CallExpression
216
+ ): TSESTree.ObjectExpression | null {
217
+ for (let i = call.arguments.length - 1; i >= 0; i--) {
218
+ const arg = call.arguments[i];
219
+
220
+ if (arg?.type === AST_NODE_TYPES.ObjectExpression) {
221
+ return arg;
222
+ }
223
+ }
224
+
225
+ return null;
226
+ }
227
+
228
+ export function findEnclosingRouteHandler(
229
+ node: TSESTree.Node,
230
+ elysiaVars: Set<string>
231
+ ): {
232
+ readonly fn: FunctionLike;
233
+ readonly routeCall: TSESTree.CallExpression;
234
+ } | null {
235
+ let current: TSESTree.Node | undefined = node.parent;
236
+
237
+ while (current) {
238
+ if (
239
+ (current.type === AST_NODE_TYPES.ArrowFunctionExpression ||
240
+ current.type === AST_NODE_TYPES.FunctionExpression) &&
241
+ current.parent?.type === AST_NODE_TYPES.CallExpression &&
242
+ current.parent.arguments.includes(current) &&
243
+ isElysiaRouteCall(current.parent, elysiaVars)
244
+ ) {
245
+ return { fn: current, routeCall: current.parent };
246
+ }
247
+
248
+ current = current.parent;
249
+ }
250
+
251
+ return null;
252
+ }
253
+
254
+ export function getCalleeName(node: TSESTree.CallExpression): string | null {
255
+ if (node.callee.type === AST_NODE_TYPES.Identifier) {
256
+ return node.callee.name;
257
+ }
258
+
259
+ if (
260
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
261
+ node.callee.property.type === AST_NODE_TYPES.Identifier
262
+ ) {
263
+ if (node.callee.object.type === AST_NODE_TYPES.Identifier) {
264
+ return `${node.callee.object.name}.${node.callee.property.name}`;
265
+ }
266
+
267
+ if (
268
+ node.callee.object.type === AST_NODE_TYPES.MemberExpression &&
269
+ node.callee.object.object.type === AST_NODE_TYPES.Identifier &&
270
+ node.callee.object.property.type === AST_NODE_TYPES.Identifier
271
+ ) {
272
+ return `${node.callee.object.object.name}.${node.callee.object.property.name}.${node.callee.property.name}`;
273
+ }
274
+
275
+ return node.callee.property.name;
276
+ }
277
+
278
+ return null;
279
+ }
280
+
281
+ export function findObjectProperty(
282
+ obj: TSESTree.ObjectExpression,
283
+ name: string
284
+ ): TSESTree.Property | null {
285
+ for (const property of obj.properties) {
286
+ if (property.type !== AST_NODE_TYPES.Property) {
287
+ continue;
288
+ }
289
+
290
+ if (
291
+ property.key.type === AST_NODE_TYPES.Identifier &&
292
+ property.key.name === name
293
+ ) {
294
+ return property;
295
+ }
296
+
297
+ if (
298
+ property.key.type === AST_NODE_TYPES.Literal &&
299
+ property.key.value === name
300
+ ) {
301
+ return property;
302
+ }
303
+ }
304
+
305
+ return null;
306
+ }
@@ -0,0 +1,23 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { noDirectProcessEnvRule } from "./rules/no-direct-process-env";
4
+ import { noProcessExitRule } from "./rules/no-process-exit";
5
+ import type { IRulePack } from "../rule-packs.types";
6
+
7
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
8
+ "no-direct-process-env": noDirectProcessEnvRule,
9
+ "no-process-exit": noProcessExitRule,
10
+ };
11
+
12
+ export const envAccessPack: IRulePack = {
13
+ id: "env-access",
14
+ description:
15
+ "Safe environment variable access patterns (validation and typing)",
16
+ rules,
17
+ rulesConfig: {
18
+ "no-direct-process-env": "error",
19
+ "no-process-exit": "error",
20
+ },
21
+ };
22
+
23
+ export default envAccessPack;
@@ -0,0 +1,2 @@
1
+ export { noDirectProcessEnvRule } from "./no-direct-process-env";
2
+ export { noProcessExitRule } from "./no-process-exit";
@@ -0,0 +1,133 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import { matchesAnyGlobPattern, toPosixRelative } from "../../utils";
6
+
7
+ export const RULE_NAME = "no-direct-process-env";
8
+
9
+ export interface NoDirectProcessEnvOptions {
10
+ readonly allowedFiles?: readonly string[];
11
+ readonly singletonSuggestion?: string;
12
+ }
13
+
14
+ type RuleOptions = [NoDirectProcessEnvOptions];
15
+ type MessageIds = "directProcessEnv";
16
+
17
+ const DEFAULT_ALLOWED_FILES: readonly string[] = [
18
+ "src/config/env/**",
19
+ "**/*.config.{ts,js,mjs}",
20
+ "scripts/**",
21
+ ];
22
+
23
+ const DEFAULT_SUGGESTION = "import { env } from '@/config/env'";
24
+
25
+ const optionSchema: JSONSchema4 = {
26
+ type: "object",
27
+ additionalProperties: false,
28
+ properties: {
29
+ allowedFiles: {
30
+ type: "array",
31
+ items: { type: "string" },
32
+ uniqueItems: true,
33
+ },
34
+ singletonSuggestion: { type: "string", minLength: 1 },
35
+ },
36
+ };
37
+
38
+ function isProcessEnv(node: TSESTree.Node): boolean {
39
+ if (node.type !== AST_NODE_TYPES.MemberExpression) {
40
+ return false;
41
+ }
42
+
43
+ if (node.computed) {
44
+ return false;
45
+ }
46
+
47
+ if (
48
+ node.object.type !== AST_NODE_TYPES.Identifier ||
49
+ node.object.name !== "process"
50
+ ) {
51
+ return false;
52
+ }
53
+
54
+ if (
55
+ node.property.type !== AST_NODE_TYPES.Identifier ||
56
+ node.property.name !== "env"
57
+ ) {
58
+ return false;
59
+ }
60
+
61
+ return true;
62
+ }
63
+
64
+ export const noDirectProcessEnvRule = createRule<RuleOptions, MessageIds>({
65
+ name: RULE_NAME,
66
+ meta: {
67
+ type: "problem",
68
+ docs: {
69
+ description:
70
+ "Disallow direct `process.env` access — force every consumer through a typed, boot-validated singleton.",
71
+ },
72
+ schema: [optionSchema],
73
+ messages: {
74
+ directProcessEnv:
75
+ "Read environment variables via the validated singleton ({{suggestion}}) — `process.env.X` bypasses the boot-time schema check.",
76
+ },
77
+ },
78
+ defaultOptions: [
79
+ {
80
+ allowedFiles: [...DEFAULT_ALLOWED_FILES],
81
+ singletonSuggestion: DEFAULT_SUGGESTION,
82
+ },
83
+ ],
84
+ create(context, [options]) {
85
+ const allowedFiles = options.allowedFiles ?? DEFAULT_ALLOWED_FILES;
86
+ const suggestion = options.singletonSuggestion ?? DEFAULT_SUGGESTION;
87
+
88
+ const relative = toPosixRelative(context.filename, context.cwd);
89
+
90
+ if (
91
+ allowedFiles.length > 0 &&
92
+ matchesAnyGlobPattern(relative, [...allowedFiles])
93
+ ) {
94
+ return {};
95
+ }
96
+
97
+ function report(node: TSESTree.Node): void {
98
+ context.report({
99
+ node,
100
+ messageId: "directProcessEnv",
101
+ data: { suggestion },
102
+ });
103
+ }
104
+
105
+ return {
106
+ // `process.env.X` (read or write) and `process.env[X]`
107
+ MemberExpression(node) {
108
+ if (isProcessEnv(node.object)) {
109
+ report(node);
110
+ }
111
+ },
112
+ // `const { X, Y } = process.env`
113
+ VariableDeclarator(node) {
114
+ if (node.init === null) {
115
+ return;
116
+ }
117
+
118
+ if (isProcessEnv(node.init)) {
119
+ report(node.init);
120
+ }
121
+ },
122
+ // `({ X } = process.env)` assignment-pattern destructure
123
+ AssignmentExpression(node) {
124
+ if (
125
+ node.left.type === AST_NODE_TYPES.ObjectPattern &&
126
+ isProcessEnv(node.right)
127
+ ) {
128
+ report(node.right);
129
+ }
130
+ },
131
+ };
132
+ },
133
+ });
@@ -0,0 +1,95 @@
1
+ import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
2
+ import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
3
+
4
+ import { createRule } from "../../create-rule";
5
+ import { matchesAnyGlobPattern, toPosixRelative } from "../../utils";
6
+
7
+ export const RULE_NAME = "no-process-exit";
8
+
9
+ export interface NoProcessExitOptions {
10
+ readonly allowedFiles?: readonly string[];
11
+ }
12
+
13
+ type RuleOptions = [NoProcessExitOptions];
14
+ type MessageIds = "processExit";
15
+
16
+ const DEFAULT_ALLOWED_FILES: readonly string[] = [
17
+ "src/config/error-handlers/**",
18
+ "scripts/**",
19
+ "**/*.test.ts",
20
+ "tests/**",
21
+ ];
22
+
23
+ const optionSchema: JSONSchema4 = {
24
+ type: "object",
25
+ additionalProperties: false,
26
+ properties: {
27
+ allowedFiles: {
28
+ type: "array",
29
+ items: { type: "string" },
30
+ uniqueItems: true,
31
+ },
32
+ },
33
+ };
34
+
35
+ function isProcessExit(node: TSESTree.CallExpression): boolean {
36
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
37
+ return false;
38
+ }
39
+
40
+ if (node.callee.computed) {
41
+ return false;
42
+ }
43
+
44
+ if (
45
+ node.callee.object.type !== AST_NODE_TYPES.Identifier ||
46
+ node.callee.object.name !== "process"
47
+ ) {
48
+ return false;
49
+ }
50
+
51
+ if (
52
+ node.callee.property.type !== AST_NODE_TYPES.Identifier ||
53
+ node.callee.property.name !== "exit"
54
+ ) {
55
+ return false;
56
+ }
57
+
58
+ return true;
59
+ }
60
+
61
+ export const noProcessExitRule = createRule<RuleOptions, MessageIds>({
62
+ name: RULE_NAME,
63
+ meta: {
64
+ type: "problem",
65
+ docs: {
66
+ description:
67
+ "Disallow `process.exit()` outside the centralized shutdown and CLI entrypoints — forces graceful teardown through the error-handlers module.",
68
+ },
69
+ schema: [optionSchema],
70
+ messages: {
71
+ processExit:
72
+ "`process.exit()` is reserved for graceful shutdown (`src/config/error-handlers/`) and CLI scripts. Route shutdown through the centralized handlers instead.",
73
+ },
74
+ },
75
+ defaultOptions: [{ allowedFiles: [...DEFAULT_ALLOWED_FILES] }],
76
+ create(context, [options]) {
77
+ const allowedFiles = options.allowedFiles ?? DEFAULT_ALLOWED_FILES;
78
+ const relative = toPosixRelative(context.filename, context.cwd);
79
+
80
+ if (
81
+ allowedFiles.length > 0 &&
82
+ matchesAnyGlobPattern(relative, [...allowedFiles])
83
+ ) {
84
+ return {};
85
+ }
86
+
87
+ return {
88
+ CallExpression(node) {
89
+ if (isProcessExit(node)) {
90
+ context.report({ node, messageId: "processExit" });
91
+ }
92
+ },
93
+ };
94
+ },
95
+ });
@@ -0,0 +1,19 @@
1
+ import type { TSESLint } from "@typescript-eslint/utils";
2
+
3
+ import { staticTranslationKeyExistsRule } from "./rules/static-translation-key-exists";
4
+ import type { IRulePack } from "../rule-packs.types";
5
+
6
+ const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
7
+ "static-translation-key-exists": staticTranslationKeyExistsRule,
8
+ };
9
+
10
+ export const i18nKeysPack: IRulePack = {
11
+ id: "i18n-keys",
12
+ description: "Internationalization key management and translation patterns",
13
+ rules,
14
+ rulesConfig: {
15
+ "static-translation-key-exists": "error",
16
+ },
17
+ };
18
+
19
+ export default i18nKeysPack;