diff --git a/.codex/actions/_artifact_env.sh b/.codex/actions/_artifact_env.sh new file mode 100755 index 0000000..4465a35 --- /dev/null +++ b/.codex/actions/_artifact_env.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +REPO_ROOT="${CODEX_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +REPO_NAME="${CODEX_REPO_NAME:-$(basename "$REPO_ROOT")}" + +if command -v shasum >/dev/null 2>&1; then + REPO_HASH="${CODEX_REPO_HASH:-$(printf '%s' "$REPO_ROOT" | shasum -a 256 | awk '{print substr($1,1,12)}')}" +else + REPO_HASH="${CODEX_REPO_HASH:-$(printf '%s' "$REPO_ROOT" | md5 | awk '{print substr($NF,1,12)}')}" +fi + +RUN_ID="${CODEX_RUN_ID:-$(date +%Y%m%dT%H%M%S)-$$}" +CODEX_CACHE_ROOT="${CODEX_CACHE_ROOT:-/Users/d/Library/Caches/Codex}" +CODEX_BUILD_ROOT="${CODEX_BUILD_ROOT:-$CODEX_CACHE_ROOT/build}" +CODEX_LOG_ROOT="${CODEX_LOG_ROOT:-$CODEX_CACHE_ROOT/logs}" + +export CODEX_REPO_ROOT="$REPO_ROOT" +export CODEX_REPO_NAME="$REPO_NAME" +export CODEX_REPO_HASH="$REPO_HASH" +export CODEX_RUN_ID="$RUN_ID" + +export CODEX_BUILD_RUST_DIR="${CODEX_BUILD_RUST_DIR:-$CODEX_BUILD_ROOT/rust/$REPO_HASH}" +export CODEX_BUILD_NEXT_DIR="${CODEX_BUILD_NEXT_DIR:-$CODEX_BUILD_ROOT/next/$REPO_HASH}" +export CODEX_BUILD_JS_DIR="${CODEX_BUILD_JS_DIR:-$CODEX_BUILD_ROOT/js/$REPO_HASH}" +export CODEX_LOG_RUN_DIR="${CODEX_LOG_RUN_DIR:-$CODEX_LOG_ROOT/$REPO_NAME/$RUN_ID}" + +mkdir -p "$CODEX_BUILD_RUST_DIR" "$CODEX_BUILD_NEXT_DIR" "$CODEX_BUILD_JS_DIR" "$CODEX_LOG_RUN_DIR" + +if [[ -z "${CARGO_TARGET_DIR:-}" ]]; then + export CARGO_TARGET_DIR="$CODEX_BUILD_RUST_DIR" +fi +if [[ -z "${NEXT_CACHE_DIR:-}" ]]; then + export NEXT_CACHE_DIR="$CODEX_BUILD_NEXT_DIR" +fi +if [[ -z "${VITE_CACHE_DIR:-}" ]]; then + export VITE_CACHE_DIR="$CODEX_BUILD_JS_DIR/vite" +fi +if [[ -z "${TURBO_CACHE_DIR:-}" ]]; then + export TURBO_CACHE_DIR="$CODEX_BUILD_JS_DIR/turbo" +fi diff --git a/.codex/bootstrap/package-bootstrap.json b/.codex/bootstrap/package-bootstrap.json new file mode 100644 index 0000000..4e81a67 --- /dev/null +++ b/.codex/bootstrap/package-bootstrap.json @@ -0,0 +1,5 @@ +{ + "schema": "codex-os-package-bootstrap/v1", + "managed_by": "codex-os-managed", + "notes": "Merged into package.json by scripts/merge_package_json.mjs" +} diff --git a/.codex/codex-os.manifest.json b/.codex/codex-os.manifest.json new file mode 100644 index 0000000..4ac7fcf --- /dev/null +++ b/.codex/codex-os.manifest.json @@ -0,0 +1,9 @@ +{ + "schema": "codex-os-manifest/v1", + "managed_by": "codex-os-managed", + "bootstrap_version": "1.0.0", + "profile": "side", + "installed_at": "2026-02-17T05:40:10Z", + "last_verified_at": "2026-02-17T05:40:10Z", + "template_pack": "react-ts" +} diff --git a/.codex/scripts/run_verify_commands.sh b/.codex/scripts/run_verify_commands.sh new file mode 100755 index 0000000..12e8105 --- /dev/null +++ b/.codex/scripts/run_verify_commands.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +COMMANDS_FILE="${1:-.codex/verify.commands}" + +if [[ ! -f "$COMMANDS_FILE" ]]; then + echo "Missing $COMMANDS_FILE" + exit 2 +fi + +while IFS= read -r cmd || [[ -n "$cmd" ]]; do + [[ -z "${cmd//[[:space:]]/}" ]] && continue + [[ "$cmd" =~ ^[[:space:]]*# ]] && continue + + echo ">> $cmd" + eval "$cmd" +done < "$COMMANDS_FILE" diff --git a/.codex/verify.commands b/.codex/verify.commands new file mode 100644 index 0000000..37d3fb0 --- /dev/null +++ b/.codex/verify.commands @@ -0,0 +1,5 @@ +# codex-os-managed +pnpm git:guard:all +pnpm perf:bundle +pnpm perf:build +pnpm perf:assets diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d0e74e6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ + +## What +- + +## Why +- + +## How +- + +## Testing +- Commands run: +- Results: + +## Performance impact +- Bundle delta: +- Build time delta: +- Lighthouse delta: +- API latency delta: +- DB query delta: + +## Risk / Notes +- + +## Screenshots (UI only) +- + +## Lockfile rationale (if lockfile changed) +- + +## Baseline governance (if .perf-baselines changed) +- `perf-baseline-update` label applied: +- Reviewer signoff: +- Rollback note: diff --git a/.github/workflows/git-hygiene.yml b/.github/workflows/git-hygiene.yml new file mode 100644 index 0000000..04ee081 --- /dev/null +++ b/.github/workflows/git-hygiene.yml @@ -0,0 +1,52 @@ +name: git-hygiene + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main, master] + +jobs: + commitlint: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed + + pr-title: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + branch-name: + runs-on: ubuntu-latest + permissions: + pull-requests: read + steps: + - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b + with: + script: | + const branch = context.payload.pull_request?.head?.ref || ""; + const pattern = /^codex\/(feat|fix|chore|refactor|docs|test|perf|ci|spike|hotfix)\/[a-z0-9]+(?:-[a-z0-9]+)*$/; + if (!pattern.test(branch)) { + core.setFailed(`Invalid branch name: ${branch}`); + } + + secrets: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lockfile-rationale.yml b/.github/workflows/lockfile-rationale.yml new file mode 100644 index 0000000..22b2cff --- /dev/null +++ b/.github/workflows/lockfile-rationale.yml @@ -0,0 +1,24 @@ +name: lockfile-rationale + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +jobs: + enforce: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c + id: changed + - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b + if: contains(steps.changed.outputs.all_changed_files, 'pnpm-lock.yaml') || contains(steps.changed.outputs.all_changed_files, 'package-lock.json') || contains(steps.changed.outputs.all_changed_files, 'yarn.lock') + with: + script: | + const body = (context.payload.pull_request?.body || ""); + if (!/## Lockfile rationale/i.test(body)) { + core.setFailed("Lockfile changed but PR body lacks 'Lockfile rationale' section."); + } diff --git a/.github/workflows/perf-enforced.yml b/.github/workflows/perf-enforced.yml new file mode 100644 index 0000000..d9d71c0 --- /dev/null +++ b/.github/workflows/perf-enforced.yml @@ -0,0 +1,113 @@ +name: perf-enforced + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + branches: [main, master] + +jobs: + perf-bundle: + if: ${{ vars.PERF_PROFILE == 'production' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build || pnpm build:ui + - run: pnpm perf:bundle + - run: node scripts/perf/compare-metric.mjs .perf-baselines/bundle.json .perf-results/bundle.json totalBytes 0.08 + + perf-build: + if: ${{ vars.PERF_PROFILE == 'production' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm perf:build + - run: node scripts/perf/compare-metric.mjs .perf-baselines/build-time.json .perf-results/build-time.json buildMs 0.15 + + perf-assets: + if: ${{ vars.PERF_PROFILE == 'production' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - run: ASSET_MAX_BYTES=250000 bash scripts/perf/check-assets.sh + + perf-memory: + if: ${{ vars.PERF_PROFILE == 'production' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: MEMORY_MAX_DELTA_MB=5 pnpm perf:memory + + perf-lighthouse: + if: ${{ vars.PERF_PROFILE == 'production' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build || pnpm build:ui + - run: pnpm perf:lhci:prod || pnpm perf:lhci + + perf-api: + if: ${{ vars.PERF_PROFILE == 'production' && vars.PERF_BASE_URL != '' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 + - run: k6 run tests/perf/api.k6.js --summary-export=.perf-results/api-summary.json + env: + BASE_URL: ${{ vars.PERF_BASE_URL }} + API_P95_MS: "250" + API_P99_MS: "700" + + perf-db: + if: ${{ vars.PERF_PROFILE == 'production' && vars.PERF_DATABASE_URL != '' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Install PostgreSQL client + run: sudo apt-get update && sudo apt-get install -y postgresql-client + - run: psql "${{ vars.PERF_DATABASE_URL }}" -f scripts/perf/db/check-pg-stat.sql diff --git a/.github/workflows/perf-foundation.yml b/.github/workflows/perf-foundation.yml new file mode 100644 index 0000000..7076fe7 --- /dev/null +++ b/.github/workflows/perf-foundation.yml @@ -0,0 +1,104 @@ +name: perf-foundation + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, master] + +jobs: + perf-bundle: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build || pnpm build:ui + - run: pnpm perf:bundle + + perf-build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm perf:build + + perf-lighthouse: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm build || pnpm build:ui + - run: pnpm perf:lhci || true + + perf-assets: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - run: bash scripts/perf/check-assets.sh + + perf-memory: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.28.1 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm perf:memory + + perf-api: + if: ${{ vars.PERF_BASE_URL != '' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 + - run: k6 run tests/perf/api.k6.js --summary-export=.perf-results/api-summary.json + env: + BASE_URL: ${{ vars.PERF_BASE_URL }} + + perf-db: + if: ${{ vars.PERF_DATABASE_URL != '' }} + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - name: Install PostgreSQL client + run: sudo apt-get update && sudo apt-get install -y postgresql-client + - run: psql "${{ vars.PERF_DATABASE_URL }}" -f scripts/perf/db/check-pg-stat.sql diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..549f131 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# codex-os-managed +. "$(dirname -- "$0")/_/husky.sh" 2>/dev/null || true + +if command -v pnpm >/dev/null 2>&1; then + pnpm commitlint --edit "$1" +elif command -v npm >/dev/null 2>&1; then + npx commitlint --edit "$1" +else + yarn commitlint --edit "$1" +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..5c088c4 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,23 @@ +#!/usr/bin/env sh +# codex-os-managed +. "$(dirname -- "$0")/_/husky.sh" 2>/dev/null || true + +run_script() { + if command -v pnpm >/dev/null 2>&1; then + pnpm "$@" + elif command -v npm >/dev/null 2>&1; then + npm run "$1" --if-present + elif command -v yarn >/dev/null 2>&1; then + yarn "$@" + else + echo "No supported package manager found." + exit 1 + fi +} + +run_script git:guard:branch +run_script git:guard:atomic +run_script lint-staged +run_script git:guard:generated +run_script git:guard:large-files +run_script git:guard:secrets diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..bf4a902 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# codex-os-managed +. "$(dirname -- "$0")/_/husky.sh" 2>/dev/null || true + +if command -v pnpm >/dev/null 2>&1; then + pnpm git:guard:no-main-push +elif command -v npm >/dev/null 2>&1; then + npm run git:guard:no-main-push --if-present +else + yarn git:guard:no-main-push +fi diff --git a/.lighthouserc.production.json b/.lighthouserc.production.json new file mode 100644 index 0000000..5840a65 --- /dev/null +++ b/.lighthouserc.production.json @@ -0,0 +1,19 @@ +{ + "ci": { + "collect": { + "numberOfRuns": 3, + "url": ["http://localhost:4173/"] + }, + "assert": { + "assertions": { + "categories:performance": ["error", { "minScore": 0.9 }], + "categories:accessibility": ["error", { "minScore": 0.95 }], + "categories:best-practices": ["error", { "minScore": 0.9 }], + "categories:seo": ["error", { "minScore": 0.9 }] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/.perf-baselines/build-time.json b/.perf-baselines/build-time.json new file mode 100644 index 0000000..5b59fc4 --- /dev/null +++ b/.perf-baselines/build-time.json @@ -0,0 +1,4 @@ +{ + "buildMs": 0, + "capturedAt": "1970-01-01T00:00:00.000Z" +} diff --git a/.perf-baselines/bundle.json b/.perf-baselines/bundle.json new file mode 100644 index 0000000..a51c22a --- /dev/null +++ b/.perf-baselines/bundle.json @@ -0,0 +1,4 @@ +{ + "totalBytes": 0, + "capturedAt": "1970-01-01T00:00:00.000Z" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8b3394b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# codex-os-managed +# Repo AGENTS baseline generated by global Codex OS bootstrap. + +## Definition of Done (Git + Performance) +- Work on non-default branch only. +- Branch must match `codex//`. +- Commit messages must follow Conventional Commits. +- Commits must be atomic by concern. +- Run reviewer/fixer loop before final completion: + - `reviewer-findings-v1` + - `fixer-apply-findings-v1` + - re-run reviewer until no P0/P1 remain +- PR must include sections: What, Why, How, Testing, Performance impact, Risk / Notes. +- If lockfile changed, include lockfile rationale in PR body. +- Required checks before done-state: + - git hygiene + - bundle delta + - build delta + - performance budgets (profile-dependent) + - assets/memory checks +- Required gates block completion when `fail` or `not-run`. + +## Verification Contract +- Canonical commands are in `.codex/verify.commands`. +- Use `.codex/scripts/run_verify_commands.sh` for deterministic execution. diff --git a/README.md b/README.md index a437270..6935867 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,48 @@ npm install # Run in development mode npm run tauri dev +# Run in low-disk lean development mode +npm run dev:lean + # Build for production npm run tauri build ``` +### Normal Dev vs Lean Dev + +- **Normal dev (`npm run tauri dev`)** + - Fastest repeated startup once Rust and frontend caches are warm. + - Uses persistent local build artifacts (for example `src-tauri/target`, Vite cache in `node_modules/.vite`). +- **Lean dev (`npm run dev:lean`)** + - Runs the same Tauri dev flow, but moves heavy build caches to temporary locations. + - Automatically removes heavy build artifacts when you exit. + - Uses less persistent disk space, but startup/rebuild can be slower. + +### Cleanup Commands + +- **Targeted heavy cleanup** (keeps dependencies for speed): + +```bash +npm run clean:heavy +``` + +Removes only heavy build artifacts: +- `dist` +- `src-tauri/target` +- `node_modules/.vite` + +- **Full local reproducible cleanup** (maximum space recovery): + +```bash +npm run clean:full +``` + +Removes: +- `dist` +- `src-tauri/target` +- `node_modules/.vite` +- `node_modules` (recreated by `npm install`) + ### First Connection 1. Click "Add" in the sidebar to create a connection diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..6509d3b --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,10 @@ +// codex-os-managed +export default { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [2, "always", ["feat", "fix", "refactor", "perf", "docs", "test", "build", "ci", "chore", "revert"]], + "header-max-length": [2, "always", 72], + "subject-empty": [2, "never"], + "subject-full-stop": [2, "never", "."], + }, +}; diff --git a/lighthouserc.json b/lighthouserc.json new file mode 100644 index 0000000..10f1165 --- /dev/null +++ b/lighthouserc.json @@ -0,0 +1,19 @@ +{ + "ci": { + "collect": { + "numberOfRuns": 3, + "url": ["http://localhost:4173/"] + }, + "assert": { + "assertions": { + "categories:performance": ["warn", { "minScore": 0.85 }], + "categories:accessibility": ["warn", { "minScore": 0.9 }], + "categories:best-practices": ["warn", { "minScore": 0.9 }], + "categories:seo": ["warn", { "minScore": 0.9 }] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/package.json b/package.json index 86ea2fd..871ed28 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,35 @@ "type": "module", "scripts": { "dev": "vite", + "dev:lean": "./scripts/lean-dev.sh", "build": "tsc && vite build", "preview": "vite preview", "lint": "tsc --noEmit", "lint:fix": "eslint . --fix && prettier --write .", "format": "prettier --write .", - "tauri": "tauri" + "tauri": "tauri", + "clean:heavy": "./scripts/cleanup-heavy.sh", + "clean:full": "./scripts/cleanup-full.sh", + "prepare": "husky", + "commit": "cz", + "commitlint": "commitlint", + "git:guard:branch": "bash scripts/git/guard-branch.sh", + "git:guard:no-main-push": "bash scripts/git/guard-no-main-push.sh", + "git:guard:atomic": "bash scripts/git/guard-atomic.sh", + "git:guard:generated": "bash scripts/git/guard-generated.sh", + "git:guard:large-files": "bash scripts/git/guard-large-files.sh", + "git:guard:secrets": "bash scripts/git/guard-secrets.sh", + "git:guard:all": "pnpm git:guard:branch && pnpm git:guard:atomic && pnpm git:guard:generated && pnpm git:guard:large-files && pnpm git:guard:secrets", + "git:commit:propose": "node scripts/git/propose-commit-message.mjs", + "git:session:save": "bash scripts/git/session-save.sh", + "git:session:resume": "bash scripts/git/session-resume.sh", + "pr:notes": "bash scripts/git/generate-pr-notes.sh", + "perf:bundle": "node scripts/perf/bundle-report.mjs", + "perf:build": "node scripts/perf/measure-build-time.mjs", + "perf:assets": "bash scripts/perf/check-assets.sh", + "perf:memory": "node --expose-gc scripts/perf/memory-smoke.mjs", + "perf:summary": "node scripts/perf/summarize.mjs", + "lint-staged": "lint-staged" }, "dependencies": { "@dagrejs/dagre": "^2.0.3", @@ -39,6 +62,23 @@ "prettier": "^3.0.0", "tailwindcss": "^4.1.18", "typescript": "~5.8.3", - "vite": "^7.0.4" + "vite": "^7.0.4", + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@commitlint/cz-commitlint": "^19.8.1", + "@lhci/cli": "^0.15.1", + "commitizen": "^4.3.1", + "husky": "^9.1.7", + "lint-staged": "^15.5.2" + }, + "config": { + "commitizen": { + "path": "@commitlint/cz-commitlint" + } + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs,json,md,css,scss,yml,yaml}": [ + "prettier --write" + ] } } diff --git a/scripts/cleanup-full.sh b/scripts/cleanup-full.sh new file mode 100755 index 0000000..6c71c25 --- /dev/null +++ b/scripts/cleanup-full.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +cd "$REPO_ROOT" + +paths=( + "dist" + "src-tauri/target" + "node_modules/.vite" + "node_modules" +) + +for path in "${paths[@]}"; do + if [ -e "$path" ]; then + rm -rf "$path" + echo "removed $path" + fi +done diff --git a/scripts/cleanup-heavy.sh b/scripts/cleanup-heavy.sh new file mode 100755 index 0000000..82aeeec --- /dev/null +++ b/scripts/cleanup-heavy.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +cd "$REPO_ROOT" + +paths=( + "dist" + "src-tauri/target" + "node_modules/.vite" +) + +for path in "${paths[@]}"; do + if [ -e "$path" ]; then + rm -rf "$path" + echo "removed $path" + fi +done diff --git a/scripts/git/create-branch.sh b/scripts/git/create-branch.sh new file mode 100755 index 0000000..083a139 --- /dev/null +++ b/scripts/git/create-branch.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +if [[ $# -lt 2 ]]; then + echo "usage: $0 " + exit 2 +fi + +branch_type="$1" +shift + +task="$*" +slug="$(echo "$task" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+|-+$//g; s/-+/-/g')" +branch="codex/${branch_type}/${slug}" + +git switch -c "$branch" +echo "$branch" diff --git a/scripts/git/generate-pr-notes.sh b/scripts/git/generate-pr-notes.sh new file mode 100755 index 0000000..4b5ce83 --- /dev/null +++ b/scripts/git/generate-pr-notes.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +base="${1:-origin/main}" +if ! git rev-parse --verify "$base" >/dev/null 2>&1; then + base="origin/master" +fi + +echo "## What" +git log --no-merges --format='- %s' "${base}..HEAD" +echo +echo "## Files changed" +git diff --name-status "${base}...HEAD" | awk '{print "- "$0}' +echo +echo "## Testing" +echo "- Add commands run and outcomes." +echo +echo "## Performance impact" +echo "- Bundle delta:" +echo "- Build time delta:" +echo "- Lighthouse/API/DB:" +echo +echo "## Risk / Notes" +echo "- Add tradeoffs and follow-ups." diff --git a/scripts/git/guard-atomic.sh b/scripts/git/guard-atomic.sh new file mode 100755 index 0000000..27e381c --- /dev/null +++ b/scripts/git/guard-atomic.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +max_files="${GIT_GUARD_MAX_FILES:-25}" +count="$(git diff --cached --name-only | sed '/^$/d' | wc -l | tr -d ' ')" + +if (( count == 0 )); then + echo "No staged files; skipping atomicity check." + exit 0 +fi + +if (( count > max_files )); then + echo "Too many staged files ($count > $max_files). Split into atomic commits." + exit 1 +fi diff --git a/scripts/git/guard-branch.sh b/scripts/git/guard-branch.sh new file mode 100755 index 0000000..271832b --- /dev/null +++ b/scripts/git/guard-branch.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +branch="$(git rev-parse --abbrev-ref HEAD)" +pattern='^codex/(feat|fix|chore|refactor|docs|test|perf|ci|spike|hotfix)/[a-z0-9]+(-[a-z0-9]+)*$' + +if [[ "$branch" == "main" || "$branch" == "master" ]]; then + echo "Direct work on $branch is blocked." + exit 1 +fi + +if ! [[ "$branch" =~ $pattern ]]; then + echo "Invalid branch: $branch" + echo "Expected: codex//" + exit 1 +fi diff --git a/scripts/git/guard-generated.sh b/scripts/git/guard-generated.sh new file mode 100755 index 0000000..31d0bb9 --- /dev/null +++ b/scripts/git/guard-generated.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +forbidden='(^|/)(node_modules|dist|build|out|coverage|\.next|target)/' +if git diff --cached --name-only | grep -E "$forbidden" >/dev/null; then + echo "Generated artifacts are staged. Unstage them before commit." + exit 1 +fi diff --git a/scripts/git/guard-large-files.sh b/scripts/git/guard-large-files.sh new file mode 100755 index 0000000..473a3b2 --- /dev/null +++ b/scripts/git/guard-large-files.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +max_bytes="${GIT_GUARD_MAX_BYTES:-2097152}" +fail=0 +while IFS= read -r file; do + [[ -f "$file" ]] || continue + size=$(wc -c <"$file") + if (( size > max_bytes )); then + echo "Large file staged (>${max_bytes} bytes): $file" + fail=1 + fi +done < <(git diff --cached --name-only --diff-filter=AM) +exit $fail diff --git a/scripts/git/guard-no-main-push.sh b/scripts/git/guard-no-main-push.sh new file mode 100755 index 0000000..51f200c --- /dev/null +++ b/scripts/git/guard-no-main-push.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +branch="$(git rev-parse --abbrev-ref HEAD)" +if [[ "$branch" == "main" || "$branch" == "master" ]]; then + echo "Pushing from $branch is blocked." + exit 1 +fi diff --git a/scripts/git/guard-secrets.sh b/scripts/git/guard-secrets.sh new file mode 100755 index 0000000..2e43b22 --- /dev/null +++ b/scripts/git/guard-secrets.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +if ! command -v gitleaks >/dev/null 2>&1; then + echo "gitleaks not found. Install gitleaks to enforce secret scanning." + exit 1 +fi + +gitleaks protect --staged --redact diff --git a/scripts/git/propose-commit-message.mjs b/scripts/git/propose-commit-message.mjs new file mode 100644 index 0000000..a7a13e4 --- /dev/null +++ b/scripts/git/propose-commit-message.mjs @@ -0,0 +1,41 @@ +import { execFileSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; + +const staged = execFileSync( + "/usr/bin/git", + ["diff", "--cached", "--name-only", "--diff-filter=ACMR"], + { encoding: "utf8" }, +) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + +if (staged.length === 0) { + console.error("No staged files."); + process.exit(1); +} + +const lower = staged.map((f) => f.toLowerCase()); +const hasDocs = lower.some((f) => f.endsWith(".md") || f.includes("docs/")); +const hasTests = lower.some((f) => f.includes("test") || f.includes("spec")); +const hasCi = lower.some((f) => f.startsWith(".github/workflows/")); +const hasPerf = lower.some((f) => f.includes("/perf/") || f.includes("lighthouserc")); +const hasDeps = lower.some((f) => f.endsWith("package.json") || f.includes("lock")); + +let type = "feat"; +if (hasCi) type = "ci"; +else if (hasPerf) type = "perf"; +else if (hasTests) type = "test"; +else if (hasDocs) type = "docs"; +else if (hasDeps) type = "build"; + +let scope = "repo"; +if (lower.some((f) => f.includes("scripts/git/"))) scope = "git"; +else if (lower.some((f) => f.includes("scripts/perf/"))) scope = "perf"; +else if (lower.some((f) => f.startsWith(".github/"))) scope = "ci"; + +const summary = `${type}(${scope}): update ${staged.length} file${staged.length === 1 ? "" : "s"}`; +const out = `.git/CODEX_COMMIT_MSG_PROPOSAL`; +writeFileSync(out, `${summary}\n`); +console.log(summary); +console.log(`written: ${out}`); diff --git a/scripts/git/session-resume.sh b/scripts/git/session-resume.sh new file mode 100755 index 0000000..d8ea70d --- /dev/null +++ b/scripts/git/session-resume.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +if [[ ! -f .git/CODEX_LAST_WIP ]]; then + echo "No saved WIP tag found." + exit 1 +fi + +tag="$(cat .git/CODEX_LAST_WIP)" +git stash list | grep -F "$tag" >/dev/null +git stash apply "stash^{/$tag}" +echo "Restored WIP: $tag" diff --git a/scripts/git/session-save.sh b/scripts/git/session-save.sh new file mode 100755 index 0000000..a4238ea --- /dev/null +++ b/scripts/git/session-save.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +tag="codex-wip/$(git rev-parse --abbrev-ref HEAD)/$(date +%Y%m%d-%H%M%S)" +git stash push -u -m "$tag" +echo "$tag" > .git/CODEX_LAST_WIP +echo "Saved WIP: $tag" diff --git a/scripts/lean-dev.sh b/scripts/lean-dev.sh new file mode 100755 index 0000000..9d84140 --- /dev/null +++ b/scripts/lean-dev.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" +cd "$REPO_ROOT" + +LEAN_TMP_ROOT="$(mktemp -d -t dbviz-lean-dev-XXXXXX)" +LEAN_CARGO_TARGET_DIR="$LEAN_TMP_ROOT/cargo-target" +LEAN_VITE_CACHE_DIR="$LEAN_TMP_ROOT/vite-cache" +LEAN_DEV_PORT="${LEAN_DEV_PORT:-1420}" + +mkdir -p "$LEAN_CARGO_TARGET_DIR" "$LEAN_VITE_CACHE_DIR" + +cleanup() { + local exit_code=$? + + if [ -d "$LEAN_TMP_ROOT" ]; then + rm -rf "$LEAN_TMP_ROOT" + fi + + npm run clean:heavy >/dev/null 2>&1 || true + + exit "$exit_code" +} +trap cleanup EXIT INT TERM + +export CARGO_TARGET_DIR="$LEAN_CARGO_TARGET_DIR" +export VITE_CACHE_DIR="$LEAN_VITE_CACHE_DIR" +export TAURI_DEV_PORT="$LEAN_DEV_PORT" + +echo "[lean-dev] temporary cargo target: $CARGO_TARGET_DIR" +echo "[lean-dev] temporary vite cache: $VITE_CACHE_DIR" +echo "[lean-dev] tauri dev port: $TAURI_DEV_PORT" + +npm run tauri dev -- --port "$TAURI_DEV_PORT" diff --git a/scripts/perf/bundle-report.mjs b/scripts/perf/bundle-report.mjs new file mode 100644 index 0000000..6f92cc8 --- /dev/null +++ b/scripts/perf/bundle-report.mjs @@ -0,0 +1,58 @@ +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +function nextBundle() { + const manifestPath = ".next/build-manifest.json"; + if (!existsSync(manifestPath)) return null; + + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + const pages = manifest.pages || {}; + const pageSizes = {}; + + for (const [route, files] of Object.entries(pages)) { + let total = 0; + for (const file of files) { + const full = path.join(".next", file.replace(/^\/?/, "")); + try { + total += statSync(full).size; + } catch {} + } + pageSizes[route] = total; + } + + return { + source: "next", + totalBytes: Object.values(pageSizes).reduce((a, b) => a + b, 0), + pages: pageSizes, + }; +} + +function viteBundle() { + const distAssets = "dist/assets"; + if (!existsSync(distAssets)) return null; + + const result = { source: "vite", totalBytes: 0, assets: {} }; + for (const file of readdirSync(distAssets)) { + const full = path.join(distAssets, file); + try { + const size = statSync(full).size; + result.assets[file] = size; + result.totalBytes += size; + } catch {} + } + return result; +} + +const run = async () => { + const report = nextBundle() || (await viteBundle()) || { source: "none", totalBytes: 0 }; + mkdirSync(".perf-results", { recursive: true }); + writeFileSync( + ".perf-results/bundle.json", + JSON.stringify({ ...report, capturedAt: new Date().toISOString() }, null, 2), + ); +}; + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/perf/check-assets.sh b/scripts/perf/check-assets.sh new file mode 100755 index 0000000..faa4da0 --- /dev/null +++ b/scripts/perf/check-assets.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# codex-os-managed +max_bytes="${ASSET_MAX_BYTES:-350000}" +if [[ ! -d public ]]; then + echo "No public directory found; skipping asset check." + exit 0 +fi + +fail=0 +while IFS= read -r file; do + size=$(wc -c < "$file") + if (( size > max_bytes )); then + echo "Asset too large (>${max_bytes} bytes): $file" + fail=1 + fi +done < <(find public -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.webp" -o -name "*.avif" \)) + +exit $fail diff --git a/scripts/perf/compare-metric.mjs b/scripts/perf/compare-metric.mjs new file mode 100644 index 0000000..6ac13ac --- /dev/null +++ b/scripts/perf/compare-metric.mjs @@ -0,0 +1,27 @@ +import { readFileSync } from "node:fs"; + +const [baselinePath, currentPath, metric, maxRatio] = process.argv.slice(2); +if (!baselinePath || !currentPath || !metric || !maxRatio) { + console.error("usage: node compare-metric.mjs "); + process.exit(2); +} + +const baseline = JSON.parse(readFileSync(baselinePath, "utf8")); +const current = JSON.parse(readFileSync(currentPath, "utf8")); +const b = baseline[metric]; +const c = current[metric]; + +if (typeof b !== "number" || typeof c !== "number") { + console.error(`Metric ${metric} missing or not numeric.`); + process.exit(2); +} + +const ratio = (c - b) / b; +console.log(JSON.stringify({ metric, baseline: b, current: c, ratio }, null, 2)); + +if (ratio > Number(maxRatio)) { + console.error( + `Regression on ${metric}: ${(ratio * 100).toFixed(2)}% > ${(Number(maxRatio) * 100).toFixed(2)}%`, + ); + process.exit(1); +} diff --git a/scripts/perf/db/check-pg-stat.sql b/scripts/perf/db/check-pg-stat.sql new file mode 100644 index 0000000..f46199d --- /dev/null +++ b/scripts/perf/db/check-pg-stat.sql @@ -0,0 +1,10 @@ +-- codex-os-managed +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + +WITH offenders AS ( + SELECT queryid, mean_exec_time, calls, query + FROM pg_stat_statements + WHERE calls >= 50 AND mean_exec_time > 100 + ORDER BY mean_exec_time DESC +) +SELECT * FROM offenders LIMIT 20; diff --git a/scripts/perf/measure-build-time.mjs b/scripts/perf/measure-build-time.mjs new file mode 100644 index 0000000..90cae5a --- /dev/null +++ b/scripts/perf/measure-build-time.mjs @@ -0,0 +1,32 @@ +import { spawnSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; + +const npmExecPath = process.env.npm_execpath; +if (!npmExecPath) { + console.error("npm_execpath is not set; run this script through pnpm, npm, or yarn."); + process.exit(1); +} + +const start = Date.now(); +const result = spawnSync(process.execPath, [npmExecPath, "run", "build"], { + stdio: "inherit", +}); +const end = Date.now(); + +mkdirSync(".perf-results", { recursive: true }); +writeFileSync( + ".perf-results/build-time.json", + JSON.stringify( + { + buildMs: end - start, + capturedAt: new Date().toISOString(), + command: "npm_execpath run build", + }, + null, + 2, + ), +); + +if (result.status !== 0) { + process.exit(result.status ?? 1); +} diff --git a/scripts/perf/memory-smoke.mjs b/scripts/perf/memory-smoke.mjs new file mode 100644 index 0000000..5545a13 --- /dev/null +++ b/scripts/perf/memory-smoke.mjs @@ -0,0 +1,40 @@ +import { mkdirSync, writeFileSync } from "node:fs"; + +if (typeof globalThis.gc !== "function") { + console.error("Run with --expose-gc."); + process.exit(1); +} + +globalThis.gc(); +const before = process.memoryUsage().heapUsed; + +const allocations = []; +for (let i = 0; i < 20_000; i += 1) { + allocations.push({ i, payload: `row-${i}`.repeat(4) }); +} + +globalThis.gc(); +const after = process.memoryUsage().heapUsed; +const deltaMb = Number(((after - before) / (1024 * 1024)).toFixed(2)); +const maxDelta = Number(process.env.MEMORY_MAX_DELTA_MB || 10); + +mkdirSync(".perf-results", { recursive: true }); +writeFileSync( + ".perf-results/memory.json", + JSON.stringify( + { + heapBefore: before, + heapAfter: after, + deltaMb, + maxDeltaMb: maxDelta, + capturedAt: new Date().toISOString(), + }, + null, + 2, + ), +); + +if (deltaMb > maxDelta) { + console.error(`Memory growth too high: ${deltaMb}MB > ${maxDelta}MB`); + process.exit(1); +} diff --git a/scripts/perf/summarize.mjs b/scripts/perf/summarize.mjs new file mode 100644 index 0000000..58a69d5 --- /dev/null +++ b/scripts/perf/summarize.mjs @@ -0,0 +1,21 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; + +const files = { + bundle: ".perf-results/bundle.json", + build: ".perf-results/build-time.json", + memory: ".perf-results/memory.json", + api: ".perf-results/api-summary.json", +}; + +const summary = { capturedAt: new Date().toISOString(), metrics: {}, status: "pass" }; +for (const [key, file] of Object.entries(files)) { + if (existsSync(file)) { + summary.metrics[key] = JSON.parse(readFileSync(file, "utf8")); + } else { + summary.metrics[key] = { status: "not-run" }; + } +} + +mkdirSync(".perf-results", { recursive: true }); +writeFileSync(".perf-results/summary.json", `${JSON.stringify(summary, null, 2)}\n`); +console.log("wrote .perf-results/summary.json"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4c378b3..a8616df 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -4,7 +4,7 @@ "version": "0.1.0", "identifier": "com.dbviz.app", "build": { - "beforeDevCommand": "npm run dev", + "beforeDevCommand": "npm run dev -- --port ${TAURI_DEV_PORT:-1420}", "devUrl": "http://localhost:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" diff --git a/vite.config.ts b/vite.config.ts index 2f56295..eeae9fe 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,12 @@ import { resolve } from "path"; // @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; +// @ts-expect-error process is a nodejs global +const envPort = Number(process.env.TAURI_DEV_PORT || process.env.PORT || "1420"); +const port = Number.isFinite(envPort) ? envPort : 1420; +const hmrPort = port + 1; +// @ts-expect-error process is a nodejs global +const cacheDir = process.env.VITE_CACHE_DIR || "node_modules/.vite"; export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], @@ -14,15 +20,16 @@ export default defineConfig(async () => ({ }, }, clearScreen: false, + cacheDir, server: { - port: 1420, + port, strictPort: true, host: host || false, hmr: host ? { protocol: "ws", host, - port: 1421, + port: hmrPort, } : undefined, watch: {