diff --git a/.coding-ci.yaml b/.coding-ci.yaml index c27de9c67..1ce88c266 100644 --- a/.coding-ci.yaml +++ b/.coding-ci.yaml @@ -19,7 +19,9 @@ stages: cmds: - node --version && npm --version - npm ci --ignore-scripts - - echo "=== Lint (typecheck) ===" + - echo "=== Lint ===" + - npm run lint + - echo "=== Typecheck ===" - npx tsc --noEmit - echo "=== Unit Tests + Coverage ===" - npx vitest run --coverage diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8808232ac..20b431824 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,11 +15,14 @@ npm install ```bash npm run build # Build with tsup → dist/ npx tsc --noEmit # Type check +npm run lint # oxlint; CI fails on any warning npx vitest run # Run unit tests npx vitest run --coverage npm run test:e2e # E2E tests (optional, requires a live test repo) ``` +`npm run lint` needs Node ^20.19 or >=22.12 (oxlint's requirement); the CLI itself still supports Node 20. + ### Running your local build ```bash @@ -75,7 +78,7 @@ See [docs/providers.md](../docs/providers.md) for how to add a new git provider. 1. Fork the repo and create a feature branch from the latest `origin/main`. Prefer a git worktree for code changes when practical. 2. Write tests for your change (we target 80%+ coverage). -3. Run `npx vitest run` and `npx tsc --noEmit` — both must pass. +3. Run `npx vitest run`, `npx tsc --noEmit` and `npm run lint` — all must pass. 4. Use conventional commits where possible: `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`. 5. Open a PR with a clear description: what's the problem, what's the fix, anything reviewers should pay attention to. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fdf24ab02..24c713c66 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,6 +19,7 @@ Thanks for opening a PR! Please fill out this template to help reviewers underst - [ ] `npx tsc --noEmit` passes +- [ ] `npm run lint` passes - [ ] `npx vitest run` passes - [ ] Added/updated tests for the change diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20648693c..20f545e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts + - name: Lint + run: npm run lint + - name: Type check run: npx tsc --noEmit diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..332c08c17 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,13 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "rules": { + "no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index 08e569325..f6a93071d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ CLI for syncing team skills, rules, docs, and env across AI coding tools. Package: [`teamai-cli`](https://www.npmjs.com/package/teamai-cli). -TypeScript, Node 20+, tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc --noEmit`, `npx vitest run`, `npm run test:e2e`. +TypeScript, Node 20+ (`npm run lint` needs ^20.19 or >=22.12), tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc --noEmit`, `npm run lint`, `npx vitest run`, `npm run test:e2e`. ## Git diff --git a/CLAUDE.md b/CLAUDE.md index 8ad520d3d..ce71e501f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ CLI for syncing team skills, rules, docs, and env across AI coding tools. Package: [`teamai-cli`](https://www.npmjs.com/package/teamai-cli). -TypeScript, Node 20+, tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc --noEmit`, `npx vitest run`, `npm run test:e2e`. +TypeScript, Node 20+ (`npm run lint` needs ^20.19 or >=22.12), tsup (ESM), Vitest. Commands: `npm run build`, `npx tsc --noEmit`, `npm run lint`, `npx vitest run`, `npm run test:e2e`. ## Git diff --git a/package-lock.json b/package-lock.json index cef754945..b0a548ae1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@vitest/coverage-v8": "^3.2.7", "fast-check": "^4.10.2", "opencode-ai": "1.18.23", + "oxlint": "1.85.0", "standard-version": "^9.5.0", "tsup": "^8.3.0", "typescript": "^5.7.0", @@ -666,6 +667,353 @@ "resolved": "https://mirrors.tencent.com/npm/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz", + "integrity": "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz", + "integrity": "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz", + "integrity": "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz", + "integrity": "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz", + "integrity": "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz", + "integrity": "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz", + "integrity": "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz", + "integrity": "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz", + "integrity": "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz", + "integrity": "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz", + "integrity": "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz", + "integrity": "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz", + "integrity": "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz", + "integrity": "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz", + "integrity": "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz", + "integrity": "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz", + "integrity": "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz", + "integrity": "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz", + "integrity": "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://mirrors.tencent.com/npm/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3865,6 +4213,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxlint": { + "version": "1.85.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.85.0.tgz", + "integrity": "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.85.0", + "@oxlint/binding-android-arm64": "1.85.0", + "@oxlint/binding-darwin-arm64": "1.85.0", + "@oxlint/binding-darwin-x64": "1.85.0", + "@oxlint/binding-freebsd-x64": "1.85.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", + "@oxlint/binding-linux-arm-musleabihf": "1.85.0", + "@oxlint/binding-linux-arm64-gnu": "1.85.0", + "@oxlint/binding-linux-arm64-musl": "1.85.0", + "@oxlint/binding-linux-ppc64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-gnu": "1.85.0", + "@oxlint/binding-linux-riscv64-musl": "1.85.0", + "@oxlint/binding-linux-s390x-gnu": "1.85.0", + "@oxlint/binding-linux-x64-gnu": "1.85.0", + "@oxlint/binding-linux-x64-musl": "1.85.0", + "@oxlint/binding-openharmony-arm64": "1.85.0", + "@oxlint/binding-win32-arm64-msvc": "1.85.0", + "@oxlint/binding-win32-ia32-msvc": "1.85.0", + "@oxlint/binding-win32-x64-msvc": "1.85.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://mirrors.tencent.com/npm/p-limit/-/p-limit-2.3.0.tgz", diff --git a/package.json b/package.json index a32f05b32..e8f15e90f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", + "lint": "oxlint --deny-warnings --report-unused-disable-directives", "release": "standard-version", "prepublishOnly": "npm run build" }, @@ -82,6 +83,7 @@ "@vitest/coverage-v8": "^3.2.7", "fast-check": "^4.10.2", "opencode-ai": "1.18.23", + "oxlint": "1.85.0", "standard-version": "^9.5.0", "tsup": "^8.3.0", "typescript": "^5.7.0", diff --git a/src/__tests__/agent-version.test.ts b/src/__tests__/agent-version.test.ts index 0229b07b3..bb3b851c1 100644 --- a/src/__tests__/agent-version.test.ts +++ b/src/__tests__/agent-version.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { execFile } from 'node:child_process'; import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; diff --git a/src/__tests__/builtin-rules.test.ts b/src/__tests__/builtin-rules.test.ts index a9c0da0e7..5f2b75356 100644 --- a/src/__tests__/builtin-rules.test.ts +++ b/src/__tests__/builtin-rules.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; diff --git a/src/__tests__/codebase-reconcile.test.ts b/src/__tests__/codebase-reconcile.test.ts index 7ffa14c69..31f795a56 100644 --- a/src/__tests__/codebase-reconcile.test.ts +++ b/src/__tests__/codebase-reconcile.test.ts @@ -228,7 +228,6 @@ describe('codebase reconciliation', () => { it('normalizes graph variants emitted by legacy aggregation', async () => { const root = createWikiFixture(); - const graphPath = path.join(root, 'teamwiki', '.indices', 'graph-index.json'); const repoGraphPath = path.join(root, 'teamwiki', 'evidence', 'code', 'auth', '.indices', 'graph-index.json'); const legacyGraph = { schemaVersion: 1, diff --git a/src/__tests__/codebase.test.ts b/src/__tests__/codebase.test.ts index 669075df2..6514cd0b6 100644 --- a/src/__tests__/codebase.test.ts +++ b/src/__tests__/codebase.test.ts @@ -164,7 +164,6 @@ describe('generateCodebaseIndex', () => { const result = await generateCodebaseIndex('# Codebase'); - expect(result).not.toThrow; expect(result).toContain('title: Codebase 索引'); expect(result).toContain('⚠️'); }); diff --git a/src/__tests__/contribute-check-phase2.test.ts b/src/__tests__/contribute-check-phase2.test.ts index 5c48c4f52..bf53e6981 100644 --- a/src/__tests__/contribute-check-phase2.test.ts +++ b/src/__tests__/contribute-check-phase2.test.ts @@ -2,13 +2,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import { applyPhase2Adjustments, hasGitCommitInSession, contributeCheckForSession, writeContributeState } from '../contribute-check.js'; +import { applyPhase2Adjustments, hasGitCommitInSession, contributeCheckForSession } from '../contribute-check.js'; import { appendEvent } from '../dashboard-collector.js'; import { CONTRIBUTE_KNOWLEDGE_GAP_BONUS, CONTRIBUTE_LOW_QUALITY_BONUS, - CONTRIBUTE_LOW_QUALITY_THRESHOLD, - CONTRIBUTE_GIT_COMMIT_DOWNWEIGHT, } from '../types.js'; function makeTmpDir(): string { diff --git a/src/__tests__/contribute-check.test.ts b/src/__tests__/contribute-check.test.ts index b062309f8..e945303fe 100644 --- a/src/__tests__/contribute-check.test.ts +++ b/src/__tests__/contribute-check.test.ts @@ -121,7 +121,7 @@ describe('contributeState', () => { const persisted = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as ContributeState; expect(persisted.promptSummary).toContain(''); expect(persisted.promptSummary).not.toContain(rawToken); - expect(persisted.promptSummary).not.toMatch(/[\r\n\t\u0000]/); + expect(persisted.promptSummary).not.toMatch(/\p{Cc}/u); expect(persisted.promptSummary).toHaveLength(160); expect(persisted.promptSummary).toMatch(/…$/); expect(await readContributeState('safe-prompt-session')).toEqual(persisted); diff --git a/src/__tests__/contribute.test.ts b/src/__tests__/contribute.test.ts index 2319b856e..b8e3a1690 100644 --- a/src/__tests__/contribute.test.ts +++ b/src/__tests__/contribute.test.ts @@ -102,14 +102,12 @@ describe('contribute', () => { })); const { contribute } = await import('../contribute.js'); + const before = fs.readdirSync(tmpDir, { recursive: true }); - // dry-run should not push + // dry-run should not push, nor write anything into the repo (tmpDir is also HOME) await contribute({ file: contentFile, title: 'Test', dryRun: true }); - // No learnings directory should be created in the repo - const aiDocsDir = path.join(tmpDir, 'learnings'); - // In dry-run, the file should NOT be copied - // (contribute exits early before mkdir) + expect(fs.readdirSync(tmpDir, { recursive: true })).toEqual(before); vi.doUnmock('../config.js'); }); }); diff --git a/src/__tests__/dashboard-report-gate.test.ts b/src/__tests__/dashboard-report-gate.test.ts index ecb7d3e39..8b7bf4fc3 100644 --- a/src/__tests__/dashboard-report-gate.test.ts +++ b/src/__tests__/dashboard-report-gate.test.ts @@ -21,7 +21,6 @@ import { _setLogFilePath, _resetState } from '../utils/logger.js'; let tmpDir: string; let originalHome: string; -let consoleLog: ReturnType; /** The machine-wide dashboard event log — one file, not per-scope. */ function eventsPath(): string { @@ -64,7 +63,7 @@ beforeEach(() => { originalHome = process.env.HOME ?? ''; process.env.HOME = tmpDir; _setLogFilePath(path.join(tmpDir, '.teamai', 'debug.log')); - consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + vi.spyOn(console, 'log').mockImplementation(() => undefined); }); afterEach(() => { diff --git a/src/__tests__/doctor.test.ts b/src/__tests__/doctor.test.ts index 4933c8391..66a42405d 100644 --- a/src/__tests__/doctor.test.ts +++ b/src/__tests__/doctor.test.ts @@ -95,15 +95,6 @@ function buildFullHooksContent(): string { return `{ "hooks": { ${lines.join(', ')} } }`; } -// Build a settings content that is missing some subcommands -function buildPartialHooksContent(exclude: string[]): string { - const subs = TEAMAI_HOOK_SUBCOMMANDS.filter((s) => !exclude.includes(s)); - const lines = subs.map( - (sub) => `"command": "bash -lc \\"teamai ${sub}\\""`, - ); - return `{ "hooks": { ${lines.join(', ')} } }`; -} - // ── Setup ──────────────────────────────────────────────── // Suppress console.log output in tests diff --git a/src/__tests__/e2e/e2e.test.ts b/src/__tests__/e2e/e2e.test.ts index 9bb4f1a56..62ff15dce 100644 --- a/src/__tests__/e2e/e2e.test.ts +++ b/src/__tests__/e2e/e2e.test.ts @@ -3,7 +3,6 @@ import { spawn } from 'node:child_process'; import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; -import { createRequire } from 'node:module'; // ─── Helpers ───────────────────────────────────────────── @@ -11,8 +10,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..', '..', '..'); const CLI = path.join(ROOT, 'dist', 'index.js'); -const require = createRequire(import.meta.url); - interface RunResult { code: number | null; stdout: string; @@ -133,7 +130,7 @@ describe('tags CLI', () => { it('teamai tags list (no init) should show error', async () => { // Run in a temp dir with no teamai init - const { output, code } = await runCLI(['tags', 'list']); + const { output } = await runCLI(['tags', 'list']); // Either shows tags or shows "not initialized" error — both valid expect(output.length).toBeGreaterThan(0); }); diff --git a/src/__tests__/e2e/init-project-all.test.ts b/src/__tests__/e2e/init-project-all.test.ts index eb39ba9be..9b26d5490 100644 --- a/src/__tests__/e2e/init-project-all.test.ts +++ b/src/__tests__/e2e/init-project-all.test.ts @@ -82,12 +82,12 @@ function installGitRemoteGetUrlWrapper(binDir: string, syntheticOrigin: string): if [ "$1" = "remote" ] && [ "$2" = "get-url" ]; then case "$3" in ""|origin|--all) - printf '%s\\n' '${syntheticOrigin.replace(/'/g, `'\"'\"'`)}' + printf '%s\\n' '${syntheticOrigin.replace(/'/g, `'"'"'`)}' exit 0 ;; esac fi -exec '${realGit.replace(/'/g, `'\"'\"'`)}' "$@" +exec '${realGit.replace(/'/g, `'"'"'`)}' "$@" `, { mode: 0o755 }, ); diff --git a/src/__tests__/git.test.ts b/src/__tests__/git.test.ts index 168d8c651..7460c0939 100644 --- a/src/__tests__/git.test.ts +++ b/src/__tests__/git.test.ts @@ -66,7 +66,6 @@ describe('generateBranchName', () => { it('should use the correct current date components', () => { const before = new Date(); const name = generateBranchName('bob'); - const after = new Date(); // Extract the date part const match = name.match(/^teamai\/push\/bob\/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/); diff --git a/src/__tests__/gitcode-provider.test.ts b/src/__tests__/gitcode-provider.test.ts index bed0cd485..379cc9a2b 100644 --- a/src/__tests__/gitcode-provider.test.ts +++ b/src/__tests__/gitcode-provider.test.ts @@ -125,7 +125,7 @@ describe('GitCode REST calls', () => { expect(await gitcodeWhoami()).toBe('octocat'); const [url, init] = fetchMock.mock.calls[0]; expect(String(url)).toBe('https://api.gitcode.com/api/v5/user'); - expect((init?.headers as Record).Authorization).toBe('Bearer test-token'); + expect((init?.headers as Record | undefined)?.Authorization).toBe('Bearer test-token'); }); it('pullCreate posts head/base/title/body and returns html_url', async () => { diff --git a/src/__tests__/graph-aggregate.test.ts b/src/__tests__/graph-aggregate.test.ts index 1362e9c9c..6a76c7ef9 100644 --- a/src/__tests__/graph-aggregate.test.ts +++ b/src/__tests__/graph-aggregate.test.ts @@ -98,7 +98,7 @@ describe('aggregateGlobalGraph', () => { edges: [], }); - const result = await aggregateGlobalGraph(tmpDir); + await aggregateGlobalGraph(tmpDir); const global = JSON.parse(await fs.readFile( path.join(tmpDir, '.indices', 'graph-index.json'), 'utf8', )); diff --git a/src/__tests__/hook-dispatch.test.ts b/src/__tests__/hook-dispatch.test.ts index cc1b2f52c..5e03197f6 100644 --- a/src/__tests__/hook-dispatch.test.ts +++ b/src/__tests__/hook-dispatch.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; // ── Test doubles ──────────────────────────────────────── @@ -19,8 +19,6 @@ function createHandler(name: string, output?: string): TestHandler { import { createDispatcher, - type HookHandler, - type DispatchResult, } from '../hook-dispatch.js'; // ── Tests ─────────────────────────────────────────────── diff --git a/src/__tests__/hook-handlers.test.ts b/src/__tests__/hook-handlers.test.ts index 506225af9..55622efde 100644 --- a/src/__tests__/hook-handlers.test.ts +++ b/src/__tests__/hook-handlers.test.ts @@ -151,7 +151,7 @@ vi.mock('../project-agent-root.js', () => ({ seedProjectAgentRoot: mockSeedProjectAgentRoot, })); -import { buildAdoptedSummary, buildHandlerRegistry, filterHandlersForConfig, type HandlerRegistration } from '../hook-handlers.js'; +import { buildAdoptedSummary, buildHandlerRegistry, filterHandlersForConfig } from '../hook-handlers.js'; import { createDispatcher } from '../hook-dispatch.js'; import type { LocalConfig } from '../types.js'; diff --git a/src/__tests__/import-repo-incremental.test.ts b/src/__tests__/import-repo-incremental.test.ts index d4f64ee57..c0c1be074 100644 --- a/src/__tests__/import-repo-incremental.test.ts +++ b/src/__tests__/import-repo-incremental.test.ts @@ -103,7 +103,6 @@ describe('importFromRepo — incremental mode', () => { await importFromRepo({ url: TEST_URL, incremental: true, - interactive: false, }); expect(shallowClone).toHaveBeenCalledTimes(1); @@ -116,7 +115,6 @@ describe('importFromRepo — incremental mode', () => { await importFromRepo({ url: TEST_URL, incremental: true, - interactive: false, }); expect(shallowFetch).toHaveBeenCalledTimes(1); @@ -130,7 +128,6 @@ describe('importFromRepo — incremental mode', () => { await importFromRepo({ url: TEST_URL, incremental: true, - interactive: false, }); expect(shallowFetch).toHaveBeenCalledTimes(1); @@ -143,7 +140,6 @@ describe('importFromRepo — incremental mode', () => { await importFromRepo({ url: TEST_URL, incremental: false, - interactive: false, }); expect(shallowClone).toHaveBeenCalledTimes(1); @@ -154,7 +150,6 @@ describe('importFromRepo — incremental mode', () => { await importFromRepo({ url: TEST_URL, incremental: false, - interactive: false, }); const lastSyncPath = path.join(workdir, 'cache', 'github', 'owner', 'testrepo', 'LAST_SYNC'); diff --git a/src/__tests__/import-repo-merge.test.ts b/src/__tests__/import-repo-merge.test.ts index 415c27825..90b65f7b8 100644 --- a/src/__tests__/import-repo-merge.test.ts +++ b/src/__tests__/import-repo-merge.test.ts @@ -35,7 +35,6 @@ vi.mock('../config.js', async (importOriginal) => ({ import { importFromRepo } from '../import-repo.js'; import { shallowClone, shallowFetch } from '../clone.js'; -import { generateCodebaseMd } from '../codebase.js'; import { extractCodebase } from '../codebase-extract.js'; // ─── Constants ────────────────────────────────────────── @@ -111,7 +110,6 @@ describe('importFromRepo — AI narrative appended to overview.md', () => { it('AI 叙事追加到 teamwiki evidence overview.md 末尾', async () => { await importFromRepo({ url: TEST_URL, - interactive: false, }); const overviewPath = path.join( @@ -133,7 +131,6 @@ describe('importFromRepo — AI narrative appended to overview.md', () => { it('docs/team-codebase/repos/ 不再被创建', async () => { await importFromRepo({ url: TEST_URL, - interactive: false, }); const oldPath = path.join( @@ -147,7 +144,6 @@ describe('importFromRepo — AI narrative appended to overview.md', () => { it('skipEnrich 时不追加 AI 叙事', async () => { await importFromRepo({ url: TEST_URL, - interactive: false, skipEnrich: true, }); @@ -164,7 +160,7 @@ describe('importFromRepo — AI narrative appended to overview.md', () => { it('retries extraction after a failed import of the same commit', async () => { vi.mocked(extractCodebase).mockRejectedValueOnce(new Error('write failed')); vi.mocked(shallowFetch).mockResolvedValue({ sha: CLONE_SHA }); - const options = { url: TEST_URL, incremental: true, interactive: false, skipEnrich: true, skipAutoPush: true }; + const options = { url: TEST_URL, incremental: true, skipEnrich: true, skipAutoPush: true }; const lastSyncPath = path.join(workdir, 'cache', 'github', 'owner', 'mergetest', 'LAST_SYNC'); await fs.ensureDir(path.join(path.dirname(lastSyncPath), '.git')); await fs.writeFile(lastSyncPath, 'previous-sha\n2024-01-01T00:00:00.000Z\n'); @@ -193,7 +189,7 @@ describe('importFromRepo — AI narrative appended to overview.md', () => { await fs.writeJson(manifestPath, { headSha: CLONE_SHA, files: [] }); }); - const options = { url: TEST_URL, incremental: true, interactive: false, skipEnrich: true, skipAutoPush: true }; + const options = { url: TEST_URL, incremental: true, skipEnrich: true, skipAutoPush: true }; const cacheDir = path.join(workdir, 'cache', 'github', 'owner', 'mergetest'); const lastSyncPath = path.join(cacheDir, 'LAST_SYNC'); const publishedManifest = path.join(workdir, '.teamai', 'team-repo', 'teamwiki', 'source-manifest.json'); diff --git a/src/__tests__/init.test.ts b/src/__tests__/init.test.ts index ec0e5f5c0..5c6d35696 100644 --- a/src/__tests__/init.test.ts +++ b/src/__tests__/init.test.ts @@ -232,7 +232,7 @@ vi.mock('../utils/prompt.js', () => ({ const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); import { init } from '../init.js'; -import { RepoNotFoundError, OrganizationNotFoundError, RepoCreatePermissionError } from '../providers/types.js'; +import { RepoNotFoundError, RepoCreatePermissionError } from '../providers/types.js'; import { CnbRepoNotFoundError } from '../providers/cnb/cnb-cli.js'; import { saveLocalConfig, loadLocalConfigForScope } from '../config.js'; import fse from 'fs-extra'; diff --git a/src/__tests__/local-agent-model-config.test.ts b/src/__tests__/local-agent-model-config.test.ts index ecd64d327..129d24c05 100644 --- a/src/__tests__/local-agent-model-config.test.ts +++ b/src/__tests__/local-agent-model-config.test.ts @@ -378,7 +378,7 @@ describe('local-agent: apply_model_config', () => { await reportAndSyncLocalAgent({ tool: 'codebuddy', status: 'running' }); expect(reports).toHaveLength(2); - expect((reports[1]?.user_level as { models?: unknown[] }).models).toEqual([ + expect((reports[1]?.user_level as { models?: unknown[] } | undefined)?.models).toEqual([ { provider: 'tokenhub', model_id: 'deepseek-v3-0324', diff --git a/src/__tests__/local-agent.test.ts b/src/__tests__/local-agent.test.ts index 30ed372d7..fd6178332 100644 --- a/src/__tests__/local-agent.test.ts +++ b/src/__tests__/local-agent.test.ts @@ -19,7 +19,6 @@ vi.mock('../utils/logger.js', () => ({ let tmpDir: string; let origHome: string | undefined; let origCopilotHome: string | undefined; -let origPpid: number; // The hint markers are machine-wide files in os.tmpdir() keyed by session id, // so a fixed id let overlapping runs of this file delete each other's (#823). @@ -30,7 +29,6 @@ beforeEach(async () => { origHome = process.env.HOME; origCopilotHome = process.env.COPILOT_HOME; process.env.HOME = tmpDir; - origPpid = process.ppid; // Bind prompt is on by default — start each test from that baseline. delete process.env.TEAMAI_BIND_PROMPT_ENABLED; // Clean hint markers (both old ppid-based and new sessionId-based) diff --git a/src/__tests__/logger.test.ts b/src/__tests__/logger.test.ts index 7884d0385..17d012753 100644 --- a/src/__tests__/logger.test.ts +++ b/src/__tests__/logger.test.ts @@ -10,7 +10,7 @@ vi.mock('chalk', () => ({ default: { blue: (s: string) => s, green: (s: string) => s, yellow: (s: string) => s, red: (s: string) => s, gray: (s: string) => s, dim: (s: string) => s }, })); -import { log, setVerbose, setSilent, setStderrOnly, MAX_LOG_BYTES, _setLogFilePath, _resetState } from '../utils/logger.js'; +import { log, setVerbose, setSilent, setStderrOnly, _setLogFilePath, _resetState } from '../utils/logger.js'; let tmpDir: string; let logFile: string; diff --git a/src/__tests__/mr-hint.test.ts b/src/__tests__/mr-hint.test.ts index 91042d2b8..ff1abc5db 100644 --- a/src/__tests__/mr-hint.test.ts +++ b/src/__tests__/mr-hint.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { parseRemoteToRepo, buildHintMessage, getGitRemote } from '../mr-hint.js'; import type { MRSummary } from '../mr-hint.js'; diff --git a/src/__tests__/provider-fallback.test.ts b/src/__tests__/provider-fallback.test.ts index e19343b69..c60802a19 100644 --- a/src/__tests__/provider-fallback.test.ts +++ b/src/__tests__/provider-fallback.test.ts @@ -36,7 +36,6 @@ vi.mock('../package-info.js', () => { }; }); -// eslint-disable-next-line @typescript-eslint/no-explicit-any const pkgInfo = (await import('../package-info.js')) as any; const setPackageName = pkgInfo.__setPackageName as (name: string) => void; diff --git a/src/__tests__/pull-env-shape-warning.test.ts b/src/__tests__/pull-env-shape-warning.test.ts index 5a5661a89..042fabca2 100644 --- a/src/__tests__/pull-env-shape-warning.test.ts +++ b/src/__tests__/pull-env-shape-warning.test.ts @@ -71,7 +71,7 @@ vi.mock('../doctor.js', async (importOriginal) => ({ buildChecks: vi.fn(), })); -import { detectProjectConfig, loadLocalConfigForScope, loadStateForScope, loadTeamConfig, saveStateForScope } from '../config.js'; +import { detectProjectConfig, loadLocalConfigForScope, loadStateForScope, loadTeamConfig } from '../config.js'; import { acquireLock } from '../update.js'; import { buildChecks, resolveDoctorContext, type DoctorContext } from '../doctor.js'; import { log } from '../utils/logger.js'; diff --git a/src/__tests__/pull-tombstone.test.ts b/src/__tests__/pull-tombstone.test.ts index de254f7b7..1dad43a9d 100644 --- a/src/__tests__/pull-tombstone.test.ts +++ b/src/__tests__/pull-tombstone.test.ts @@ -561,7 +561,6 @@ describe('pull role-aware sync and cleanup', () => { await fse.ensureDir(path.join(repoPath, 'skills', 'hai', 'shared-skill')); await fse.writeFile(path.join(repoPath, 'skills', 'hai', 'shared-skill', 'SKILL.md'), '# HAI'); - const teamConfig = await vi.mocked(loadTeamConfig).mock.results.at(-1)?.value; const localConfig: LocalConfig = { repo: { localPath: repoPath, remote: 'https://git.woa.com/test/repo.git' }, username: 'testuser', diff --git a/src/__tests__/recall-rules.test.ts b/src/__tests__/recall-rules.test.ts index e135f339b..3d9c30eb2 100644 --- a/src/__tests__/recall-rules.test.ts +++ b/src/__tests__/recall-rules.test.ts @@ -100,7 +100,7 @@ Custom user content below. // New block present expect(content).toContain('teamai-recall'); // Only one occurrence of the markers - const startMatches = content.match(new RegExp(TEAMAI_RECALL_RULES_START.replace(/[\[\]\-]/g, '\\$&'), 'g')) ?? []; + const startMatches = content.match(new RegExp(TEAMAI_RECALL_RULES_START.replace(/[[\]-]/g, '\\$&'), 'g')) ?? []; expect(startMatches.length).toBe(1); }); diff --git a/src/__tests__/recall.test.ts b/src/__tests__/recall.test.ts index 21a9c4e03..215a781cb 100644 --- a/src/__tests__/recall.test.ts +++ b/src/__tests__/recall.test.ts @@ -6,7 +6,7 @@ import YAML from 'yaml'; import { autoUpvote } from '../recall.js'; import { buildIndex, loadIndex, search } from '../utils/search-index.js'; import type { SearchResult } from '../utils/search-index.js'; -import type { LocalConfig, SearchIndex, UserVotesV2 } from '../types.js'; +import type { LocalConfig, UserVotesV2 } from '../types.js'; // ─── Test helpers ────────────────────────────────────────── diff --git a/src/__tests__/roles-cmd.test.ts b/src/__tests__/roles-cmd.test.ts index 810cf4f20..57459198f 100644 --- a/src/__tests__/roles-cmd.test.ts +++ b/src/__tests__/roles-cmd.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync, existsSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { mkdtempSync, writeFileSync, rmSync, mkdirSync, existsSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import YAML from 'yaml'; diff --git a/src/__tests__/search-idf-domain-isolation.test.ts b/src/__tests__/search-idf-domain-isolation.test.ts index 2b6e1e288..b325c8859 100644 --- a/src/__tests__/search-idf-domain-isolation.test.ts +++ b/src/__tests__/search-idf-domain-isolation.test.ts @@ -25,12 +25,6 @@ import { buildIndex, loadIndex, search } from '../utils/search-index.js'; const N_TECH = 3; const N_OPS = 12; -// --------------------------------------------------------------------------- -// Shared token set: words that appear in BOTH technical AND ops fixture docs. -// Must be plain ASCII so tokenize() handles them predictably. -// --------------------------------------------------------------------------- -const SHARED_TOKENS = ['timeout', 'retry', 'config']; - // --------------------------------------------------------------------------- // Fixture helpers // --------------------------------------------------------------------------- diff --git a/src/__tests__/section-merge.test.ts b/src/__tests__/section-merge.test.ts index 5330a95c3..364b8a175 100644 --- a/src/__tests__/section-merge.test.ts +++ b/src/__tests__/section-merge.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { mergeWithAnchors, splitToSections, joinSections } from '../section-patcher.js'; +import { mergeWithAnchors } from '../section-patcher.js'; const META = { source: 'https://github.com/org/repo@deadbeef', syncedAt: '2024-01-01T00:00:00.000Z' }; const META2 = { source: 'https://github.com/org/repo@cafebabe', syncedAt: '2024-06-01T00:00:00.000Z' }; diff --git a/src/__tests__/section-patcher.test.ts b/src/__tests__/section-patcher.test.ts index 472efb570..794bb91d2 100644 --- a/src/__tests__/section-patcher.test.ts +++ b/src/__tests__/section-patcher.test.ts @@ -6,7 +6,6 @@ import { parseSections, patchManagedSection, hashBody, - mergeWithAnchors, } from '../section-patcher.js'; // ─── hashBody ──────────────────────────────────────────── diff --git a/src/__tests__/self-mode-no-business-reset.test.ts b/src/__tests__/self-mode-no-business-reset.test.ts index 19f0f65ea..c821a5829 100644 --- a/src/__tests__/self-mode-no-business-reset.test.ts +++ b/src/__tests__/self-mode-no-business-reset.test.ts @@ -53,14 +53,6 @@ describe('E2E self-mode: business repo working tree is never reset', () => { const teamaiDir = path.join(businessRoot, '.teamai'); fs.mkdirSync(teamaiDir, { recursive: true }); - // self-mode user-scope config: localPath = /.teamai - const cfg: LocalConfig = { - repo: { localPath: teamaiDir, remote: '', kind: 'self', businessRepoRoot: businessRoot }, - username: 'me', - scope: 'user', - additionalRoles: [], - } as unknown as LocalConfig; - // The bug path: pull passes selfConfig now (fix 1). Even without it, the // isDedicatedRoot guard (fix 2) must protect the tree — test the guard by // NOT passing selfConfig, forcing the else branch. diff --git a/src/__tests__/session-analytics.test.ts b/src/__tests__/session-analytics.test.ts index f5ef5872f..64029010d 100644 --- a/src/__tests__/session-analytics.test.ts +++ b/src/__tests__/session-analytics.test.ts @@ -239,22 +239,22 @@ describe('timeAnalytics', () => { it('returns empty analytics for no events', () => { const ta = timeAnalytics([]); - expect(ta).toEqual({ byHour: new Array(24).fill(0), peakHour: -1, nightOwlRatio: 0, activeMinutes: 0, totalEvents: 0 }); + expect(ta).toEqual({ byHour: Array.from({ length: 24 }, () => 0), peakHour: -1, nightOwlRatio: 0, activeMinutes: 0, totalEvents: 0 }); }); }); describe('renderHourSparkline', () => { it('renders 24 characters', () => { - const spark = renderHourSparkline(new Array(24).fill(0).map((_, i) => i)); + const spark = renderHourSparkline(Array.from({ length: 24 }, (_, i) => i)); expect(spark.length).toBe(24); }); it('renders all-zero input as 24 dots (idle is distinct from low activity)', () => { - expect(renderHourSparkline(new Array(24).fill(0))).toBe('·'.repeat(24)); + expect(renderHourSparkline(Array.from({ length: 24 }, () => 0))).toBe('·'.repeat(24)); }); it('renders a single peak as a full bar and empty hours as dots', () => { - const byHour = new Array(24).fill(0); + const byHour = Array.from({ length: 24 }, () => 0); byHour[9] = 42; const spark = renderHourSparkline(byHour); expect(spark[9]).toBe('█'); diff --git a/src/__tests__/tags-cmd.test.ts b/src/__tests__/tags-cmd.test.ts index d0a6b7b1c..af6e822d7 100644 --- a/src/__tests__/tags-cmd.test.ts +++ b/src/__tests__/tags-cmd.test.ts @@ -188,7 +188,7 @@ describe('tags list', () => { addSkill('hai', 'c'); writeFileSync(path.join(repoPath, 'tags.yaml'), 'skills:\n a: [frontend]\nrules: {}\n'); - await tagsList({}); + await tagsList(); expect(log.dim).toHaveBeenCalledWith(' 3 skill(s) have no tags and are always synced.'); }); @@ -206,7 +206,7 @@ describe('tags list', () => { writeFileSync(path.join(repoPath, 'tags.yaml'), 'skills:\n a: [frontend]\nrules: {}\n'); useConfig({ primaryRole: 'hai' }); - await tagsList({}); + await tagsList(); // pm/x is untagged but outside the member's namespaces, so pull never delivers it. expect(log.dim).toHaveBeenCalledWith(' 1 skill(s) have no tags and are always synced.'); @@ -218,7 +218,7 @@ describe('tags list', () => { addSkill('hai', 'a'); writeFileSync(path.join(repoPath, 'tags.yaml'), 'skills:\n a: [frontend]\nrules: {}\n'); - await tagsList({}); + await tagsList(); expect(log.dim).toHaveBeenCalledWith(' 1 skill(s) have no tags and are always synced.'); }); @@ -230,7 +230,7 @@ describe('tags list', () => { writeFileSync(path.join(repoPath, 'tags.yaml'), 'skills:\n a: [frontend]\nrules: {}\n'); useConfig({ primaryRole: 'hai' }); - await expect(tagsList({})).resolves.toBeUndefined(); + await expect(tagsList()).resolves.toBeUndefined(); expect(console.log).toHaveBeenCalledWith(expect.stringContaining('frontend')); expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('Could not count untagged skills')); diff --git a/src/__tests__/tclaude-tcodex.test.ts b/src/__tests__/tclaude-tcodex.test.ts index c89c4eda3..b82b89aa1 100644 --- a/src/__tests__/tclaude-tcodex.test.ts +++ b/src/__tests__/tclaude-tcodex.test.ts @@ -1,6 +1,5 @@ // -*- coding: utf-8 -*- import { describe, it, expect } from 'vitest'; -import os from 'node:os'; import path from 'node:path'; import fs from 'fs-extra'; diff --git a/src/__tests__/votes-e2e.test.ts b/src/__tests__/votes-e2e.test.ts index d3e988071..c03521518 100644 --- a/src/__tests__/votes-e2e.test.ts +++ b/src/__tests__/votes-e2e.test.ts @@ -17,7 +17,6 @@ import { incrementRecalled, incrementUpvoted, syncVotesToTeam, loadUserVotes } f import { parseTranscriptForVotes } from '../transcript-parser.js'; import { buildIndex, loadIndex, search } from '../utils/search-index.js'; import { computeAllConfidence, writeBackConfidence } from '../maintenance/confidence.js'; -import { annotateHotness, HOT_THRESHOLD } from '../maintenance/hot-cold.js'; import { findPruneCandidates } from '../maintenance/prune.js'; import type { UserVotesV2 } from '../types.js'; diff --git a/src/__tests__/webhook-formatters.test.ts b/src/__tests__/webhook-formatters.test.ts index 46c654d42..d43dde7af 100644 --- a/src/__tests__/webhook-formatters.test.ts +++ b/src/__tests__/webhook-formatters.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { formatFeishuMessage, formatWecomMessage, formatGenericJson } from '../webhook-formatters.js'; import type { WebhookPayload } from '../types.js'; diff --git a/src/cache-cmd.ts b/src/cache-cmd.ts index 1c2d4b9e9..8d3a96569 100644 --- a/src/cache-cmd.ts +++ b/src/cache-cmd.ts @@ -94,7 +94,6 @@ async function runStatus(opts: CacheCmdOptions): Promise { const colKey = 50; const colSize = 12; const colUsed = 26; - const colSha = 10; const header = [ 'KEY'.padEnd(colKey), diff --git a/src/coauthor-reconcile.ts b/src/coauthor-reconcile.ts index 6f137ac1a..0e03c1100 100644 --- a/src/coauthor-reconcile.ts +++ b/src/coauthor-reconcile.ts @@ -258,7 +258,7 @@ export async function reconcileCoAuthorForConfig( localConfig: LocalConfig, state: State, ): Promise { - const managed: Record = { ...(state.coAuthorManaged ?? {}) }; + const managed: Record = { ...state.coAuthorManaged }; const changes: CoAuthorChange[] = []; const intent = resolveCoAuthor(localConfig, teamConfig); diff --git a/src/codebase-extract.ts b/src/codebase-extract.ts index 04e85e1a9..9a6b5fb99 100644 --- a/src/codebase-extract.ts +++ b/src/codebase-extract.ts @@ -89,7 +89,6 @@ function detectKnowledgeGaps( ): KnowledgeGap[] { const gaps: KnowledgeGap[] = []; const scannedFiles = new Set(files.map((f) => f.relativePath)); - const nodeSlugs = new Set(graph.nodes.map((n) => n.slug)); const connectedNodes = new Set(); for (const edge of graph.edges) { connectedNodes.add(edge.from); diff --git a/src/codebase-upgrade-wiki.ts b/src/codebase-upgrade-wiki.ts index aaacca010..e65a32864 100644 --- a/src/codebase-upgrade-wiki.ts +++ b/src/codebase-upgrade-wiki.ts @@ -1,4 +1,4 @@ -import { readdir, readFile, rm } from 'node:fs/promises'; +import { readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; import chalk from 'chalk'; diff --git a/src/codebase-wiki-lint.ts b/src/codebase-wiki-lint.ts index eb5c83118..2c0e28b0a 100644 --- a/src/codebase-wiki-lint.ts +++ b/src/codebase-wiki-lint.ts @@ -165,7 +165,6 @@ export async function lintTeamwiki(opts: { // Graph health metrics let graphHealth = { nodeCount: 0, edgeCount: 0, orphanNodes: 0, connectivity: 0 }; if (graph) { - const nodeSlugs = new Set(graph.nodes.map(n => n.slug)); const connectedNodes = new Set(); for (const edge of graph.edges) { connectedNodes.add(edge.from); diff --git a/src/contribute-check.ts b/src/contribute-check.ts index 3564df0e4..ebb28de65 100644 --- a/src/contribute-check.ts +++ b/src/contribute-check.ts @@ -106,7 +106,7 @@ function normalizePromptSummary(raw?: string): string | undefined { if (typeof raw !== 'string') return undefined; const normalized = redactWithEnv(raw) .replace(/[\r\n\t]+/g, ' ') - .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '') + .replace(/\p{Cc}/gu, '') .replace(/\s+/g, ' ') .trim(); if (!normalized) return undefined; diff --git a/src/dashboard-collector.ts b/src/dashboard-collector.ts index 30b9415ef..d636a0d1d 100644 --- a/src/dashboard-collector.ts +++ b/src/dashboard-collector.ts @@ -30,7 +30,6 @@ import { type DashboardEvent, type DashboardEventType, type DashboardSession, - type DashboardSessionStatus, type TokenUsage, type TokenSnapshotScope, type SessionMetrics, diff --git a/src/deep-enrich.ts b/src/deep-enrich.ts index 9acdd8159..9d284c3ba 100644 --- a/src/deep-enrich.ts +++ b/src/deep-enrich.ts @@ -410,7 +410,7 @@ async function runPhaseComponents( let results: string[]; try { results = await callClaudeParallel(tasks, BATCH); - } catch (err) { + } catch { // AggregateError — 部分可能成功,graceful fallback log.warn(`deep-enrich[${project}]: batch[${i}] partially failed, falling back to sequential`); results = await Promise.all( @@ -624,7 +624,7 @@ async function runPhaseAiGraph( ctx: EnrichContext, docsDir: string, ): Promise<{ g5Generated: boolean; g6Generated: boolean }> { - const { project, evidenceDir } = opts; + const { project } = opts; log.info(`deep-enrich[${project}]: Phase 4 — Generating AI graph docs (G5/G6)`); await mkdir(docsDir, { recursive: true }); diff --git a/src/digest.ts b/src/digest.ts index 199834071..49de982e4 100644 --- a/src/digest.ts +++ b/src/digest.ts @@ -7,7 +7,7 @@ import { parseLearningDoc, titleFromFilename } from './utils/search-index.js'; import { requireInit, detectProjectConfig } from './config.js'; import { calculateTeamHealth } from './skill-health.js'; import { createGit } from './utils/git.js'; -import type { GlobalOptions, UserStats, TokenUsage } from './types.js'; +import type { UserStats, TokenUsage } from './types.js'; import { totalTokens } from './types.js'; import { mergeDailyStats, summarizeTrendWindow, type TrendPeriod } from './session-trends.js'; @@ -405,7 +405,7 @@ export function formatTrendLines(periods: { current: TrendPeriod; previous: Tren /** * Generate and display weekly team digest. */ -export async function generateDigest(options: GlobalOptions): Promise { +export async function generateDigest(): Promise { try { const projectConfig = await detectProjectConfig(); const localConfig = projectConfig ?? (await requireInit()).localConfig; diff --git a/src/hooks.ts b/src/hooks.ts index 32b2f245d..2a6a32f8f 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -218,11 +218,6 @@ const TEAMAI_COMMAND_MARKERS = [ 'teamai auto-recall', 'teamai todowrite-hint', 'teamai mr-hint', 'teamai hook-dispatch', ]; -function extractTeamaiSubcommand(command: string): string | null { - const match = command.match(/teamai\s+([\w-]+)/); - return match ? match[1] : null; -} - function isTeamaiHookCommand(command: string): boolean { return /(?:^|"|\s)teamai\s/.test(command); } diff --git a/src/import-iwiki.ts b/src/import-iwiki.ts index c1b0c5467..7aaa3f3b6 100644 --- a/src/import-iwiki.ts +++ b/src/import-iwiki.ts @@ -6,7 +6,7 @@ */ import path from 'node:path'; -import { readFile, mkdir, writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { classifyWithAI, interactiveReview, pushAccepted } from './import-local.js'; import { IWikiClient } from './utils/iwiki-client.js'; diff --git a/src/import-local.ts b/src/import-local.ts index 950e50a1f..46036370d 100644 --- a/src/import-local.ts +++ b/src/import-local.ts @@ -446,7 +446,6 @@ export async function interactiveReview( let answered = false; while (!answered) { - // eslint-disable-next-line no-await-in-loop const input = await question('[A]ccept [E]dit [S]kip > '); const choice = input.trim().toLowerCase(); @@ -457,7 +456,6 @@ export async function interactiveReview( sessionItem.status = 'skipped'; answered = true; } else if (choice === 'e') { - // eslint-disable-next-line no-await-in-loop const newTitle = await question(' New title: '); const trimmedTitle = newTitle.trim(); if (trimmedTitle.length > 0 && sessionItem.learningDraft) { @@ -477,7 +475,6 @@ export async function interactiveReview( processedCount++; session.progress = processedCount; // 每次选择后立即持久化,支持中断恢复 - // eslint-disable-next-line no-await-in-loop await persistSession(session, sessionPath); } diff --git a/src/import-repo-list.ts b/src/import-repo-list.ts index b2f20d3c9..c9dc4860b 100644 --- a/src/import-repo-list.ts +++ b/src/import-repo-list.ts @@ -121,7 +121,6 @@ export async function importFromRepoList( explicitDomain: entry.domain, dryRun, output, - interactive: false, incremental, skipAutoPush: true, skipEnrich, diff --git a/src/import-repo.ts b/src/import-repo.ts index c3b5fc1e0..4395d88c6 100644 --- a/src/import-repo.ts +++ b/src/import-repo.ts @@ -34,12 +34,6 @@ export interface ImportFromRepoOptions { dryRun?: boolean; /** Custom output root directory; defaults to .teamai/team-repo/teamwiki */ output?: string; - /** - * Whether to enable interactive confirmation. - * Default true (shows AI recommendation and waits for user input in TTY); - * pass false for batch imports → non-TTY path (assign to uncategorized when confidence is low). - */ - interactive?: boolean; /** Incremental mode: on cache hit do fetch+reset only; on miss fall back to full clone */ incremental?: boolean; /** In batch mode, skip per-repo autoPushTeamRepo (caller handles it collectively) */ @@ -193,7 +187,7 @@ export function detectCrossRepoEdges( export async function importFromRepo(opts: ImportFromRepoOptions): Promise { const { url, depth = 1, forceSsh = false, forceAnonymous = false, - explicitDomain, dryRun = false, output, interactive = true, + explicitDomain, dryRun = false, output, incremental = false, skipAutoPush = false, skipEnrich = false, sourceMrUrl, } = opts; diff --git a/src/import.ts b/src/import.ts index 6a5ef18e6..5272948f8 100644 --- a/src/import.ts +++ b/src/import.ts @@ -328,7 +328,6 @@ export async function importCmd(opts: ImportOptions): Promise { await importFromRepo({ url: ctx.repoUrl, incremental: true, - interactive: false, skipAutoPush: true, sourceMrUrl: opts.fromMr, }); diff --git a/src/index.ts b/src/index.ts index 1d84f6e48..2e1ffa9de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -334,9 +334,8 @@ const rolesCmd = program .description('Manage team roles and resource namespaces') .action(async () => { // Default action: list roles - const globalOpts = program.opts() as GlobalOptions; const { rolesList } = await import('./roles-cmd.js'); - await rolesList(globalOpts); + await rolesList(); }); rolesCmd @@ -352,9 +351,8 @@ rolesCmd .command('list') .description('List all defined roles and your current role') .action(async () => { - const globalOpts = program.opts() as GlobalOptions; const { rolesList } = await import('./roles-cmd.js'); - await rolesList(globalOpts); + await rolesList(); }); rolesCmd @@ -479,18 +477,16 @@ const tagsCmd = program .description('Manage tag-based skill/rule filtering') .action(async () => { // Default action: list tags - const globalOpts = program.opts() as GlobalOptions; const { tagsList } = await import('./tags.js'); - await tagsList(globalOpts); + await tagsList(); }); tagsCmd .command('list') .description('List all available tags and subscription status') .action(async () => { - const globalOpts = program.opts() as GlobalOptions; const { tagsList } = await import('./tags.js'); - await tagsList(globalOpts); + await tagsList(); }); tagsCmd @@ -559,7 +555,6 @@ const sourceCmd = program .command('source') .description('Manage cross-team skill sources') .action(async () => { - const globalOpts = program.opts() as GlobalOptions; const { sourceList } = await import('./source.js'); await sourceList(); }); @@ -934,9 +929,8 @@ program .command('digest') .description('Generate weekly team activity digest') .action(async () => { - const globalOpts = program.opts() as GlobalOptions; const { generateDigest } = await import('./digest.js'); - await generateDigest(globalOpts); + await generateDigest(); }); // ─── Dashboard commands ───────────────────────────────── diff --git a/src/local-agent.ts b/src/local-agent.ts index b185090c8..99c50ab1e 100644 --- a/src/local-agent.ts +++ b/src/local-agent.ts @@ -73,7 +73,6 @@ import { type ManagedMcpRecord, type McpServerDef, type McpTransport, - type Scope, type TeamaiConfig, } from './types.js'; import { getUserHome } from './utils/home.js'; @@ -748,7 +747,7 @@ async function localAgentFetch( const url = `${config.endpoint}${resolveRoute(config, route)}`; const headers: Record = { ...authHeaders(config, init?.body !== undefined), - ...((init?.headers as Record | undefined) ?? {}), + ...(init?.headers as Record | undefined), }; logHttpRequest(tag, method, url, headers, init?.body); @@ -806,7 +805,7 @@ function redactSecrets(s: string): string { return s .replace(new RegExp(`(--(?:${names})[= ]+)\\S+`, 'gi'), '$1***') .replace(new RegExp(`((?:${names})"?\\s*[:=]\\s*"?)[^"\\s,}]+`, 'gi'), '$1***') - .replace(/(bearer\s+)[\w.\-]+/gi, '$1***'); + .replace(/(bearer\s+)[\w.-]+/gi, '$1***'); } /** @@ -1041,7 +1040,7 @@ async function persistWorkspaceBinding( const boundAt = new Date().toISOString(); for (const key of keys) { config.workspaceBindings[key] = { - ...(config.workspaceBindings[key] ?? {}), + ...config.workspaceBindings[key], projectId, projectName, boundAt, @@ -1067,7 +1066,7 @@ async function inheritWorktreeBinding( const anchorBinding = config.workspaceBindings[anchors.projectAnchor]; if (!anchorBinding) return false; config.workspaceBindings[resolvedPath] = { - ...(config.workspaceBindings[resolvedPath] ?? {}), + ...config.workspaceBindings[resolvedPath], projectId: anchorBinding.projectId, projectName: anchorBinding.projectName, boundAt: new Date().toISOString(), diff --git a/src/maintenance/confidence.ts b/src/maintenance/confidence.ts index cd0575885..0136a3758 100644 --- a/src/maintenance/confidence.ts +++ b/src/maintenance/confidence.ts @@ -98,7 +98,7 @@ export async function writeBackConfidence( let updated = 0; const files = await listLearningFiles(learningsDirs); - for (const { file, absPath, root } of files) { + for (const { file, absPath } of files) { const docId = file.replace(/\.md$/i, ''); const newConf = confidenceMap.get(docId); if (newConf === undefined) continue; diff --git a/src/maintenance/promote.ts b/src/maintenance/promote.ts index 5e4ca9307..0e5b187f3 100644 --- a/src/maintenance/promote.ts +++ b/src/maintenance/promote.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import matter from 'gray-matter'; -import { readFileSafe, writeFile, listFiles, ensureDir, copyFile } from '../utils/fs.js'; +import { readFileSafe, writeFile, listFiles, ensureDir } from '../utils/fs.js'; import { isInWriteRoot, listLearningFiles } from '../utils/learnings-roots.js'; import { log } from '../utils/logger.js'; import { computeAllConfidence } from './confidence.js'; diff --git a/src/manifest-schema.ts b/src/manifest-schema.ts index 28c4c84c4..19302e277 100644 --- a/src/manifest-schema.ts +++ b/src/manifest-schema.ts @@ -20,7 +20,7 @@ import { warnOnce } from './utils/warn-once.js'; // The control ranges are both of them, C0 with DEL and C1: a segment carrying one // is a name no admin typed on purpose, and it renders as something other than // what it is in a terminal that reports the path back. -const UNSAFE_SEGMENT = /[/\\:\u0000-\u001f\u007f-\u009f]/; +const UNSAFE_SEGMENT = /[/\\:\p{Cc}]/u; // Win32 strips trailing spaces and periods from every path component, so a // namespace ending in one is not the directory the manifest names: `.. ` arrives diff --git a/src/projects-cmd.ts b/src/projects-cmd.ts index 443401668..f136a6cb1 100644 --- a/src/projects-cmd.ts +++ b/src/projects-cmd.ts @@ -2,7 +2,6 @@ import path from 'node:path'; import YAML from 'yaml'; import { autoDetectInit, - loadLocalConfig, saveLocalConfig, saveLocalConfigForScope, loadStateForScope, @@ -14,7 +13,6 @@ import { validateProjectsManifest, findProject, listProjectIds, - describeProjects, unknownProjectMessage, PROJECT_RESOURCE_TYPES, } from './projects.js'; diff --git a/src/providers/github/gh-cli.ts b/src/providers/github/gh-cli.ts index 5b85e8046..387c19747 100644 --- a/src/providers/github/gh-cli.ts +++ b/src/providers/github/gh-cli.ts @@ -1,6 +1,6 @@ import { spawnSync } from 'node:child_process'; import crossSpawn from 'cross-spawn'; -import { log, spinner } from '../../utils/logger.js'; +import { log } from '../../utils/logger.js'; import { resolveCliPath } from '../../utils/cli-path.js'; import { isInteractive } from '../../utils/prompt.js'; @@ -8,13 +8,6 @@ import { isInteractive } from '../../utils/prompt.js'; const GITHUB_API = 'https://api.github.com'; -// ─── Shell helpers ─────────────────────────────────────── - -/** Shell-quote a string using single quotes. */ -function shellQuote(s: string): string { - return "'" + s.replace(/'/g, "'\\''") + "'"; -} - // ─── gh CLI detection ──────────────────────────────────── /** @@ -62,14 +55,14 @@ export function ghExec( if (options?.inheritStdio) { const result = crossSpawn.sync(ghPath, args, { stdio: 'inherit', - env: { ...process.env, ...(options.env ?? {}) }, + env: { ...process.env, ...options.env }, cwd: options.cwd, }); return { stdout: '', stderr: '', status: result.status ?? 1 }; } const result = crossSpawn.sync(ghPath, args, { - env: { ...process.env, ...(options?.env ?? {}) }, + env: { ...process.env, ...options?.env }, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, cwd: options?.cwd, diff --git a/src/providers/github/repo-url.ts b/src/providers/github/repo-url.ts index 816e96a9d..346946f56 100644 --- a/src/providers/github/repo-url.ts +++ b/src/providers/github/repo-url.ts @@ -31,7 +31,7 @@ export function parseGitHubRepoInput(input: string): RepoInfo { // Short format: owner/repo (single slash only, no subgroups on GitHub) const shortMatch = trimmed.match( - /^([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+)$/, + /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/, ); if (shortMatch) { return buildRepoInfo(shortMatch[1], shortMatch[2]); diff --git a/src/providers/gitlab/mr-fetch.ts b/src/providers/gitlab/mr-fetch.ts index c2f02feb3..8c89e1eac 100644 --- a/src/providers/gitlab/mr-fetch.ts +++ b/src/providers/gitlab/mr-fetch.ts @@ -1,6 +1,6 @@ import { type MRData } from '../../types.js'; import { log } from '../../utils/logger.js'; -import { getGitLabToken, gitlabApiBase } from './gitlab-api.js'; +import { getGitLabToken } from './gitlab-api.js'; import { GITLAB_HOST } from './repo-url.js'; /** GitLab MR URL 解析结果 */ diff --git a/src/providers/gitlab/repo-url.ts b/src/providers/gitlab/repo-url.ts index a69918181..0e7baa276 100644 --- a/src/providers/gitlab/repo-url.ts +++ b/src/providers/gitlab/repo-url.ts @@ -53,7 +53,7 @@ export function parseGitLabRepoInput(input: string): RepoInfo { // Short format: owner/repo or group/subgroup/repo (host = configured default) const shortMatch = trimmed.match( - /^([A-Za-z0-9_.\-]+(?:\/[A-Za-z0-9_.\-]+)*)\/([A-Za-z0-9_.\-]+)$/, + /^([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*)\/([A-Za-z0-9_.-]+)$/, ); if (shortMatch) { return buildRepoInfo(GITLAB_HOST, shortMatch[1], shortMatch[2]); diff --git a/src/providers/tgit/gf-cli.ts b/src/providers/tgit/gf-cli.ts index d329699b9..3103cb73e 100644 --- a/src/providers/tgit/gf-cli.ts +++ b/src/providers/tgit/gf-cli.ts @@ -3,7 +3,7 @@ import { createHash, randomBytes } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { pathExists, ensureDir } from '../../utils/fs.js'; +import { ensureDir } from '../../utils/fs.js'; import { log, spinner } from '../../utils/logger.js'; import { isInteractive } from '../../utils/prompt.js'; import { getTeamaiHomeDir } from '../../types.js'; diff --git a/src/providers/tgit/index.ts b/src/providers/tgit/index.ts index 03b8f1452..1aacddfad 100644 --- a/src/providers/tgit/index.ts +++ b/src/providers/tgit/index.ts @@ -8,7 +8,6 @@ import { gfRepoClone, gfCreateRepo, gfMrCreate, - gfGetOAuthToken, RepoNotFoundError as GfRepoNotFoundError, } from './gf-cli.js'; import { gfListOrgRepos } from './gf-org.js'; diff --git a/src/providers/tgit/repo-url.ts b/src/providers/tgit/repo-url.ts index a33f845ec..cf87b9ff3 100644 --- a/src/providers/tgit/repo-url.ts +++ b/src/providers/tgit/repo-url.ts @@ -31,7 +31,7 @@ export function parseTGitRepoInput(input: string): RepoInfo { // Short format: owner/repo or group/subgroup/repo const shortMatch = trimmed.match( - /^([A-Za-z0-9_.\-]+(?:\/[A-Za-z0-9_.\-]+)*)\/([A-Za-z0-9_.\-]+)$/, + /^([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*)\/([A-Za-z0-9_.-]+)$/, ); if (shortMatch) { return buildRepoInfo(shortMatch[1], shortMatch[2]); diff --git a/src/pull.ts b/src/pull.ts index 90a0934f0..2faf84926 100644 --- a/src/pull.ts +++ b/src/pull.ts @@ -6,7 +6,7 @@ import { indexedSkills, resolveDesiredAgents, resolveDesiredRules, resolveDesiredSkills, type RolePullContext, } from './resources/desired.js'; import type { IndexedSkills } from './utils/search-index.js'; -import { requireInit, loadState, saveState, detectProjectConfig, describeUnreadableConfig, loadLocalConfigForScope, loadTeamConfig, loadStateForScope, saveStateForScope } from './config.js'; +import { detectProjectConfig, describeUnreadableConfig, loadLocalConfigForScope, loadTeamConfig, loadStateForScope, saveStateForScope } from './config.js'; import { pullRepo, getHeadRev, createGit, getDefaultBranch, listWorktrees } from './utils/git.js'; import { publishQueuedLearnings } from './utils/learnings-publish.js'; import { pendingLearningsDir } from './utils/pending-learnings.js'; @@ -22,7 +22,7 @@ import { skillsDirForTool } from './resources/skills.js'; import { ruleFileExtensionForTool } from './resources/rule-format.js'; import { AGENT_FILE_EXTENSIONS } from './resources/agent-format.js'; import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; -import type { GlobalOptions, ResourceType, ResourceItem, TeamaiConfig, LocalConfig, State, TagsConfig } from './types.js'; +import type { GlobalOptions, ResourceType, ResourceItem, TeamaiConfig, LocalConfig, State } from './types.js'; import { getUserLearningsDir, TEAMAI_CULTURE_START, diff --git a/src/push.ts b/src/push.ts index 2058a71f5..091e7828b 100644 --- a/src/push.ts +++ b/src/push.ts @@ -21,7 +21,7 @@ import type { } from './types.js'; import { getDataHome, SELF_KNOWLEDGE_SCAN_KEY, SYNC_LOCK_FILENAME } from './types.js'; import { acquireLock, releaseLock } from './update.js'; -import { assertSafePath, assertSafeResourceName, defaultAllowedRoots } from './utils/path-safety.js'; +import { assertSafeResourceName } from './utils/path-safety.js'; import { loadRolesManifest, resolveRoleResourceNamespaces, RolesManifestNotFoundError } from './roles.js'; import type { ProjectsManifest } from './projects.js'; import { isSafeNamespaceSegment, NAMESPACE_RULE, fallbackNamespaceError } from './manifest-schema.js'; @@ -848,7 +848,6 @@ async function pushCore( result?: { completed: boolean }, ): Promise { const selfMode = localConfig.repo.kind === 'self'; - const scopeLabel = localConfig.scope; // Pull latest default branch BEFORE scanning so detection runs against up-to-date repo. // The team repo may be in various broken states from previous failed pushes: diff --git a/src/recall.ts b/src/recall.ts index e656ee644..94905ffef 100644 --- a/src/recall.ts +++ b/src/recall.ts @@ -3,12 +3,12 @@ import { existsSync } from 'node:fs'; import { requireInit, detectProjectConfig, describeUnreadableConfig, loadLocalConfigForScope } from './config.js'; import { loadIndex, buildIndex, search, isLegacyIndex } from './utils/search-index.js'; import type { BuildIndexOptions, SearchResult } from './utils/search-index.js'; -import { readFileSafe, ensureDir, pathExists } from './utils/fs.js'; +import { ensureDir, pathExists } from './utils/fs.js'; import { log } from './utils/logger.js'; import type { GlobalOptions, SearchIndex, LocalConfig } from './types.js'; import { getProjectSearchIndexPath, getUserSearchIndexPath, getVotesDir } from './types.js'; import { queryCodeKnowledge } from './code-knowledge-recall.js'; -import type { CodeKnowledgeResult, SourceAnchor } from './code-knowledge-recall.js'; +import type { SourceAnchor } from './code-knowledge-recall.js'; import { recordRecallQuality } from './recall-quality.js'; import { deriveSessionId } from './utils/session-id.js'; diff --git a/src/resources/agent-format.ts b/src/resources/agent-format.ts index e4302188b..9fbb37344 100644 --- a/src/resources/agent-format.ts +++ b/src/resources/agent-format.ts @@ -262,7 +262,7 @@ function isManagedKiroSessionHook(value: unknown): boolean { * Kiro-private fields and hook entries from `tool_extras.kiro`. */ export function renderForKiro(spec: AgentSpec): RenderResult { - const extras = { ...(spec.tool_extras?.['kiro'] ?? {}) }; + const extras = { ...spec.tool_extras?.['kiro'] }; const existingHooks = isRecord(extras['hooks']) ? { ...extras['hooks'] } : {}; const existingAgentSpawn = Array.isArray(existingHooks['agentSpawn']) ? existingHooks['agentSpawn'].filter((entry) => !isManagedKiroSessionHook(entry)) @@ -449,12 +449,10 @@ function canUseTomlLiteral(value: string): boolean { if (!value.includes('\n')) return false; if (value.includes("'''")) return false; if (value.endsWith("'")) return false; - // Control characters a literal string cannot carry (`\r`, NUL, ...). - // U+007F DEL is one of them — TOML 1.0 bans it from literal strings - // alongside C0, and smol-toml rejects the whole document on it. The range - // `\x7f` sits outside the `\x00-\x1f` C0 block, so it needs its own - // alternative in the class. - if (/[\r\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) return false; + // Control characters a literal string cannot carry (`\r`, NUL, DEL, ...): + // TOML 1.0 bans C0 other than tab and newline, plus DEL, and smol-toml + // rejects the whole document on them. C1 (U+0080-U+009F) is allowed. + if (/(?![\t\n\u0080-\u009f])\p{Cc}/u.test(value)) return false; return true; } diff --git a/src/review-cmd.ts b/src/review-cmd.ts index f9aa413ad..994a2811b 100644 --- a/src/review-cmd.ts +++ b/src/review-cmd.ts @@ -16,7 +16,6 @@ import { log } from './utils/logger.js'; import { loadPendingReview, removePendingReview, - savePendingReview, type PendingReviewItem, type Risk, } from './review-store.js'; diff --git a/src/roles-cmd.ts b/src/roles-cmd.ts index 421a39da2..bcad0fc14 100644 --- a/src/roles-cmd.ts +++ b/src/roles-cmd.ts @@ -1,12 +1,12 @@ import path from 'node:path'; import YAML from 'yaml'; -import { autoDetectInit, loadLocalConfig, saveLocalConfig, loadTeamConfig, saveLocalConfigForScope, loadStateForScope, saveStateForScope } from './config.js'; -import { loadRolesManifest, saveRolesManifest, findRole, describeRoles, listRoleIds } from './roles.js'; +import { autoDetectInit, saveLocalConfig, saveLocalConfigForScope, loadStateForScope, saveStateForScope } from './config.js'; +import { loadRolesManifest, saveRolesManifest, findRole, listRoleIds } from './roles.js'; import type { RolesManifest, TeamRole } from './roles.js'; -import { ensureDir, pathExists, writeFile, expandHome } from './utils/fs.js'; +import { pathExists } from './utils/fs.js'; import { log } from './utils/logger.js'; import { pullLatest, runManifestEdit, pushManifestChange } from './manifest-edit.js'; -import type { GlobalOptions, LocalConfig } from './types.js'; +import type { GlobalOptions } from './types.js'; import { askQuestion, askConfirmation } from './utils/prompt.js'; /** @@ -139,7 +139,7 @@ export async function rolesInit(options: GlobalOptions): Promise { // ─── roles list ───────────────────────────────────────── -export async function rolesList(options: GlobalOptions): Promise { +export async function rolesList(): Promise { const { localConfig } = await autoDetectInit(); const repoPath = localConfig.repo.localPath; diff --git a/src/section-patcher.ts b/src/section-patcher.ts index b47a68fd3..9839a8ba6 100644 --- a/src/section-patcher.ts +++ b/src/section-patcher.ts @@ -25,7 +25,7 @@ export interface ManagedSection { * 重复 slug 在调用处处理(加 -2 / -3 后缀)。 */ function slugify(title: string): string { - return title.trim().replace(/\s+/g, '-').replace(/[\/\\:]/g, '_'); + return title.trim().replace(/\s+/g, '-').replace(/[/\\:]/g, '_'); } /** @@ -76,7 +76,7 @@ export function splitToSections(md: string): { prelude: string; sections: Manage // 找出所有 ## 标题的行号 const headerIndices: number[] = []; for (let i = 0; i < lines.length; i++) { - if (/^## /.test(lines[i])) { + if (lines[i].startsWith('## ')) { headerIndices.push(i); } } @@ -209,7 +209,7 @@ export function parseSections(md: string): { prelude: string; sections: ManagedS if (innerLines[i].trim() === '') { continue; } - if (/^## /.test(innerLines[i])) { + if (innerLines[i].startsWith('## ')) { titleLine = innerLines[i].replace(/^## /, '').trim(); bodyStartLine = i + 1; } @@ -292,7 +292,7 @@ export function patchManagedSection( const oldInner = md.slice(openEnd, closeStart); let title = ''; for (const line of oldInner.split('\n')) { - if (/^## /.test(line)) { + if (line.startsWith('## ')) { title = line.replace(/^## /, '').trim(); break; } @@ -356,16 +356,8 @@ export function mergeWithAnchors( } // 解析旧文件 - let oldPrelude: string; - let oldSections: ManagedSection[]; - try { - const parsed = parseSections(oldFile); - oldPrelude = parsed.prelude; - oldSections = parsed.sections; - } catch (err) { - // 解析失败(如未闭合锚点):重新抛出,由调用方(import-repo)决定是否备份后 fallback - throw err; - } + // A parse failure (e.g. an unclosed anchor) throws; the caller (import-repo) decides whether to back up and fall back. + const { prelude: oldPrelude, sections: oldSections } = parseSections(oldFile); // 无旧锚点:视为首次写入 if (oldSections.length === 0) { diff --git a/src/session-analytics.ts b/src/session-analytics.ts index 1816183f5..edcacdf91 100644 --- a/src/session-analytics.ts +++ b/src/session-analytics.ts @@ -86,7 +86,7 @@ const ACTIVE_GAP_MS = 5 * 60_000; * stream. Hours use the local timezone of the machine running the command. */ export function timeAnalytics(events: DashboardEvent[]): TimeAnalytics { - const byHour = new Array(24).fill(0) as number[]; + const byHour = Array.from({ length: 24 }, () => 0); let nightOwl = 0; let total = 0; diff --git a/src/session-trends.ts b/src/session-trends.ts index 70849099a..c89549e5a 100644 --- a/src/session-trends.ts +++ b/src/session-trends.ts @@ -301,7 +301,7 @@ export function mergeDailyStats( existing: Record | undefined, delta: Record, ): Record { - const merged: Record = { ...(existing ?? {}) }; + const merged: Record = { ...existing }; for (const [date, increment] of Object.entries(delta)) { const current = merged[date] ?? emptyDaily(); merged[date] = { diff --git a/src/source.ts b/src/source.ts index ddf083d3b..e37ddcee3 100644 --- a/src/source.ts +++ b/src/source.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import fse from 'fs-extra'; import YAML from 'yaml'; import { loadTeamConfig, autoDetectInit, loadLocalConfig, detectProjectConfig } from './config.js'; -import { createGit, pullRepo } from './utils/git.js'; +import { pullRepo } from './utils/git.js'; import { parseFrontmatter } from './utils/frontmatter.js'; import { detectProvider, getProvider } from './providers/index.js'; import { log, spinner } from './utils/logger.js'; @@ -644,7 +644,7 @@ async function removeSkillFromToolPaths(skillName: string, teamConfig: TeamaiCon return; } - for (const [tool, toolPath] of Object.entries(scopedToolPaths(teamConfig, localConfig))) { + for (const toolPath of Object.values(scopedToolPaths(teamConfig, localConfig))) { if (!toolPath.skills) continue; const skillDir = path.join(baseDir, toolPath.skills, skillName); if (await pathExists(skillDir)) { diff --git a/src/status.ts b/src/status.ts index ebf4e6b3a..8717f4612 100644 --- a/src/status.ts +++ b/src/status.ts @@ -11,7 +11,6 @@ import { DocsHandler } from './resources/docs.js'; import { detectInstalledAgents, type ResolvedAgent } from './known-agents.js'; import { buildClassifyContext, - classifySkill, formatSkillSource, scanAgentSkills, truncate, diff --git a/src/tags.ts b/src/tags.ts index 4781118d6..59d1b5035 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -1,5 +1,3 @@ -import path from 'node:path'; -import YAML from 'yaml'; import { requireInit, saveLocalConfig, @@ -12,7 +10,6 @@ import { import { buildRolePullContext, resolveDesiredSkills } from './resources/desired.js'; import { loadTagsConfig, collectTagStats, saveTagsConfig } from './utils/tags.js'; import { log } from './utils/logger.js'; -import { readFileSafe } from './utils/fs.js'; import type { GlobalOptions, LocalConfig, TagsConfig } from './types.js'; /** @@ -51,7 +48,7 @@ async function saveTagsScopeConfig(localConfig: LocalConfig): Promise { * List all available tags from the team repo's tags.yaml. * Shows tag name, skill count, and rule count. */ -export async function tagsList(options: GlobalOptions): Promise { +export async function tagsList(): Promise { const localConfig = await resolveTagsScope(); const tagsConfig = await loadTagsConfig(localConfig.repo.localPath); @@ -88,7 +85,6 @@ export async function tagsList(options: GlobalOptions): Promise { ); } - const totalRules = Object.keys(tagsConfig.rules).length; const untaggedSkills = await countUntaggedDeliveredSkills(localConfig, tagsConfig); console.log(''); diff --git a/src/team-push.ts b/src/team-push.ts index 5f65065b2..cb49bff83 100644 --- a/src/team-push.ts +++ b/src/team-push.ts @@ -448,7 +448,7 @@ export function droppedRollouts( const day = parseDailySnapshot(daily[sid]); const left = { prompts: total.prompts, tokens: { ...total.tokens }, interrupt: iv.interrupt, toolReject: iv.toolReject, - correction: iv.correction, durationMs: day?.durationMs ?? 0, requestDaily: { ...(day?.requestDaily ?? {}) }, + correction: iv.correction, durationMs: day?.durationMs ?? 0, requestDaily: { ...day?.requestDaily }, }; const covered = Object.values((writtenAt !== undefined && before?.get(sid)?.segments) || cur.segments) .filter((segment) => writtenAt === undefined || Date.parse(segment.since) <= writtenAt) @@ -708,7 +708,7 @@ function raiseToCredit( cacheRead: Math.max(a.cacheRead, b.cacheRead), cacheCreation: Math.max(a.cacheCreation, b.cacheCreation), }); result.promptTokens[runId] = { - ...(own ?? {}), + ...own, prompts: Math.max(ownTokens.prompts, credit.promptTokens.prompts), tokens: greater(ownTokens.tokens, credit.promptTokens.tokens), }; diff --git a/src/uninstall.ts b/src/uninstall.ts index f6fc522b9..a8d9de59f 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -557,7 +557,7 @@ async function buildRemovalPlan( tool, toolPath, resolveToolBaseDir(tool, localConfig), - resolveBaseDir(localConfig), + baseDir, teamSkillNames, teamRuleNames, teamAgentNames, diff --git a/src/utils/ai-client.ts b/src/utils/ai-client.ts index 7b57551b0..ddb458e7f 100644 --- a/src/utils/ai-client.ts +++ b/src/utils/ai-client.ts @@ -203,7 +203,7 @@ async function runWithConcurrency( tasks: Array<{ prompt: string; parse: (output: string) => T }>, concurrency: number ): Promise[]> { - const results: PromiseSettledResult[] = new Array(tasks.length); + const results = Array.from>({ length: tasks.length }); let running = 0; let index = 0; diff --git a/src/utils/repo-attribution.ts b/src/utils/repo-attribution.ts index 1f2215b02..5dbe11128 100644 --- a/src/utils/repo-attribution.ts +++ b/src/utils/repo-attribution.ts @@ -53,7 +53,7 @@ export function attributeRepo(cwd: string | undefined): string { if (!cwd || !cwd.trim()) return 'no_repo'; const raw = cwd.trim(); - if (/:\/\//.test(raw) || /^git@/.test(raw) || /^[^/\s]+\.[^/\s]+\//.test(raw)) { + if (/:\/\//.test(raw) || raw.startsWith('git@') || /^[^/\s]+\.[^/\s]+\//.test(raw)) { const c = canonicalRepo(raw); if (c) return c; } diff --git a/src/wiki-engine/adapters/templates.ts b/src/wiki-engine/adapters/templates.ts index 1bec1f780..20123555e 100644 --- a/src/wiki-engine/adapters/templates.ts +++ b/src/wiki-engine/adapters/templates.ts @@ -139,7 +139,6 @@ export function indexTemplate( sections.push('## 文档导航', ''); for (const p of projects) { - const desc = p.description ?? p.label; sections.push(`### ${p.slug}`, ''); sections.push(`- [overview.md](./evidence/code/${p.slug}/overview.md) — 架构概览`); sections.push(`- [modules/](./evidence/code/${p.slug}/modules/) — 模块级摘要(推荐首选)`); diff --git a/src/wiki-engine/code-knowledge/code-extractors.ts b/src/wiki-engine/code-knowledge/code-extractors.ts index 180a6b547..7624014e4 100644 --- a/src/wiki-engine/code-knowledge/code-extractors.ts +++ b/src/wiki-engine/code-knowledge/code-extractors.ts @@ -67,16 +67,3 @@ function groupByLanguage(files: CodeCollectedFile[]): Map(); - const result: CodeFact[] = []; - for (const fact of facts) { - const key = `${fact.kind}:${fact.name}:${fact.file}:${fact.lineStart}`; - if (!seen.has(key)) { - seen.add(key); - result.push(fact); - } - } - return result; -} diff --git a/src/wiki-engine/code-knowledge/extractors/rust.ts b/src/wiki-engine/code-knowledge/extractors/rust.ts index 7a7111898..be9c10f29 100644 --- a/src/wiki-engine/code-knowledge/extractors/rust.ts +++ b/src/wiki-engine/code-knowledge/extractors/rust.ts @@ -97,7 +97,7 @@ export function extractRust(files: CodeCollectedFile[]): CodeFact[] { const errorEnum = /^pub(?:\(crate\))?\s+enum\s+([A-Z][A-Za-z0-9_]*(?:Error)?)/u.exec(line); if (errorEnum && thiserror) { facts.push(makeFact("error", errorEnum[1], file.relativePath, lineNumber, line, "EXTRACTED")); - } else if (errorEnum && /Error$/u.test(errorEnum[1])) { + } else if (errorEnum && errorEnum[1].endsWith('Error')) { facts.push(makeFact("error", errorEnum[1], file.relativePath, lineNumber, line, "INFERRED")); } diff --git a/src/wiki-engine/knowledge-reconciler.ts b/src/wiki-engine/knowledge-reconciler.ts index 1b2065e19..c1ef81539 100644 --- a/src/wiki-engine/knowledge-reconciler.ts +++ b/src/wiki-engine/knowledge-reconciler.ts @@ -1,4 +1,4 @@ -import { readFile, readdir, stat, mkdir, writeFile } from 'node:fs/promises'; +import { readFile, readdir, stat } from 'node:fs/promises'; import path from 'node:path'; import { loadGraphIndex, @@ -13,7 +13,6 @@ import type { WikiConfidence } from './core/wiki-protocol.js'; import { buildConfidence } from './reconciler-v2-types.js'; import type { ConfidenceFactor, - NumericConfidence, ApiInterfaceMatch, RuleCodeMatch, ReconcileStaleWarning, diff --git a/test/e2e.mjs b/test/e2e.mjs deleted file mode 100644 index e69de29bb..000000000