vendor: import codex-plugins as packages/omo-codex/{plugin,scripts,marketplace.json,MARKETPLACE.md}
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Normalize line endings: store LF in git, check out LF on every platform.
|
||||
# Required so biome's --check passes on Windows (default core.autocrlf=true).
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicit binary types
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.zip binary
|
||||
*.tgz binary
|
||||
*.gz binary
|
||||
@@ -0,0 +1,12 @@
|
||||
* @code-yeongyu
|
||||
|
||||
.github/workflows/* @code-yeongyu
|
||||
.github/dependabot.yml @code-yeongyu
|
||||
package.json @code-yeongyu
|
||||
package-lock.json @code-yeongyu
|
||||
LICENSE @code-yeongyu
|
||||
NOTICE @code-yeongyu
|
||||
README.md @code-yeongyu
|
||||
CHANGELOG.md @code-yeongyu
|
||||
.codex-plugin/plugin.json @code-yeongyu
|
||||
hooks/hooks.json @code-yeongyu
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
name: Bug Report
|
||||
description: Report broken Codex hook, MCP, or comment-checking behavior
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Include the Codex tool payload, hook output, and plugin version needed to reproduce.
|
||||
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Include exact output/errors.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: payload
|
||||
attributes:
|
||||
label: Tool payload
|
||||
description: Paste the minimal PostToolUse or MCP payload that reproduces the issue.
|
||||
render: json
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: codex-comment-checker version
|
||||
placeholder: 0.1.0
|
||||
validations:
|
||||
required: false
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
name: Feature Request
|
||||
description: Propose a Codex comment-checker hook or MCP improvement
|
||||
labels: [enhancement]
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: What workflow is blocked or awkward today?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposal
|
||||
description: What should codex-comment-checker do?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: What else could solve this?
|
||||
validations:
|
||||
required: false
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "main protection",
|
||||
"target": "branch",
|
||||
"enforcement": "active",
|
||||
"conditions": {
|
||||
"ref_name": {
|
||||
"include": ["~DEFAULT_BRANCH"],
|
||||
"exclude": []
|
||||
}
|
||||
},
|
||||
"rules": [
|
||||
{ "type": "deletion" },
|
||||
{ "type": "non_fast_forward" },
|
||||
{ "type": "required_linear_history" },
|
||||
{
|
||||
"type": "pull_request",
|
||||
"parameters": {
|
||||
"required_approving_review_count": 1,
|
||||
"dismiss_stale_reviews_on_push": true,
|
||||
"require_code_owner_review": true,
|
||||
"require_last_push_approval": false,
|
||||
"required_review_thread_resolution": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "required_status_checks",
|
||||
"parameters": {
|
||||
"strict_required_status_checks_policy": true,
|
||||
"required_status_checks": [
|
||||
{ "context": "test (ubuntu-latest · node 20)" },
|
||||
{ "context": "test (ubuntu-latest · node 22)" },
|
||||
{ "context": "test (macos-latest · node 20)" },
|
||||
{ "context": "test (macos-latest · node 22)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"bypass_actors": [
|
||||
{
|
||||
"actor_id": 5,
|
||||
"actor_type": "RepositoryRole",
|
||||
"bypass_mode": "always"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
dev-dependencies:
|
||||
dependency-type: development
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
## Summary
|
||||
|
||||
<!-- Brief description, 1-3 bullets -->
|
||||
|
||||
-
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] `npm run check` (typecheck + biome + build)
|
||||
- [ ] `npm test` (unit tests)
|
||||
- [ ] `npm pack --dry-run` (release sanity)
|
||||
- [ ] Hook smoke-tested locally with `node dist/cli.js hook post-tool-use`
|
||||
|
||||
## Codex plugin impact
|
||||
|
||||
- [ ] `.codex-plugin/plugin.json` remains valid
|
||||
- [ ] `hooks/hooks.json` still uses stable Codex hook JSON
|
||||
- [ ] No MCP server or MCP tool is exposed
|
||||
- [ ] CHANGELOG entry added for user-facing changes
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test (${{ matrix.os }} · node ${{ matrix.node }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: ["20", "22"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check
|
||||
run: npm run check
|
||||
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Package smoke
|
||||
run: npm pack --dry-run
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node 22
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Check
|
||||
run: npm run check
|
||||
|
||||
- name: Unit tests
|
||||
run: npm test
|
||||
|
||||
- name: Package smoke
|
||||
run: npm pack --dry-run
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ -z "$NODE_AUTH_TOKEN" ]; then
|
||||
echo "NODE_AUTH_TOKEN is not configured; skipping npm publish."
|
||||
exit 0
|
||||
fi
|
||||
npm publish --access public --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
coverage/
|
||||
.vitest/
|
||||
@@ -0,0 +1,35 @@
|
||||
# Repository Conventions
|
||||
|
||||
Conventions for human contributors and AI agents working on this repository.
|
||||
|
||||
## Style
|
||||
|
||||
- Terse technical prose. No emojis in commits, issues, PR comments, or code.
|
||||
- TypeScript strict mode. No `any`, no `unknown` casts where avoidable, no `@ts-ignore`, no `@ts-expect-error`, no enums.
|
||||
- ESM modules with `.js` suffix in runtime import paths.
|
||||
- Tabs for indentation. Double quotes for strings.
|
||||
- Tests use vitest with `#given .. #when .. #then` descriptions or plain `// given / // when / // then` body comments.
|
||||
|
||||
## Commands
|
||||
|
||||
- `npm install` - install dependencies.
|
||||
- `npm test` - run vitest once.
|
||||
- `npm run typecheck` - strict TypeScript check.
|
||||
- `npm run check` - type check, biome, and build.
|
||||
- `npm pack --dry-run` - release package smoke test.
|
||||
- `node dist/cli.js hook post-tool-use < fixture.json` - smoke-test the Codex hook.
|
||||
|
||||
## Constraints
|
||||
|
||||
- No Bun APIs. Runtime is Node only because Codex launches plugin hooks with Node.
|
||||
- Keep Codex `PostToolUse` hook behavior covered by tests.
|
||||
- Keep `apply_patch` extraction covered by tests.
|
||||
- `apply_patch` must support Codex `tool_input.command`, raw patch text, and OMO-compatible metadata.
|
||||
- Hook output must use the stable Codex hook JSON contract.
|
||||
- Do not expose an MCP server or MCP tool from this plugin.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- No `git add -A` or `git add .`. Stage only the files you changed.
|
||||
- No `git commit --no-verify`. No force pushes. No history rewriting on shared branches.
|
||||
- Do not couple this package back to pi, omo, or senpi internal source paths.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Restore `write`, `edit`, `multi_edit`, and `multiedit` PostToolUse coverage alongside `apply_patch`.
|
||||
- Forward Codex `transcript_path` into native comment-checker hook input when available.
|
||||
- Add package smoke coverage for portable hook entrypoints.
|
||||
|
||||
### Changed
|
||||
|
||||
- Treat the native checker binary as an optional dependency for unsupported platforms.
|
||||
- Cap child process stdout/stderr captured from the native checker.
|
||||
- Run CI on Windows in addition to Ubuntu and macOS.
|
||||
|
||||
## [0.1.1] - 2026-05-15
|
||||
|
||||
### Changed
|
||||
|
||||
- Limit automatic comment checking to successful `apply_patch` hook events.
|
||||
- Remove the `comment_check` MCP tool and MCP server configuration.
|
||||
- Update plugin metadata, docs, and contributor guidance to describe hook-only behavior.
|
||||
|
||||
## [0.1.0] - 2026-05-15
|
||||
|
||||
### Added
|
||||
|
||||
- Initial `codex-comment-checker` Codex plugin.
|
||||
- `PostToolUse` hook for `apply_patch`, `write`, `edit`, and `multiedit` style tool calls.
|
||||
- Blocking hook feedback when `comment-checker` reports warnings.
|
||||
- `comment_check` MCP tool for explicit write/edit/multiedit checks.
|
||||
- Codex plugin manifest, local MCP config, bundled skill, and GitHub repository metadata.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Yeongyu Kim
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,6 @@
|
||||
codex-comment-checker
|
||||
|
||||
This package ports the pi-comment-checker hook into a Codex plugin repository.
|
||||
|
||||
The plugin targets Codex plugin manifests and plugin-bundled lifecycle hooks.
|
||||
The checker engine is provided by @code-yeongyu/comment-checker.
|
||||
@@ -0,0 +1,90 @@
|
||||
# codex-comment-checker
|
||||
|
||||
[](https://github.com/code-yeongyu/codex-comment-checker/actions/workflows/ci.yml) [](LICENSE)
|
||||
|
||||
Codex plugin that runs [`@code-yeongyu/comment-checker`](https://github.com/code-yeongyu/go-claude-code-comment-checker) after successful edit-like `PostToolUse` hook calls.
|
||||
|
||||
## Behavior
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| `apply_patch` succeeds | parses `tool_input.command` and checks added/updated files |
|
||||
| `write`, `edit`, `multi_edit`, or `multiedit` succeeds | maps the Codex payload to the native checker hook input |
|
||||
| non-edit tool succeeds | ignored |
|
||||
| checker exits `2` | returns Codex `PostToolUse` blocking feedback so the model fixes or explains the warning |
|
||||
| checker binary missing or unavailable on the current platform | emits no hook output |
|
||||
| checker exits unexpectedly | leaves hook output unchanged |
|
||||
|
||||
Deletes are ignored because they cannot introduce new comments.
|
||||
|
||||
## Codex Plugin
|
||||
|
||||
The plugin ships:
|
||||
|
||||
- `.codex-plugin/plugin.json` for Codex plugin discovery.
|
||||
- `hooks/hooks.json` for the `PostToolUse` hook.
|
||||
- `skills/comment-checker/SKILL.md` with usage guidance.
|
||||
|
||||
The hook command is:
|
||||
|
||||
```bash
|
||||
node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use
|
||||
```
|
||||
|
||||
No MCP server or `comment_check` tool is exposed.
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run check
|
||||
npm pack --dry-run
|
||||
```
|
||||
|
||||
Smoke-test the hook:
|
||||
|
||||
```bash
|
||||
node dist/cli.js hook post-tool-use < test/fixtures/post-tool-use.json
|
||||
```
|
||||
|
||||
## Local Codex Installation
|
||||
|
||||
From the marketplace root containing this plugin:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add /path/to/codex-plugins
|
||||
node /path/to/codex-plugins/scripts/install-local.mjs /path/to/codex-plugins
|
||||
```
|
||||
|
||||
If your local Codex build exposes plugin install commands, you can install from the UI or CLI instead. For older local builds, the marketplace installer builds and copies the plugin into `~/.codex/plugins/cache/<marketplace>/omo/0.1.0`, installs runtime dependencies there, and enables:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
plugins = true
|
||||
plugin_hooks = true
|
||||
|
||||
[plugins."omo@code-yeongyu-codex-plugins"]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
## Branch Rules and Releases
|
||||
|
||||
- `main` is protected by `.github/branch-ruleset.json`.
|
||||
- CI runs Node 20 and 22 on Ubuntu, macOS, and Windows.
|
||||
- Releases are GitHub Releases tagged as `v<semver>`.
|
||||
- Publishing runs from the `publish` workflow after a GitHub Release is published.
|
||||
|
||||
## Privacy
|
||||
|
||||
This plugin runs locally. It sends hook input to the optional local `comment-checker` binary when available and does not call a network service by itself.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE).
|
||||
|
||||
## Related
|
||||
|
||||
- [pi-comment-checker](https://github.com/code-yeongyu/pi-comment-checker) - source extension this Codex plugin ports.
|
||||
- [comment-checker](https://github.com/code-yeongyu/go-claude-code-comment-checker) - native checker binary.
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noDefaultExport": "error",
|
||||
"noEnum": "error",
|
||||
"noNonNullAssertion": "error",
|
||||
"useImportType": "error",
|
||||
"useConst": "error",
|
||||
"useNodejsImportProtocol": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"useLiteralKeys": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "error",
|
||||
"noTsIgnore": "error",
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noEmptyInterface": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 3,
|
||||
"lineWidth": 120
|
||||
},
|
||||
"files": {
|
||||
"includes": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "!**/node_modules/**/*", "!**/dist/**/*"]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["vitest.config.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noDefaultExport": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "^(apply_patch|write|Write|edit|Edit|multi_edit|multiedit|MultiEdit)$",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
|
||||
"timeout": 30,
|
||||
"statusMessage": "checking comments"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@code-yeongyu/codex-comment-checker",
|
||||
"version": "0.1.1",
|
||||
"description": "Codex plugin that runs comment-checker after edit-like PostToolUse hooks.",
|
||||
"type": "module",
|
||||
"packageManager": "npm@11.12.1",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/code-yeongyu/codex-comment-checker",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/code-yeongyu/codex-comment-checker.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/code-yeongyu/codex-comment-checker/issues"
|
||||
},
|
||||
"keywords": [
|
||||
"codex",
|
||||
"codex-plugin",
|
||||
"comment-checker",
|
||||
"hooks",
|
||||
"typescript"
|
||||
],
|
||||
"bin": {
|
||||
"codex-comment-checker": "./dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"hooks",
|
||||
"skills",
|
||||
".codex-plugin",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check --write .",
|
||||
"check": "tsc --noEmit && biome check . && npm run build"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@code-yeongyu/comment-checker": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.15",
|
||||
"@types/node": "^25.7.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: comment-checker
|
||||
description: Use when Codex needs to understand or respond to automatic comment-checker feedback emitted after an edit-like PostToolUse hook.
|
||||
---
|
||||
|
||||
# Codex Comment Checker
|
||||
|
||||
The plugin registers a `PostToolUse` hook for successful `apply_patch`, `write`, `edit`, `multi_edit`, and `multiedit` calls.
|
||||
|
||||
When comment-checker reports a warning after a patch, Codex receives blocking feedback and should fix or explain the flagged comment before moving on.
|
||||
|
||||
## Scope
|
||||
|
||||
- No MCP tool is exposed.
|
||||
- Non-edit tools are ignored by this plugin.
|
||||
- Missing checker binaries emit no hook output so normal Codex work can continue.
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { runCodexHookCli } from "./codex-hook.js";
|
||||
|
||||
const [command, subcommand] = process.argv.slice(2);
|
||||
|
||||
if (command === "hook" && subcommand === "post-tool-use") {
|
||||
await runCodexHookCli();
|
||||
} else {
|
||||
process.stderr.write("Usage: codex-comment-checker hook post-tool-use\n");
|
||||
process.exitCode = 2;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { stdin as processStdin, stdout as processStdout } from "node:process";
|
||||
|
||||
import {
|
||||
type CommentCheckRequest,
|
||||
extractCommentCheckRequests,
|
||||
isRecord,
|
||||
type ToolResultContent,
|
||||
type ToolResultLike,
|
||||
toHookInput,
|
||||
} from "./core.js";
|
||||
import { type CommentCheckerRunner, runCommentChecker } from "./runner.js";
|
||||
|
||||
export type CodexPostToolUseInput = {
|
||||
session_id: string;
|
||||
turn_id: string;
|
||||
transcript_path: string | null;
|
||||
cwd: string;
|
||||
hook_event_name: "PostToolUse";
|
||||
model: string;
|
||||
permission_mode: string;
|
||||
tool_name: string;
|
||||
tool_input: Record<string, unknown>;
|
||||
tool_response: unknown;
|
||||
tool_use_id: string;
|
||||
};
|
||||
|
||||
export type CodexHookOptions = {
|
||||
run?: CommentCheckerRunner;
|
||||
};
|
||||
|
||||
export function extractCodexCommentCheckRequests(input: CodexPostToolUseInput): CommentCheckRequest[] {
|
||||
return extractCommentCheckRequests(toToolResultLike(input));
|
||||
}
|
||||
|
||||
export async function runCommentCheckerPostToolUse(
|
||||
input: CodexPostToolUseInput,
|
||||
options: CodexHookOptions = {},
|
||||
): Promise<string> {
|
||||
const requests = extractCodexCommentCheckRequests(input);
|
||||
if (requests.length === 0) return "";
|
||||
|
||||
const runner = options.run ?? runCommentChecker;
|
||||
const warnings: Array<{ filePath: string; message: string }> = [];
|
||||
|
||||
for (const request of requests) {
|
||||
const context = {
|
||||
sessionId: input.session_id,
|
||||
cwd: input.cwd,
|
||||
...(input.transcript_path === null ? {} : { transcriptPath: input.transcript_path }),
|
||||
};
|
||||
const result = await runner(toHookInput(request, context));
|
||||
if (result.status === "missing" || result.status === "pass") continue;
|
||||
if (result.status === "error") continue;
|
||||
const message = result.message.trim();
|
||||
if (message.length > 0) {
|
||||
warnings.push({ filePath: request.filePath, message });
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length === 0) return "";
|
||||
|
||||
return JSON.stringify({
|
||||
decision: "block",
|
||||
reason: formatWarnings(warnings),
|
||||
});
|
||||
}
|
||||
|
||||
export async function runCodexHookCli(): Promise<void> {
|
||||
const input = await readStdin();
|
||||
if (input.trim().length === 0) return;
|
||||
const parsed = parseCodexPostToolUseInput(input);
|
||||
if (!parsed) return;
|
||||
const output = await runCommentCheckerPostToolUse(parsed);
|
||||
if (output.length > 0) {
|
||||
processStdout.write(output);
|
||||
processStdout.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCodexPostToolUseInput(input: string): CodexPostToolUseInput | undefined {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(input);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return isCodexPostToolUseInput(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function toToolResultLike(input: CodexPostToolUseInput): ToolResultLike {
|
||||
return {
|
||||
toolName: input.tool_name,
|
||||
input: normalizeToolInput(input.tool_name, input.tool_input),
|
||||
content: normalizeToolResponse(input.tool_response),
|
||||
isError: isErrorResponse(input.tool_response),
|
||||
details: isRecord(input.tool_response) ? input.tool_response : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeToolInput(toolName: string, toolInput: Record<string, unknown>): Record<string, unknown> {
|
||||
if (toolName === "apply_patch" && typeof toolInput["command"] === "string") {
|
||||
return {
|
||||
...toolInput,
|
||||
input: toolInput["command"],
|
||||
patch: toolInput["command"],
|
||||
};
|
||||
}
|
||||
return toolInput;
|
||||
}
|
||||
|
||||
function normalizeToolResponse(toolResponse: unknown): ToolResultContent[] {
|
||||
if (typeof toolResponse === "string") {
|
||||
return [{ type: "text", text: toolResponse }];
|
||||
}
|
||||
if (isRecord(toolResponse) && typeof toolResponse["text"] === "string") {
|
||||
return [{ type: "text", text: toolResponse["text"] }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function isErrorResponse(toolResponse: unknown): boolean {
|
||||
return isRecord(toolResponse) && toolResponse["is_error"] === true;
|
||||
}
|
||||
|
||||
function formatWarnings(warnings: Array<{ filePath: string; message: string }>): string {
|
||||
return warnings
|
||||
.map((warning) => `comment-checker found issues in ${warning.filePath}:\n${warning.message}`)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function isCodexPostToolUseInput(value: unknown): value is CodexPostToolUseInput {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value["hook_event_name"] === "PostToolUse" &&
|
||||
typeof value["session_id"] === "string" &&
|
||||
typeof value["turn_id"] === "string" &&
|
||||
(typeof value["transcript_path"] === "string" || value["transcript_path"] === null) &&
|
||||
typeof value["cwd"] === "string" &&
|
||||
typeof value["model"] === "string" &&
|
||||
typeof value["permission_mode"] === "string" &&
|
||||
typeof value["tool_name"] === "string" &&
|
||||
isRecord(value["tool_input"]) &&
|
||||
typeof value["tool_use_id"] === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function readStdin(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = "";
|
||||
processStdin.setEncoding("utf-8");
|
||||
processStdin.on("data", (chunk: string) => {
|
||||
data += chunk;
|
||||
});
|
||||
processStdin.once("error", reject);
|
||||
processStdin.once("end", () => {
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
export type TextContent = {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ImageContent = {
|
||||
type: "image";
|
||||
data: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
export type CheckerToolName = "Write" | "Edit" | "MultiEdit";
|
||||
|
||||
export type CheckerEdit = {
|
||||
old_string: string;
|
||||
new_string: string;
|
||||
};
|
||||
|
||||
export type CheckerToolInput = {
|
||||
file_path: string;
|
||||
content?: string;
|
||||
old_string?: string;
|
||||
new_string?: string;
|
||||
edits?: CheckerEdit[];
|
||||
};
|
||||
|
||||
export type CommentCheckRequest = {
|
||||
sourceToolName: string;
|
||||
toolName: CheckerToolName;
|
||||
filePath: string;
|
||||
toolInput: CheckerToolInput;
|
||||
};
|
||||
|
||||
export type CommentCheckerHookInput = {
|
||||
session_id: string;
|
||||
tool_name: CheckerToolName;
|
||||
transcript_path: string;
|
||||
cwd: string;
|
||||
hook_event_name: "PostToolUse";
|
||||
tool_input: CheckerToolInput;
|
||||
};
|
||||
|
||||
export type ToolResultContent = TextContent | ImageContent;
|
||||
|
||||
export type ToolResultLike = {
|
||||
toolName: string;
|
||||
input: Record<string, unknown>;
|
||||
content?: ToolResultContent[];
|
||||
isError?: boolean;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
type ApplyPatchAccumulator = {
|
||||
operation: "add" | "delete" | "update";
|
||||
filePath: string;
|
||||
movePath?: string;
|
||||
oldLines: string[];
|
||||
newLines: string[];
|
||||
};
|
||||
|
||||
type ApplyPatchFileMetadata = {
|
||||
filePath: string;
|
||||
movePath?: string;
|
||||
before: string;
|
||||
after: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export function extractCommentCheckRequests(event: ToolResultLike): CommentCheckRequest[] {
|
||||
if (event.isError) return [];
|
||||
if (isToolFailureOutput(getContentText(event.content))) return [];
|
||||
|
||||
const toolName = event.toolName.toLowerCase();
|
||||
if (toolName === "write") return extractWriteRequest(event);
|
||||
if (toolName === "edit") return extractEditRequest(event);
|
||||
if (toolName === "multiedit" || toolName === "multi_edit") return extractMultiEditRequest(event);
|
||||
if (toolName === "apply_patch") return extractApplyPatchRequests(event);
|
||||
return [];
|
||||
}
|
||||
|
||||
export function toHookInput(
|
||||
request: CommentCheckRequest,
|
||||
context: {
|
||||
sessionId: string;
|
||||
cwd: string;
|
||||
transcriptPath?: string;
|
||||
},
|
||||
): CommentCheckerHookInput {
|
||||
return {
|
||||
session_id: context.sessionId,
|
||||
tool_name: request.toolName,
|
||||
transcript_path: context.transcriptPath ?? "",
|
||||
cwd: context.cwd,
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_input: request.toolInput,
|
||||
};
|
||||
}
|
||||
|
||||
export function isToolFailureOutput(text: string): boolean {
|
||||
const lower = text.trim().toLowerCase();
|
||||
return (
|
||||
lower.startsWith("error") ||
|
||||
lower.includes("error:") ||
|
||||
lower.includes("failed to") ||
|
||||
lower.includes("could not")
|
||||
);
|
||||
}
|
||||
|
||||
function extractWriteRequest(event: ToolResultLike): CommentCheckRequest[] {
|
||||
const filePath = getString(event.input, ["filePath", "file_path", "path"]);
|
||||
const content = getString(event.input, ["content"]);
|
||||
if (!filePath || content === undefined) return [];
|
||||
return [
|
||||
{
|
||||
sourceToolName: event.toolName,
|
||||
toolName: "Write",
|
||||
filePath,
|
||||
toolInput: {
|
||||
file_path: filePath,
|
||||
content,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function extractEditRequest(event: ToolResultLike): CommentCheckRequest[] {
|
||||
const filePath = getString(event.input, ["filePath", "file_path", "path"]);
|
||||
const oldString = getString(event.input, ["oldString", "old_string"]);
|
||||
const newString = getString(event.input, ["newString", "new_string"]);
|
||||
if (!filePath || oldString === undefined || newString === undefined) return [];
|
||||
const toolInput: CheckerToolInput = { file_path: filePath };
|
||||
toolInput.old_string = oldString;
|
||||
toolInput.new_string = newString;
|
||||
return [
|
||||
{
|
||||
sourceToolName: event.toolName,
|
||||
toolName: "Edit",
|
||||
filePath,
|
||||
toolInput,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function extractMultiEditRequest(event: ToolResultLike): CommentCheckRequest[] {
|
||||
const filePath = getString(event.input, ["filePath", "file_path", "path"]);
|
||||
const edits = getEdits(event.input["edits"]);
|
||||
if (!filePath || edits.length === 0) return [];
|
||||
return [
|
||||
{
|
||||
sourceToolName: event.toolName,
|
||||
toolName: "MultiEdit",
|
||||
filePath,
|
||||
toolInput: {
|
||||
file_path: filePath,
|
||||
edits,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function extractApplyPatchRequests(event: ToolResultLike): CommentCheckRequest[] {
|
||||
const metadataRequests = extractApplyPatchMetadataRequests(event.details, event.toolName);
|
||||
if (metadataRequests.length > 0) return metadataRequests;
|
||||
|
||||
const patch = getString(event.input, ["input", "patch", "command"]);
|
||||
if (!patch) return [];
|
||||
return parseApplyPatchRequests(patch, event.toolName);
|
||||
}
|
||||
|
||||
function extractApplyPatchMetadataRequests(details: unknown, sourceToolName: string): CommentCheckRequest[] {
|
||||
const metadataFiles = getApplyPatchMetadataFiles(details);
|
||||
if (metadataFiles.length === 0) return [];
|
||||
|
||||
const requests: CommentCheckRequest[] = [];
|
||||
for (const file of metadataFiles) {
|
||||
if (file.type === "delete") continue;
|
||||
const filePath = file.movePath ?? file.filePath;
|
||||
if (file.before.length === 0) {
|
||||
requests.push({
|
||||
sourceToolName,
|
||||
toolName: "Write",
|
||||
filePath,
|
||||
toolInput: {
|
||||
file_path: filePath,
|
||||
content: file.after,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
requests.push({
|
||||
sourceToolName,
|
||||
toolName: "Edit",
|
||||
filePath,
|
||||
toolInput: {
|
||||
file_path: filePath,
|
||||
old_string: file.before,
|
||||
new_string: file.after,
|
||||
},
|
||||
});
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
|
||||
function getApplyPatchMetadataFiles(details: unknown): ApplyPatchFileMetadata[] {
|
||||
if (!isRecord(details)) return [];
|
||||
const direct = readApplyPatchMetadataFiles(details["files"]);
|
||||
if (direct.length > 0) return direct;
|
||||
const resultDetails = details["result"];
|
||||
const result = isRecord(resultDetails) ? readApplyPatchMetadataFiles(resultDetails["files"]) : [];
|
||||
if (result.length > 0) return result;
|
||||
const metadataDetails = details["metadata"];
|
||||
const metadata = isRecord(metadataDetails) ? readApplyPatchMetadataFiles(metadataDetails["files"]) : [];
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function readApplyPatchMetadataFiles(value: unknown): ApplyPatchFileMetadata[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const files: ApplyPatchFileMetadata[] = [];
|
||||
for (const item of value) {
|
||||
if (!isRecord(item)) continue;
|
||||
const filePath = getString(item, ["filePath", "file_path", "path"]);
|
||||
const movePath = getString(item, ["movePath", "move_path"]);
|
||||
const before = getString(item, ["before", "old", "oldString", "old_string"]);
|
||||
const after = getString(item, ["after", "new", "newString", "new_string"]);
|
||||
const type = getString(item, ["type", "operation"]);
|
||||
if (!filePath || before === undefined || after === undefined) continue;
|
||||
files.push({
|
||||
filePath,
|
||||
before,
|
||||
after,
|
||||
...(movePath === undefined ? {} : { movePath }),
|
||||
...(type === undefined ? {} : { type }),
|
||||
});
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export function parseApplyPatchRequests(patch: string, sourceToolName = "apply_patch"): CommentCheckRequest[] {
|
||||
const requests: CommentCheckRequest[] = [];
|
||||
let current: ApplyPatchAccumulator | undefined;
|
||||
|
||||
const flush = (): void => {
|
||||
if (!current) return;
|
||||
if (current.operation === "add") {
|
||||
const content = joinPatchLines(current.newLines);
|
||||
if (content.length > 0) {
|
||||
requests.push({
|
||||
sourceToolName,
|
||||
toolName: "Write",
|
||||
filePath: current.filePath,
|
||||
toolInput: {
|
||||
file_path: current.filePath,
|
||||
content,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (current.operation === "update") {
|
||||
const newString = joinPatchLines(current.newLines);
|
||||
if (newString.length > 0) {
|
||||
const filePath = current.movePath ?? current.filePath;
|
||||
requests.push({
|
||||
sourceToolName,
|
||||
toolName: "Edit",
|
||||
filePath,
|
||||
toolInput: {
|
||||
file_path: filePath,
|
||||
old_string: joinPatchLines(current.oldLines),
|
||||
new_string: newString,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
current = undefined;
|
||||
};
|
||||
|
||||
for (const line of patch.split(/\r?\n/)) {
|
||||
if (line === "*** Begin Patch" || line === "*** End Patch") continue;
|
||||
if (line.startsWith("*** Add File: ")) {
|
||||
flush();
|
||||
current = makeAccumulator("add", line.slice("*** Add File: ".length).trim());
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("*** Update File: ")) {
|
||||
flush();
|
||||
current = makeAccumulator("update", line.slice("*** Update File: ".length).trim());
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("*** Delete File: ")) {
|
||||
flush();
|
||||
current = makeAccumulator("delete", line.slice("*** Delete File: ".length).trim());
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith("*** Move to: ")) {
|
||||
if (current?.operation === "update") current.movePath = line.slice("*** Move to: ".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (!current) continue;
|
||||
if (line.startsWith("@@")) continue;
|
||||
if (current.operation === "add") {
|
||||
if (line.startsWith("+")) current.newLines.push(line.slice(1));
|
||||
continue;
|
||||
}
|
||||
if (current.operation === "update") {
|
||||
if (line.startsWith("+")) current.newLines.push(line.slice(1));
|
||||
if (line.startsWith("-")) current.oldLines.push(line.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
return requests;
|
||||
}
|
||||
|
||||
function makeAccumulator(operation: ApplyPatchAccumulator["operation"], filePath: string): ApplyPatchAccumulator {
|
||||
return {
|
||||
operation,
|
||||
filePath,
|
||||
oldLines: [],
|
||||
newLines: [],
|
||||
};
|
||||
}
|
||||
|
||||
function getEdits(value: unknown): CheckerEdit[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const edits: CheckerEdit[] = [];
|
||||
for (const item of value) {
|
||||
if (!isRecord(item)) continue;
|
||||
const oldString = getString(item, ["oldString", "old_string"]);
|
||||
const newString = getString(item, ["newString", "new_string"]);
|
||||
if (oldString === undefined || newString === undefined) continue;
|
||||
edits.push({
|
||||
old_string: oldString,
|
||||
new_string: newString,
|
||||
});
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
function getContentText(content: ToolResultContent[] | undefined): string {
|
||||
if (!content) return "";
|
||||
return content
|
||||
.filter((block): block is TextContent => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function getString(input: Record<string, unknown>, keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
if (typeof value === "string") return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function joinPatchLines(lines: string[]): string {
|
||||
return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import type { CommentCheckerHookInput } from "./core.js";
|
||||
|
||||
export type ProcessResult = {
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
export const MAX_PROCESS_OUTPUT_BYTES = 64 * 1024;
|
||||
|
||||
export type ProcessExecutor = (command: string, args: string[], stdin: string) => Promise<ProcessResult>;
|
||||
|
||||
export type RunCommentCheckerOptions = {
|
||||
binaryPath?: string;
|
||||
customPrompt?: string;
|
||||
resolveBinary?: () => string | undefined;
|
||||
executor?: ProcessExecutor;
|
||||
};
|
||||
|
||||
export type CommentCheckerRunResult = {
|
||||
status: "pass" | "warning" | "error" | "missing";
|
||||
message: string;
|
||||
binaryPath?: string;
|
||||
exitCode?: number | null;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
};
|
||||
|
||||
export type CommentCheckerRunner = (input: CommentCheckerHookInput) => Promise<CommentCheckerRunResult>;
|
||||
|
||||
export async function runCommentChecker(
|
||||
input: CommentCheckerHookInput,
|
||||
options: RunCommentCheckerOptions = {},
|
||||
): Promise<CommentCheckerRunResult> {
|
||||
const binaryPath =
|
||||
options.binaryPath ?? (options.resolveBinary ? options.resolveBinary() : resolveCommentCheckerBinary());
|
||||
if (!binaryPath) {
|
||||
return {
|
||||
status: "missing",
|
||||
message: "comment-checker binary not found. Run npm install for the codex-comment-checker plugin.",
|
||||
};
|
||||
}
|
||||
|
||||
const args = ["check"];
|
||||
if (options.customPrompt) {
|
||||
args.push("--prompt", options.customPrompt);
|
||||
}
|
||||
|
||||
const executor = options.executor ?? spawnProcess;
|
||||
const result = await executor(binaryPath, args, JSON.stringify(input));
|
||||
const message = result.stderr || result.stdout;
|
||||
if (result.exitCode === 0) {
|
||||
return {
|
||||
status: "pass",
|
||||
message: "",
|
||||
binaryPath,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
}
|
||||
if (result.exitCode === 2) {
|
||||
return {
|
||||
status: "warning",
|
||||
message,
|
||||
binaryPath,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "error",
|
||||
message,
|
||||
binaryPath,
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCommentCheckerBinary(): string | undefined {
|
||||
const binaryName = process.platform === "win32" ? "comment-checker.exe" : "comment-checker";
|
||||
const fromPackageApi = resolvePackageApiBinary();
|
||||
if (fromPackageApi) return fromPackageApi;
|
||||
const fromPackage = resolvePackageBinary(binaryName);
|
||||
if (fromPackage) return fromPackage;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolvePackageApiBinary(): string | undefined {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageExports: unknown = require("@code-yeongyu/comment-checker");
|
||||
if (!isCommentCheckerPackage(packageExports)) return undefined;
|
||||
const binaryPath = packageExports.getBinaryPath();
|
||||
return existsSync(binaryPath) ? binaryPath : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePackageBinary(binaryName: string): string | undefined {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const packagePath = require.resolve("@code-yeongyu/comment-checker/package.json");
|
||||
const binaryPath = join(dirname(packagePath), "bin", binaryName);
|
||||
return existsSync(binaryPath) ? binaryPath : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isCommentCheckerPackage(value: unknown): value is { getBinaryPath: () => string } {
|
||||
return isRecord(value) && typeof value["getBinaryPath"] === "function";
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
interface OutputAccumulator {
|
||||
text: string;
|
||||
bytes: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
function appendOutput(output: OutputAccumulator, chunk: string, maxOutputBytes: number): void {
|
||||
if (output.truncated) return;
|
||||
|
||||
const remainingBytes = maxOutputBytes - output.bytes;
|
||||
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
||||
if (chunkBytes <= remainingBytes) {
|
||||
output.text += chunk;
|
||||
output.bytes += chunkBytes;
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainingBytes > 0) {
|
||||
output.text += Buffer.from(chunk, "utf8").subarray(0, remainingBytes).toString("utf8");
|
||||
output.bytes += remainingBytes;
|
||||
}
|
||||
output.truncated = true;
|
||||
}
|
||||
|
||||
function formatOutput(output: OutputAccumulator, streamName: "stdout" | "stderr", maxOutputBytes: number): string {
|
||||
if (!output.truncated) return output.text;
|
||||
return `${output.text}\n[${streamName} truncated after ${maxOutputBytes} bytes]`;
|
||||
}
|
||||
|
||||
export function spawnProcess(
|
||||
command: string,
|
||||
args: string[],
|
||||
stdin: string,
|
||||
maxOutputBytes: number = MAX_PROCESS_OUTPUT_BYTES,
|
||||
): Promise<ProcessResult> {
|
||||
return new Promise((resolve) => {
|
||||
const outputByteLimit = Number.isFinite(maxOutputBytes) && maxOutputBytes > 0 ? Math.floor(maxOutputBytes) : 0;
|
||||
const proc = spawn(command, args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
const stdout: OutputAccumulator = { text: "", bytes: 0, truncated: false };
|
||||
const stderr: OutputAccumulator = { text: "", bytes: 0, truncated: false };
|
||||
|
||||
proc.stdout.setEncoding("utf-8");
|
||||
proc.stderr.setEncoding("utf-8");
|
||||
proc.stdout.on("data", (chunk: string) => {
|
||||
appendOutput(stdout, chunk, outputByteLimit);
|
||||
});
|
||||
proc.stderr.on("data", (chunk: string) => {
|
||||
appendOutput(stderr, chunk, outputByteLimit);
|
||||
});
|
||||
proc.once("error", (error) => {
|
||||
appendOutput(stderr, error.message, outputByteLimit);
|
||||
resolve({
|
||||
exitCode: null,
|
||||
stdout: formatOutput(stdout, "stdout", outputByteLimit),
|
||||
stderr: formatOutput(stderr, "stderr", outputByteLimit),
|
||||
});
|
||||
});
|
||||
proc.once("close", (exitCode) => {
|
||||
resolve({
|
||||
exitCode,
|
||||
stdout: formatOutput(stdout, "stdout", outputByteLimit),
|
||||
stderr: formatOutput(stderr, "stderr", outputByteLimit),
|
||||
});
|
||||
});
|
||||
proc.stdin.end(stdin);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type CodexPostToolUseInput,
|
||||
extractCodexCommentCheckRequests,
|
||||
runCommentCheckerPostToolUse,
|
||||
} from "../src/codex-hook.ts";
|
||||
|
||||
type CliResult = {
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const CLI_PATH = fileURLToPath(new URL("../dist/cli.js", import.meta.url));
|
||||
|
||||
function runHookCli(input: string): Promise<CliResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [CLI_PATH, "hook", "post-tool-use"], {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk: string) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk: string) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (exitCode) => {
|
||||
resolve({ exitCode, stdout, stderr });
|
||||
});
|
||||
child.stdin.end(input);
|
||||
});
|
||||
}
|
||||
|
||||
function postToolUseInput(overrides: Partial<CodexPostToolUseInput> = {}): CodexPostToolUseInput {
|
||||
return {
|
||||
session_id: "thread-1",
|
||||
turn_id: "turn-1",
|
||||
transcript_path: null,
|
||||
cwd: "/repo",
|
||||
hook_event_name: "PostToolUse",
|
||||
model: "gpt-5.5",
|
||||
permission_mode: "never",
|
||||
tool_name: "apply_patch",
|
||||
tool_input: {
|
||||
command: [
|
||||
"*** Begin Patch",
|
||||
"*** Update File: src/example.ts",
|
||||
"@@",
|
||||
"-const value = 1;",
|
||||
"+// explains value",
|
||||
"+const value = 2;",
|
||||
"*** End Patch",
|
||||
].join("\n"),
|
||||
},
|
||||
tool_response: "Success. Updated files.",
|
||||
tool_use_id: "call-1",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("extractCodexCommentCheckRequests", () => {
|
||||
it("#given codex apply_patch command #when extracting #then returns edit request for changed file", () => {
|
||||
const requests = extractCodexCommentCheckRequests(postToolUseInput());
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
sourceToolName: "apply_patch",
|
||||
toolName: "Edit",
|
||||
filePath: "src/example.ts",
|
||||
toolInput: {
|
||||
file_path: "src/example.ts",
|
||||
old_string: "const value = 1;\n",
|
||||
new_string: "// explains value\nconst value = 2;\n",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("#given unsupported post tool event #when extracting #then returns no requests", () => {
|
||||
const requests = extractCodexCommentCheckRequests(
|
||||
postToolUseInput({
|
||||
tool_name: "read",
|
||||
tool_input: { file_path: "src/example.ts", content: "// hi\nconst value = 1;\n" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
it("#given codex write payload #when extracting #then returns write request", () => {
|
||||
const requests = extractCodexCommentCheckRequests(
|
||||
postToolUseInput({
|
||||
tool_name: "write",
|
||||
tool_input: {
|
||||
file_path: "src/example.ts",
|
||||
content: "// explains value\nconst value = 1;\n",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
sourceToolName: "write",
|
||||
toolName: "Write",
|
||||
filePath: "src/example.ts",
|
||||
toolInput: {
|
||||
file_path: "src/example.ts",
|
||||
content: "// explains value\nconst value = 1;\n",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("#given codex edit payload #when extracting #then returns edit request", () => {
|
||||
const requests = extractCodexCommentCheckRequests(
|
||||
postToolUseInput({
|
||||
tool_name: "edit",
|
||||
tool_input: {
|
||||
path: "src/example.ts",
|
||||
oldString: "const value = 1;\n",
|
||||
newString: "// explains value\nconst value = 2;\n",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
sourceToolName: "edit",
|
||||
toolName: "Edit",
|
||||
filePath: "src/example.ts",
|
||||
toolInput: {
|
||||
file_path: "src/example.ts",
|
||||
old_string: "const value = 1;\n",
|
||||
new_string: "// explains value\nconst value = 2;\n",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("#given one-sided codex edit payload #when extracting #then returns no requests", () => {
|
||||
const requests = extractCodexCommentCheckRequests(
|
||||
postToolUseInput({
|
||||
tool_name: "edit",
|
||||
tool_input: {
|
||||
path: "src/example.ts",
|
||||
oldString: "const value = 1;\n",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(requests).toEqual([]);
|
||||
});
|
||||
|
||||
it("#given codex multi_edit payload #when extracting #then returns multiedit request", () => {
|
||||
const requests = extractCodexCommentCheckRequests(
|
||||
postToolUseInput({
|
||||
tool_name: "multi_edit",
|
||||
tool_input: {
|
||||
filePath: "src/example.ts",
|
||||
edits: [
|
||||
{ old_string: "const a = 1;\n", new_string: "// explains a\nconst a = 2;\n" },
|
||||
{ oldString: "const b = 1;\n", newString: "// explains b\nconst b = 2;\n" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
sourceToolName: "multi_edit",
|
||||
toolName: "MultiEdit",
|
||||
filePath: "src/example.ts",
|
||||
toolInput: {
|
||||
file_path: "src/example.ts",
|
||||
edits: [
|
||||
{ old_string: "const a = 1;\n", new_string: "// explains a\nconst a = 2;\n" },
|
||||
{ old_string: "const b = 1;\n", new_string: "// explains b\nconst b = 2;\n" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runCommentCheckerPostToolUse", () => {
|
||||
it("#given checker warning #when hook runs #then returns blocking feedback JSON", async () => {
|
||||
const output = await runCommentCheckerPostToolUse(postToolUseInput(), {
|
||||
run: async () => ({
|
||||
status: "warning",
|
||||
message: "comment warning: explain less",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(JSON.parse(output)).toEqual({
|
||||
decision: "block",
|
||||
reason: "comment-checker found issues in src/example.ts:\ncomment warning: explain less",
|
||||
});
|
||||
});
|
||||
|
||||
it("#given missing checker binary #when hook runs #then emits no hook output", async () => {
|
||||
const output = await runCommentCheckerPostToolUse(postToolUseInput(), {
|
||||
run: async () => ({
|
||||
status: "missing",
|
||||
message: "not installed",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given transcript path #when hook runs #then forwards it to checker input", async () => {
|
||||
let transcriptPath = "";
|
||||
|
||||
await runCommentCheckerPostToolUse(
|
||||
postToolUseInput({
|
||||
transcript_path: "/tmp/codex-comment-checker-transcript.jsonl",
|
||||
tool_name: "write",
|
||||
tool_input: {
|
||||
file_path: "src/example.ts",
|
||||
content: "// explains value\nconst value = 1;\n",
|
||||
},
|
||||
}),
|
||||
{
|
||||
run: async (input) => {
|
||||
transcriptPath = input.transcript_path;
|
||||
return {
|
||||
status: "pass",
|
||||
message: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(transcriptPath).toBe("/tmp/codex-comment-checker-transcript.jsonl");
|
||||
});
|
||||
|
||||
it("#given null transcript path #when hook runs #then forwards empty string fallback", async () => {
|
||||
let transcriptPath = "unset";
|
||||
|
||||
await runCommentCheckerPostToolUse(
|
||||
postToolUseInput({
|
||||
transcript_path: null,
|
||||
tool_name: "write",
|
||||
tool_input: {
|
||||
file_path: "src/example.ts",
|
||||
content: "// explains value\nconst value = 1;\n",
|
||||
},
|
||||
}),
|
||||
{
|
||||
run: async (input) => {
|
||||
transcriptPath = input.transcript_path;
|
||||
return {
|
||||
status: "pass",
|
||||
message: "",
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(transcriptPath).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runCodexHookCli", () => {
|
||||
it("#given malformed post-tool-use stdin #when hook CLI runs #then it no-ops without stderr", async () => {
|
||||
// given
|
||||
const input = "break;\n";
|
||||
|
||||
// when
|
||||
const result = await runHookCli(input);
|
||||
|
||||
// then
|
||||
expect(result).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("#given non-object post-tool-use JSON #when hook CLI runs #then it no-ops without stderr", async () => {
|
||||
// given
|
||||
const input = '"break;"\n';
|
||||
|
||||
// when
|
||||
const result = await runHookCli(input);
|
||||
|
||||
// then
|
||||
expect(result).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("#given non-string transcript path #when hook CLI runs #then it no-ops without stderr", async () => {
|
||||
// given
|
||||
const input = `${JSON.stringify({ ...postToolUseInput(), transcript_path: 42 })}\n`;
|
||||
|
||||
// when
|
||||
const result = await runHookCli(input);
|
||||
|
||||
// then
|
||||
expect(result).toEqual({
|
||||
exitCode: 0,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"session_id": "00000000-0000-0000-0000-000000000000",
|
||||
"turn_id": "00000000-0000-0000-0000-000000000001",
|
||||
"transcript_path": "/tmp/codex-comment-checker-transcript.jsonl",
|
||||
"cwd": ".",
|
||||
"hook_event_name": "PostToolUse",
|
||||
"model": "gpt-5.5",
|
||||
"permission_mode": "default",
|
||||
"tool_name": "apply_patch",
|
||||
"tool_input": {
|
||||
"command": "*** Begin Patch\n*** Add File: src/example.ts\n+export const meaning = 42;\n*** End Patch\n"
|
||||
},
|
||||
"tool_response": "Success. Updated files.",
|
||||
"tool_use_id": "toolu_000000000000000000000000"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
type PackageJson = {
|
||||
readonly type: string;
|
||||
readonly packageManager: string;
|
||||
readonly bin: Record<string, string>;
|
||||
readonly dependencies?: Record<string, unknown>;
|
||||
readonly optionalDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
type PluginJson = {
|
||||
readonly hooks: string;
|
||||
};
|
||||
|
||||
type HookCommand = {
|
||||
readonly command: string;
|
||||
};
|
||||
|
||||
type HookEntry = {
|
||||
readonly hooks: readonly HookCommand[];
|
||||
};
|
||||
|
||||
type HooksJson = {
|
||||
readonly hooks: Record<string, readonly HookEntry[]>;
|
||||
};
|
||||
|
||||
function readPackageJson(path: string): PackageJson {
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (!isPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readPluginJson(path: string): PluginJson {
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (!isPluginJson(parsed)) throw new TypeError(`Invalid plugin metadata: ${path}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function readHooksJson(path: string): HooksJson {
|
||||
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (!isHooksJson(parsed)) throw new TypeError(`Invalid hooks metadata: ${path}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
describe("plugin package metadata", () => {
|
||||
it("#given packaged plugin files #when validating entrypoints #then hook command uses portable plugin root interpolation", () => {
|
||||
// given
|
||||
const packageJson = readPackageJson("package.json");
|
||||
const pluginJson = readPluginJson(".codex-plugin/plugin.json");
|
||||
const hooksJson = readHooksJson("hooks/hooks.json");
|
||||
const cliSource = readFileSync("src/cli.ts", "utf8");
|
||||
|
||||
// when
|
||||
const command = hooksJson.hooks["PostToolUse"]?.[0]?.hooks[0]?.command;
|
||||
const pluginRoot = ["$", "{PLUGIN_ROOT}"].join("");
|
||||
|
||||
// then
|
||||
expect(packageJson.type).toBe("module");
|
||||
expect(packageJson.packageManager).toBe("npm@11.12.1");
|
||||
expect(packageJson.dependencies ?? {}).not.toHaveProperty("@code-yeongyu/comment-checker");
|
||||
expect(packageJson.optionalDependencies).toHaveProperty("@code-yeongyu/comment-checker");
|
||||
expect(packageJson.bin["codex-comment-checker"]).toBe("./dist/cli.js");
|
||||
expect(pluginJson.hooks).toBe("./hooks/hooks.json");
|
||||
expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);
|
||||
expect(command).toBe(`node "${pluginRoot}/dist/cli.js" hook post-tool-use`);
|
||||
});
|
||||
});
|
||||
|
||||
function isPackageJson(value: unknown): value is PackageJson {
|
||||
if (!isRecord(value)) return false;
|
||||
const dependencies = value["dependencies"];
|
||||
return (
|
||||
value["type"] === "module" &&
|
||||
value["packageManager"] === "npm@11.12.1" &&
|
||||
isStringRecord(value["bin"]) &&
|
||||
isStringRecord(value["optionalDependencies"]) &&
|
||||
(dependencies === undefined || isRecord(dependencies))
|
||||
);
|
||||
}
|
||||
|
||||
function isPluginJson(value: unknown): value is PluginJson {
|
||||
return isRecord(value) && typeof value["hooks"] === "string";
|
||||
}
|
||||
|
||||
function isHooksJson(value: unknown): value is HooksJson {
|
||||
if (!isRecord(value) || !isRecord(value["hooks"])) return false;
|
||||
return Object.values(value["hooks"]).every(isHookEntries);
|
||||
}
|
||||
|
||||
function isHookEntries(value: unknown): value is readonly HookEntry[] {
|
||||
return Array.isArray(value) && value.every(isHookEntry);
|
||||
}
|
||||
|
||||
function isHookEntry(value: unknown): value is HookEntry {
|
||||
return isRecord(value) && Array.isArray(value["hooks"]) && value["hooks"].every(isHookCommand);
|
||||
}
|
||||
|
||||
function isHookCommand(value: unknown): value is HookCommand {
|
||||
return isRecord(value) && typeof value["command"] === "string";
|
||||
}
|
||||
|
||||
function isStringRecord(value: unknown): value is Record<string, string> {
|
||||
return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
MAX_PROCESS_OUTPUT_BYTES,
|
||||
resolveCommentCheckerBinary,
|
||||
runCommentChecker,
|
||||
spawnProcess,
|
||||
} from "../src/runner.js";
|
||||
|
||||
describe("spawnProcess", () => {
|
||||
it("#given noisy checker process #when output exceeds cap #then stderr is bounded", async () => {
|
||||
// given
|
||||
const maxOutputBytes = 16;
|
||||
|
||||
// when
|
||||
const result = await spawnProcess(
|
||||
process.execPath,
|
||||
["-e", "process.stderr.write('x'.repeat(40)); process.exit(2);"],
|
||||
"",
|
||||
maxOutputBytes,
|
||||
);
|
||||
|
||||
// then
|
||||
expect(MAX_PROCESS_OUTPUT_BYTES).toBeGreaterThan(maxOutputBytes);
|
||||
expect(result.exitCode).toBe(2);
|
||||
expect(result.stderr).toBe(`${"x".repeat(maxOutputBytes)}\n[stderr truncated after 16 bytes]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCommentCheckerBinary", () => {
|
||||
it("#given installed checker package #when resolving binary #then returns existing checker binary", () => {
|
||||
// given / when
|
||||
const binaryPath = resolveCommentCheckerBinary();
|
||||
|
||||
// then
|
||||
expect(binaryPath).toBeDefined();
|
||||
expect(binaryPath ?? "").toContain("comment-checker");
|
||||
expect(existsSync(binaryPath ?? "")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runCommentChecker", () => {
|
||||
it("#given missing checker binary #when runner starts #then returns missing result", async () => {
|
||||
// given / when
|
||||
const result = await runCommentChecker(
|
||||
{
|
||||
session_id: "session-1",
|
||||
tool_name: "Write",
|
||||
transcript_path: "",
|
||||
cwd: "/repo",
|
||||
hook_event_name: "PostToolUse",
|
||||
tool_input: {
|
||||
file_path: "src/example.ts",
|
||||
content: "const value = 1;\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
resolveBinary: () => undefined,
|
||||
},
|
||||
);
|
||||
|
||||
// then
|
||||
expect(result.status).toBe("missing");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["test/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"esModuleInterop": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"useDefineForClassFields": false,
|
||||
"types": ["node"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*", "test/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["test/**/*.test.ts"],
|
||||
environment: "node",
|
||||
pool: "threads",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user