diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 00000000..f9e7f47f --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,15 @@ +name: Setup Project +description: Install pnpm, Node.js 22, and dependencies + +runs: + using: composite + steps: + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..4bc49a0d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,52 @@ +version: 2 + +updates: + # npm/pnpm dependencies (root lockfile covers entire monorepo) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 10 + groups: + production-deps: + dependency-type: "production" + update-types: + - "minor" + - "patch" + dev-deps: + dependency-type: "development" + update-types: + - "minor" + - "patch" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + # Python dependencies (AI sidecar) + - package-ecosystem: "pip" + directory: "/packages/ai/python" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + + # Docker base images + - package-ecosystem: "docker" + directory: "/docker" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 3 + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + groups: + actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59b18ed1..c4212699 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,15 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - run: pnpm lint typecheck: @@ -46,15 +38,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - run: pnpm typecheck test: @@ -66,14 +50,7 @@ jobs: - name: Install system dependencies (HEIC + ExifTool) run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends libheif-examples libheif-plugin-x265 libheif-plugin-libde265 libimage-exiftool-perl - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - run: pnpm test:ci pip-audit: @@ -102,15 +79,7 @@ jobs: needs: [lint, typecheck, test] steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - run: pnpm build # docker: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..3928e321 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,18 @@ +name: Dependency Review + +on: [pull_request] + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + name: Review + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: always diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 0d5237fe..ff5f5fd1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -17,15 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - name: Build Docs run: pnpm --filter @snapotter/docs docs:build diff --git a/.github/workflows/deploy-landing.yml b/.github/workflows/deploy-landing.yml index 63652674..adce8f9c 100644 --- a/.github/workflows/deploy-landing.yml +++ b/.github/workflows/deploy-landing.yml @@ -17,15 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - name: Build Landing Page run: pnpm --filter @snapotter/landing build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16efd876..2fafe3a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,17 +22,7 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/setup - name: Run semantic-release env: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..874739f7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,40 @@ +{ + "recommendations": [ + // ── Core: Linting & Formatting ── + "biomejs.biome", + + // ── Languages ── + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.debugpy", + + // ── Frontend ── + "bradlc.vscode-tailwindcss", + + // ── Testing ── + "vitest.explorer", + "ms-playwright.playwright", + + // ── Infrastructure ── + "ms-azuretools.vscode-docker", + + // ── GitHub ── + "GitHub.vscode-pull-request-github", + "GitHub.vscode-github-actions", + + // ── DX Enhancements ── + "yoavbls.pretty-ts-errors", + "usernamehw.errorlens", + "eamodio.gitlens", + "vivaxy.vscode-conventional-commits", + "redhat.vscode-yaml", + "qwtel.sqlite-viewer", + "mikestead.dotenv", + "EditorConfig.EditorConfig" + ], + "unwantedRecommendations": [ + // Conflicts with Biome + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..69f5945d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,186 @@ +{ + "version": "0.2.0", + "configurations": [ + // ══════════════════════════════════════════════════════════════════════ + // Backend + // ══════════════════════════════════════════════════════════════════════ + { + "name": "API: Fastify (tsx)", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx", + "args": ["watch", "src/index.ts"], + "cwd": "${workspaceFolder}/apps/api", + "env": { + "PORT": "13490", + "NODE_ENV": "development" + }, + "sourceMaps": true, + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + + // ══════════════════════════════════════════════════════════════════════ + // Frontend + // ══════════════════════════════════════════════════════════════════════ + { + "name": "Web: Vite Dev Server", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/vite", + "args": ["--port", "1351"], + "cwd": "${workspaceFolder}/apps/web", + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Web: Debug in Chrome", + "type": "chrome", + "request": "launch", + "url": "http://localhost:1351", + "webRoot": "${workspaceFolder}/apps/web/src", + "sourceMaps": true, + "sourceMapPathOverrides": { + "webpack:///./~/*": "${workspaceFolder}/node_modules/*", + "webpack:///src/*": "${workspaceFolder}/apps/web/src/*" + } + }, + + // ══════════════════════════════════════════════════════════════════════ + // Vitest + // ══════════════════════════════════════════════════════════════════════ + { + "name": "Vitest: Current File", + "type": "node", + "request": "launch", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Vitest: All Tests", + "type": "node", + "request": "launch", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": ["run"], + "smartStep": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Vitest: Watch Mode", + "type": "node", + "request": "launch", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": ["--watch", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + + // ══════════════════════════════════════════════════════════════════════ + // Playwright + // ══════════════════════════════════════════════════════════════════════ + { + "name": "Playwright: Current File", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/@playwright/test/cli.js", + "args": ["test", "${relativeFile}", "--workers=1"], + "cwd": "${workspaceFolder}", + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "Playwright: Debug Current File", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/@playwright/test/cli.js", + "args": ["test", "${relativeFile}", "--workers=1", "--headed"], + "cwd": "${workspaceFolder}", + "env": { + "PWDEBUG": "1" + }, + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + + // ══════════════════════════════════════════════════════════════════════ + // Python Sidecar + // ══════════════════════════════════════════════════════════════════════ + { + "name": "Python: Dispatcher", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/packages/ai/python/dispatcher.py", + "cwd": "${workspaceFolder}/packages/ai", + "console": "integratedTerminal", + "justMyCode": true + }, + { + "name": "Python: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "cwd": "${workspaceFolder}/packages/ai", + "console": "integratedTerminal", + "justMyCode": true + }, + + // ══════════════════════════════════════════════════════════════════════ + // Attach + // ══════════════════════════════════════════════════════════════════════ + { + "name": "Attach: Node (port 9229)", + "type": "node", + "request": "attach", + "port": 9229, + "restart": true, + "sourceMaps": true, + "skipFiles": ["/**", "**/node_modules/**"] + }, + { + "name": "Attach: Python (port 5678)", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "justMyCode": true + } + ], + + // ══════════════════════════════════════════════════════════════════════ + // Compound (multi-process) Configurations + // ══════════════════════════════════════════════════════════════════════ + "compounds": [ + { + "name": "Full Stack: API + Web", + "configurations": ["API: Fastify (tsx)", "Web: Vite Dev Server"], + "stopAll": true + }, + { + "name": "Full Stack: API + Web + Chrome", + "configurations": ["API: Fastify (tsx)", "Web: Vite Dev Server", "Web: Debug in Chrome"], + "stopAll": true, + "presentation": { + "group": "compound", + "order": 1 + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e95f2c9f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,165 @@ +{ + // ── Editor Defaults ────────────────────────────────────────────────── + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active", + "editor.linkedEditing": true, + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit" + }, + + // ── Biome ───────────────────────────────────────────────────────────── + "biome.enabled": true, + "biome.requireConfig": true, + + // Disable competing formatters/linters + "prettier.enable": false, + "eslint.enable": false, + + // ── Language-specific formatter overrides ────────────────────────────── + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[css]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.python", + "editor.tabSize": 4 + }, + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "editor.wordWrap": "on" + }, + "[dockercompose]": { + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "[dockerfile]": { + "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "[yaml]": { + "editor.tabSize": 2 + }, + + // ── TypeScript ──────────────────────────────────────────────────────── + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.quoteStyle": "double", + "typescript.suggest.autoImports": true, + "typescript.updateImportsOnFileMove.enabled": "always", + "javascript.preferences.quoteStyle": "double", + "javascript.updateImportsOnFileMove.enabled": "always", + + // ── Tailwind CSS ────────────────────────────────────────────────────── + "tailwindCSS.emmetCompletions": true, + "tailwindCSS.includeLanguages": { + "typescriptreact": "html" + }, + "tailwindCSS.classAttributes": ["class", "className", "ngClass", "variants"], + // v4 auto-detects from @import "tailwindcss" in CSS files; no configFile needed. + + // ── Python (AI sidecar) ─────────────────────────────────────────────── + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoSearchPaths": true, + "python.analysis.extraPaths": ["packages/ai/python"], + "python.defaultInterpreterPath": "${workspaceFolder}/packages/ai/.venv/bin/python", + + // ── Testing ─────────────────────────────────────────────────────────── + "vitest.rootConfig": "vitest.config.ts", + + // ── File Associations ───────────────────────────────────────────────── + "files.associations": { + "*.css": "tailwindcss", + ".env*": "dotenv", + "Dockerfile*": "dockerfile", + "*.yml": "yaml", + "*.yaml": "yaml" + }, + + // ── File Exclusions (Explorer) ──────────────────────────────────────── + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/.turbo": true + }, + + // ── Search Exclusions ───────────────────────────────────────────────── + "search.exclude": { + "**/node_modules": true, + "**/dist": true, + "**/coverage": true, + "**/.turbo": true, + "**/playwright-report": true, + "**/test-results": true, + "**/blob-report": true, + "**/pnpm-lock.yaml": true, + "**/apps/api/drizzle": true, + "**/.pnpm-store": true, + "**/.worktrees": true + }, + + // ── File Watcher Exclusions ─────────────────────────────────────────── + "files.watcherExclude": { + "**/node_modules/**": true, + "**/dist/**": true, + "**/coverage/**": true, + "**/.turbo/**": true, + "**/playwright-report/**": true, + "**/test-results/**": true, + "**/blob-report/**": true, + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/.worktrees/**": true + }, + + // ── Emmet ───────────────────────────────────────────────────────────── + "emmet.includeLanguages": { + "typescriptreact": "html" + }, + + // ── Terminal ────────────────────────────────────────────────────────── + "terminal.integrated.defaultProfile.osx": "zsh", + "terminal.integrated.env.osx": { + "NODE_OPTIONS": "" + }, + + // ── Git ─────────────────────────────────────────────────────────────── + "git.autofetch": true, + "git.enableSmartCommit": true, + "git.confirmSync": false, + + // ── Misc ────────────────────────────────────────────────────────────── + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "package.json": "pnpm-lock.yaml, pnpm-workspace.yaml, .npmrc, turbo.json", + "tsconfig.json": "tsconfig.*.json", + "tsconfig.base.json": "tsconfig.*.json", + "biome.json": ".editorconfig", + "vite.config.*": "postcss.config.*, tailwind.config.*", + ".gitignore": ".gitattributes, .dockerignore", + "Dockerfile": "docker-compose.yml, docker-compose.*.yml, .dockerignore", + "*.ts": "${capture}.test.ts, ${capture}.spec.ts", + "*.tsx": "${capture}.test.tsx, ${capture}.spec.tsx" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..90619a2d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,282 @@ +{ + "version": "2.0.0", + "tasks": [ + // ══════════════════════════════════════════════════════════════════════ + // Dev Servers + // ══════════════════════════════════════════════════════════════════════ + { + "label": "dev", + "detail": "Start all dev servers (turbo dev)", + "type": "shell", + "command": "pnpm dev", + "group": "none", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + } + }, + { + "label": "dev:api", + "detail": "Start Fastify API server only", + "type": "shell", + "command": "pnpm --filter @snapotter/api dev", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + } + }, + { + "label": "dev:web", + "detail": "Start Vite frontend only", + "type": "shell", + "command": "pnpm --filter @snapotter/web dev", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + } + }, + { + "label": "dev:docs", + "detail": "Start VitePress docs server", + "type": "shell", + "command": "pnpm docs:dev", + "isBackground": true, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev" + } + }, + + // ══════════════════════════════════════════════════════════════════════ + // Build + // ══════════════════════════════════════════════════════════════════════ + { + "label": "build", + "detail": "Build all workspaces (turbo build)", + "type": "shell", + "command": "pnpm build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$tsc"], + "presentation": { + "reveal": "silent", + "panel": "shared" + } + }, + { + "label": "build:api", + "detail": "Build API workspace", + "type": "shell", + "command": "pnpm --filter @snapotter/api build", + "group": "build", + "problemMatcher": ["$tsc"] + }, + { + "label": "build:web", + "detail": "Build Web workspace", + "type": "shell", + "command": "pnpm --filter @snapotter/web build", + "group": "build", + "problemMatcher": ["$tsc"] + }, + + // ══════════════════════════════════════════════════════════════════════ + // Type Check + // ══════════════════════════════════════════════════════════════════════ + { + "label": "typecheck", + "detail": "TypeScript check across monorepo", + "type": "shell", + "command": "pnpm typecheck", + "group": "build", + "problemMatcher": ["$tsc"] + }, + + // ══════════════════════════════════════════════════════════════════════ + // Lint & Format + // ══════════════════════════════════════════════════════════════════════ + { + "label": "lint", + "detail": "Biome lint + format check", + "type": "shell", + "command": "pnpm lint", + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "panel": "shared" + } + }, + { + "label": "lint:fix", + "detail": "Biome auto-fix lint + format issues", + "type": "shell", + "command": "pnpm lint:fix", + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "panel": "shared" + } + }, + + // ══════════════════════════════════════════════════════════════════════ + // Testing + // ══════════════════════════════════════════════════════════════════════ + { + "label": "test", + "detail": "Run all Vitest tests", + "type": "shell", + "command": "pnpm test", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "test:unit", + "detail": "Unit tests only", + "type": "shell", + "command": "pnpm test:unit", + "group": "test", + "problemMatcher": [] + }, + { + "label": "test:integration", + "detail": "Integration tests only", + "type": "shell", + "command": "pnpm test:integration", + "group": "test", + "problemMatcher": [] + }, + { + "label": "test:watch", + "detail": "Vitest in watch mode", + "type": "shell", + "command": "pnpm test:watch", + "isBackground": true, + "group": "test", + "problemMatcher": [] + }, + { + "label": "test:coverage", + "detail": "Tests with coverage report", + "type": "shell", + "command": "pnpm test:coverage", + "group": "test", + "problemMatcher": [] + }, + { + "label": "test:e2e", + "detail": "Playwright e2e tests", + "type": "shell", + "command": "pnpm test:e2e", + "group": "test", + "problemMatcher": [] + }, + { + "label": "test:e2e:ui", + "detail": "Playwright e2e with interactive UI", + "type": "shell", + "command": "pnpm test:e2e:ui", + "isBackground": true, + "group": "test", + "problemMatcher": [] + }, + + // ══════════════════════════════════════════════════════════════════════ + // Database + // ══════════════════════════════════════════════════════════════════════ + { + "label": "db:generate", + "detail": "Generate Drizzle migration from schema changes", + "type": "shell", + "command": "npx drizzle-kit generate", + "options": { + "cwd": "${workspaceFolder}/apps/api" + }, + "problemMatcher": [] + }, + { + "label": "db:migrate", + "detail": "Apply pending Drizzle migrations", + "type": "shell", + "command": "npx drizzle-kit migrate", + "options": { + "cwd": "${workspaceFolder}/apps/api" + }, + "problemMatcher": [] + }, + { + "label": "db:studio", + "detail": "Open Drizzle Studio (database browser)", + "type": "shell", + "command": "npx drizzle-kit studio", + "options": { + "cwd": "${workspaceFolder}/apps/api" + }, + "isBackground": true, + "problemMatcher": [] + }, + + // ══════════════════════════════════════════════════════════════════════ + // Docker + // ══════════════════════════════════════════════════════════════════════ + { + "label": "docker:build", + "detail": "Build Docker image", + "type": "shell", + "command": "docker build -f docker/Dockerfile -t snapotter:local .", + "problemMatcher": [] + }, + { + "label": "docker:run", + "detail": "Run Docker container locally", + "type": "shell", + "command": "docker run --rm -p 1349:1349 -p 13490:13490 snapotter:local", + "isBackground": true, + "problemMatcher": [] + }, + { + "label": "docker:test", + "detail": "Run Docker integration tests", + "type": "shell", + "command": "pnpm test:docker", + "problemMatcher": [] + }, + + // ══════════════════════════════════════════════════════════════════════ + // Utilities + // ══════════════════════════════════════════════════════════════════════ + { + "label": "clean", + "detail": "Clean all dist/build artifacts", + "type": "shell", + "command": "pnpm clean", + "problemMatcher": [] + }, + { + "label": "install", + "detail": "Install all dependencies", + "type": "shell", + "command": "pnpm install", + "problemMatcher": [] + } + ] +}