mirror of
https://github.com/actualbudget/actual.git
synced 2026-10-11 22:49:59 +00:00
[AI] Consolidate agent tooling and simplify developer guidance (#8089)
* [AI] Replace mechanical agent rules with shared agent hooks + nano-staged Move the mechanical "remember to run X" rules out of AGENTS.md and the agent guidance docs into deterministic, cross-agent hooks, and migrate the pre-commit runner from lint-staged to nano-staged. - Add shared hook scripts in scripts/agent-hooks/ (git-guard, format-edited-file, no-strict-ignore-new-file, prefer-one-component, check-on-stop, common helpers) wired for Claude (.claude/settings.json), Codex (.codex/config.toml), and Cursor (.cursor/hooks.json + adapters). - Enforce "avoid enum" via a new actual/no-enum lint rule, grandfathering the two existing declarations in .oxlintrc.json overrides. - Migrate lint-staged -> nano-staged (.nano-staged.json, .husky/pre-commit, package.json). - Trim AGENTS.md, .github/agents/pr-and-commit-rules.md, the Cursor rule, and the committing skill down to the rules that aren't auto-enforced. - Add release note 8089.md. * [AI] Align PR-title wording in AGENTS.md with canonical rules * [AI] Fix check-on-stop hook on bash 3.2 and TSV column collapse The Stop hook silently no-op'd in two ways: - `declare -A` isn't supported on stock macOS bash 3.2, and the slash/hyphen path subscript aborted under `set -u`. Dedupe via a space-padded string match instead. - jq emitted an empty middle TSV field for a package with test but no typecheck; tab is IFS-whitespace so `read` collapsed it and shifted the columns (eslint-plugin-actual then ran neither). Emit non-empty sentinels (-, yes/no) so no field can collapse. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
27c6ded5a9
commit
328417f8de
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/format-edited-file.sh\""
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/prefer-one-component.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/git-guard.sh\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/no-strict-ignore-new-file.sh\""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR/scripts/agent-hooks/check-on-stop.sh\"",
|
||||
"timeout": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,19 @@ Before creating any git commit or pull request in this repo, read:
|
||||
|
||||
**`.github/agents/pr-and-commit-rules.md`**
|
||||
|
||||
It is the authoritative source for the `[AI]` prefix requirement, the git-safety rules, the pre-commit quality checklist (`yarn typecheck`, `yarn lint:fix`, relevant tests, translated strings), the rule about leaving the PR template blank, and the quick-reference workflow. Read it on every commit/PR session so any updates to those rules are picked up automatically.
|
||||
It is the authoritative source for the rules you must apply yourself: the `[AI]` PR-title prefix and leaving the PR template blank (and the Chinese exception). The mechanical commit/git rules are enforced by tooling and aren't restated there. Read it on every commit/PR session so any updates are picked up automatically.
|
||||
|
||||
## The one rule worth restating
|
||||
|
||||
Every commit message and every pull request title MUST begin with `[AI]`. This is the single most-violated rule and the cost of forgetting it is high — the commit has to be amended or the PR has to be renamed by hand. Examples:
|
||||
Every commit message and every pull request title MUST begin with `[AI]`. Commit messages are checked by the git-guard hook (a missing prefix is blocked before the commit lands), so the one that needs _your_ attention is the PR title — nothing checks it for you, and the cost of forgetting it is high since the PR has to be renamed by hand. Examples:
|
||||
|
||||
- `[AI] Fix type error in account validation` — correct
|
||||
- `Fix type error in account validation` — wrong, missing prefix
|
||||
|
||||
Everything else (git safety, hooks, label, PR template, pre-commit checklist) lives in the rules file. Read it before each commit/PR rather than relying on this skill's summary, because the rules file evolves and this skill deliberately does not restate it.
|
||||
The PR-template rule and the rest live in the rules file. Read it before each commit/PR rather than relying on this skill's summary, because the rules file evolves and this skill deliberately does not restate it.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Maintainers triage AI-authored contributions separately, and the `[AI]` prefix is what makes that triage fast — it also drives the automatic `"AI generated"` PR label, so getting the prefix right is the one action that gates the whole triage path. Without it the PR looks like a normal human contribution and the wrong review process gets applied. The "do not fill in the PR template" rule exists because the human is the one who actually tested the change and can write the Description / Testing / Checklist sections honestly — an AI-filled template misrepresents who did what. The hook-skipping prohibition (`--no-verify`, `--no-gpg-sign`) exists because this codebase enforces formatting and lint at commit time deliberately; bypassing the hook is how broken code lands on a branch.
|
||||
Maintainers triage AI-authored contributions separately, and the `[AI]` prefix is what makes that triage fast — it also drives the automatic `"AI generated"` PR label, so getting the prefix right is the one action that gates the whole triage path. Without it the PR looks like a normal human contribution and the wrong review process gets applied. The "do not fill in the PR template" rule exists because the human is the one who actually tested the change and can write the Description / Testing / Checklist sections honestly — an AI-filled template misrepresents who did what.
|
||||
|
||||
The rules file is short. Read it.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Codex project hooks for actualbudget/actual.
|
||||
#
|
||||
# Codex discovers project config by walking up to the .git root, so this file
|
||||
# applies to the whole repo. The hook scripts are shared with Claude Code and
|
||||
# Cursor (see scripts/agent-hooks/). Codex's Bash hook is Claude-compatible
|
||||
# (tool_input.command), so the git guard is reused directly; exit code 2 + stderr
|
||||
# blocks the call.
|
||||
#
|
||||
# Known limitation: Codex's `apply_patch` PreToolUse payload carries the diff as a
|
||||
# raw string in tool_input.command (no tool_input.file_path / .content), so the
|
||||
# file-content hooks below (strict-ignore, format, one-component) only fire for
|
||||
# the Write/Edit tools and no-op for apply_patch. They never false-block. The
|
||||
# Bash/git guard and the Stop check work regardless. The file hooks are fully
|
||||
# effective under Claude Code and Cursor.
|
||||
#
|
||||
# Non-managed hooks must be trusted once via `/hooks` in the Codex CLI.
|
||||
|
||||
# Block unsafe git/workspace commands ([AI] prefix, --no-verify, force-push,
|
||||
# push-to-main, git config writes, yarn-from-a-workspace).
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/git-guard.sh"'
|
||||
statusMessage = "Checking shell command against repo policy"
|
||||
|
||||
# Block creating a new file that contains // @ts-strict-ignore.
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^apply_patch$|^Write$|^Edit$"
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/no-strict-ignore-new-file.sh"'
|
||||
statusMessage = "Checking new file is type-strict"
|
||||
|
||||
# Format/lint the edited file, and warn on multiple components in a new .tsx.
|
||||
[[hooks.PostToolUse]]
|
||||
matcher = "^apply_patch$|^Write$|^Edit$"
|
||||
[[hooks.PostToolUse.hooks]]
|
||||
type = "command"
|
||||
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/format-edited-file.sh"'
|
||||
[[hooks.PostToolUse.hooks]]
|
||||
type = "command"
|
||||
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/prefer-one-component.sh"'
|
||||
|
||||
# When the turn ends, typecheck + test the workspaces this branch touched.
|
||||
[[hooks.Stop]]
|
||||
[[hooks.Stop.hooks]]
|
||||
type = "command"
|
||||
command = '"$(git rev-parse --show-toplevel)/scripts/agent-hooks/check-on-stop.sh"'
|
||||
timeout = 600
|
||||
statusMessage = "Typecheck + test touched workspaces"
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": {
|
||||
"beforeShellExecution": [{ "command": "./.cursor/hooks/guard-shell.sh" }],
|
||||
"afterFileEdit": [{ "command": "./.cursor/hooks/after-file-edit.sh" }]
|
||||
}
|
||||
}
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# Cursor `afterFileEdit` adapter.
|
||||
#
|
||||
# afterFileEdit is informational-only: it can format the edited file (a side
|
||||
# effect on disk) and PRINT advisories to Cursor's Hooks output channel, but it
|
||||
# cannot block or message the agent. So under Cursor the new-file rules
|
||||
# (no @ts-strict-ignore, one component per file) are advisory, not enforced.
|
||||
# Cursor input on stdin: { file_path, edits, ... }.
|
||||
|
||||
input=$(cat)
|
||||
file=$(printf '%s' "$input" | jq -r '.file_path // empty')
|
||||
[ -n "$file" ] && [ -f "$file" ] || exit 0
|
||||
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
|
||||
payload=$(jq -n --arg f "$file" '{tool_input:{file_path:$f}}')
|
||||
|
||||
# Format/lint the edited file (side effect on disk).
|
||||
printf '%s' "$payload" | "$ROOT/scripts/agent-hooks/format-edited-file.sh" >/dev/null 2>&1
|
||||
|
||||
dir=$(dirname "$file")
|
||||
# Normalize to a repo-root-relative path so the git pathspec is unambiguous
|
||||
# whether Cursor passed an absolute or relative file_path.
|
||||
abs=$(CDPATH= cd "$dir" 2>/dev/null && pwd)/$(basename "$file")
|
||||
rel=${abs#"$ROOT"/}
|
||||
is_new() { ! git -C "$ROOT" ls-files --error-unmatch "$rel" >/dev/null 2>&1; }
|
||||
|
||||
# Advisory: new file that is not type-strict.
|
||||
case "$file" in
|
||||
*.ts | *.tsx)
|
||||
if is_new && grep -q "@ts-strict-ignore" "$file" 2>/dev/null; then
|
||||
echo "[actual] Advisory: new file '$file' uses // @ts-strict-ignore — new files should be type-strict (AGENTS.md)."
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
# Advisory: new .tsx with multiple components (reuse the shared check).
|
||||
msg=$(printf '%s' "$payload" | "$ROOT/scripts/agent-hooks/prefer-one-component.sh" 2>&1)
|
||||
[ -n "$msg" ] && echo "[actual] Advisory: $msg"
|
||||
|
||||
exit 0
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# Cursor `beforeShellExecution` adapter.
|
||||
#
|
||||
# Translates Cursor's hook I/O to the shared guard in
|
||||
# scripts/agent-hooks/git-guard.sh. Cursor input on stdin: { command, cwd, ... }.
|
||||
# Output on stdout: { permission: "allow" | "deny", userMessage, agentMessage }.
|
||||
|
||||
input=$(cat)
|
||||
ROOT=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
|
||||
|
||||
deny() {
|
||||
jq -n --arg m "$1" \
|
||||
'{permission:"deny", userMessage:"Blocked by repo policy", agentMessage:$m}'
|
||||
exit 0
|
||||
}
|
||||
allow() {
|
||||
jq -n '{permission:"allow"}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Fail closed: a malformed payload (jq parse failure / missing command) must
|
||||
# deny, not silently allow.
|
||||
cmd=$(printf '%s' "$input" | jq -re '.command // empty') ||
|
||||
deny "Invalid Cursor hook payload: could not read .command."
|
||||
cwd=$(printf '%s' "$input" | jq -r '.cwd // ""')
|
||||
|
||||
# Cursor passes cwd rather than a literal `cd packages/...`, so catch
|
||||
# yarn-run-from-a-workspace here before delegating.
|
||||
case "$cwd" in
|
||||
"$ROOT"/packages | "$ROOT"/packages/*)
|
||||
case "$cmd" in
|
||||
yarn* | *" yarn "*)
|
||||
deny "Run yarn from the repo root, not a child workspace (AGENTS.md). Use 'yarn workspace <name> <cmd>' from $ROOT." ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# Delegate the git-safety checks to the shared guard (reads .tool_input.command).
|
||||
# Fail closed: deny on a block (exit 2) AND on any unexpected guard failure.
|
||||
status=0
|
||||
err=$(printf '{"tool_input":{"command":%s}}' "$(jq -n --arg c "$cmd" '$c')" |
|
||||
"$ROOT/scripts/agent-hooks/git-guard.sh" 2>&1) || status=$?
|
||||
case "$status" in
|
||||
0) allow ;;
|
||||
2) deny "$err" ;;
|
||||
*) deny "git-guard failed (exit $status): $err" ;;
|
||||
esac
|
||||
@@ -6,69 +6,14 @@ alwaysApply: true
|
||||
|
||||
# PR and Commit Rules for AI Agents
|
||||
|
||||
Canonical source: `.github/agents/pr-and-commit-rules.md`
|
||||
|
||||
## Commit Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Git Safety Rules
|
||||
|
||||
- **Never** update git config
|
||||
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
|
||||
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
|
||||
- **Never** force push to `main`/`master`
|
||||
- **Never** commit unless explicitly asked by the user
|
||||
|
||||
## Pre-Commit Quality Checklist
|
||||
|
||||
Before committing, ensure all of the following:
|
||||
|
||||
- [ ] Commit message is prefixed with `[AI]`
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
|
||||
Canonical source: `.github/agents/pr-and-commit-rules.md`. The mechanical commit
|
||||
and git rules are handled by tooling; only the rules you must apply yourself are
|
||||
listed here.
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Labels
|
||||
|
||||
Add the **"AI generated"** label to all AI-created pull requests.
|
||||
|
||||
### PR Template: Do Not Fill In
|
||||
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is.
|
||||
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese** (简体中文).
|
||||
|
||||
## Quick-Reference Workflow
|
||||
|
||||
1. Make your changes
|
||||
2. Run `yarn typecheck` — fix any errors
|
||||
3. Run `yarn lint:fix` — fix any remaining lint errors
|
||||
4. Run relevant tests (`yarn test` for all, or workspace-specific)
|
||||
5. Stage files and commit with `[AI]` prefix — do not skip hooks
|
||||
6. When creating a PR:
|
||||
- Use `[AI]` prefix in the title
|
||||
- Add the `"AI generated"` label
|
||||
- Leave the PR template blank (do not fill it in)
|
||||
- **ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced
|
||||
automatically — you have to apply it yourself.
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`) — leave it
|
||||
blank for the human. Exception: if a human explicitly asks, fill it out **in
|
||||
Chinese** (简体中文).
|
||||
|
||||
@@ -1,67 +1,24 @@
|
||||
# PR and Commit Rules for AI Agents
|
||||
|
||||
This is the single source of truth for all commit and pull request rules that AI agents must follow when working with Actual Budget.
|
||||
|
||||
## Commit Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
|
||||
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
|
||||
|
||||
### Git Safety Rules
|
||||
|
||||
- **Never** update git config
|
||||
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
|
||||
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
|
||||
- **Never** force push to `main`/`master`
|
||||
- **Never** commit unless explicitly asked by the user
|
||||
|
||||
## Pre-Commit Quality Checklist
|
||||
|
||||
Before committing, ensure all of the following:
|
||||
|
||||
- [ ] Commit message is prefixed with `[AI]`
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
|
||||
The mechanical commit and git rules are handled by tooling. This file lists only
|
||||
the rules you have to apply yourself.
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
### [AI] Prefix Requirement
|
||||
### `[AI]` prefix on PR titles
|
||||
|
||||
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
|
||||
**ALL pull request titles MUST be prefixed with `[AI]`.** This isn't enforced
|
||||
automatically — you have to apply it yourself.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `[AI] Fix type error in account validation`
|
||||
- `[AI] Add support for new transaction categories`
|
||||
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
|
||||
- `Fix type error in account validation` (MISSING PREFIX — NOT ALLOWED)
|
||||
|
||||
The `"AI generated"` label is applied automatically to any PR whose title starts with `[AI]`, so there is nothing extra to do — and outside contributors cannot apply labels anyway.
|
||||
### Do not fill in the PR template
|
||||
|
||||
### PR Template: Do Not Fill In
|
||||
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. Humans are expected to fill in the Description, Related issue(s), Testing, and Checklist sections.
|
||||
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
|
||||
|
||||
## Quick-Reference Workflow
|
||||
|
||||
Follow these steps when committing and creating PRs:
|
||||
|
||||
1. Make your changes
|
||||
2. Run `yarn typecheck` — fix any errors
|
||||
3. Run `yarn lint:fix` — fix any remaining lint errors
|
||||
4. Run relevant tests (`yarn test` for all, or workspace-specific)
|
||||
5. Stage files and commit with `[AI]` prefix — do not skip hooks
|
||||
6. When creating a PR:
|
||||
- Use `[AI]` prefix in the title (the `"AI generated"` label is auto-applied based on this prefix)
|
||||
- Leave the PR template blank (do not fill it in)
|
||||
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all
|
||||
blank spaces and placeholder comments as-is — the human who tested the change
|
||||
fills in the Description, Related issue(s), Testing, and Checklist sections.
|
||||
- **Exception**: if a human **explicitly asks** you to fill it out, do so **in
|
||||
Chinese**, using Chinese characters (简体中文) for all content you add.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
yarn lint-staged
|
||||
yarn nano-staged
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"packages/*/{package.json,tsconfig.json}": [
|
||||
"ts-node ./bin/validate-publish-imports.ts --fix",
|
||||
"yarn sync:tsconfig-references"
|
||||
],
|
||||
"**/*.{js,mjs,jsx,ts,tsx,md,json,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern",
|
||||
"**/*.{js,mjs,jsx,ts,tsx}": "oxlint --fix --type-aware --quiet"
|
||||
}
|
||||
@@ -359,6 +359,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"actual/no-enum": "error",
|
||||
"eslint/no-useless-constructor": "error",
|
||||
"eslint/no-undef": "error",
|
||||
"eslint/no-unused-expressions": "error",
|
||||
@@ -372,6 +373,16 @@
|
||||
"react/rules-of-hooks": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
// Grandfathered pre-existing enums; new enums are disallowed everywhere else.
|
||||
"files": [
|
||||
"packages/component-library/src/tokens.ts",
|
||||
"packages/desktop-client/src/auth/types.ts"
|
||||
],
|
||||
"rules": {
|
||||
"actual/no-enum": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["**/*.test.{js,ts,jsx,tsx}", "packages/docs/**/*"],
|
||||
"rules": {
|
||||
|
||||
@@ -42,11 +42,10 @@ yarn start:desktop
|
||||
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
|
||||
- Tests run once and exit by default (using `vitest --run`)
|
||||
|
||||
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
|
||||
### ⚠️ PR titles must start with `[AI]`
|
||||
|
||||
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
|
||||
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for the full specification, including git safety rules, pre-commit checklist, and PR workflow.
|
||||
Every pull request title must be prefixed with `[AI]` — this isn't enforced
|
||||
automatically, so you have to apply it yourself. See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md).
|
||||
|
||||
### Task Orchestration with Lage
|
||||
|
||||
@@ -200,10 +199,6 @@ When implementing changes:
|
||||
|
||||
1. Read relevant files to understand current implementation
|
||||
2. Make focused, incremental changes
|
||||
3. Run type checking: `yarn typecheck`
|
||||
4. Run linting: `yarn lint:fix`
|
||||
5. Run relevant tests
|
||||
6. Fix any linter errors that are introduced
|
||||
|
||||
### 2. Testing Strategy
|
||||
|
||||
@@ -253,21 +248,20 @@ yarn workspace @actual-app/web e2e
|
||||
TypeScript configuration uses:
|
||||
|
||||
- Incremental compilation
|
||||
- Strict type checking with `typescript-strict-plugin`
|
||||
- Strict type checking with `typescript-strict-plugin`. New files must be
|
||||
type-strict — don't add `// @ts-strict-ignore` to a new file (existing files
|
||||
are grandfathered).
|
||||
- Platform-specific exports in `loot-core` (node vs browser)
|
||||
|
||||
Always run `yarn typecheck` before committing.
|
||||
|
||||
### 4. Internationalization (i18n)
|
||||
|
||||
- Use `Trans` component instead of `t()` function when possible
|
||||
- All user-facing strings must be translated
|
||||
- Generate i18n files: `yarn generate:i18n`
|
||||
- Custom ESLint rules enforce translation usage
|
||||
Use the `Trans` component (and translated strings) for user-facing text.
|
||||
Regenerate i18n files with `yarn generate:i18n`.
|
||||
|
||||
### 5. Financial Number Typography
|
||||
|
||||
- Wrap standalone financial numbers with `FinancialText` or apply `styles.tnum` directly if wrapping is not possible
|
||||
Wrap standalone financial numbers with `FinancialText` (or `styles.tnum` where
|
||||
wrapping isn't possible).
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
@@ -275,17 +269,12 @@ Always run `yarn typecheck` before committing.
|
||||
|
||||
**Type Usage:**
|
||||
|
||||
- Use TypeScript for all code
|
||||
- Prefer `type` over `interface`
|
||||
- Avoid `enum` - use objects or maps
|
||||
- Avoid `any` or `unknown` unless absolutely necessary
|
||||
- Look for existing type definitions in the codebase
|
||||
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
|
||||
- Use TypeScript for all code; look for existing type definitions before adding new ones
|
||||
- Prefer `satisfies` over type assertions (`as`, `!`) for narrowing
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., `isLoaded`, `hasError`)
|
||||
- Named exports for components and utilities (avoid default exports except in specific cases)
|
||||
|
||||
**Code Structure:**
|
||||
|
||||
@@ -298,14 +287,7 @@ Always run `yarn typecheck` before committing.
|
||||
**React Patterns:**
|
||||
|
||||
- The project uses **React Compiler** (`babel-plugin-react-compiler`) in the desktop-client. The compiler auto-memoizes component bodies, so you can omit manual `useCallback`, `useMemo`, and `React.memo` when adding or refactoring code; prefer inline callbacks and values unless a stable identity is required by a non-compiled dependency.
|
||||
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
|
||||
- Don't use `React.*` patterns - use named imports instead
|
||||
- Use `<Link>` instead of `<a>` tags
|
||||
- Use custom hooks from `src/hooks` (not react-router directly):
|
||||
- `useNavigate()` from `src/hooks` (not react-router)
|
||||
- `useDispatch()`, `useSelector()`, `useStore()` from `src/redux` (not react-redux)
|
||||
- Avoid unstable nested components
|
||||
- Use `satisfies` for type narrowing
|
||||
|
||||
**JSX Style:**
|
||||
|
||||
@@ -314,37 +296,14 @@ Always run `yarn typecheck` before committing.
|
||||
- Use concise syntax for simple statements
|
||||
- Prefer explicit expressions (`condition && <Component />`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically organized by ESLint with the following order:
|
||||
|
||||
1. React imports (first)
|
||||
2. Built-in Node.js modules
|
||||
3. External packages
|
||||
4. Actual packages (`loot-core`, `@actual-app/components` - legacy pattern `loot-design` may appear in old code)
|
||||
5. Parent imports
|
||||
6. Sibling imports
|
||||
7. Index imports
|
||||
|
||||
Always maintain newlines between import groups.
|
||||
|
||||
### Platform-Specific Code
|
||||
|
||||
- Don't directly reference platform-specific imports (`.api`, `.electron`)
|
||||
- Use conditional exports in `loot-core` for platform-specific code
|
||||
- Platform resolution happens at build time via package.json exports
|
||||
- Use conditional exports in `loot-core` for platform-specific code; platform
|
||||
resolution happens at build time via package.json exports. Don't directly
|
||||
import another platform's modules (`.api`, `.electron`).
|
||||
|
||||
### Restricted Patterns
|
||||
|
||||
**Never:**
|
||||
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
|
||||
For commit and PR rules, see
|
||||
[PR and Commit Rules](.github/agents/pr-and-commit-rules.md).
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
@@ -386,7 +345,10 @@ describe('ComponentName', () => {
|
||||
|
||||
- `/package.json` - Root workspace configuration, scripts
|
||||
- `/lage.config.js` - Lage task runner configuration
|
||||
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
||||
- `/.oxlintrc.json` - Lint rules (oxlint); `/.oxfmtrc.json` - formatting (oxfmt)
|
||||
- `/.nano-staged.json` - pre-commit format/lint config (run via Husky)
|
||||
- `/.claude/settings.json`, `/.codex/config.toml`, `/.cursor/hooks.json` - agent
|
||||
hook wiring; shared scripts live in `/scripts/agent-hooks/`
|
||||
- `/tsconfig.json` - Root TypeScript configuration
|
||||
- `/.cursorignore`, `/.gitignore` - Ignored files
|
||||
- `/yarn.lock` - Dependency lockfile (Yarn 4)
|
||||
@@ -479,13 +441,9 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
|
||||
### Linter Errors
|
||||
|
||||
1. Run `yarn lint:fix` to auto-fix many issues
|
||||
2. Check ESLint output for specific rule violations
|
||||
3. Custom rules:
|
||||
- `actual/no-untranslated-strings` - Add i18n
|
||||
- `actual/prefer-trans-over-t` - Use Trans component
|
||||
- `actual/prefer-logger-over-console` - Use logger
|
||||
- Check `eslint.config.mjs` for complete rules
|
||||
Run `yarn lint` to check. All rules — including the custom `actual/*` rules
|
||||
(`no-untranslated-strings`, `prefer-trans-over-t`, `prefer-logger-over-console`,
|
||||
`typography`, …) — are defined in [`.oxlintrc.json`](.oxlintrc.json).
|
||||
|
||||
### Test Failures
|
||||
|
||||
@@ -501,7 +459,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
1. Check `tsconfig.json` for path mappings
|
||||
2. Check package.json `exports` field (especially for loot-core)
|
||||
3. Verify platform-specific imports (`.electron`, `.api`)
|
||||
4. Use absolute imports in `desktop-client` (enforced by ESLint)
|
||||
4. Use absolute imports in `desktop-client`
|
||||
|
||||
### Build Failures
|
||||
|
||||
@@ -544,15 +502,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Prefer `type` over `interface`
|
||||
- [ ] Named exports used (not default exports)
|
||||
- [ ] Imports are properly ordered
|
||||
- [ ] Platform-specific code uses proper exports
|
||||
- [ ] No unnecessary type assertions
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
@@ -633,7 +583,7 @@ When running the app for manual testing or demos, use **"View demo"** on the ini
|
||||
### Gotchas
|
||||
|
||||
- The `engines` field requires **Node.js >=22** and **Yarn ^4.9.1**. The `.nvmrc` specifies `v22/*`.
|
||||
- Pre-commit hook runs `lint-staged` (oxfmt + oxlint) via Husky. Run `yarn prepare` once after install to set up hooks.
|
||||
- Pre-commit hook runs `nano-staged` (oxfmt + oxlint, configured in `.nano-staged.json`) via Husky. Run `yarn prepare` once after install to set up hooks.
|
||||
- Lage caches test results in `.lage/`. If tests behave unexpectedly, clear with `rm -rf .lage`.
|
||||
- Native modules (`better-sqlite3`, `bcrypt`) require build tools (`gcc`, `make`, `python3`). These are pre-installed in the Cloud VM.
|
||||
- All yarn commands must be run from the repository root, never from child workspaces.
|
||||
|
||||
+1
-13
@@ -75,8 +75,8 @@
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"husky": "^9.1.7",
|
||||
"lage": "^2.15.5",
|
||||
"lint-staged": "^16.4.0",
|
||||
"minimatch": "^10.2.5",
|
||||
"nano-staged": "^0.8.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"oxfmt": "^0.44.0",
|
||||
"oxlint": "^1.59.0",
|
||||
@@ -114,18 +114,6 @@
|
||||
"socks": ">=2.8.3",
|
||||
"webpackbar": "^7.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"packages/*/{package.json,tsconfig.json}": [
|
||||
"ts-node ./bin/validate-publish-imports.ts --fix",
|
||||
"yarn sync:tsconfig-references"
|
||||
],
|
||||
"*.{js,mjs,jsx,ts,tsx,md,json,yml,yaml}": [
|
||||
"oxfmt --no-error-on-unmatched-pattern"
|
||||
],
|
||||
"*.{js,mjs,jsx,ts,tsx}": [
|
||||
"oxlint --fix --type-aware --quiet"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
"electron >= 35.0",
|
||||
"defaults"
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = eslintCompatPlugin({
|
||||
'object-shorthand-properties': require('./rules/object-shorthand-properties'),
|
||||
'prefer-const': require('./rules/prefer-const'),
|
||||
'no-anchor-tag': require('./rules/no-anchor-tag'),
|
||||
'no-enum': require('./rules/no-enum'),
|
||||
'no-react-default-import': require('./rules/no-react-default-import'),
|
||||
'prefer-subpath-imports': require('./rules/prefer-subpath-imports'),
|
||||
'enforce-boundaries': require('./rules/enforce-boundaries'),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Forbid TypeScript enum declarations in favor of objects or maps',
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
noEnum:
|
||||
'Avoid `enum` — use an object or a map instead (see contributing/code-style.md).',
|
||||
},
|
||||
},
|
||||
|
||||
createOnce(context) {
|
||||
return {
|
||||
TSEnumDeclaration(node) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'noEnum',
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
# Agent hook (Stop), shared across Claude and Codex.
|
||||
#
|
||||
# When the agent finishes its turn, run typecheck + test for each workspace this
|
||||
# branch has touched (scoped — far cheaper than the whole-monorepo check).
|
||||
# Failures are surfaced back to the agent via exit code 2 so it can fix them.
|
||||
# Adapted from a suggestion by @StephenBrown2 on PR #8089.
|
||||
set -uo pipefail
|
||||
|
||||
# Read the hook payload (Claude provides stop_hook_active to prevent loops).
|
||||
payload=$(cat 2>/dev/null || true)
|
||||
if printf '%s' "$payload" | jq -e '.stop_hook_active == true' >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Resolve the repo root regardless of which agent invoked us.
|
||||
. "$(dirname "$0")/common.sh"
|
||||
ROOT=$(resolve_repo_root)
|
||||
cd "$ROOT" 2>/dev/null || exit 0
|
||||
|
||||
# Skip if dependencies aren't installed.
|
||||
[ -f "$ROOT/node_modules/.yarn-state.yml" ] || exit 0
|
||||
|
||||
# Collect changed files, NUL-delimited so paths with spaces survive: this
|
||||
# branch's commits vs master, plus tracked + untracked working-tree changes.
|
||||
base=$(git merge-base HEAD origin/master 2>/dev/null || true)
|
||||
files=()
|
||||
while IFS= read -r -d '' f; do
|
||||
[ -n "$f" ] && files+=("$f")
|
||||
done < <(
|
||||
{
|
||||
[ -n "$base" ] && git diff -z --name-only "$base"...HEAD
|
||||
git diff -z --name-only HEAD
|
||||
git ls-files -z --others --exclude-standard
|
||||
} 2>/dev/null
|
||||
)
|
||||
[ "${#files[@]}" -gt 0 ] || exit 0
|
||||
|
||||
# Map changed source files to the workspace dir that owns them (deduped).
|
||||
# Stock macOS ships bash 3.2, which has no associative arrays (`declare -A`) and
|
||||
# evaluates `arr[$d]` subscripts as arithmetic (slashes/hyphens in a path then
|
||||
# blow up under `set -u`). So dedupe via a space-padded string match instead —
|
||||
# `ws_dirs` stays a plain indexed array, which bash 3.2 handles fine.
|
||||
seen=" "
|
||||
ws_dirs=()
|
||||
for f in "${files[@]}"; do
|
||||
case "$f" in
|
||||
*.js | *.mjs | *.jsx | *.ts | *.tsx) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
d=$(dirname "$f")
|
||||
while [ "$d" != "." ] && [ "$d" != "/" ]; do
|
||||
if [ -f "$ROOT/$d/package.json" ]; then
|
||||
case "$seen" in
|
||||
*" $d "*) ;;
|
||||
*)
|
||||
seen="$seen$d "
|
||||
ws_dirs+=("$d")
|
||||
;;
|
||||
esac
|
||||
break
|
||||
fi
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
done
|
||||
[ "${#ws_dirs[@]}" -gt 0 ] || exit 0
|
||||
|
||||
fail=0
|
||||
report=""
|
||||
for d in "${ws_dirs[@]}"; do
|
||||
# One jq read per package.json: name + which scripts exist. Every field is a
|
||||
# non-empty token (the name falls back to "-", the flags to yes/no) so that
|
||||
# `read` can't collapse an empty middle field — tab is IFS-whitespace, so a
|
||||
# package with test-but-no-typecheck would otherwise shift the columns.
|
||||
IFS=$'\t' read -r name tc te < <(
|
||||
jq -r '[.name // "-",
|
||||
(if .scripts.typecheck then "yes" else "no" end),
|
||||
(if .scripts.test then "yes" else "no" end)] | @tsv' "$ROOT/$d/package.json"
|
||||
)
|
||||
[ "$name" != "-" ] || continue
|
||||
|
||||
if [ "$tc" = "yes" ]; then
|
||||
if ! out=$(yarn workspace "$name" run typecheck 2>&1); then
|
||||
report="$report"$'\n'"[typecheck failed: $name]"$'\n'"$(printf '%s' "$out" | tail -25)"
|
||||
fail=1
|
||||
fi
|
||||
fi
|
||||
if [ "$te" = "yes" ]; then
|
||||
if ! out=$(yarn workspace "$name" run test 2>&1); then
|
||||
report="$report"$'\n'"[tests failed: $name]"$'\n'"$(printf '%s' "$out" | tail -25)"
|
||||
fail=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -eq 1 ]; then
|
||||
printf 'Stop check found failures in touched workspaces:%s\n' "$report" >&2
|
||||
exit 2
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# Shared helpers for the agent-hook scripts. Source this file; don't execute it.
|
||||
|
||||
# Resolve the repo root regardless of which agent invoked the hook.
|
||||
# Optional $1: a directory hint for the `git` fallback (e.g. an edited file's dir).
|
||||
resolve_repo_root() {
|
||||
if [ -n "${CLAUDE_PROJECT_DIR:-}" ]; then
|
||||
printf '%s\n' "$CLAUDE_PROJECT_DIR"
|
||||
return 0
|
||||
fi
|
||||
_hint=${1:-.}
|
||||
_root=$(git -C "$_hint" rev-parse --show-toplevel 2>/dev/null) || _root=
|
||||
[ -n "$_root" ] || _root=$(CDPATH= cd "$(dirname "$0")/../.." && pwd)
|
||||
printf '%s\n' "$_root"
|
||||
}
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Agent hook (PostToolUse / afterFileEdit), shared across Claude, Codex, Cursor.
|
||||
#
|
||||
# Formats and lints just the single file the agent edited, so generated code
|
||||
# stays consistent with oxfmt/oxlint without relying on the agent remembering to
|
||||
# run `yarn lint:fix`. See AGENTS.md "Agent hooks".
|
||||
#
|
||||
# `--type-aware` is intentionally skipped here to keep the edit loop fast; the
|
||||
# type-aware pass still runs at commit time via nano-staged.
|
||||
|
||||
file=$(jq -r '.tool_input.file_path // empty')
|
||||
[ -n "$file" ] && [ -f "$file" ] || exit 0
|
||||
|
||||
# Resolve the repo root regardless of which agent invoked us.
|
||||
. "$(dirname "$0")/common.sh"
|
||||
ROOT=$(resolve_repo_root "$(dirname "$file")")
|
||||
BIN="$ROOT/node_modules/.bin"
|
||||
[ -x "$BIN/oxfmt" ] || exit 0
|
||||
|
||||
case "$file" in
|
||||
*.js | *.mjs | *.jsx | *.ts | *.tsx | *.md | *.json | *.yml | *.yaml)
|
||||
"$BIN/oxfmt" --no-error-on-unmatched-pattern "$file" >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$file" in
|
||||
*.js | *.mjs | *.jsx | *.ts | *.tsx)
|
||||
"$BIN/oxlint" --fix --quiet "$file" >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
# Shared agent guard for shell/Bash commands (Claude PreToolUse[Bash],
|
||||
# Codex PreToolUse[^Bash$], Cursor beforeShellExecution via adapter).
|
||||
#
|
||||
# Deterministically enforces the project's git-safety and workspace rules that
|
||||
# used to live as prose in AGENTS.md / .github/agents/pr-and-commit-rules.md.
|
||||
# Reads the command from `.tool_input.command` on stdin. Exit code 2 + stderr
|
||||
# blocks the call and feeds the reason back to the agent.
|
||||
#
|
||||
# Best-effort by design: it catches the honest mistakes agents actually make
|
||||
# (plain push-to-main, a forgotten [AI] commit prefix, --no-verify, yarn in a
|
||||
# workspace). It does NOT try to defend against deliberate evasion via unusual
|
||||
# shell forms (git global options before the subcommand, `git commit -F`/`-C`/
|
||||
# `--amend`, etc.) — CI and branch protection are the real gates for what lands.
|
||||
|
||||
block() {
|
||||
echo "$1" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Fail closed: a malformed payload (jq parse failure) blocks rather than allows.
|
||||
cmd=$(jq -r '.tool_input.command // empty' 2>/dev/null) ||
|
||||
block "Blocked: could not parse the hook payload (.tool_input.command)."
|
||||
[ -z "$cmd" ] && exit 0
|
||||
|
||||
# Enforce: always run yarn from the repo root, never in a child workspace.
|
||||
# Catch cd/pushd into a package dir, or `yarn --cwd packages/...`.
|
||||
case "$cmd" in
|
||||
*"cd packages/"* | *"cd ./packages/"* | *"cd packages "* | *"cd ./packages "* | \
|
||||
*"pushd packages/"* | *"pushd ./packages/"* | *"pushd packages "* | *"pushd ./packages "* | \
|
||||
*"yarn --cwd packages/"* | *"yarn --cwd ./packages/"* | *"yarn --cwd packages "* | *"yarn --cwd ./packages "* | \
|
||||
*"yarn --cwd=packages/"* | *"yarn --cwd=./packages/"* | *"yarn --cwd=packages "* | *"yarn --cwd=./packages "*)
|
||||
case "$cmd" in
|
||||
*yarn*)
|
||||
block "Blocked: run yarn from the repo root, not a child workspace (AGENTS.md). Use 'yarn workspace <name> <cmd>' from the root instead." ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# Everything below only applies to actual git invocations. Match `git` as a
|
||||
# whole token (not a substring) so commands that merely mention git — e.g.
|
||||
# `rg "git config" AGENTS.md` — aren't falsely blocked.
|
||||
is_git=0
|
||||
set -f
|
||||
for tok in $cmd; do
|
||||
[ "$tok" = "git" ] && {
|
||||
is_git=1
|
||||
break
|
||||
}
|
||||
done
|
||||
set +f
|
||||
[ "$is_git" -eq 1 ] || exit 0
|
||||
|
||||
# Never skip git hooks.
|
||||
case "$cmd" in
|
||||
*--no-verify* | *--no-gpg-sign*)
|
||||
block "Blocked: skipping git hooks (--no-verify/--no-gpg-sign) is forbidden." ;;
|
||||
esac
|
||||
|
||||
# Never change git config (reads are fine).
|
||||
case "$cmd" in
|
||||
*"git config"*)
|
||||
case "$cmd" in
|
||||
*--get* | *--list* | *" -l "* | *" -l") ;;
|
||||
*) block "Blocked: 'git config' changes are forbidden." ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# Never force-push or push to main/master.
|
||||
case "$cmd" in
|
||||
*"git push"*)
|
||||
case "$cmd" in
|
||||
*--force* | *" -f "* | *" -f")
|
||||
block "Blocked: force push only on explicit user request. Use --force-with-lease and confirm with the user first." ;;
|
||||
esac
|
||||
# Match main/master only as a whole ref/token (padding avoids matching
|
||||
# branches like "maintenance" or "main-feature"). Also catch explicit
|
||||
# refspecs like `HEAD:refs/heads/main` / `origin refs/heads/master`.
|
||||
case " $cmd " in
|
||||
*" main "* | *" master "* | *":main "* | *":master "* | \
|
||||
*" refs/heads/main "* | *" refs/heads/master "* | \
|
||||
*":refs/heads/main "* | *":refs/heads/master "*)
|
||||
block "Blocked: never push to main/master. Push the feature branch instead." ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# Commit messages must start with [AI].
|
||||
case "$cmd" in
|
||||
*"git commit"*)
|
||||
# Extract the message from -m / --message (quoted first, then unquoted).
|
||||
# An empty result means no inline message (editor/--amend) — left alone.
|
||||
msg=$(printf '%s' "$cmd" | sed -n "s/.*-m[[:space:]]*['\"]\\([^'\"]*\\).*/\\1/p")
|
||||
[ -n "$msg" ] || msg=$(printf '%s' "$cmd" | sed -n "s/.*--message[=[:space:]][[:space:]]*['\"]\\([^'\"]*\\).*/\\1/p")
|
||||
[ -n "$msg" ] || msg=$(printf '%s' "$cmd" | sed -n "s/.*-m[[:space:]][[:space:]]*\\([^[:space:]'\"][^[:space:]]*\\).*/\\1/p")
|
||||
[ -n "$msg" ] || msg=$(printf '%s' "$cmd" | sed -n "s/.*--message[=[:space:]][[:space:]]*\\([^[:space:]'\"][^[:space:]]*\\).*/\\1/p")
|
||||
case "$msg" in
|
||||
"" | "[AI]"*) ;;
|
||||
*) block "Blocked: commit messages must start with '[AI]'. Got: $msg" ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Shared agent guard for new-file creation (Claude PreToolUse[Write],
|
||||
# Codex PreToolUse[file-edit]). Cursor can only run this advisorily (its
|
||||
# afterFileEdit cannot block) — see .cursor/hooks/after-file-edit.sh.
|
||||
#
|
||||
# New TypeScript files must be type-strict: block creating a brand-new file that
|
||||
# contains `// @ts-strict-ignore`. Existing files (241 of them) are grandfathered
|
||||
# automatically because this only fires when the target path does not yet exist.
|
||||
|
||||
input=$(cat)
|
||||
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty')
|
||||
[ -n "$file" ] || exit 0
|
||||
case "$file" in
|
||||
*.ts | *.tsx) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# Only guard new files; edits to existing files are untouched.
|
||||
[ -f "$file" ] && exit 0
|
||||
|
||||
content=$(printf '%s' "$input" | jq -r '.tool_input.content // empty')
|
||||
case "$content" in
|
||||
*"@ts-strict-ignore"*)
|
||||
echo "Blocked: new files must be type-strict — remove '// @ts-strict-ignore' and fix the types instead (AGENTS.md)." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# Agent hook (PostToolUse / afterFileEdit), shared across Claude, Codex, Cursor.
|
||||
#
|
||||
# "Prefer one component per file" — surfaced as a soft warning for *newly
|
||||
# created* .tsx files only. Existing multi-component files are grandfathered
|
||||
# (skipped if already tracked by git), and this never touches `yarn lint`, so
|
||||
# react/no-multi-comp stays out of .oxlintrc.json.
|
||||
|
||||
file=$(jq -r '.tool_input.file_path // empty')
|
||||
[ -n "$file" ] && [ -f "$file" ] || exit 0
|
||||
case "$file" in
|
||||
*.tsx) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
dir=$(dirname "$file")
|
||||
|
||||
# Resolve the repo root regardless of which agent invoked us.
|
||||
. "$(dirname "$0")/common.sh"
|
||||
ROOT=$(resolve_repo_root "$dir")
|
||||
|
||||
# Normalize to an absolute + repo-root-relative path so the git pathspec is
|
||||
# unambiguous whether the agent passed an absolute or relative file_path.
|
||||
abs=$(CDPATH= cd "$dir" 2>/dev/null && pwd)/$(basename "$file")
|
||||
rel=${abs#"$ROOT"/}
|
||||
|
||||
# Only new (untracked) files; grandfather everything already in git.
|
||||
git -C "$ROOT" ls-files --error-unmatch "$rel" >/dev/null 2>&1 && exit 0
|
||||
|
||||
BIN="$ROOT/node_modules/.bin"
|
||||
[ -x "$BIN/oxlint" ] || exit 0
|
||||
|
||||
out=$(cd "$ROOT" && "$BIN/oxlint" -D react/no-multi-comp "$abs" 2>&1)
|
||||
case "$out" in
|
||||
*no-multi-comp*)
|
||||
echo "Prefer one component per file — this new file defines multiple components; split them into separate files (AGENTS.md)." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Move mechanical agent rules into shared agent hooks (wired for Claude, Codex, and Cursor).
|
||||
@@ -10969,8 +10969,8 @@ __metadata:
|
||||
glob-hasher-linux-arm64-gnu: "npm:1.4.2"
|
||||
husky: "npm:^9.1.7"
|
||||
lage: "npm:^2.15.5"
|
||||
lint-staged: "npm:^16.4.0"
|
||||
minimatch: "npm:^10.2.5"
|
||||
nano-staged: "npm:^0.8.0"
|
||||
npm-run-all: "npm:^4.1.5"
|
||||
oxfmt: "npm:^0.44.0"
|
||||
oxlint: "npm:^1.59.0"
|
||||
@@ -11153,15 +11153,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-escapes@npm:^7.0.0":
|
||||
version: 7.1.1
|
||||
resolution: "ansi-escapes@npm:7.1.1"
|
||||
dependencies:
|
||||
environment: "npm:^1.0.0"
|
||||
checksum: 10/a67a9fd832e5e4e9c854bb1aab233416b1fcac373377fd6cc703926eb7da990b5fd12f5e37fab00f3d88dc1ebc8bd1ecffb76750e92c8cbdaa3d82038d3907d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-html-community@npm:^0.0.8":
|
||||
version: 0.0.8
|
||||
resolution: "ansi-html-community@npm:0.0.8"
|
||||
@@ -12764,15 +12755,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-cursor@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "cli-cursor@npm:5.0.0"
|
||||
dependencies:
|
||||
restore-cursor: "npm:^5.0.0"
|
||||
checksum: 10/1eb9a3f878b31addfe8d82c6d915ec2330cec8447ab1f117f4aa34f0137fbb3137ec3466e1c9a65bcb7557f6e486d343f2da57f253a2f668d691372dfa15c090
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-spinners@npm:^2.5.0":
|
||||
version: 2.9.2
|
||||
resolution: "cli-spinners@npm:2.9.2"
|
||||
@@ -12803,16 +12785,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cli-truncate@npm:^5.0.0":
|
||||
version: 5.1.0
|
||||
resolution: "cli-truncate@npm:5.1.0"
|
||||
dependencies:
|
||||
slice-ansi: "npm:^7.1.0"
|
||||
string-width: "npm:^8.0.0"
|
||||
checksum: 10/3a45844202d456548b371f6b99c5af50c43dc8cc7384378d754e5f36c302eb589c50dc6dbbd815c42411d37895d19ae88228062767e37439de55acd0de137f50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"client-only@npm:^0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "client-only@npm:0.0.1"
|
||||
@@ -13025,7 +12997,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"colorette@npm:^2.0.10, colorette@npm:^2.0.20":
|
||||
"colorette@npm:^2.0.10":
|
||||
version: 2.0.20
|
||||
resolution: "colorette@npm:2.0.20"
|
||||
checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f
|
||||
@@ -15272,13 +15244,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"environment@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "environment@npm:1.1.0"
|
||||
checksum: 10/dd3c1b9825e7f71f1e72b03c2344799ac73f2e9ef81b78ea8b373e55db021786c6b9f3858ea43a436a2c4611052670ec0afe85bc029c384cc71165feee2f4ba6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eol@npm:^0.9.1":
|
||||
version: 0.9.1
|
||||
resolution: "eol@npm:0.9.1"
|
||||
@@ -16864,7 +16829,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0, get-east-asian-width@npm:^1.3.1":
|
||||
"get-east-asian-width@npm:^1.0.0":
|
||||
version: 1.4.0
|
||||
resolution: "get-east-asian-width@npm:1.4.0"
|
||||
checksum: 10/c9ae85bfc2feaf4cc71cdb236e60f1757ae82281964c206c6aa89a25f1987d326ddd8b0de9f9ccd56e37711b9fcd988f7f5137118b49b0b45e19df93c3be8f45
|
||||
@@ -18535,15 +18500,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-fullwidth-code-point@npm:^5.0.0":
|
||||
version: 5.1.0
|
||||
resolution: "is-fullwidth-code-point@npm:5.1.0"
|
||||
dependencies:
|
||||
get-east-asian-width: "npm:^1.3.1"
|
||||
checksum: 10/4700d8a82cb71bd2a2955587b2823c36dc4660eadd4047bfbd070821ddbce8504fc5f9b28725567ecddf405b1e06c6692c9b719f65df6af9ec5262bc11393a6a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-generator-function@npm:^1.0.10, is-generator-function@npm:^1.0.7":
|
||||
version: 1.1.2
|
||||
resolution: "is-generator-function@npm:1.1.2"
|
||||
@@ -19590,22 +19546,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lint-staged@npm:^16.4.0":
|
||||
version: 16.4.0
|
||||
resolution: "lint-staged@npm:16.4.0"
|
||||
dependencies:
|
||||
commander: "npm:^14.0.3"
|
||||
listr2: "npm:^9.0.5"
|
||||
picomatch: "npm:^4.0.3"
|
||||
string-argv: "npm:^0.3.2"
|
||||
tinyexec: "npm:^1.0.4"
|
||||
yaml: "npm:^2.8.2"
|
||||
bin:
|
||||
lint-staged: bin/lint-staged.js
|
||||
checksum: 10/eb7aa0d43e321bbf282ce0c693a3db762aa9b8e5bf29419a49c8877a247cd3a14cf8d519f914f8c8dcd2aea73ac83c0bb44fadb97a30004219e060a780dda74e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listify@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "listify@npm:1.0.3"
|
||||
@@ -19613,20 +19553,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"listr2@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "listr2@npm:9.0.5"
|
||||
dependencies:
|
||||
cli-truncate: "npm:^5.0.0"
|
||||
colorette: "npm:^2.0.20"
|
||||
eventemitter3: "npm:^5.0.1"
|
||||
log-update: "npm:^6.1.0"
|
||||
rfdc: "npm:^1.4.1"
|
||||
wrap-ansi: "npm:^9.0.0"
|
||||
checksum: 10/b78ffd60443aed9a8e0fc9162eb941ea4d63210700d61a895eb29348f23fc668327e26bbca87a9e6a6208e7fa96c475fe1f1c6c19b46f376f547e0cba3b935ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"load-json-file@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "load-json-file@npm:4.0.0"
|
||||
@@ -19804,19 +19730,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"log-update@npm:^6.1.0":
|
||||
version: 6.1.0
|
||||
resolution: "log-update@npm:6.1.0"
|
||||
dependencies:
|
||||
ansi-escapes: "npm:^7.0.0"
|
||||
cli-cursor: "npm:^5.0.0"
|
||||
slice-ansi: "npm:^7.1.0"
|
||||
strip-ansi: "npm:^7.1.0"
|
||||
wrap-ansi: "npm:^9.0.0"
|
||||
checksum: 10/5abb4131e33b1e7f8416bb194fe17a3603d83e4657c5bf5bb81ce4187f3b00ea481643b85c3d5cefe6037a452cdcf7f1391ab8ea0d9c23e75d19589830ec4f11
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"logform@npm:^2.7.0":
|
||||
version: 2.7.0
|
||||
resolution: "logform@npm:2.7.0"
|
||||
@@ -21085,13 +20998,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-function@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "mimic-function@npm:5.0.1"
|
||||
checksum: 10/eb5893c99e902ccebbc267c6c6b83092966af84682957f79313311edb95e8bb5f39fb048d77132b700474d1c86d90ccc211e99bae0935447a4834eb4c882982c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-response@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "mimic-response@npm:1.0.1"
|
||||
@@ -21385,6 +21291,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nano-staged@npm:^0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "nano-staged@npm:0.8.0"
|
||||
dependencies:
|
||||
picocolors: "npm:^1.0.0"
|
||||
bin:
|
||||
nano-staged: lib/bin.js
|
||||
checksum: 10/33051331ee280e42fcb4863662678d9eabb54869282dda4d1425880d1f179a9a720369f9e9b66d551cda334cb41c447209d5884a9172783b0393069f96ae549e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^3.3.11":
|
||||
version: 3.3.11
|
||||
resolution: "nanoid@npm:3.3.11"
|
||||
@@ -21879,15 +21796,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"onetime@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "onetime@npm:7.0.0"
|
||||
dependencies:
|
||||
mimic-function: "npm:^5.0.0"
|
||||
checksum: 10/eb08d2da9339819e2f9d52cab9caf2557d80e9af8c7d1ae86e1a0fef027d00a88e9f5bd67494d350df360f7c559fbb44e800b32f310fb989c860214eacbb561c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"open@npm:^10.0.3, open@npm:^10.2.0":
|
||||
version: 10.2.0
|
||||
resolution: "open@npm:10.2.0"
|
||||
@@ -25330,16 +25238,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"restore-cursor@npm:^5.0.0":
|
||||
version: 5.1.0
|
||||
resolution: "restore-cursor@npm:5.1.0"
|
||||
dependencies:
|
||||
onetime: "npm:^7.0.0"
|
||||
signal-exit: "npm:^4.1.0"
|
||||
checksum: 10/838dd54e458d89cfbc1a923b343c1b0f170a04100b4ce1733e97531842d7b440463967e521216e8ab6c6f8e89df877acc7b7f4c18ec76e99fb9bf5a60d358d2c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"retry@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "retry@npm:0.12.0"
|
||||
@@ -25361,13 +25259,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rfdc@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "rfdc@npm:1.4.1"
|
||||
checksum: 10/2f3d11d3d8929b4bfeefc9acb03aae90f971401de0add5ae6c5e38fec14f0405e6a4aad8fdb76344bfdd20c5193110e3750cbbd28ba86d73729d222b6cf4a729
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rimraf@npm:^2.5.4":
|
||||
version: 2.7.1
|
||||
resolution: "rimraf@npm:2.7.1"
|
||||
@@ -26164,7 +26055,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0":
|
||||
"signal-exit@npm:^4.0.1":
|
||||
version: 4.1.0
|
||||
resolution: "signal-exit@npm:4.1.0"
|
||||
checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f
|
||||
@@ -26273,16 +26164,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slice-ansi@npm:^7.1.0":
|
||||
version: 7.1.2
|
||||
resolution: "slice-ansi@npm:7.1.2"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^6.2.1"
|
||||
is-fullwidth-code-point: "npm:^5.0.0"
|
||||
checksum: 10/75f61e1285c294b18c88521a0cdb22cdcbe9b0fd5e8e26f649be804cc43122aa7751bd960a968e3ed7f5aa7f3c67ac605c939019eae916870ec288e878b6fafb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slug@npm:^8.2.2":
|
||||
version: 8.2.3
|
||||
resolution: "slug@npm:8.2.3"
|
||||
@@ -26652,13 +26533,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-argv@npm:^0.3.2":
|
||||
version: 0.3.2
|
||||
resolution: "string-argv@npm:0.3.2"
|
||||
checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "string-width@npm:4.2.3"
|
||||
@@ -26692,16 +26566,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-width@npm:^8.0.0":
|
||||
version: 8.1.0
|
||||
resolution: "string-width@npm:8.1.0"
|
||||
dependencies:
|
||||
get-east-asian-width: "npm:^1.3.0"
|
||||
strip-ansi: "npm:^7.1.0"
|
||||
checksum: 10/51ee97c4ffee7b94f8a2ee785fac14f81ec9809b9fcec9a4db44e25c717c263af0cc4387c111aef76195c0718dc43766f3678c07fb542294fb0244f7bfbde883
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string.prototype.matchall@npm:^4.0.6":
|
||||
version: 4.0.12
|
||||
resolution: "string.prototype.matchall@npm:4.0.12"
|
||||
@@ -27343,7 +27207,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4":
|
||||
"tinyexec@npm:^1.0.1, tinyexec@npm:^1.0.2":
|
||||
version: 1.0.4
|
||||
resolution: "tinyexec@npm:1.0.4"
|
||||
checksum: 10/ccebe4044eef6fa5050929df7862fda70b4fb700f15d94aef8ae6109b9d194dbc3a990125d99944fd25b90fe2115e1927f055b909a604c571a81b647ede5757a
|
||||
@@ -29592,15 +29456,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yaml@npm:^2.8.2":
|
||||
version: 2.8.2
|
||||
resolution: "yaml@npm:2.8.2"
|
||||
bin:
|
||||
yaml: bin.mjs
|
||||
checksum: 10/4eab0074da6bc5a5bffd25b9b359cf7061b771b95d1b3b571852098380db3b1b8f96e0f1f354b56cc7216aa97cea25163377ccbc33a2e9ce00316fe8d02f4539
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs-parser@npm:^18.1.3":
|
||||
version: 18.1.3
|
||||
resolution: "yargs-parser@npm:18.1.3"
|
||||
|
||||
Reference in New Issue
Block a user