@nt-ai-lab/opencode-skillz 0.3.15 → 0.4.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/AGENTS.md +31 -0
- package/agents/default.md +4 -0
- package/agents/tdd.md +1 -0
- package/commands/plan.md +135 -45
- package/commands/resolve-pr-feedback.md +138 -0
- package/dist/commands/dont-stop/hooks.js +4 -10
- package/dist/git-workflow-gates.d.ts +21 -0
- package/dist/git-workflow-gates.js +103 -0
- package/dist/plugin-registry/agents.js +0 -4
- package/dist/plugin-registry/commands.js +0 -2
- package/dist/plugin-registry/index.js +29 -2
- package/dist/tools/create-pr-tool.d.ts +4 -0
- package/dist/tools/create-pr-tool.js +22 -0
- package/dist/tools/infra/lint/guidance.d.ts +8 -0
- package/dist/tools/infra/lint/guidance.js +98 -0
- package/dist/tools/{lint-review.d.ts → infra/lint/review.d.ts} +1 -1
- package/dist/tools/{lint-review.js → infra/lint/review.js} +1 -1
- package/dist/tools/infra/pull-request/create-draft-pull-request.d.ts +11 -0
- package/dist/tools/infra/pull-request/create-draft-pull-request.js +115 -0
- package/dist/tools/infra/pull-request/feedback.d.ts +14 -0
- package/dist/tools/infra/pull-request/feedback.js +280 -0
- package/dist/tools/infra/vitest-coverage/command.d.ts +16 -0
- package/dist/tools/infra/vitest-coverage/command.js +85 -0
- package/dist/tools/{vitest-coverage.d.ts → infra/vitest-coverage/review.d.ts} +1 -18
- package/dist/tools/{vitest-coverage.js → infra/vitest-coverage/review.js} +15 -52
- package/dist/tools/infra/vitest-coverage/test-support.d.ts +15 -0
- package/dist/tools/infra/vitest-coverage/test-support.js +156 -0
- package/dist/tools/lint.d.ts +3 -17
- package/dist/tools/lint.js +29 -18
- package/dist/tools/pull-request-feedback-tool.d.ts +5 -0
- package/dist/tools/pull-request-feedback-tool.js +23 -0
- package/dist/tools/vitest-coverage-tool.d.ts +4 -0
- package/dist/tools/vitest-coverage-tool.js +31 -0
- package/dist/types.d.ts +8 -0
- package/package.json +4 -3
- package/scripts/check-tools-folder-boundary.mjs +78 -0
- package/scripts/install-git-hooks.mjs +42 -4
- package/scripts/lint-ts.mjs +32 -7
- package/scripts/living-architecture-eslint.config.mjs +2 -2
- package/scripts/no-generic-names-eslint-rule.mjs +2 -24
- package/scripts/no-generic-names-eslint-rule.mjs.d.ts +28 -0
- /package/dist/tools/{pull-request-files.d.ts → infra/source-control/changed-files.d.ts} +0 -0
- /package/dist/tools/{pull-request-files.js → infra/source-control/changed-files.js} +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const topLevelToolFilePattern = /^src\/tools\/[^/]+\.ts$/u
|
|
7
|
+
const testFilePattern = /\.(?:spec|test)\.ts$/u
|
|
8
|
+
const pluginImportPattern = /from\s+["']@opencode-ai\/plugin["']/u
|
|
9
|
+
const exportedToolDefinitionPattern = /export\s+(?:const|function)\s+\w+[^\n]*:\s*ToolDefinition/u
|
|
10
|
+
const toolCallPattern = /\btool\s*\(/u
|
|
11
|
+
|
|
12
|
+
const boundaryFailureMessage = 'Top-level src/tools files must export a real OpenCode ToolDefinition. Move support code to src/tools/infra/<concept>/. Renaming a support file to *-tool.ts is not valid.'
|
|
13
|
+
|
|
14
|
+
export function isTopLevelToolsTypeScriptFile(repositoryRoot, filePath) {
|
|
15
|
+
const relativeFilePath = path.relative(repositoryRoot, filePath).split(path.sep).join('/')
|
|
16
|
+
return topLevelToolFilePattern.test(relativeFilePath) && !testFilePattern.test(relativeFilePath)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasRealToolDefinition(sourceText) {
|
|
20
|
+
return pluginImportPattern.test(sourceText)
|
|
21
|
+
&& sourceText.includes('ToolDefinition')
|
|
22
|
+
&& sourceText.includes('tool')
|
|
23
|
+
&& exportedToolDefinitionPattern.test(sourceText)
|
|
24
|
+
&& toolCallPattern.test(sourceText)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readTopLevelToolsTypeScriptFiles(repositoryRoot) {
|
|
28
|
+
const toolsDirectory = path.join(repositoryRoot, 'src', 'tools')
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(toolsDirectory)) {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return fs.readdirSync(toolsDirectory, { withFileTypes: true })
|
|
35
|
+
.filter((directoryEntry) => directoryEntry.isFile())
|
|
36
|
+
.map((directoryEntry) => path.join(toolsDirectory, directoryEntry.name))
|
|
37
|
+
.filter((filePath) => isTopLevelToolsTypeScriptFile(repositoryRoot, filePath))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readToolsFolderBoundaryViolations(repositoryRoot) {
|
|
41
|
+
return readTopLevelToolsTypeScriptFiles(repositoryRoot)
|
|
42
|
+
.filter((filePath) => !hasRealToolDefinition(fs.readFileSync(filePath, 'utf8')))
|
|
43
|
+
.map((filePath) => path.relative(repositoryRoot, filePath).split(path.sep).join('/'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatToolsFolderBoundaryViolations(violations) {
|
|
47
|
+
if (violations.length === 0) {
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return [
|
|
52
|
+
boundaryFailureMessage,
|
|
53
|
+
...violations.map((violation) => `- ${violation}`),
|
|
54
|
+
].join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function runToolsFolderBoundaryCheck(repositoryRoot = process.cwd(), stderr = process.stderr) {
|
|
58
|
+
const violations = readToolsFolderBoundaryViolations(repositoryRoot)
|
|
59
|
+
const output = formatToolsFolderBoundaryViolations(violations)
|
|
60
|
+
|
|
61
|
+
if (!output) {
|
|
62
|
+
return 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stderr.write(`${output}\n`)
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runToolsFolderBoundaryScriptMain(currentScriptPath = process.argv[1]) {
|
|
70
|
+
if (currentScriptPath !== fileURLToPath(import.meta.url)) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exitCode = runToolsFolderBoundaryCheck()
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await runToolsFolderBoundaryScriptMain()
|
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const preCommitHookPath = path.join(hooksDirectory, 'pre-commit')
|
|
5
|
+
function resolveGitDirectory(repositoryRoot) {
|
|
6
|
+
const gitPath = path.resolve(repositoryRoot, '.git')
|
|
7
7
|
|
|
8
|
-
if (fs.existsSync(
|
|
8
|
+
if (!fs.existsSync(gitPath)) {
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (fs.statSync(gitPath).isDirectory()) {
|
|
13
|
+
return gitPath
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const gitFileContent = fs.readFileSync(gitPath, 'utf8').trim()
|
|
17
|
+
const gitDirectoryPrefix = 'gitdir: '
|
|
18
|
+
|
|
19
|
+
if (!gitFileContent.startsWith(gitDirectoryPrefix)) {
|
|
20
|
+
throw new Error(`Expected .git file to start with "${gitDirectoryPrefix}". Got ${gitFileContent}.`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return path.resolve(repositoryRoot, gitFileContent.slice(gitDirectoryPrefix.length))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function installGitHooks(repositoryRoot = process.cwd()) {
|
|
27
|
+
const gitDirectory = resolveGitDirectory(repositoryRoot)
|
|
28
|
+
|
|
29
|
+
if (!gitDirectory) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hooksDirectory = path.join(gitDirectory, 'hooks')
|
|
34
|
+
const preCommitHookPath = path.join(hooksDirectory, 'pre-commit')
|
|
9
35
|
fs.mkdirSync(hooksDirectory, { recursive: true })
|
|
10
36
|
fs.writeFileSync(preCommitHookPath, [
|
|
11
37
|
'#!/usr/bin/env bash',
|
|
12
38
|
'set -euo pipefail',
|
|
13
39
|
'npm run lint',
|
|
40
|
+
'npm run coverage',
|
|
14
41
|
'',
|
|
15
42
|
].join('\n'), { mode: 0o755 })
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function runInstallGitHooksScript(currentScriptPath = process.argv[1], repositoryRoot = process.cwd()) {
|
|
47
|
+
if (currentScriptPath !== fileURLToPath(import.meta.url)) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return installGitHooks(repositoryRoot)
|
|
16
52
|
}
|
|
53
|
+
|
|
54
|
+
runInstallGitHooksScript()
|
package/scripts/lint-ts.mjs
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import {
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const message = error instanceof Error ? error.message : `Expected an error message. Got ${String(error)}.`
|
|
8
|
-
process.stderr.write(`${message}\n`)
|
|
9
|
-
process.exitCode = 1
|
|
4
|
+
async function runBundledPortableLint(commandLineArguments) {
|
|
5
|
+
const lintModule = await import('../dist/tools/lint.js')
|
|
6
|
+
return lintModule.runPortableLintFromCommandLine(commandLineArguments)
|
|
10
7
|
}
|
|
8
|
+
|
|
9
|
+
export function readErrorMessage(error) {
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
return error.message
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `Expected an error message. Got ${String(error)}.`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runLintScript(commandLineArguments, stderr = process.stderr, runPortableLint = runBundledPortableLint) {
|
|
18
|
+
try {
|
|
19
|
+
return await runPortableLint(commandLineArguments)
|
|
20
|
+
} catch (error) {
|
|
21
|
+
stderr.write(`${readErrorMessage(error)}\n`)
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runLintScriptMain(currentScriptPath = process.argv[1], commandLineArguments = process.argv.slice(2), runPortableLint = runBundledPortableLint) {
|
|
27
|
+
if (currentScriptPath !== fileURLToPath(import.meta.url)) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.exitCode = await runLintScript(commandLineArguments, process.stderr, runPortableLint)
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await runLintScriptMain()
|
|
@@ -154,13 +154,13 @@ export default tseslint.config(
|
|
|
154
154
|
sourceType: 'module',
|
|
155
155
|
parserOptions: {
|
|
156
156
|
projectService: {
|
|
157
|
-
allowDefaultProject: ['
|
|
157
|
+
allowDefaultProject: ['vitest.config.ts'],
|
|
158
|
+
maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 50,
|
|
158
159
|
},
|
|
159
160
|
tsconfigRootDir: lintRepositoryRoot,
|
|
160
161
|
},
|
|
161
162
|
},
|
|
162
163
|
rules: {
|
|
163
|
-
'import/extensions': ['error', 'ignorePackages', { ts: 'never', tsx: 'never', js: 'always', json: 'always' }],
|
|
164
164
|
'custom/no-generic-names': 'error',
|
|
165
165
|
'no-warning-comments': 'off',
|
|
166
166
|
'multiline-comment-style': 'off',
|
|
@@ -35,28 +35,6 @@ const isForbiddenName = (name) => {
|
|
|
35
35
|
})
|
|
36
36
|
}
|
|
37
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
38
|
const noGenericNames = {
|
|
61
39
|
meta: {
|
|
62
40
|
type: 'problem',
|
|
@@ -80,7 +58,7 @@ const noGenericNames = {
|
|
|
80
58
|
|
|
81
59
|
context.report({
|
|
82
60
|
node: node.id,
|
|
83
|
-
message:
|
|
61
|
+
message: `Generic word "${findForbiddenWord(node.id.name)}" in class "${node.id.name}". ${forbiddenWordSuggestions[findForbiddenWord(node.id.name)]}`,
|
|
84
62
|
})
|
|
85
63
|
},
|
|
86
64
|
Program(node) {
|
|
@@ -90,7 +68,7 @@ const noGenericNames = {
|
|
|
90
68
|
|
|
91
69
|
context.report({
|
|
92
70
|
node,
|
|
93
|
-
message:
|
|
71
|
+
message: `Generic word "${findForbiddenWord(filename)}" in filename. ${forbiddenWordSuggestions[findForbiddenWord(filename)]}`,
|
|
94
72
|
})
|
|
95
73
|
},
|
|
96
74
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface NoGenericNamesRuleReport {
|
|
2
|
+
message: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface NoGenericNamesRuleContext {
|
|
6
|
+
getFilename(): string | undefined
|
|
7
|
+
report(report: NoGenericNamesRuleReport): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface NoGenericNamesRuleListener {
|
|
11
|
+
Program(node: unknown): void
|
|
12
|
+
ClassDeclaration(node: { id: { name: string } | null }): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface NoGenericNamesRule {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "problem"
|
|
18
|
+
docs: {
|
|
19
|
+
description: string
|
|
20
|
+
recommended: boolean
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
create(context: NoGenericNamesRuleContext): NoGenericNamesRuleListener
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare const noGenericNames: NoGenericNamesRule
|
|
27
|
+
|
|
28
|
+
export default noGenericNames
|
|
File without changes
|
|
File without changes
|