@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,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
class VitestCoverageCommandResolutionError extends Error {
|
|
4
|
+
}
|
|
5
|
+
function formatMissingVitestCommandMessage(checkedBinaryPaths, checkedLockFilePaths) {
|
|
6
|
+
return [
|
|
7
|
+
"Expected Vitest binary in one of:",
|
|
8
|
+
...checkedBinaryPaths.map((binaryPath) => `- ${binaryPath}`),
|
|
9
|
+
"or workspace package manager lockfile in one of:",
|
|
10
|
+
...checkedLockFilePaths.map((lockFilePath) => `- ${lockFilePath}`),
|
|
11
|
+
].join("\n");
|
|
12
|
+
}
|
|
13
|
+
function collectVitestBinaryPathsFromDirectory(repositoryRoot, currentDirectory, checkedBinaryPaths) {
|
|
14
|
+
const binaryPath = path.join(currentDirectory, "node_modules", ".bin", "vitest");
|
|
15
|
+
const nextCheckedBinaryPaths = [...checkedBinaryPaths, binaryPath];
|
|
16
|
+
if (currentDirectory === repositoryRoot) {
|
|
17
|
+
return nextCheckedBinaryPaths;
|
|
18
|
+
}
|
|
19
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
20
|
+
return collectVitestBinaryPathsFromDirectory(repositoryRoot, parentDirectory, nextCheckedBinaryPaths);
|
|
21
|
+
}
|
|
22
|
+
function resolveVitestBinaryPath(checkedBinaryPaths) {
|
|
23
|
+
return checkedBinaryPaths.find((binaryPath) => fs.existsSync(binaryPath));
|
|
24
|
+
}
|
|
25
|
+
function createVitestArguments(coverageFilePath, reportsDirectory) {
|
|
26
|
+
return [
|
|
27
|
+
"--run",
|
|
28
|
+
"--coverage.enabled",
|
|
29
|
+
`--coverage.include=${coverageFilePath}`,
|
|
30
|
+
"--coverage.reporter=json-summary",
|
|
31
|
+
"--coverage.reporter=text",
|
|
32
|
+
`--coverage.reportsDirectory=${reportsDirectory}`,
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
function createDirectVitestCommandSpec(vitestBinaryPath, request) {
|
|
36
|
+
return {
|
|
37
|
+
executable: vitestBinaryPath,
|
|
38
|
+
commandArguments: createVitestArguments(request.packageRelativeFilePath, request.reportsDirectory),
|
|
39
|
+
workingDirectory: request.packageRoot,
|
|
40
|
+
coverageSummaryRoot: request.packageRoot,
|
|
41
|
+
coverageSummaryFilePath: request.packageRelativeFilePath,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function getCheckedLockFilePaths(repositoryRoot) {
|
|
45
|
+
return [
|
|
46
|
+
path.join(repositoryRoot, "yarn.lock"),
|
|
47
|
+
path.join(repositoryRoot, "pnpm-lock.yaml"),
|
|
48
|
+
path.join(repositoryRoot, "package-lock.json"),
|
|
49
|
+
path.join(repositoryRoot, "npm-shrinkwrap.json"),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
function createWorkspaceCommandSpec(executable, commandArguments, request) {
|
|
53
|
+
return {
|
|
54
|
+
executable,
|
|
55
|
+
commandArguments,
|
|
56
|
+
workingDirectory: request.repositoryRoot,
|
|
57
|
+
coverageSummaryRoot: request.repositoryRoot,
|
|
58
|
+
coverageSummaryFilePath: request.filePath,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function createWorkspaceVitestCommandSpec(request) {
|
|
62
|
+
const vitestArguments = createVitestArguments(request.filePath, request.reportsDirectory);
|
|
63
|
+
if (fs.existsSync(path.join(request.repositoryRoot, "yarn.lock"))) {
|
|
64
|
+
return createWorkspaceCommandSpec("yarn", ["vitest", ...vitestArguments], request);
|
|
65
|
+
}
|
|
66
|
+
if (fs.existsSync(path.join(request.repositoryRoot, "pnpm-lock.yaml"))) {
|
|
67
|
+
return createWorkspaceCommandSpec("pnpm", ["exec", "vitest", ...vitestArguments], request);
|
|
68
|
+
}
|
|
69
|
+
if (fs.existsSync(path.join(request.repositoryRoot, "package-lock.json")) || fs.existsSync(path.join(request.repositoryRoot, "npm-shrinkwrap.json"))) {
|
|
70
|
+
return createWorkspaceCommandSpec("npm", ["exec", "--", "vitest", ...vitestArguments], request);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export function createVitestCoverageCommandSpec(request) {
|
|
75
|
+
const checkedBinaryPaths = collectVitestBinaryPathsFromDirectory(path.resolve(request.repositoryRoot), path.resolve(request.packageRoot), []);
|
|
76
|
+
const vitestBinaryPath = resolveVitestBinaryPath(checkedBinaryPaths);
|
|
77
|
+
if (vitestBinaryPath !== undefined) {
|
|
78
|
+
return createDirectVitestCommandSpec(vitestBinaryPath, request);
|
|
79
|
+
}
|
|
80
|
+
const workspaceVitestCommandSpec = createWorkspaceVitestCommandSpec(request);
|
|
81
|
+
if (workspaceVitestCommandSpec !== undefined) {
|
|
82
|
+
return workspaceVitestCommandSpec;
|
|
83
|
+
}
|
|
84
|
+
throw new VitestCoverageCommandResolutionError(formatMissingVitestCommandMessage(checkedBinaryPaths, getCheckedLockFilePaths(request.repositoryRoot)));
|
|
85
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommandRunner } from "
|
|
1
|
+
import { type CommandRunner } from "../source-control/changed-files.js";
|
|
2
2
|
export declare const VITEST_COVERAGE_TOOL_NAME = "nt_skillz_vitest_coverage";
|
|
3
3
|
interface CoverageRequest {
|
|
4
4
|
repositoryRoot?: string;
|
|
@@ -47,21 +47,4 @@ export declare function runVitestCoverageReview(request: CoverageRequest, enviro
|
|
|
47
47
|
markdown: string;
|
|
48
48
|
results: CoverageFileResult[];
|
|
49
49
|
}>;
|
|
50
|
-
export declare const vitestCoverageTool: {
|
|
51
|
-
description: string;
|
|
52
|
-
args: {
|
|
53
|
-
mode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
54
|
-
pullRequest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
55
|
-
base: import("zod").ZodOptional<import("zod").ZodString>;
|
|
56
|
-
head: import("zod").ZodOptional<import("zod").ZodString>;
|
|
57
|
-
files: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
58
|
-
};
|
|
59
|
-
execute(args: {
|
|
60
|
-
mode?: string | undefined;
|
|
61
|
-
pullRequest?: string | undefined;
|
|
62
|
-
base?: string | undefined;
|
|
63
|
-
head?: string | undefined;
|
|
64
|
-
files?: string[] | undefined;
|
|
65
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
66
|
-
};
|
|
67
50
|
export {};
|
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { childProcessCommandRunner, resolvePullRequestChangedFiles, } from "../source-control/changed-files.js";
|
|
6
|
+
import { createVitestCoverageCommandSpec, } from "./command.js";
|
|
7
7
|
export const VITEST_COVERAGE_TOOL_NAME = "nt_skillz_vitest_coverage";
|
|
8
8
|
class CoverageUsageError extends Error {
|
|
9
9
|
}
|
|
@@ -87,24 +87,8 @@ function findPackageRootFromDirectory(repositoryRoot, directoryPath) {
|
|
|
87
87
|
}
|
|
88
88
|
return findPackageRootFromDirectory(repositoryRoot, parentDirectoryPath);
|
|
89
89
|
}
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
if (fs.existsSync(binaryPath)) {
|
|
93
|
-
return binaryPath;
|
|
94
|
-
}
|
|
95
|
-
throw new CoverageUsageError(`Expected Vitest binary at ${binaryPath}.`);
|
|
96
|
-
}
|
|
97
|
-
function runCoverageCommand(packageRoot, packageRelativeFilePath, reportsDirectory, environment) {
|
|
98
|
-
const commandResult = environment.commandRunner.run(resolveVitestBinary(packageRoot), [
|
|
99
|
-
"related",
|
|
100
|
-
packageRelativeFilePath,
|
|
101
|
-
"--run",
|
|
102
|
-
"--coverage.enabled",
|
|
103
|
-
`--coverage.include=${packageRelativeFilePath}`,
|
|
104
|
-
"--coverage.reporter=json-summary",
|
|
105
|
-
"--coverage.reporter=text",
|
|
106
|
-
`--coverage.reportsDirectory=${reportsDirectory}`,
|
|
107
|
-
], packageRoot);
|
|
90
|
+
function runCoverageCommand(commandSpec, environment) {
|
|
91
|
+
const commandResult = environment.commandRunner.run(commandSpec.executable, commandSpec.commandArguments, commandSpec.workingDirectory);
|
|
108
92
|
const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n").trim();
|
|
109
93
|
return {
|
|
110
94
|
status: commandResult.status,
|
|
@@ -179,10 +163,17 @@ function formatCoverageErrorMessage(error) {
|
|
|
179
163
|
}
|
|
180
164
|
return `Expected coverage error message. Got ${String(error)}.`;
|
|
181
165
|
}
|
|
182
|
-
function executeCoverageWithReports(filePath, packageRoot, packageRelativeFilePath, environment) {
|
|
166
|
+
function executeCoverageWithReports(repositoryRoot, filePath, packageRoot, packageRelativeFilePath, environment) {
|
|
183
167
|
const reportsDirectory = environment.temporaryDirectoryCreator(path.join(os.tmpdir(), "nt-skillz-coverage-"));
|
|
184
168
|
try {
|
|
185
|
-
const
|
|
169
|
+
const commandSpec = createVitestCoverageCommandSpec({
|
|
170
|
+
repositoryRoot,
|
|
171
|
+
filePath,
|
|
172
|
+
packageRoot,
|
|
173
|
+
packageRelativeFilePath,
|
|
174
|
+
reportsDirectory,
|
|
175
|
+
});
|
|
176
|
+
const commandResult = runCoverageCommand(commandSpec, environment);
|
|
186
177
|
if (commandResult.errorMessage) {
|
|
187
178
|
return {
|
|
188
179
|
status: "errored",
|
|
@@ -191,7 +182,7 @@ function executeCoverageWithReports(filePath, packageRoot, packageRelativeFilePa
|
|
|
191
182
|
commandOutput: commandResult.output,
|
|
192
183
|
};
|
|
193
184
|
}
|
|
194
|
-
const summary = readCoverageSummary(
|
|
185
|
+
const summary = readCoverageSummary(commandSpec.coverageSummaryRoot, reportsDirectory, commandSpec.coverageSummaryFilePath);
|
|
195
186
|
if (commandResult.status === 0 && hasCompleteCoverage(summary)) {
|
|
196
187
|
return {
|
|
197
188
|
status: "passed",
|
|
@@ -224,7 +215,7 @@ function executeFileCoverage(repositoryRoot, filePath, environment) {
|
|
|
224
215
|
const absoluteFilePath = path.resolve(repositoryRoot, filePath);
|
|
225
216
|
const packageRoot = findPackageRootFromDirectory(repositoryRoot, path.dirname(absoluteFilePath));
|
|
226
217
|
const packageRelativeFilePath = path.relative(packageRoot, absoluteFilePath);
|
|
227
|
-
return executeCoverageWithReports(filePath, packageRoot, packageRelativeFilePath, environment);
|
|
218
|
+
return executeCoverageWithReports(repositoryRoot, filePath, packageRoot, packageRelativeFilePath, environment);
|
|
228
219
|
}
|
|
229
220
|
catch (error) {
|
|
230
221
|
const message = formatCoverageErrorMessage(error);
|
|
@@ -305,31 +296,3 @@ export async function runVitestCoverageReview(request, environment = {
|
|
|
305
296
|
results,
|
|
306
297
|
};
|
|
307
298
|
}
|
|
308
|
-
export const vitestCoverageTool = tool({
|
|
309
|
-
description: "Run Vitest coverage for changed TypeScript source files.",
|
|
310
|
-
args: {
|
|
311
|
-
mode: tool.schema.string().optional().describe("Use 'pr-review' for pull request coverage."),
|
|
312
|
-
pullRequest: tool.schema.string().optional().describe("Pull request number or URL for pr-review mode."),
|
|
313
|
-
base: tool.schema.string().optional().describe("Base git reference for pr-review mode when no pull request is provided."),
|
|
314
|
-
head: tool.schema.string().optional().describe("Head git reference for pr-review mode when base is provided."),
|
|
315
|
-
files: tool.schema.array(tool.schema.string()).optional().describe("Repository-relative files for files mode."),
|
|
316
|
-
},
|
|
317
|
-
async execute(request, context) {
|
|
318
|
-
context.metadata({ title: "Vitest coverage review" });
|
|
319
|
-
const outcome = await runVitestCoverageReview({
|
|
320
|
-
repositoryRoot: context.worktree,
|
|
321
|
-
mode: request.mode,
|
|
322
|
-
pullRequest: request.pullRequest,
|
|
323
|
-
base: request.base,
|
|
324
|
-
head: request.head,
|
|
325
|
-
files: request.files,
|
|
326
|
-
});
|
|
327
|
-
return {
|
|
328
|
-
output: outcome.markdown,
|
|
329
|
-
metadata: {
|
|
330
|
-
fileCount: outcome.results.length,
|
|
331
|
-
failedCount: outcome.results.filter((result) => result.status !== "passed").length,
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
},
|
|
335
|
-
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CommandRunner } from "../source-control/changed-files.js";
|
|
2
|
+
export interface CapturedCoverageRun {
|
|
3
|
+
executable: string;
|
|
4
|
+
commandArguments: string[];
|
|
5
|
+
workingDirectory: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRepository(sourceFilePath: string): string;
|
|
8
|
+
export declare function createRepositoryWithoutVitest(sourceFilePath: string): string;
|
|
9
|
+
export declare function createRepositoryWithoutPackage(sourceFilePath: string): string;
|
|
10
|
+
export declare function createCoverageCommandRunner(percent: number, capturedRuns: CapturedCoverageRun[], commandOutput?: string): CommandRunner;
|
|
11
|
+
export declare function createPullRequestCoverageCommandRunner(changedFiles: string[], percent: number): CommandRunner;
|
|
12
|
+
export declare function createCoverageSummaryCommandRunner(summaryJson: string): CommandRunner;
|
|
13
|
+
export declare function createCoverageErrorCommandRunner(): CommandRunner;
|
|
14
|
+
export declare function installFailingVitestBinary(repositoryRoot: string): void;
|
|
15
|
+
export declare function removeDirectory(directoryPath: string): void;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function createRepository(sourceFilePath) {
|
|
5
|
+
const repositoryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nt-skillz-test-repo-"));
|
|
6
|
+
const sourceAbsolutePath = path.join(repositoryRoot, sourceFilePath);
|
|
7
|
+
const vitestBinaryPath = path.join(repositoryRoot, "node_modules", ".bin", "vitest");
|
|
8
|
+
fs.mkdirSync(path.dirname(sourceAbsolutePath), { recursive: true });
|
|
9
|
+
fs.mkdirSync(path.dirname(vitestBinaryPath), { recursive: true });
|
|
10
|
+
fs.writeFileSync(path.join(repositoryRoot, "package.json"), "{}");
|
|
11
|
+
fs.writeFileSync(sourceAbsolutePath, "export const answer = 42\n");
|
|
12
|
+
fs.writeFileSync(vitestBinaryPath, "");
|
|
13
|
+
return repositoryRoot;
|
|
14
|
+
}
|
|
15
|
+
export function createRepositoryWithoutVitest(sourceFilePath) {
|
|
16
|
+
const repositoryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nt-skillz-test-repo-"));
|
|
17
|
+
const sourceAbsolutePath = path.join(repositoryRoot, sourceFilePath);
|
|
18
|
+
fs.mkdirSync(path.dirname(sourceAbsolutePath), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(repositoryRoot, "package.json"), "{}");
|
|
20
|
+
fs.writeFileSync(sourceAbsolutePath, "export const answer = 42\n");
|
|
21
|
+
return repositoryRoot;
|
|
22
|
+
}
|
|
23
|
+
export function createRepositoryWithoutPackage(sourceFilePath) {
|
|
24
|
+
const repositoryRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nt-skillz-test-repo-"));
|
|
25
|
+
const sourceAbsolutePath = path.join(repositoryRoot, sourceFilePath);
|
|
26
|
+
fs.mkdirSync(path.dirname(sourceAbsolutePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(sourceAbsolutePath, "export const answer = 42\n");
|
|
28
|
+
return repositoryRoot;
|
|
29
|
+
}
|
|
30
|
+
function createCoverageMetric(percent) {
|
|
31
|
+
return {
|
|
32
|
+
total: 1,
|
|
33
|
+
covered: percent === 100 ? 1 : 0,
|
|
34
|
+
skipped: 0,
|
|
35
|
+
pct: percent,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function writeCoverageSummary(reportsDirectory, filePath, percent) {
|
|
39
|
+
fs.mkdirSync(reportsDirectory, { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(reportsDirectory, "coverage-summary.json"), JSON.stringify({
|
|
41
|
+
total: {
|
|
42
|
+
lines: createCoverageMetric(percent),
|
|
43
|
+
statements: createCoverageMetric(percent),
|
|
44
|
+
functions: createCoverageMetric(percent),
|
|
45
|
+
branches: createCoverageMetric(percent),
|
|
46
|
+
},
|
|
47
|
+
[filePath]: {
|
|
48
|
+
lines: createCoverageMetric(percent),
|
|
49
|
+
statements: createCoverageMetric(percent),
|
|
50
|
+
functions: createCoverageMetric(percent),
|
|
51
|
+
branches: createCoverageMetric(percent),
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function createCoverageCommandRunner(percent, capturedRuns, commandOutput = "coverage output") {
|
|
56
|
+
return {
|
|
57
|
+
run(executable, commandArguments, workingDirectory) {
|
|
58
|
+
capturedRuns.push({
|
|
59
|
+
executable,
|
|
60
|
+
commandArguments,
|
|
61
|
+
workingDirectory,
|
|
62
|
+
});
|
|
63
|
+
const reportsDirectoryArgument = commandArguments.find((commandArgument) => commandArgument.startsWith("--coverage.reportsDirectory="));
|
|
64
|
+
if (reportsDirectoryArgument === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
status: 1,
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: "missing reports directory",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const reportsDirectory = reportsDirectoryArgument.replace("--coverage.reportsDirectory=", "");
|
|
72
|
+
const coverageIncludeArgument = commandArguments.find((commandArgument) => commandArgument.startsWith("--coverage.include="));
|
|
73
|
+
if (coverageIncludeArgument === undefined) {
|
|
74
|
+
return {
|
|
75
|
+
status: 1,
|
|
76
|
+
stdout: "",
|
|
77
|
+
stderr: "missing coverage include",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const coveredFilePath = coverageIncludeArgument.replace("--coverage.include=", "");
|
|
81
|
+
writeCoverageSummary(reportsDirectory, path.join(workingDirectory, coveredFilePath), percent);
|
|
82
|
+
return {
|
|
83
|
+
status: percent === 100 ? 0 : 1,
|
|
84
|
+
stdout: commandOutput,
|
|
85
|
+
stderr: "",
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function createPullRequestCoverageCommandRunner(changedFiles, percent) {
|
|
91
|
+
return {
|
|
92
|
+
run(executable, commandArguments, workingDirectory) {
|
|
93
|
+
if (executable === "gh") {
|
|
94
|
+
return {
|
|
95
|
+
status: 0,
|
|
96
|
+
stdout: changedFiles.join("\n"),
|
|
97
|
+
stderr: "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return createCoverageCommandRunner(percent, []).run(executable, commandArguments, workingDirectory);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export function createCoverageSummaryCommandRunner(summaryJson) {
|
|
105
|
+
return {
|
|
106
|
+
run(_executable, commandArguments) {
|
|
107
|
+
const reportsDirectoryArgument = commandArguments.find((commandArgument) => commandArgument.startsWith("--coverage.reportsDirectory="));
|
|
108
|
+
if (reportsDirectoryArgument !== undefined) {
|
|
109
|
+
const reportsDirectory = reportsDirectoryArgument.replace("--coverage.reportsDirectory=", "");
|
|
110
|
+
fs.mkdirSync(reportsDirectory, { recursive: true });
|
|
111
|
+
fs.writeFileSync(path.join(reportsDirectory, "coverage-summary.json"), summaryJson);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
status: 0,
|
|
115
|
+
stdout: "",
|
|
116
|
+
stderr: "",
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function createCoverageErrorCommandRunner() {
|
|
122
|
+
return {
|
|
123
|
+
run() {
|
|
124
|
+
return {
|
|
125
|
+
status: null,
|
|
126
|
+
stdout: "coverage stdout",
|
|
127
|
+
stderr: "coverage stderr",
|
|
128
|
+
errorMessage: "spawn failed",
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function installFailingVitestBinary(repositoryRoot) {
|
|
134
|
+
const vitestBinaryPath = path.join(repositoryRoot, "node_modules", ".bin", "vitest");
|
|
135
|
+
const scriptContent = [
|
|
136
|
+
"#!/usr/bin/env node",
|
|
137
|
+
"const fs = require('node:fs')",
|
|
138
|
+
"const path = require('node:path')",
|
|
139
|
+
"const filePath = path.join(process.cwd(), process.argv[3])",
|
|
140
|
+
"const reportsArgument = process.argv.find((argument) => argument.startsWith('--coverage.reportsDirectory='))",
|
|
141
|
+
"const reportsDirectory = reportsArgument.replace('--coverage.reportsDirectory=', '')",
|
|
142
|
+
"const metric = { total: 1, covered: 0, skipped: 0, pct: 50 }",
|
|
143
|
+
"fs.mkdirSync(reportsDirectory, { recursive: true })",
|
|
144
|
+
"fs.writeFileSync(path.join(reportsDirectory, 'coverage-summary.json'), JSON.stringify({ total: { lines: metric, statements: metric, functions: metric, branches: metric }, [filePath]: { lines: metric, statements: metric, functions: metric, branches: metric } }))",
|
|
145
|
+
"process.stdout.write('tool coverage output')",
|
|
146
|
+
"process.exit(1)",
|
|
147
|
+
].join("\n");
|
|
148
|
+
fs.rmSync(vitestBinaryPath, { force: true });
|
|
149
|
+
fs.writeFileSync(vitestBinaryPath, scriptContent, { mode: 0o755 });
|
|
150
|
+
}
|
|
151
|
+
export function removeDirectory(directoryPath) {
|
|
152
|
+
fs.rmSync(directoryPath, {
|
|
153
|
+
recursive: true,
|
|
154
|
+
force: true,
|
|
155
|
+
});
|
|
156
|
+
}
|
package/dist/tools/lint.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin";
|
|
1
2
|
interface PortableLintRequest {
|
|
2
3
|
repositoryRoot?: string;
|
|
3
4
|
files?: string[];
|
|
@@ -9,23 +10,8 @@ interface PortableLintOutcome {
|
|
|
9
10
|
output: string;
|
|
10
11
|
}
|
|
11
12
|
export declare const LINT_TOOL_NAME = "nt_skillz_lint";
|
|
13
|
+
export declare function prependLintFailureGuidance(formattedOutput: string, errorCount: number, lintFailureGuidance: string): string;
|
|
12
14
|
export declare function runPortableLint(request: PortableLintRequest): Promise<PortableLintOutcome>;
|
|
13
15
|
export declare function runPortableLintFromCommandLine(commandLineArguments: string[]): Promise<number>;
|
|
14
|
-
export declare const lintTool:
|
|
15
|
-
description: string;
|
|
16
|
-
args: {
|
|
17
|
-
mode: import("zod").ZodOptional<import("zod").ZodString>;
|
|
18
|
-
pullRequest: import("zod").ZodOptional<import("zod").ZodString>;
|
|
19
|
-
files: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
20
|
-
base: import("zod").ZodOptional<import("zod").ZodString>;
|
|
21
|
-
head: import("zod").ZodOptional<import("zod").ZodString>;
|
|
22
|
-
};
|
|
23
|
-
execute(args: {
|
|
24
|
-
mode?: string | undefined;
|
|
25
|
-
pullRequest?: string | undefined;
|
|
26
|
-
files?: string[] | undefined;
|
|
27
|
-
base?: string | undefined;
|
|
28
|
-
head?: string | undefined;
|
|
29
|
-
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
30
|
-
};
|
|
16
|
+
export declare const lintTool: ToolDefinition;
|
|
31
17
|
export {};
|
package/dist/tools/lint.js
CHANGED
|
@@ -5,9 +5,10 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import { ESLint } from "eslint";
|
|
8
|
-
import { tool } from "@opencode-ai/plugin";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { tool, } from "@opencode-ai/plugin";
|
|
9
|
+
import { createLintFailureGuidance } from "./infra/lint/guidance.js";
|
|
10
|
+
import { childProcessCommandRunner } from "./infra/source-control/changed-files.js";
|
|
11
|
+
import { runPrReviewLint } from "./infra/lint/review.js";
|
|
11
12
|
class UsageError extends Error {
|
|
12
13
|
constructor(message) {
|
|
13
14
|
super(message);
|
|
@@ -33,6 +34,14 @@ const toolDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
|
33
34
|
const toolRepositoryRoot = path.resolve(toolDirectory, "..", "..");
|
|
34
35
|
const eslintConfigPath = path.join(toolRepositoryRoot, "scripts", "living-architecture-eslint.config.mjs");
|
|
35
36
|
const gitBinaryPath = "/usr/bin/git";
|
|
37
|
+
const helpText = [
|
|
38
|
+
"Usage: ./scripts/lint-ts.sh [--repo PATH] [--base REF] [--head REF] [file ...]",
|
|
39
|
+
"",
|
|
40
|
+
"Examples:",
|
|
41
|
+
" ./scripts/lint-ts.sh --repo ../living-architecture --base origin/main",
|
|
42
|
+
" ./scripts/lint-ts.sh --repo ../living-architecture packages/example/src/example.ts",
|
|
43
|
+
" ./scripts/lint-ts.sh src/example.ts",
|
|
44
|
+
].join("\n");
|
|
36
45
|
export const LINT_TOOL_NAME = "nt_skillz_lint";
|
|
37
46
|
function normalizeOptionalText(value) {
|
|
38
47
|
if (typeof value !== "string") {
|
|
@@ -89,8 +98,8 @@ function runGitCommand(repositoryRoot, gitArguments) {
|
|
|
89
98
|
if (gitResult.status === 0) {
|
|
90
99
|
return gitResult.stdout;
|
|
91
100
|
}
|
|
92
|
-
const
|
|
93
|
-
throw new GitCommandError(`Expected git command to succeed. Got ${
|
|
101
|
+
const failureOutput = gitResult.stderr.trim() || gitResult.stdout.trim() || `exit status ${gitResult.status}`;
|
|
102
|
+
throw new GitCommandError(`Expected git command to succeed. Got ${failureOutput}.`);
|
|
94
103
|
}
|
|
95
104
|
function readChangedTypeScriptFiles(repositoryRoot, baseReference, headReference) {
|
|
96
105
|
const diffRange = `${baseReference}...${headReference}`;
|
|
@@ -146,6 +155,15 @@ function createLintTitle(filePaths, baseReference) {
|
|
|
146
155
|
function removeAnsiEscapeSequences(value) {
|
|
147
156
|
return value.replaceAll(ansiEscapeSequencePattern, "");
|
|
148
157
|
}
|
|
158
|
+
export function prependLintFailureGuidance(formattedOutput, errorCount, lintFailureGuidance) {
|
|
159
|
+
if (errorCount === 0) {
|
|
160
|
+
return formattedOutput;
|
|
161
|
+
}
|
|
162
|
+
if (!formattedOutput) {
|
|
163
|
+
return lintFailureGuidance;
|
|
164
|
+
}
|
|
165
|
+
return [lintFailureGuidance, "", formattedOutput].join("\n");
|
|
166
|
+
}
|
|
149
167
|
async function runEslint(repositoryRoot, lintTargets) {
|
|
150
168
|
const previousLintRepositoryRoot = process.env.NT_SKILLZ_LINT_REPO_ROOT;
|
|
151
169
|
process.env.NT_SKILLZ_LINT_REPO_ROOT = repositoryRoot;
|
|
@@ -162,7 +180,7 @@ async function runEslint(repositoryRoot, lintTargets) {
|
|
|
162
180
|
const errorCount = lintResults.reduce((count, lintResult) => count + lintResult.errorCount + lintResult.fatalErrorCount, 0);
|
|
163
181
|
return {
|
|
164
182
|
exitCode: errorCount > 0 ? 1 : 0,
|
|
165
|
-
output: formattedOutput,
|
|
183
|
+
output: prependLintFailureGuidance(formattedOutput, errorCount, createLintFailureGuidance(lintResults)),
|
|
166
184
|
};
|
|
167
185
|
}
|
|
168
186
|
finally {
|
|
@@ -188,17 +206,6 @@ function parsePortableLintCommandLine(commandLineArguments) {
|
|
|
188
206
|
},
|
|
189
207
|
allowPositionals: true,
|
|
190
208
|
});
|
|
191
|
-
if (parsedArguments.values.help) {
|
|
192
|
-
process.stdout.write([
|
|
193
|
-
"Usage: ./scripts/lint-ts.sh [--repo PATH] [--base REF] [--head REF] [file ...]",
|
|
194
|
-
"",
|
|
195
|
-
"Examples:",
|
|
196
|
-
" ./scripts/lint-ts.sh --repo ../living-architecture --base origin/main",
|
|
197
|
-
" ./scripts/lint-ts.sh --repo ../living-architecture packages/example/src/example.ts",
|
|
198
|
-
" ./scripts/lint-ts.sh src/example.ts",
|
|
199
|
-
].join("\n") + "\n");
|
|
200
|
-
process.exit(0);
|
|
201
|
-
}
|
|
202
209
|
return {
|
|
203
210
|
repositoryRoot: resolveDirectory(parsedArguments.values.repo ?? process.cwd()),
|
|
204
211
|
files: normalizeFilePaths(parsedArguments.positionals),
|
|
@@ -223,6 +230,10 @@ export async function runPortableLint(request) {
|
|
|
223
230
|
return runEslint(repositoryRoot, lintTargets);
|
|
224
231
|
}
|
|
225
232
|
export async function runPortableLintFromCommandLine(commandLineArguments) {
|
|
233
|
+
if (commandLineArguments.includes("--help") || commandLineArguments.includes("-h")) {
|
|
234
|
+
process.stdout.write(`${helpText}\n`);
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
226
237
|
const request = parsePortableLintCommandLine(commandLineArguments);
|
|
227
238
|
const outcome = await runPortableLint(request);
|
|
228
239
|
if (outcome.output) {
|
|
@@ -267,7 +278,7 @@ export const lintTool = tool({
|
|
|
267
278
|
head: headReference,
|
|
268
279
|
});
|
|
269
280
|
if (outcome.exitCode !== 0) {
|
|
270
|
-
throw new LintExecutionError(outcome.output
|
|
281
|
+
throw new LintExecutionError(outcome.output);
|
|
271
282
|
}
|
|
272
283
|
return {
|
|
273
284
|
output: outcome.output || "Lint passed.",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import { PULL_REQUEST_FEEDBACK_TOOL_NAME, type PullRequestFeedbackDependencies } from "./infra/pull-request/feedback.js";
|
|
3
|
+
export declare function createPullRequestFeedbackTool(dependencies?: PullRequestFeedbackDependencies): ToolDefinition;
|
|
4
|
+
export declare const pullRequestFeedbackTool: ToolDefinition;
|
|
5
|
+
export { PULL_REQUEST_FEEDBACK_TOOL_NAME };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { tool, } from "@opencode-ai/plugin";
|
|
2
|
+
import { PULL_REQUEST_FEEDBACK_TOOL_NAME, readPullRequestFeedback, } from "./infra/pull-request/feedback.js";
|
|
3
|
+
export function createPullRequestFeedbackTool(dependencies = {}) {
|
|
4
|
+
return tool({
|
|
5
|
+
description: "Fetch unresolved GitHub pull request review feedback with diff hunks and local code excerpts.",
|
|
6
|
+
args: {
|
|
7
|
+
pullRequestNumber: tool.schema.string().describe("Pull request number from GitHub."),
|
|
8
|
+
pullRequestUrl: tool.schema.string().describe("Full GitHub pull request URL."),
|
|
9
|
+
},
|
|
10
|
+
async execute(request, context) {
|
|
11
|
+
context.metadata({ title: "Fetch unresolved PR feedback" });
|
|
12
|
+
return {
|
|
13
|
+
output: readPullRequestFeedback({
|
|
14
|
+
repositoryRoot: context.worktree,
|
|
15
|
+
pullRequestNumber: request.pullRequestNumber,
|
|
16
|
+
pullRequestUrl: request.pullRequestUrl,
|
|
17
|
+
}, dependencies),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export const pullRequestFeedbackTool = createPullRequestFeedbackTool();
|
|
23
|
+
export { PULL_REQUEST_FEEDBACK_TOOL_NAME };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { tool, } from "@opencode-ai/plugin";
|
|
2
|
+
import { runVitestCoverageReview, VITEST_COVERAGE_TOOL_NAME, } from "./infra/vitest-coverage/review.js";
|
|
3
|
+
export const vitestCoverageTool = tool({
|
|
4
|
+
description: "Run Vitest coverage for changed TypeScript source files.",
|
|
5
|
+
args: {
|
|
6
|
+
mode: tool.schema.string().optional().describe("Use 'pr-review' for pull request coverage."),
|
|
7
|
+
pullRequest: tool.schema.string().optional().describe("Pull request number or URL for pr-review mode."),
|
|
8
|
+
base: tool.schema.string().optional().describe("Base git reference for pr-review mode when no pull request is provided."),
|
|
9
|
+
head: tool.schema.string().optional().describe("Head git reference for pr-review mode when base is provided."),
|
|
10
|
+
files: tool.schema.array(tool.schema.string()).optional().describe("Repository-relative files for files mode."),
|
|
11
|
+
},
|
|
12
|
+
async execute(request, context) {
|
|
13
|
+
context.metadata({ title: "Vitest coverage review" });
|
|
14
|
+
const outcome = await runVitestCoverageReview({
|
|
15
|
+
repositoryRoot: context.worktree,
|
|
16
|
+
mode: request.mode,
|
|
17
|
+
pullRequest: request.pullRequest,
|
|
18
|
+
base: request.base,
|
|
19
|
+
head: request.head,
|
|
20
|
+
files: request.files,
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
output: outcome.markdown,
|
|
24
|
+
metadata: {
|
|
25
|
+
fileCount: outcome.results.length,
|
|
26
|
+
failedCount: outcome.results.filter((result) => result.status !== "passed").length,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export { VITEST_COVERAGE_TOOL_NAME };
|
package/dist/types.d.ts
CHANGED
|
@@ -71,12 +71,20 @@ export interface OpencodeClient {
|
|
|
71
71
|
export interface PluginHooks {
|
|
72
72
|
config?: (config: PluginConfig) => Promise<void>;
|
|
73
73
|
tool?: Record<string, ToolDefinition>;
|
|
74
|
+
"tool.execute.before"?: (input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput) => Promise<void>;
|
|
74
75
|
"command.execute.before"?: (input: CommandExecuteBeforeInput, output: CommandExecuteBeforeOutput) => Promise<void>;
|
|
75
76
|
"experimental.chat.system.transform"?: (input: ChatSystemTransformInput, output: ChatSystemTransformOutput) => Promise<void>;
|
|
76
77
|
event?: (input: {
|
|
77
78
|
event: SessionEvent;
|
|
78
79
|
}) => Promise<void>;
|
|
79
80
|
}
|
|
81
|
+
export interface ToolExecuteBeforeInput {
|
|
82
|
+
tool: string;
|
|
83
|
+
}
|
|
84
|
+
export interface ToolExecuteBeforeOutput {
|
|
85
|
+
args: Record<string, unknown>;
|
|
86
|
+
}
|
|
80
87
|
export interface PluginInput {
|
|
81
88
|
client: OpencodeClient;
|
|
89
|
+
worktree?: string;
|
|
82
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nt-ai-lab/opencode-skillz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Bundled OpenCode commands and agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.json",
|
|
16
|
-
"lint": "npm run build && node scripts/lint-ts.mjs",
|
|
16
|
+
"lint": "npm run build && node scripts/check-tools-folder-boundary.mjs && node scripts/lint-ts.mjs",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"coverage": "vitest run --coverage",
|
|
19
19
|
"prepare": "node scripts/install-git-hooks.mjs",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"eslint-plugin-sonarjs": "^3.0.5",
|
|
39
39
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"typescript-eslint": "^8.50.1"
|
|
41
|
+
"typescript-eslint": "^8.50.1",
|
|
42
|
+
"zod": "^4.4.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^24.7.2",
|