@nt-ai-lab/opencode-skillz 0.3.3 → 0.3.5

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.
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: Implement the requested changes or specified document with small-step lint verification
3
+ ---
4
+
5
+ Implement:
6
+ $ARGUMENTS
7
+
8
+
9
+ Implement the required task to the highest standards possible. Do not take shortcuts, do not rush, do not jump to the easiest solution to make the code pass. Think carefully, explore options and write the code that is easiest to read, easiest change, and will not break in production.
10
+
11
+ ## Linting
12
+
13
+ After each small TypeScript code change, call `nt_skillz_lint` for the changed `.ts` or `.tsx` file or files.
14
+
15
+ - all lint errors on new code must be addressed before continuing
16
+ - if the lint fails on existing code, ignore the error unless it is very close to the new code
17
+ - line-length limits do not count as existing code; if new code causes a file-length lint error, it must be fixed
@@ -1,17 +1,19 @@
1
1
  import { buildCommandName } from "../../plugin-registry/command-names.js";
2
2
  export const DONT_STOP_COMMAND_NAME = buildCommandName("dont-stop");
3
3
  export const CLEAR_DONT_STOP_COMMAND_NAME = buildCommandName("clear-dont-stop");
4
+ const DONT_STOP_TEMPLATE = "dont-stop enabled";
5
+ const CLEAR_DONT_STOP_TEMPLATE = "dont-stop cleared";
4
6
  export function registerDontStopCommands(commandConfig) {
5
7
  if (!commandConfig[DONT_STOP_COMMAND_NAME]) {
6
8
  commandConfig[DONT_STOP_COMMAND_NAME] = {
7
9
  description: "Activate idle continuation for this session",
8
- template: "",
10
+ template: DONT_STOP_TEMPLATE,
9
11
  };
10
12
  }
11
13
  if (!commandConfig[CLEAR_DONT_STOP_COMMAND_NAME]) {
12
14
  commandConfig[CLEAR_DONT_STOP_COMMAND_NAME] = {
13
15
  description: "Disable idle continuation for this session",
14
- template: "",
16
+ template: CLEAR_DONT_STOP_TEMPLATE,
15
17
  };
16
18
  }
17
19
  }
@@ -1,10 +1,12 @@
1
1
  import { createDontStopHooks } from "../commands/dont-stop/index.js";
2
+ import { LINT_TOOL_NAME, lintTool, } from "../tools/lint.js";
2
3
  import { registerAgents } from "./agents.js";
3
4
  import { registerCommands } from "./commands.js";
4
5
  export function createPluginRegistry(input, pluginRoot) {
5
6
  const dontStopHooks = createDontStopHooks(input.client);
6
7
  return {
7
8
  ...dontStopHooks,
9
+ tool: { [LINT_TOOL_NAME]: lintTool },
8
10
  config: async (config) => {
9
11
  config.command ??= {};
10
12
  config.agent ??= {};
@@ -0,0 +1,15 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ export declare const LINT_TOOL_NAME = "nt_skillz_lint";
3
+ export declare const lintTool: {
4
+ description: string;
5
+ args: {
6
+ files: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
7
+ base: import("zod").ZodOptional<import("zod").ZodString>;
8
+ head: import("zod").ZodOptional<import("zod").ZodString>;
9
+ };
10
+ execute(args: {
11
+ files?: string[] | undefined;
12
+ base?: string | undefined;
13
+ head?: string | undefined;
14
+ }, context: ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
15
+ };
@@ -0,0 +1,111 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { tool, } from "@opencode-ai/plugin";
4
+ class InvalidLintRequestError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ }
8
+ }
9
+ class LintRunFailedError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ }
13
+ }
14
+ const lintScriptPath = fileURLToPath(new URL("../../scripts/lint-ts.mjs", import.meta.url));
15
+ export const LINT_TOOL_NAME = "nt_skillz_lint";
16
+ function validateLintRequest(request) {
17
+ if (request.base && request.files?.length) {
18
+ throw new InvalidLintRequestError("Expected either file paths or base reference. Got both.");
19
+ }
20
+ if (!request.base && request.head) {
21
+ throw new InvalidLintRequestError("Expected head reference to be used together with base reference.");
22
+ }
23
+ }
24
+ function createLintCommandArguments(request) {
25
+ const commandArguments = [lintScriptPath];
26
+ if (request.base) {
27
+ commandArguments.push("--base", request.base);
28
+ if (request.head) {
29
+ commandArguments.push("--head", request.head);
30
+ }
31
+ }
32
+ for (const filePath of request.files ?? []) {
33
+ commandArguments.push(filePath);
34
+ }
35
+ return commandArguments;
36
+ }
37
+ function createLintTitle(request) {
38
+ if (request.files?.length) {
39
+ return `Lint ${request.files.length} TypeScript file(s)`;
40
+ }
41
+ if (request.base) {
42
+ return `Lint TypeScript changes from ${request.base}`;
43
+ }
44
+ return "Lint current TypeScript files";
45
+ }
46
+ function createLintOutput(standardOutputParts, standardErrorParts) {
47
+ const sections = [standardOutputParts.join("").trim(), standardErrorParts.join("").trim()].filter(Boolean);
48
+ return sections.join("\n");
49
+ }
50
+ function normalizeExitCode(value) {
51
+ if (typeof value === "number") {
52
+ return value;
53
+ }
54
+ return 1;
55
+ }
56
+ function runLintCommand(request, context) {
57
+ return new Promise((resolve, reject) => {
58
+ const lintCommand = spawn(process.execPath, createLintCommandArguments(request), {
59
+ cwd: context.worktree,
60
+ signal: context.abort,
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ });
63
+ const standardOutputParts = [];
64
+ const standardErrorParts = [];
65
+ lintCommand.stdout.on("data", (chunk) => {
66
+ standardOutputParts.push(chunk.toString());
67
+ });
68
+ lintCommand.stderr.on("data", (chunk) => {
69
+ standardErrorParts.push(chunk.toString());
70
+ });
71
+ lintCommand.on("error", (error) => {
72
+ reject(error);
73
+ });
74
+ lintCommand.on("close", (exitCode) => {
75
+ resolve({
76
+ exitCode: normalizeExitCode(exitCode),
77
+ output: createLintOutput(standardOutputParts, standardErrorParts),
78
+ });
79
+ });
80
+ });
81
+ }
82
+ function createLintFailureMessage(outcome) {
83
+ if (outcome.output) {
84
+ return outcome.output;
85
+ }
86
+ return `Lint failed with exit code ${outcome.exitCode}.`;
87
+ }
88
+ export const lintTool = tool({
89
+ description: "Run bundled TypeScript lint rules against current project files.",
90
+ args: {
91
+ files: tool.schema.array(tool.schema.string()).optional().describe("Relative .ts or .tsx file paths to lint."),
92
+ base: tool.schema.string().optional().describe("Base git reference for PR-style changed-file linting."),
93
+ head: tool.schema.string().optional().describe("Optional head git reference used with base."),
94
+ },
95
+ async execute(request, context) {
96
+ validateLintRequest(request);
97
+ context.metadata({ title: createLintTitle(request) });
98
+ const outcome = await runLintCommand(request, context);
99
+ if (outcome.exitCode !== 0) {
100
+ throw new LintRunFailedError(createLintFailureMessage(outcome));
101
+ }
102
+ return {
103
+ output: outcome.output || "Lint passed.",
104
+ metadata: {
105
+ base: request.base ?? null,
106
+ fileCount: request.files?.length ?? 0,
107
+ head: request.head ?? null,
108
+ },
109
+ };
110
+ },
111
+ });
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from "@opencode-ai/plugin";
1
2
  export interface CommandDefinition {
2
3
  description?: string;
3
4
  template: string;
@@ -69,6 +70,7 @@ export interface OpencodeClient {
69
70
  }
70
71
  export interface PluginHooks {
71
72
  config?: (config: PluginConfig) => Promise<void>;
73
+ tool?: Record<string, ToolDefinition>;
72
74
  "command.execute.before"?: (input: CommandExecuteBeforeInput, output: CommandExecuteBeforeOutput) => Promise<void>;
73
75
  "experimental.chat.system.transform"?: (input: ChatSystemTransformInput, output: ChatSystemTransformOutput) => Promise<void>;
74
76
  event?: (input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nt-ai-lab/opencode-skillz",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Bundled OpenCode commands and agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,12 +19,25 @@
19
19
  "dist",
20
20
  "commands",
21
21
  "agents",
22
+ "scripts",
22
23
  "AGENTS.md",
23
24
  "README.md"
24
25
  ],
26
+ "dependencies": {
27
+ "@opencode-ai/plugin": "^1.14.28"
28
+ },
25
29
  "devDependencies": {
30
+ "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
31
+ "@eslint/js": "^9.39.0",
32
+ "@stylistic/eslint-plugin": "^5.6.1",
33
+ "@vitest/eslint-plugin": "^1.0.1",
26
34
  "@types/node": "^24.7.2",
27
- "typescript": "^5.9.3"
35
+ "eslint": "^9.39.0",
36
+ "eslint-plugin-import": "^2.32.0",
37
+ "eslint-plugin-sonarjs": "^3.0.5",
38
+ "eslint-plugin-unicorn": "^62.0.0",
39
+ "typescript": "^5.9.3",
40
+ "typescript-eslint": "^8.50.1"
28
41
  },
29
42
  "publishConfig": {
30
43
  "access": "public"
@@ -0,0 +1,220 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { spawnSync } from 'node:child_process'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { parseArgs } from 'node:util'
7
+
8
+ class UsageError extends Error {
9
+ constructor(message) {
10
+ super(message)
11
+ }
12
+ }
13
+
14
+ class GitCommandError extends Error {
15
+ constructor(message) {
16
+ super(message)
17
+ }
18
+ }
19
+
20
+ class MissingLocalDependencyError extends Error {
21
+ constructor(message) {
22
+ super(message)
23
+ }
24
+ }
25
+
26
+ class LintExecutionError extends Error {
27
+ constructor(message) {
28
+ super(message)
29
+ }
30
+ }
31
+
32
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
33
+ const toolRepositoryRoot = path.resolve(scriptDirectory, '..')
34
+ const eslintCliPath = path.join(toolRepositoryRoot, 'node_modules', 'eslint', 'bin', 'eslint.js')
35
+ const eslintConfigPath = path.join(toolRepositoryRoot, 'scripts', 'living-architecture-eslint.config.mjs')
36
+
37
+ const printUsage = () => {
38
+ const usage = [
39
+ 'Usage: ./scripts/lint-ts.sh [--repo PATH] [--base REF] [--head REF] [file ...]',
40
+ '',
41
+ 'Examples:',
42
+ ' ./scripts/lint-ts.sh --repo ../living-architecture --base origin/main',
43
+ ' ./scripts/lint-ts.sh --repo ../living-architecture packages/example/src/example.ts',
44
+ ' ./scripts/lint-ts.sh src/example.ts',
45
+ ].join('\n')
46
+
47
+ process.stdout.write(`${usage}\n`)
48
+ }
49
+
50
+ const parseCommandLine = () => {
51
+ return parseArgs({
52
+ options: {
53
+ repo: { type: 'string' },
54
+ base: { type: 'string' },
55
+ head: { type: 'string' },
56
+ help: { type: 'boolean', short: 'h' },
57
+ },
58
+ allowPositionals: true,
59
+ })
60
+ }
61
+
62
+ const resolveDirectory = (directoryPath) => {
63
+ const absoluteDirectoryPath = path.resolve(directoryPath)
64
+
65
+ if (!fs.existsSync(absoluteDirectoryPath)) {
66
+ throw new UsageError(`Expected repository path to exist. Got ${absoluteDirectoryPath}.`)
67
+ }
68
+
69
+ if (!fs.statSync(absoluteDirectoryPath).isDirectory()) {
70
+ throw new UsageError(`Expected repository path to be a directory. Got ${absoluteDirectoryPath}.`)
71
+ }
72
+
73
+ return absoluteDirectoryPath
74
+ }
75
+
76
+ const ensureSupportedFilePath = (filePath) => {
77
+ if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
78
+ return
79
+ }
80
+
81
+ throw new UsageError(`Expected a TypeScript file path ending in .ts or .tsx. Got ${filePath}.`)
82
+ }
83
+
84
+ const runGit = (repositoryRoot, args) => {
85
+ const gitResult = spawnSync('git', ['-C', repositoryRoot, ...args], {
86
+ encoding: 'utf8',
87
+ })
88
+
89
+ if (gitResult.error) {
90
+ throw new GitCommandError(`Expected git command to run. Got ${gitResult.error.message}.`)
91
+ }
92
+
93
+ if (gitResult.status === 0) {
94
+ return gitResult.stdout
95
+ }
96
+
97
+ const errorOutput = gitResult.stderr.trim() || gitResult.stdout.trim() || 'git command failed'
98
+ throw new GitCommandError(`Expected git command to succeed. Got ${errorOutput}.`)
99
+ }
100
+
101
+ const readChangedTypeScriptFiles = (repositoryRoot, baseReference, headReference) => {
102
+ const diffRange = `${baseReference}...${headReference}`
103
+ const output = runGit(repositoryRoot, [
104
+ 'diff',
105
+ '--name-only',
106
+ '--diff-filter=ACMR',
107
+ diffRange,
108
+ '--',
109
+ '*.ts',
110
+ '*.tsx',
111
+ ])
112
+
113
+ return output.split('\n').filter(Boolean)
114
+ }
115
+
116
+ const readTrackedAndUntrackedTypeScriptFiles = (repositoryRoot) => {
117
+ const output = runGit(repositoryRoot, [
118
+ 'ls-files',
119
+ '--cached',
120
+ '--others',
121
+ '--exclude-standard',
122
+ '--',
123
+ '*.ts',
124
+ '*.tsx',
125
+ ])
126
+
127
+ return output.split('\n').filter(Boolean)
128
+ }
129
+
130
+ const resolveLintTargets = (repositoryRoot, baseReference, headReference, positionalArguments) => {
131
+ if (positionalArguments.length > 0) {
132
+ positionalArguments.forEach(ensureSupportedFilePath)
133
+ return positionalArguments
134
+ }
135
+
136
+ if (baseReference) {
137
+ return readChangedTypeScriptFiles(repositoryRoot, baseReference, headReference)
138
+ }
139
+
140
+ return readTrackedAndUntrackedTypeScriptFiles(repositoryRoot)
141
+ }
142
+
143
+ const ensureValidArguments = (baseReference, headReference, positionalArguments) => {
144
+ if (baseReference && positionalArguments.length > 0) {
145
+ throw new UsageError('Expected either explicit file paths or --base. Got both.')
146
+ }
147
+
148
+ if (!baseReference && headReference !== 'HEAD') {
149
+ throw new UsageError('Expected --head to be used together with --base.')
150
+ }
151
+ }
152
+
153
+ const ensureLocalDependencies = () => {
154
+ if (fs.existsSync(eslintCliPath)) {
155
+ return
156
+ }
157
+
158
+ throw new MissingLocalDependencyError(
159
+ `Expected ESLint dependencies to be installed in ${toolRepositoryRoot}. Run npm install in this repository.`,
160
+ )
161
+ }
162
+
163
+ const runEslint = (repositoryRoot, lintTargets) => {
164
+ const eslintResult = spawnSync(
165
+ process.execPath,
166
+ [eslintCliPath, '--no-config-lookup', '--no-warn-ignored', '--config', eslintConfigPath, ...lintTargets],
167
+ {
168
+ cwd: repositoryRoot,
169
+ env: {
170
+ ...process.env,
171
+ NT_SKILLZ_LINT_REPO_ROOT: repositoryRoot,
172
+ },
173
+ stdio: 'inherit',
174
+ },
175
+ )
176
+
177
+ if (eslintResult.error) {
178
+ throw new LintExecutionError(`Expected ESLint to run. Got ${eslintResult.error.message}.`)
179
+ }
180
+
181
+ if (typeof eslintResult.status === 'number') {
182
+ return eslintResult.status
183
+ }
184
+
185
+ throw new LintExecutionError(`Expected ESLint to exit with a status code. Got signal ${eslintResult.signal}.`)
186
+ }
187
+
188
+ const main = () => {
189
+ const parsedArguments = parseCommandLine()
190
+
191
+ if (parsedArguments.values.help) {
192
+ printUsage()
193
+ return 0
194
+ }
195
+
196
+ const repositoryRoot = resolveDirectory(parsedArguments.values.repo ?? process.cwd())
197
+ const baseReference = parsedArguments.values.base
198
+ const headReference = parsedArguments.values.head ?? 'HEAD'
199
+ const positionalArguments = parsedArguments.positionals
200
+
201
+ ensureValidArguments(baseReference, headReference, positionalArguments)
202
+ ensureLocalDependencies()
203
+
204
+ const lintTargets = resolveLintTargets(repositoryRoot, baseReference, headReference, positionalArguments)
205
+
206
+ if (lintTargets.length === 0) {
207
+ process.stdout.write('No TypeScript files matched.\n')
208
+ return 0
209
+ }
210
+
211
+ return runEslint(repositoryRoot, lintTargets)
212
+ }
213
+
214
+ try {
215
+ process.exitCode = main()
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : `Expected an error message. Got ${String(error)}.`
218
+ process.stderr.write(`${message}\n`)
219
+ process.exitCode = 1
220
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
5
+ exec node "$script_dir/lint-ts.mjs" "$@"
@@ -0,0 +1,275 @@
1
+ import js from '@eslint/js'
2
+ import tseslint from 'typescript-eslint'
3
+ import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs'
4
+ import importPlugin from 'eslint-plugin-import'
5
+ import sonarjs from 'eslint-plugin-sonarjs'
6
+ import stylistic from '@stylistic/eslint-plugin'
7
+ import unicorn from 'eslint-plugin-unicorn'
8
+ import vitest from '@vitest/eslint-plugin'
9
+ import noGenericNames from './no-generic-names-eslint-rule.mjs'
10
+
11
+ class MissingLintRepositoryRootError extends Error {
12
+ constructor() {
13
+ super('Expected NT_SKILLZ_LINT_REPO_ROOT environment variable.')
14
+ }
15
+ }
16
+
17
+ const lintRepositoryRoot = process.env.NT_SKILLZ_LINT_REPO_ROOT
18
+
19
+ if (!lintRepositoryRoot) {
20
+ throw new MissingLintRepositoryRootError()
21
+ }
22
+
23
+ const typescriptFiles = ['**/*.ts', '**/*.tsx']
24
+ const testFiles = ['**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx']
25
+ const thinLayerFiles = ['**/entrypoint/**/*.ts', '**/commands/**/*.ts', '**/queries/**/*.ts']
26
+ const thinLayerIgnoredFiles = ['**/*.spec.ts', '**/*.test.ts']
27
+ const ignoredPaths = [
28
+ '**/dist',
29
+ '**/out-tsc',
30
+ '**/node_modules',
31
+ '**/.nx',
32
+ '*.config.ts',
33
+ '*.config.mjs',
34
+ '*.config.js',
35
+ 'vitest.workspace.ts',
36
+ '**/*.d.ts',
37
+ '**/test-output',
38
+ '**/api/generated/**',
39
+ '**/.vitepress/cache/**',
40
+ '.riviere/**',
41
+ ]
42
+
43
+ const noLetRule = {
44
+ selector: 'VariableDeclaration[kind="let"]',
45
+ message: 'Use const. Avoid mutation.',
46
+ }
47
+
48
+ const noGenericErrorRule = {
49
+ selector: 'NewExpression[callee.name="Error"]',
50
+ message: 'Use custom precise error classes instead of generic Error or fail assertions in tests.',
51
+ }
52
+
53
+ const noEmptyStringFallbackRule = {
54
+ selector: 'LogicalExpression[operator="??"][right.type="Literal"][right.value=""]',
55
+ message:
56
+ 'Banned: `?? \'\'` violates fail-fast principle. Never use empty string fallback. Options: (1) Fail fast if value should exist, (2) Handle undefined explicitly without empty string, (3) Create a type that represents emptiness.',
57
+ }
58
+
59
+ const restrictedSyntaxRules = ['error', noLetRule, noGenericErrorRule, noEmptyStringFallbackRule]
60
+
61
+ const entrypointRestrictedSyntaxRules = [
62
+ 'error',
63
+ noLetRule,
64
+ noGenericErrorRule,
65
+ {
66
+ selector: 'FunctionDeclaration:not([parent.type="ExportNamedDeclaration"])',
67
+ message: 'Entrypoints must not define private functions. Move logic to commands/, queries/, or infra/.',
68
+ },
69
+ {
70
+ selector: 'VariableDeclarator > ArrowFunctionExpression',
71
+ message: 'Entrypoints must not define private arrow functions. Move logic to commands/, queries/, or infra/.',
72
+ },
73
+ noEmptyStringFallbackRule,
74
+ ]
75
+
76
+ const restrictedImportPatterns = [
77
+ {
78
+ group: ['*/utils/*', '*/utils', '*/utilities'],
79
+ message: 'No utils folders. Use domain-specific names.',
80
+ },
81
+ {
82
+ group: ['*/helpers/*', '*/helpers'],
83
+ message: 'No helpers folders. Use domain-specific names.',
84
+ },
85
+ {
86
+ group: ['*/common/*', '*/common'],
87
+ message: 'No common folders. Use domain-specific names.',
88
+ },
89
+ {
90
+ group: ['*/shared/*', '*/shared'],
91
+ message: 'No shared folders. Use domain-specific names.',
92
+ },
93
+ {
94
+ group: ['*/core/*', '*/core'],
95
+ message: 'No core folders. Use domain-specific names.',
96
+ },
97
+ {
98
+ group: ['*/src/lib/*', '*/src/lib', './lib/*', './lib', '../lib/*', '../lib'],
99
+ message: 'No lib folders in projects. Use domain-specific names.',
100
+ },
101
+ ]
102
+
103
+ const namingConventionRules = [
104
+ 'error',
105
+ {
106
+ selector: 'variable',
107
+ format: ['camelCase'],
108
+ },
109
+ {
110
+ selector: 'variable',
111
+ modifiers: ['const'],
112
+ format: ['camelCase', 'UPPER_CASE'],
113
+ },
114
+ {
115
+ selector: 'function',
116
+ format: ['camelCase', 'PascalCase'],
117
+ },
118
+ {
119
+ selector: 'parameter',
120
+ format: ['camelCase'],
121
+ leadingUnderscore: 'allow',
122
+ },
123
+ {
124
+ selector: 'typeLike',
125
+ format: ['PascalCase'],
126
+ },
127
+ {
128
+ selector: 'enumMember',
129
+ format: ['PascalCase'],
130
+ },
131
+ {
132
+ selector: 'objectLiteralProperty',
133
+ format: null,
134
+ },
135
+ ]
136
+
137
+ export default tseslint.config(
138
+ {
139
+ files: typescriptFiles,
140
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
141
+ },
142
+ {
143
+ ignores: ignoredPaths,
144
+ },
145
+ eslintComments.recommended,
146
+ {
147
+ rules: {
148
+ '@eslint-community/eslint-comments/no-use': ['error', { allow: [] }],
149
+ },
150
+ },
151
+ sonarjs.configs.recommended,
152
+ {
153
+ rules: {
154
+ 'sonarjs/void-use': 'off',
155
+ },
156
+ },
157
+ {
158
+ files: typescriptFiles,
159
+ plugins: {
160
+ '@typescript-eslint': tseslint.plugin,
161
+ custom: {
162
+ rules: {
163
+ 'no-generic-names': noGenericNames,
164
+ },
165
+ },
166
+ import: importPlugin,
167
+ },
168
+ languageOptions: {
169
+ parser: tseslint.parser,
170
+ ecmaVersion: 2020,
171
+ sourceType: 'module',
172
+ parserOptions: {
173
+ projectService: true,
174
+ tsconfigRootDir: lintRepositoryRoot,
175
+ },
176
+ },
177
+ rules: {
178
+ 'import/extensions': ['error', 'never', { ts: 'never', tsx: 'never', js: 'never', json: 'always' }],
179
+ 'custom/no-generic-names': 'error',
180
+ 'no-warning-comments': 'off',
181
+ 'multiline-comment-style': 'off',
182
+ 'capitalized-comments': 'off',
183
+ 'no-inline-comments': 'error',
184
+ 'spaced-comment': 'off',
185
+ 'no-negated-condition': 'error',
186
+ 'no-restricted-syntax': restrictedSyntaxRules,
187
+ 'prefer-const': 'error',
188
+ 'no-var': 'error',
189
+ '@typescript-eslint/no-explicit-any': 'error',
190
+ '@typescript-eslint/no-unsafe-assignment': 'error',
191
+ '@typescript-eslint/no-unsafe-member-access': 'error',
192
+ '@typescript-eslint/no-unsafe-call': 'error',
193
+ '@typescript-eslint/no-unsafe-return': 'error',
194
+ '@typescript-eslint/consistent-type-assertions': ['error', { assertionStyle: 'never' }],
195
+ '@typescript-eslint/no-non-null-assertion': 'error',
196
+ '@typescript-eslint/prefer-includes': 'error',
197
+ '@typescript-eslint/prefer-nullish-coalescing': 'error',
198
+ '@typescript-eslint/prefer-optional-chain': 'error',
199
+ '@typescript-eslint/await-thenable': 'error',
200
+ '@typescript-eslint/no-floating-promises': 'error',
201
+ '@typescript-eslint/no-misused-promises': 'error',
202
+ 'import/no-duplicates': 'error',
203
+ 'no-restricted-imports': ['error', { patterns: restrictedImportPatterns }],
204
+ 'max-lines': ['error', { max: 400, skipBlankLines: true, skipComments: true }],
205
+ 'max-depth': ['error', 3],
206
+ complexity: ['error', 12],
207
+ 'no-restricted-globals': [
208
+ 'error',
209
+ {
210
+ name: '__dirname',
211
+ message: 'Use dirname(fileURLToPath(import.meta.url)) in ESM.',
212
+ },
213
+ {
214
+ name: '__filename',
215
+ message: 'Use fileURLToPath(import.meta.url) in ESM.',
216
+ },
217
+ ],
218
+ '@typescript-eslint/naming-convention': namingConventionRules,
219
+ },
220
+ },
221
+ {
222
+ files: typescriptFiles,
223
+ plugins: {
224
+ '@stylistic': stylistic,
225
+ },
226
+ rules: {
227
+ '@stylistic/indent': ['error', 2],
228
+ '@stylistic/object-curly-newline': ['error', { multiline: true, minProperties: 2 }],
229
+ '@stylistic/object-property-newline': ['error', { allowAllPropertiesOnSameLine: false }],
230
+ },
231
+ },
232
+ {
233
+ files: thinLayerFiles,
234
+ ignores: thinLayerIgnoredFiles,
235
+ rules: {
236
+ 'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }],
237
+ },
238
+ },
239
+ {
240
+ files: ['**/entrypoint/**/*.ts'],
241
+ ignores: thinLayerIgnoredFiles,
242
+ rules: {
243
+ 'no-restricted-syntax': entrypointRestrictedSyntaxRules,
244
+ },
245
+ },
246
+ {
247
+ files: typescriptFiles,
248
+ plugins: {
249
+ unicorn,
250
+ },
251
+ rules: {
252
+ 'unicorn/prefer-string-replace-all': 'error',
253
+ 'unicorn/prefer-type-error': 'error',
254
+ },
255
+ },
256
+ {
257
+ files: testFiles,
258
+ plugins: {
259
+ vitest,
260
+ },
261
+ rules: {
262
+ 'vitest/no-conditional-expect': 'error',
263
+ 'vitest/no-conditional-in-test': 'error',
264
+ 'vitest/prefer-strict-equal': 'error',
265
+ 'vitest/consistent-test-it': ['error', { fn: 'it' }],
266
+ 'vitest/consistent-test-filename': ['error', { pattern: '.*\\.spec\\.[tj]sx?$' }],
267
+ 'vitest/max-expects': ['error', { max: 4 }],
268
+ 'vitest/prefer-called-with': 'error',
269
+ 'vitest/prefer-to-have-length': 'error',
270
+ 'vitest/require-to-throw-message': 'error',
271
+ 'vitest/prefer-spy-on': 'error',
272
+ '@typescript-eslint/no-unsafe-assignment': 'off',
273
+ },
274
+ },
275
+ )
@@ -0,0 +1,100 @@
1
+ import path from 'node:path'
2
+
3
+ const forbiddenWordSuggestions = {
4
+ utils: 'Use a domain-specific name that describes what it does.',
5
+ helpers: 'Use a purpose-specific name or fixtures for test data.',
6
+ helper: 'Use a purpose-specific name or fixtures for test data.',
7
+ service: 'Name it for the domain action it performs.',
8
+ services: 'Name it for the domain action it performs.',
9
+ manager: 'Name it for the responsibility it owns.',
10
+ managers: 'Name it for the responsibility it owns.',
11
+ processor: 'Name it for the domain work it performs.',
12
+ processors: 'Name it for the domain work it performs.',
13
+ data: 'Name it for the domain concept it represents.',
14
+ }
15
+
16
+ const forbiddenWords = Object.keys(forbiddenWordSuggestions)
17
+ const forbiddenFilenamePattern = new RegExp(
18
+ `(^|/|-|[a-z])(${forbiddenWords.join('|')})(-|[.]ts$|[.]tsx$|/|$)`,
19
+ 'i',
20
+ )
21
+
22
+ const findForbiddenWord = (text) => {
23
+ const lowercaseText = text.toLowerCase()
24
+ return forbiddenWords.find((forbiddenWord) => lowercaseText.includes(forbiddenWord))
25
+ }
26
+
27
+ const isForbiddenName = (name) => {
28
+ if (!name) {
29
+ return false
30
+ }
31
+
32
+ const lowercaseName = name.toLowerCase()
33
+ return forbiddenWords.some((forbiddenWord) => {
34
+ return lowercaseName === forbiddenWord || lowercaseName.startsWith(forbiddenWord) || lowercaseName.endsWith(forbiddenWord)
35
+ })
36
+ }
37
+
38
+ const createFilenameMessage = (filename) => {
39
+ const forbiddenWord = findForbiddenWord(filename)
40
+
41
+ if (!forbiddenWord) {
42
+ return 'Generic filename. Use domain-specific naming.'
43
+ }
44
+
45
+ const guidance = forbiddenWordSuggestions[forbiddenWord]
46
+ return `Generic word "${forbiddenWord}" in filename. ${guidance}`
47
+ }
48
+
49
+ const createClassMessage = (className) => {
50
+ const forbiddenWord = findForbiddenWord(className)
51
+
52
+ if (!forbiddenWord) {
53
+ return `Generic class name "${className}". Use domain-specific naming.`
54
+ }
55
+
56
+ const guidance = forbiddenWordSuggestions[forbiddenWord]
57
+ return `Generic word "${forbiddenWord}" in class "${className}". ${guidance}`
58
+ }
59
+
60
+ const noGenericNames = {
61
+ meta: {
62
+ type: 'problem',
63
+ docs: {
64
+ description: 'Forbid generic names in filenames and class names.',
65
+ recommended: true,
66
+ },
67
+ },
68
+ create(context) {
69
+ const filename = path.basename(context.getFilename() || '')
70
+
71
+ return {
72
+ ClassDeclaration(node) {
73
+ if (!node.id) {
74
+ return
75
+ }
76
+
77
+ if (!isForbiddenName(node.id.name)) {
78
+ return
79
+ }
80
+
81
+ context.report({
82
+ node: node.id,
83
+ message: createClassMessage(node.id.name),
84
+ })
85
+ },
86
+ Program(node) {
87
+ if (!forbiddenFilenamePattern.test(filename)) {
88
+ return
89
+ }
90
+
91
+ context.report({
92
+ node,
93
+ message: createFilenameMessage(filename),
94
+ })
95
+ },
96
+ }
97
+ },
98
+ }
99
+
100
+ export default noGenericNames
@@ -1,7 +0,0 @@
1
- ---
2
- description: Disable idle continuation for this session
3
- ---
4
-
5
- If you are receiving this instruction, an error has happened. Report it to the user immediately.
6
-
7
- Context: this command is defined programatically and this file is just a stub.
@@ -1,7 +0,0 @@
1
- ---
2
- description: Enable idle continuation for this session
3
- ---
4
-
5
- If you are receiving this instruction, an error has happened. Report it to the user immediately.
6
-
7
- Context: this command is defined programatically and this file is just a stub.