build: streamline local and CI validation
This commit is contained in:
parent
9d84cad622
commit
c08b9a7d6c
|
|
@ -6,6 +6,13 @@ on:
|
|||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
jobs:
|
||||
pr-title:
|
||||
name: Conventional PR title
|
||||
|
|
@ -23,77 +30,78 @@ jobs:
|
|||
process.exit(1);
|
||||
}'
|
||||
|
||||
typecheck:
|
||||
changes:
|
||||
name: Classify changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
package: ${{ steps.package.outputs.required }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Select package and Windows lanes
|
||||
id: package
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
|
||||
echo "required=true" >> "${GITHUB_OUTPUT}"
|
||||
elif git diff --quiet "${BASE_SHA}" "${HEAD_SHA}" -- \
|
||||
. \
|
||||
':(exclude)docs/**' \
|
||||
':(exclude)AGENTS.md' \
|
||||
':(exclude)CONTRIBUTING.md'; then
|
||||
echo "required=false" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "required=true" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
static:
|
||||
name: Static checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run check:ci
|
||||
|
||||
typecheck-compatibility:
|
||||
name: Typecheck
|
||||
needs: static
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run typecheck
|
||||
- name: Require successful static checks
|
||||
env:
|
||||
STATIC_RESULT: ${{ needs.static.result }}
|
||||
run: test "${STATIC_RESULT}" = "success"
|
||||
|
||||
lint:
|
||||
lint-compatibility:
|
||||
name: Lint (oxlint)
|
||||
needs: static
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- name: Require successful static checks
|
||||
env:
|
||||
STATIC_RESULT: ${{ needs.static.result }}
|
||||
run: test "${STATIC_RESULT}" = "success"
|
||||
|
||||
format:
|
||||
format-compatibility:
|
||||
name: Format Check
|
||||
needs: static
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run format:check
|
||||
|
||||
dead-code:
|
||||
name: Dead Code (knip)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run lint:dead
|
||||
|
||||
duplicates:
|
||||
name: Duplicate Code (jscpd)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run jscpd
|
||||
|
||||
tech-debt:
|
||||
name: Tech Debt Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run scan:todos
|
||||
- name: Require successful static checks
|
||||
env:
|
||||
STATIC_RESULT: ${{ needs.static.result }}
|
||||
run: test "${STATIC_RESULT}" = "success"
|
||||
|
||||
test:
|
||||
name: Test (coverage + timing)
|
||||
|
|
@ -105,7 +113,7 @@ jobs:
|
|||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml
|
||||
- run: npm run test:ci
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
|
|
@ -115,6 +123,8 @@ jobs:
|
|||
|
||||
package-e2e:
|
||||
name: Package E2E (${{ matrix.os }})
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
|
@ -131,6 +141,8 @@ jobs:
|
|||
|
||||
windows-curated:
|
||||
name: Windows Ghidra P0 and package lane
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
|
|
@ -140,7 +152,7 @@ jobs:
|
|||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run build:cached
|
||||
- run: npm run typecheck
|
||||
- name: Record named Windows host capabilities
|
||||
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ on:
|
|||
- "package-lock.json"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: real-browser-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ on:
|
|||
repository_dispatch:
|
||||
types: [real-ghidra-windows]
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ name: Real Hopper Linux E2E
|
|||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ name: Real Hopper E2E
|
|||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ on:
|
|||
- "package-lock.json"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: real-replay-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ on:
|
|||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
|
|
|||
10
AGENTS.md
10
AGENTS.md
|
|
@ -50,14 +50,20 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
|
|||
- `npm ci`: install exact lockfile dependencies.
|
||||
- `npm run deps:check`: verify that installed direct dependency versions match the root lockfile; on failure, run `npm ci`, then retry.
|
||||
- `npm run metadata:generate`: refresh checked-in package metadata after package, lockfile, or skill-version changes.
|
||||
- `npm run metadata:check`: verify checked-in package metadata without rewriting it.
|
||||
- `npm run build`: compile `src/` into `dist/`.
|
||||
- `npm run build:cached`: compile through Turbo and reuse a valid local build cache entry.
|
||||
- `npm test`: build, then run the Vitest suite once.
|
||||
- `npm test`: restore or build `dist/`, then run the Vitest suite once without coverage or retries locally.
|
||||
- `npm run test:fast`: run the parallel test project without the serial integration group.
|
||||
- `npm run test:integration`: run the serial filesystem, process, and CLI integration group.
|
||||
- `npm run test:watch`: watch tests affected by working-tree changes in the parallel project, without coverage.
|
||||
- `npm run test:coverage`: run the complete suite with coverage; CI owns verbose and JUnit reporting.
|
||||
- `npm run typecheck`: run strict TypeScript checks without emitting files.
|
||||
- `npm run lint`: apply oxlint rules (complexity, max-lines, unused vars, and TypeScript-specific checks).
|
||||
- `npm run lint:fix`: auto-fix oxlint violations where possible.
|
||||
- `npm run format:check`: verify Prettier formatting.
|
||||
- `npm run check`: run cached typecheck, lint, format:check, and knip tasks.
|
||||
- `npm run check`: run cached typecheck, lint, format:check, knip, and package-metadata freshness tasks.
|
||||
- `npm run check:ci`: run the static gate plus duplicate-code and debt-marker scans.
|
||||
- `npm run check:test`: run the cached static tasks plus the uncached complete test suite.
|
||||
- `npm run check:pr`: run the test gate plus uncached generated-document validation for PR preparation.
|
||||
- `npm run knip`: detect unused files, dependencies, and exports.
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ npm test
|
|||
```
|
||||
|
||||
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
||||
building the project. Run `npm run build` when you need the standalone CLI or
|
||||
MCP server. Turbo caches deterministic local builds and static checks across
|
||||
Git worktrees; use `npm run build:cached` when you want that cache. After a
|
||||
package, lockfile, or managed-skill version change, run
|
||||
building the project. Run `npm run build:cached` when you need the standalone
|
||||
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by
|
||||
Turbo. Turbo caches deterministic builds and static checks across Git
|
||||
worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
`npm run metadata:generate` before building.
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
|
||||
|
|
@ -28,15 +28,26 @@ npm run verify:package
|
|||
npm pack --dry-run
|
||||
```
|
||||
|
||||
`npm run check` runs the cached typecheck, lint, formatting, and dead-code tasks;
|
||||
`check:fast` remains as an alias. Use `npm run check:test` when the complete test
|
||||
suite is relevant. Pre-commit formats and lints only staged files, and pre-push
|
||||
runs `check`. `check:pr` additionally renders API documentation and checks all
|
||||
committed generated metadata. Real provider and replay verifiers remain outside
|
||||
Turbo's cache. CI uploads the rendered TypeDoc site as an `api-docs` artifact;
|
||||
the generated HTML is not committed. The normal test and CI coverage runs
|
||||
enforce the thresholds in `vitest.config.ts`; `npm run lint:dead` rejects unused
|
||||
files, exports, and dependencies.
|
||||
`npm run check` runs cached typecheck, lint, formatting, dead-code, and package
|
||||
metadata freshness tasks; `check:fast` remains as an alias. Use `npm run
|
||||
check:test` when the complete test suite is relevant. Pre-commit formats and
|
||||
lints only staged files, and pre-push runs `check`. `check:pr` additionally
|
||||
renders API documentation and checks all committed generated metadata. Real
|
||||
provider and replay execution remains uncached, but their deterministic build
|
||||
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
|
||||
artifact; the generated HTML is not committed.
|
||||
|
||||
Local `npm test` runs without coverage, retries, or verbose output. Use `npm run
|
||||
test:fast` for the parallel group, `npm run test:integration` for serial
|
||||
filesystem/process/CLI cases, and `npm run test:watch` to watch tests affected by
|
||||
working-tree changes. CI runs `npm run test:ci`, which adds coverage, retries,
|
||||
verbose output, and JUnit results. Coverage thresholds remain in
|
||||
`vitest.config.ts`.
|
||||
|
||||
CI installs dependencies once for all static checks, cancels superseded PR
|
||||
runs, and skips package and Windows lanes when a PR changes only files outside
|
||||
the package/runtime/test boundary. TypeDoc renders only in pull-request CI; it
|
||||
does not run in local commit or pre-push hooks, or in post-merge `main` CI.
|
||||
|
||||
Tests that need a temporary directory must use
|
||||
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The
|
||||
|
|
|
|||
67
package.json
67
package.json
|
|
@ -44,6 +44,8 @@
|
|||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"scripts/rea.mjs",
|
||||
"scripts/hopper-demo-x11.py",
|
||||
"scripts/prepack.mjs",
|
||||
"scripts/prepare.mjs",
|
||||
"skills",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
|
|
@ -51,16 +53,18 @@
|
|||
"scripts": {
|
||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
||||
"prebuild": "npm run deps:check && node scripts/generate-package-metadata.mjs --check",
|
||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"postbuild": "node scripts/record-build-cache.mjs",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"check:fast": "npm run check",
|
||||
"precheck:test": "npm run deps:check",
|
||||
"check:test": "turbo run typecheck lint format:check knip test:run",
|
||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip test:run docs:check:built",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run docs:check:built",
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "turbo run build",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
|
|
@ -71,42 +75,49 @@
|
|||
"jscpd": "jscpd src/ tests/",
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
"start": "node scripts/rea.mjs mcp",
|
||||
"pretest": "npm run build",
|
||||
"pretest": "npm run build:cached",
|
||||
"test": "vitest run",
|
||||
"test:run": "vitest run",
|
||||
"pretest:coverage": "npm run build",
|
||||
"pretest:fast": "npm run build:cached",
|
||||
"test:fast": "vitest run --project parallel",
|
||||
"pretest:integration": "npm run build:cached",
|
||||
"test:integration": "vitest run --project serial-integration",
|
||||
"pretest:coverage": "npm run build:cached",
|
||||
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
|
||||
"test:ci": "npm run test:coverage -- --reporter=verbose --reporter=junit --outputFile.junit=test-results.xml",
|
||||
"pretest:verbose": "npm run build:cached",
|
||||
"test:verbose": "vitest run --reporter=verbose",
|
||||
"test:watch": "vitest",
|
||||
"pretest:watch": "npm run build:cached",
|
||||
"test:watch": "vitest --project parallel --changed",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build && node scripts/verify-real-hopper.mjs",
|
||||
"verify:ghidra": "npm run build && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:windows": "npm run build && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
|
||||
"verify:browser": "npm run build && node scripts/verify-real-browser.mjs",
|
||||
"verify:javascript-runtime": "npm run build && node scripts/verify-local-javascript-runtime.mjs",
|
||||
"verify:application-workflows": "npm run build && node scripts/verify-local-application-workflows.mjs",
|
||||
"verify:managed": "npm run build && node scripts/verify-managed-conformance.mjs",
|
||||
"verify:replay": "npm run build && node scripts/verify-real-javascript-replay.mjs",
|
||||
"verify:agent": "npm run build && node scripts/verify-agent-experience.mjs",
|
||||
"verify:hopper:linux": "npm run verify:fixtures && npm run build && node scripts/verify-real-hopper-linux.mjs",
|
||||
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
|
||||
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
|
||||
"verify:replay": "npm run build:cached && node scripts/verify-real-javascript-replay.mjs",
|
||||
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
|
||||
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
|
||||
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
|
||||
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",
|
||||
"build:ghidra-windows-fixture": "node scripts/create-ghidra-windows-fixture.mjs",
|
||||
"verify:fixtures": "npm run build:fixtures && node scripts/verify-conformance-fixtures.mjs",
|
||||
"verify:package": "npm run build && node scripts/verify-package.mjs",
|
||||
"verify:package:windows": "npm run build && node scripts/verify-windows-package.mjs",
|
||||
"probe:windows-capabilities": "npm run build && node scripts/windows-capabilities.mjs",
|
||||
"verify:package": "npm run build:cached && node scripts/verify-package.mjs",
|
||||
"verify:package:windows": "npm run build:cached && node scripts/verify-windows-package.mjs",
|
||||
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
|
||||
"verify:published": "node scripts/verify-published-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"docs:api": "typedoc",
|
||||
"docs:generate": "npm run metadata:generate && npm run build && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && npm run docs:api && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "npm run build && npm run docs:check:built",
|
||||
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && npm run docs:api && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/check-doc-facts.mjs && npm run docs:api && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "npm run build && node scripts/generate-skill-metadata.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepublishOnly": "npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
|
||||
"prepare": "husky"
|
||||
"prepare": "node scripts/prepare.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,mjs,json,md,yml,yaml}": "prettier --write",
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
const CACHE_DIRECTORY = ["node_modules", ".cache"];
|
||||
const BUILD_INFO_NAME = "rea-tsconfig-build.tsbuildinfo";
|
||||
const MANIFEST_NAME = "rea-tsconfig-build-outputs.json";
|
||||
|
||||
const isMissing = (cause) =>
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
cause.code === "ENOENT";
|
||||
|
||||
const outputFiles = async (root) => {
|
||||
const sourceRoot = join(root, "src");
|
||||
const entries = await readdir(sourceRoot, { recursive: true });
|
||||
return entries
|
||||
.filter((entry) => entry.endsWith(".ts") && !entry.endsWith(".d.ts"))
|
||||
.sort()
|
||||
.map((entry) => {
|
||||
const output = `${entry.slice(0, -3)}.js`;
|
||||
return { output, path: join(root, "dist", output) };
|
||||
});
|
||||
};
|
||||
|
||||
const digest = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
|
||||
const cachePath = (root, name) => join(root, ...CACHE_DIRECTORY, name);
|
||||
|
||||
const invalidate = async (root) => {
|
||||
await Promise.all([
|
||||
rm(cachePath(root, BUILD_INFO_NAME), { force: true }),
|
||||
rm(cachePath(root, MANIFEST_NAME), { force: true }),
|
||||
]);
|
||||
};
|
||||
|
||||
const readManifest = async (root) => {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
await readFile(cachePath(root, MANIFEST_NAME), "utf8"),
|
||||
);
|
||||
if (
|
||||
typeof parsed !== "object" ||
|
||||
parsed === null ||
|
||||
parsed.version !== 1 ||
|
||||
typeof parsed.outputs !== "object" ||
|
||||
parsed.outputs === null ||
|
||||
Array.isArray(parsed.outputs)
|
||||
)
|
||||
return undefined;
|
||||
return parsed.outputs;
|
||||
} catch (cause) {
|
||||
if (isMissing(cause) || cause instanceof SyntaxError) return undefined;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
/** Invalidates incremental state when generated JavaScript was removed or changed. */
|
||||
export const validateBuildCache = async (root) => {
|
||||
const [files, manifest] = await Promise.all([
|
||||
outputFiles(root),
|
||||
readManifest(root),
|
||||
]);
|
||||
if (manifest === undefined || Object.keys(manifest).length !== files.length) {
|
||||
await invalidate(root);
|
||||
return;
|
||||
}
|
||||
const matches = await Promise.all(
|
||||
files.map(async ({ output, path }) => {
|
||||
if (typeof manifest[output] !== "string") return false;
|
||||
try {
|
||||
return (await digest(path)) === manifest[output];
|
||||
} catch (cause) {
|
||||
if (isMissing(cause)) return false;
|
||||
throw cause;
|
||||
}
|
||||
}),
|
||||
);
|
||||
if (!matches.every(Boolean)) await invalidate(root);
|
||||
};
|
||||
|
||||
/** Records generated JavaScript so a later incremental build can verify its cache. */
|
||||
export const recordBuildCache = async (root) => {
|
||||
const files = await outputFiles(root);
|
||||
const entries = await Promise.all(
|
||||
files.map(async ({ output, path }) => [output, await digest(path)]),
|
||||
);
|
||||
const source = `${JSON.stringify({ version: 1, outputs: Object.fromEntries(entries) })}\n`;
|
||||
const directory = join(root, ...CACHE_DIRECTORY);
|
||||
await mkdir(directory, { recursive: true });
|
||||
const path = cachePath(root, MANIFEST_NAME);
|
||||
let existing;
|
||||
try {
|
||||
existing = await readFile(path, "utf8");
|
||||
} catch (cause) {
|
||||
if (!isMissing(cause)) throw cause;
|
||||
}
|
||||
if (existing !== source) await writeFile(path, source, "utf8");
|
||||
};
|
||||
|
|
@ -2,7 +2,6 @@ import { readFile } from "node:fs/promises";
|
|||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { validateBuildCache } from "./build-cache.mjs";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
|
|
@ -47,4 +46,3 @@ await ensureGeneratedFile({
|
|||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run metadata:generate",
|
||||
});
|
||||
await validateBuildCache(root);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { access } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const pathExists = async (path) => {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch (cause) {
|
||||
if (
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
cause.code === "ENOENT"
|
||||
)
|
||||
return false;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 4 * 1_024 * 1_024,
|
||||
});
|
||||
if (result.error !== undefined) throw result.error;
|
||||
if (result.status !== 0) {
|
||||
if (result.stdout.length > 0) process.stdout.write(result.stdout);
|
||||
if (result.stderr.length > 0) process.stderr.write(result.stderr);
|
||||
throw new Error(
|
||||
`Prepack command failed with status ${String(result.status)}: ${command} ${args.join(" ")}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (await pathExists(join(root, "src"))) {
|
||||
const packageManagerPath = process.env.npm_execpath;
|
||||
if (packageManagerPath === undefined)
|
||||
throw new Error("Prepack could not resolve the active package manager");
|
||||
run(process.execPath, [packageManagerPath, "run", "build:cached"]);
|
||||
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
|
||||
} else {
|
||||
for (const path of [
|
||||
"dist/main.js",
|
||||
"dist/mcpDoctor.js",
|
||||
"dist/cli.js",
|
||||
"dist/cliOutput.js",
|
||||
"skills/reverse-engineer-anything/SKILL.md",
|
||||
])
|
||||
await access(join(root, path));
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { access } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const sourceHuskyIsAvailable = async () => {
|
||||
try {
|
||||
await access(join(root, "src"));
|
||||
await access(join(root, "node_modules", "husky", "index.js"));
|
||||
return true;
|
||||
} catch (cause) {
|
||||
if (
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
cause.code === "ENOENT"
|
||||
)
|
||||
return false;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.HUSKY !== "0" && (await sourceHuskyIsAvailable())) {
|
||||
const { default: installHusky } = await import("husky");
|
||||
const message = installHusky();
|
||||
if (message.length > 0) process.stdout.write(`${message}\n`);
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { recordBuildCache } from "./build-cache.mjs";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
await recordBuildCache(root);
|
||||
|
|
@ -100,5 +100,17 @@ export async function verifyPackageInstall({
|
|||
throw new Error(
|
||||
`packaged CLI did not degrade without the optional PTY binary: ${JSON.stringify(noOptionalCapability)}`,
|
||||
);
|
||||
await verifyInstalledPackageCanRepack(prefix, environment);
|
||||
return { cli, packageRunnerCli };
|
||||
}
|
||||
|
||||
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
|
||||
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
|
||||
const repackedTarball = (
|
||||
await exec("npm", ["pack", "--silent"], {
|
||||
cwd: installedPackageRoot,
|
||||
env: environment,
|
||||
})
|
||||
).stdout.trim();
|
||||
await rm(join(installedPackageRoot, repackedTarball), { force: true });
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ describe("package identity", () => {
|
|||
bin: z.record(z.string(), z.string()),
|
||||
scripts: z.object({
|
||||
start: z.string(),
|
||||
"metadata:check": z.string(),
|
||||
prebuild: z.string(),
|
||||
prepare: z.string(),
|
||||
prepack: z.string(),
|
||||
|
|
@ -42,13 +43,14 @@ describe("package identity", () => {
|
|||
`npx -y ${packageJson.name}@${PRODUCT_IDENTITY.packageVersion} mcp`,
|
||||
);
|
||||
expect(packageJson.scripts.start).toBe("node scripts/rea.mjs mcp");
|
||||
expect(packageJson.scripts["metadata:check"]).toBe(
|
||||
"node scripts/generate-package-metadata.mjs --check",
|
||||
);
|
||||
expect(packageJson.scripts.prebuild).toBe(
|
||||
"npm run deps:check && node scripts/generate-package-metadata.mjs --check",
|
||||
);
|
||||
expect(packageJson.scripts.prepare).toBe("husky");
|
||||
expect(packageJson.scripts.prepack).toBe(
|
||||
"npm run build && node scripts/generate-skill-metadata.mjs --check",
|
||||
"npm run deps:check && npm run metadata:check",
|
||||
);
|
||||
expect(packageJson.scripts.prepare).toBe("node scripts/prepare.mjs");
|
||||
expect(packageJson.scripts.prepack).toBe("node scripts/prepack.mjs");
|
||||
expect(packageJson.scripts.prepublishOnly).toBe(
|
||||
"npm run deps:check && node scripts/generate-skill-metadata.mjs --check",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ describe("package installation workflows", () => {
|
|||
|
||||
expect(continuousIntegration).toContain("os: [ubuntu-latest, macos-14]");
|
||||
expect(continuousIntegration).toContain("npm run verify:package");
|
||||
expect(continuousIntegration).toContain("name: Static checks");
|
||||
expect(continuousIntegration).toContain("npm run check:ci");
|
||||
expect(continuousIntegration).toContain("needs: changes");
|
||||
expect(continuousIntegration).toContain(
|
||||
"cancel-in-progress: ${{ github.event_name == 'pull_request' }}",
|
||||
);
|
||||
for (const workflow of [
|
||||
continuousIntegration,
|
||||
realHopperLinux,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { availableParallelism } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SERIAL_INTEGRATION_TESTS } from "../vitest.config.js";
|
||||
import vitestConfiguration, {
|
||||
SERIAL_INTEGRATION_TESTS,
|
||||
} from "../vitest.config.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
describe("Vitest project configuration", () => {
|
||||
it("keeps coverage and noisy reporting out of the local test loop", () => {
|
||||
const continuousIntegration = process.env.CI === "true";
|
||||
|
||||
expect(vitestConfiguration.test?.coverage?.enabled).toBe(false);
|
||||
expect(vitestConfiguration.test?.reporters).toEqual(["default"]);
|
||||
expect(vitestConfiguration.test?.retry).toBe(continuousIntegration ? 2 : 0);
|
||||
expect(vitestConfiguration.test?.maxWorkers).toBe(
|
||||
Math.min(2, availableParallelism()),
|
||||
);
|
||||
});
|
||||
|
||||
it("collects exact disjoint parallel and serial test sets", async () => {
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "node_modules/.cache/rea-tsconfig-build.tsbuildinfo",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
|
|
|
|||
17
turbo.json
17
turbo.json
|
|
@ -4,10 +4,11 @@
|
|||
"build": {
|
||||
"inputs": [
|
||||
".nvmrc",
|
||||
"bridge/**",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/**",
|
||||
"scripts/check-dependency-install.mjs",
|
||||
"scripts/generate-package-metadata.mjs",
|
||||
"scripts/lib/generated-file.mjs",
|
||||
"skills/reverse-engineer-anything/SKILL.md",
|
||||
"src/**/*.ts",
|
||||
"tsconfig.json",
|
||||
|
|
@ -27,9 +28,21 @@
|
|||
"vitest.config.ts"
|
||||
]
|
||||
},
|
||||
"metadata:check": {
|
||||
"inputs": [
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"scripts/generate-package-metadata.mjs",
|
||||
"scripts/lib/generated-file.mjs",
|
||||
"skills/reverse-engineer-anything/SKILL.md",
|
||||
"src/generatedPackageMetadata.ts"
|
||||
]
|
||||
},
|
||||
"lint": {},
|
||||
"format:check": {},
|
||||
"knip": {},
|
||||
"jscpd": {},
|
||||
"scan:todos": {},
|
||||
"test:run": {
|
||||
"dependsOn": ["build"],
|
||||
"cache": false
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { join } from "node:path";
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export const TEST_FILES = ["tests/**/*.test.ts"];
|
||||
const IS_CONTINUOUS_INTEGRATION = process.env.CI === "true";
|
||||
// Subprocess-heavy parallel tests exceed their existing deadlines above two workers.
|
||||
const MAX_TEST_WORKERS = 2;
|
||||
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
|
||||
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
|
||||
export const SERIAL_INTEGRATION_TESTS = [
|
||||
|
|
@ -17,7 +20,7 @@ export const SERIAL_INTEGRATION_TESTS = [
|
|||
"tests/runtime.test.ts",
|
||||
];
|
||||
const isolatedProjects = {
|
||||
maxWorkers: Math.min(2, availableParallelism()),
|
||||
maxWorkers: Math.min(MAX_TEST_WORKERS, availableParallelism()),
|
||||
projects: [
|
||||
{
|
||||
extends: true as const,
|
||||
|
|
@ -44,10 +47,10 @@ export default defineConfig({
|
|||
test: {
|
||||
env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY },
|
||||
...isolatedProjects,
|
||||
retry: 2,
|
||||
reporters: ["default", "verbose"],
|
||||
retry: IS_CONTINUOUS_INTEGRATION ? 2 : 0,
|
||||
reporters: ["default"],
|
||||
coverage: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
provider: "v8",
|
||||
reportsDirectory: join(
|
||||
tmpdir(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue