diff --git a/.gitignore b/.gitignore index b3b32f5a..acffc298 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,6 @@ apps/desktop/src-tauri/target/release/ .claude /docs -.superpowers \ No newline at end of file +.superpowers/apps/desktop/node_modules +/apps/desktop/src-tauri/target +/apps/desktop/node_modules diff --git a/apps/api-runner/node_modules/.bin/tsc b/apps/api-runner/node_modules/.bin/tsc index 44a8dcb8..bc3dbe24 100755 --- a/apps/api-runner/node_modules/.bin/tsc +++ b/apps/api-runner/node_modules/.bin/tsc @@ -2,21 +2,16 @@ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") case `uname` in - *CYGWIN*|*MINGW*|*MSYS*) - if command -v cygpath > /dev/null 2>&1; then - basedir=`cygpath -w "$basedir"` - fi - ;; + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/node_modules" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules" else - export NODE_PATH="/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@" else exec node "$basedir/../typescript/bin/tsc" "$@" fi -# cmd-shim-target=/Users/anuphilipp/Documents/GitHub/mydevtools/apps/api-runner/node_modules/typescript/bin/tsc diff --git a/apps/api-runner/node_modules/.bin/tsserver b/apps/api-runner/node_modules/.bin/tsserver index 0acb3ed7..03c2f186 100755 --- a/apps/api-runner/node_modules/.bin/tsserver +++ b/apps/api-runner/node_modules/.bin/tsserver @@ -2,21 +2,16 @@ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") case `uname` in - *CYGWIN*|*MINGW*|*MSYS*) - if command -v cygpath > /dev/null 2>&1; then - basedir=`cygpath -w "$basedir"` - fi - ;; + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/node_modules" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules" else - export NODE_PATH="/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/anuphilipp/Documents/GitHub/mydevtools/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/max/Works/Personal/mydevtools.tech/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@" else exec node "$basedir/../typescript/bin/tsserver" "$@" fi -# cmd-shim-target=/Users/anuphilipp/Documents/GitHub/mydevtools/apps/api-runner/node_modules/typescript/bin/tsserver diff --git a/apps/backend/app/api/routes/auth/api.py b/apps/backend/app/api/routes/auth/api.py index 084f0b88..f0cdc2ae 100644 --- a/apps/backend/app/api/routes/auth/api.py +++ b/apps/backend/app/api/routes/auth/api.py @@ -8,6 +8,7 @@ from app.core.limiter import limiter from app.core import audit +from app.core.config import get_settings from app.api.routes.auth.cookie_attach import attach_auth_cookies, clear_auth_cookies from app.api.routes.auth.schema import ( @@ -80,8 +81,16 @@ async def create_session( access = create_access_token(uid) raw_refresh = new_refresh_token() - await set_refresh_token_hash(uid, hash_refresh_token(raw_refresh)) - attach_auth_cookies(response, access, raw_refresh) + settings = get_settings() + refresh_days = ( + settings.LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS + if payload.long_lived + else settings.REFRESH_TOKEN_EXPIRE_DAYS + ) + await set_refresh_token_hash( + uid, hash_refresh_token(raw_refresh), long_lived=payload.long_lived + ) + attach_auth_cookies(response, access, raw_refresh, refresh_days=refresh_days) doc = await get_user_doc(uid) if not doc: @@ -132,10 +141,20 @@ async def refresh_session( detail="Invalid refresh token.", ) + # Preserve the session's long-lived flag across rotation so desktop + # sessions keep their 60-day cookie TTL on every refresh. + user_doc = await get_user_doc(uid) + long_lived = bool(user_doc.get("refresh_long_lived", False)) if user_doc else False + settings = get_settings() + refresh_days = ( + settings.LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS + if long_lived + else settings.REFRESH_TOKEN_EXPIRE_DAYS + ) new_raw = new_refresh_token() - await set_refresh_token_hash(uid, hash_refresh_token(new_raw)) + await set_refresh_token_hash(uid, hash_refresh_token(new_raw), long_lived=long_lived) access = create_access_token(uid) - attach_auth_cookies(response, access, new_raw) + attach_auth_cookies(response, access, new_raw, refresh_days=refresh_days) audit.set_action("auth.token_refresh") audit.set_entity("user", uid) audit.set_summary("Refreshed session") @@ -300,6 +319,26 @@ async def session_check(_uid: Annotated[str, Depends(get_current_uid)]) -> OkRes return OkResponse(ok=True) +@router.post( + "/desktop-token", + summary="Mint a Firebase custom token for the desktop-app sign-in handoff", +) +async def desktop_token(uid: Annotated[str, Depends(get_current_uid)]) -> dict: + # The web session (browser) mints a short-lived Firebase custom token which + # is handed to the desktop app via the mydevtools:// deep link; the app + # signs in with it and runs the normal session exchange. + from app.core.firebase import get_firebase_app + + try: + from firebase_admin import auth as firebase_auth + except Exception: # pragma: no cover - firebase_admin always present in prod + raise HTTPException(status_code=503, detail="Firebase unavailable") + get_firebase_app() + token_bytes = firebase_auth.create_custom_token(uid) + token = token_bytes.decode("utf-8") if isinstance(token_bytes, bytes) else token_bytes + return {"token": token} + + # ── Master-password vault ───────────────────────────────────────────────────── diff --git a/apps/backend/app/api/routes/auth/cookie_attach.py b/apps/backend/app/api/routes/auth/cookie_attach.py index 71cf0898..2fa4f6c7 100644 --- a/apps/backend/app/api/routes/auth/cookie_attach.py +++ b/apps/backend/app/api/routes/auth/cookie_attach.py @@ -4,10 +4,16 @@ from app.core.config import get_settings -def attach_auth_cookies(response: Response, access: str, refresh_plain: str) -> None: +def attach_auth_cookies( + response: Response, + access: str, + refresh_plain: str, + refresh_days: int | None = None, +) -> None: settings = get_settings() access_max = settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 - refresh_max = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 3600 + days = refresh_days if refresh_days is not None else settings.REFRESH_TOKEN_EXPIRE_DAYS + refresh_max = days * 24 * 3600 common: dict = { "httponly": True, "secure": settings.AUTH_COOKIE_SECURE, diff --git a/apps/backend/app/api/routes/auth/schema.py b/apps/backend/app/api/routes/auth/schema.py index 9bd0e2a6..355ed933 100644 --- a/apps/backend/app/api/routes/auth/schema.py +++ b/apps/backend/app/api/routes/auth/schema.py @@ -7,6 +7,8 @@ class SessionRequest(BaseModel): id_token: str = Field(min_length=1) check_revoked: bool = True + # Desktop app requests a long-lived (60-day) refresh cookie. + long_lived: bool = False class SocialLinks(BaseModel): diff --git a/apps/backend/app/api/routes/auth/users_repo.py b/apps/backend/app/api/routes/auth/users_repo.py index e10b8af0..2180aed9 100644 --- a/apps/backend/app/api/routes/auth/users_repo.py +++ b/apps/backend/app/api/routes/auth/users_repo.py @@ -83,12 +83,20 @@ async def update_user_profile(uid: str, updates: dict[str, Any]) -> None: await bump_version(ns="auth_user", uid=uid) -async def set_refresh_token_hash(uid: str, token_hash: str) -> None: +async def set_refresh_token_hash( + uid: str, token_hash: str, long_lived: bool = False +) -> None: now = create_timestamp() await db_manager.update_one( USERS, {"_id": uid}, - {"$set": {"refresh_token_hash": token_hash, "updated_at": now}}, + { + "$set": { + "refresh_token_hash": token_hash, + "refresh_long_lived": long_lived, + "updated_at": now, + } + }, ) await bump_version(ns="auth_user", uid=uid) diff --git a/apps/backend/app/api/routes/nosql/schema.py b/apps/backend/app/api/routes/nosql/schema.py index d72c1262..ddcfb8b7 100644 --- a/apps/backend/app/api/routes/nosql/schema.py +++ b/apps/backend/app/api/routes/nosql/schema.py @@ -37,3 +37,4 @@ class ConnectionOut(BaseModel): name: str createdAt: int lastUsedAt: int + updatedAt: int diff --git a/apps/backend/app/api/routes/nosql/services.py b/apps/backend/app/api/routes/nosql/services.py index 898ad1f4..8a147724 100644 --- a/apps/backend/app/api/routes/nosql/services.py +++ b/apps/backend/app/api/routes/nosql/services.py @@ -17,6 +17,9 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> ConnectionOut: created_at = int(doc.get("createdAt", 0)) or create_timestamp() last_used_at = int(doc.get("lastUsedAt", 0)) or created_at + # Content-edit clock for sync LWW. Falls back to lastUsedAt for legacy docs + # written before updatedAt existed. + updated_at = int(doc.get("updatedAt", 0)) or last_used_at return ConnectionOut( id=connection_id, userId=str(doc.get("created_by", "")), @@ -25,6 +28,7 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> ConnectionOut: name=str(doc.get("name", "")), createdAt=created_at, lastUsedAt=last_used_at, + updatedAt=updated_at, ) @@ -52,6 +56,7 @@ async def create_connection(ctx: WorkspaceContext, body: ConnectionCreate) -> Co "name": body.name or "My Connection", "createdAt": ts, "lastUsedAt": ts, + "updatedAt": ts, } try: await db_manager.insert_one(NOSQL_CONNECTIONS, doc) @@ -69,7 +74,9 @@ async def update_connection(ctx: WorkspaceContext, connection_id: str, body: Con raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found.") ts = create_timestamp() - patch: dict[str, Any] = {"lastUsedAt": ts} + # updatedAt is the sync LWW clock — it advances on every content edit (unlike + # lastUsedAt, which also moves on a bare connect/touch). + patch: dict[str, Any] = {"lastUsedAt": ts, "updatedAt": ts} if body.encryptedData is not None: patch["encryptedData"] = body.encryptedData if body.iv is not None: diff --git a/apps/backend/app/api/routes/redis_commander/schema.py b/apps/backend/app/api/routes/redis_commander/schema.py index 8b32d409..670e8c4e 100644 --- a/apps/backend/app/api/routes/redis_commander/schema.py +++ b/apps/backend/app/api/routes/redis_commander/schema.py @@ -39,3 +39,4 @@ class RedisConnectionOut(BaseModel): name: str createdAt: int lastUsedAt: int + updatedAt: int diff --git a/apps/backend/app/api/routes/redis_commander/services.py b/apps/backend/app/api/routes/redis_commander/services.py index 68b3829e..b32c1827 100644 --- a/apps/backend/app/api/routes/redis_commander/services.py +++ b/apps/backend/app/api/routes/redis_commander/services.py @@ -21,6 +21,9 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> RedisConnectionOut: created_at = int(doc.get("createdAt", 0)) or create_timestamp() last_used_at = int(doc.get("lastUsedAt", 0)) or created_at + # Content-edit clock for sync LWW. Falls back to lastUsedAt for legacy docs + # written before updatedAt existed. + updated_at = int(doc.get("updatedAt", 0)) or last_used_at return RedisConnectionOut( id=connection_id, userId=str(doc.get("created_by", "")), @@ -29,6 +32,7 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> RedisConnectionOu name=str(doc.get("name", "")), createdAt=created_at, lastUsedAt=last_used_at, + updatedAt=updated_at, ) @@ -56,6 +60,7 @@ async def create_connection(ctx: WorkspaceContext, body: RedisConnectionCreate) "name": body.name or "My Redis Connection", "createdAt": ts, "lastUsedAt": ts, + "updatedAt": ts, } try: await db_manager.insert_one(REDIS_CONNECTIONS, doc) @@ -74,7 +79,9 @@ async def update_connection(ctx: WorkspaceContext, connection_id: str, body: Red raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found.") ts = create_timestamp() - patch: dict[str, Any] = {"lastUsedAt": ts} + # updatedAt is the sync LWW clock — it advances on every content edit (unlike + # lastUsedAt, which also moves on a bare connect/touch). + patch: dict[str, Any] = {"lastUsedAt": ts, "updatedAt": ts} if body.encryptedData is not None: patch["encryptedData"] = body.encryptedData if body.iv is not None: diff --git a/apps/backend/app/api/routes/sql_client/schema.py b/apps/backend/app/api/routes/sql_client/schema.py index 01bc7711..624a8dfe 100644 --- a/apps/backend/app/api/routes/sql_client/schema.py +++ b/apps/backend/app/api/routes/sql_client/schema.py @@ -45,3 +45,4 @@ class SqlConnectionOut(BaseModel): type: DbType createdAt: int lastUsedAt: int + updatedAt: int diff --git a/apps/backend/app/api/routes/sql_client/services.py b/apps/backend/app/api/routes/sql_client/services.py index 2a09c715..6d8209b4 100644 --- a/apps/backend/app/api/routes/sql_client/services.py +++ b/apps/backend/app/api/routes/sql_client/services.py @@ -21,6 +21,9 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> SqlConnectionOut: created_at = int(doc.get("createdAt", 0)) or create_timestamp() last_used_at = int(doc.get("lastUsedAt", 0)) or created_at + # Content-edit clock for sync LWW. Falls back to lastUsedAt for legacy docs + # written before updatedAt existed. + updated_at = int(doc.get("updatedAt", 0)) or last_used_at return SqlConnectionOut( id=connection_id, userId=str(doc.get("created_by", "")), @@ -30,6 +33,7 @@ def _doc_to_out(doc: dict[str, Any], *, connection_id: str) -> SqlConnectionOut: type=doc.get("type", "postgresql"), createdAt=created_at, lastUsedAt=last_used_at, + updatedAt=updated_at, ) @@ -58,6 +62,7 @@ async def create_connection(ctx: WorkspaceContext, body: SqlConnectionCreate) -> "type": body.type, "createdAt": ts, "lastUsedAt": ts, + "updatedAt": ts, } try: await db_manager.insert_one(SQL_CONNECTIONS, doc) @@ -75,7 +80,9 @@ async def update_connection(ctx: WorkspaceContext, connection_id: str, body: Sql raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found.") ts = create_timestamp() - patch: dict[str, Any] = {"lastUsedAt": ts} + # updatedAt is the sync LWW clock — it advances on every content edit (unlike + # lastUsedAt, which also moves on a bare connect/touch). + patch: dict[str, Any] = {"lastUsedAt": ts, "updatedAt": ts} if body.encryptedData is not None: patch["encryptedData"] = body.encryptedData if body.iv is not None: diff --git a/apps/backend/app/core/config.py b/apps/backend/app/core/config.py index c2be51c3..cabeba47 100644 --- a/apps/backend/app/core/config.py +++ b/apps/backend/app/core/config.py @@ -23,7 +23,10 @@ class Settings(BaseSettings): ) JWT_ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int - REFRESH_TOKEN_EXPIRE_DAYS: int + REFRESH_TOKEN_EXPIRE_DAYS: int + # Long-lived sessions (desktop app): refresh cookie TTL when the client + # requests `long_lived`. Decoupled from the web REFRESH_TOKEN_EXPIRE_DAYS. + LONG_LIVED_REFRESH_TOKEN_EXPIRE_DAYS: int = 60 # HttpOnly cookies: use Secure=true over HTTPS (recommended in production). AUTH_COOKIE_SECURE: bool = False diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 00000000..239cef27 --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,66 @@ +# MyDevTools Desktop (macOS, Tauri v2) + +Offline-first desktop app. All 57 tools work; **all data lives on this Mac** +(SQLCipher database keyed by a macOS Keychain device key) unless you turn on +Cloud Sync for your personal workspace. Shared/team workspaces are always +cloud-backed and require sign-in. + +## Architecture + +- **Frontend**: the Next.js web app, statically exported by + `apps/web/scripts/build-tauri.mjs` (exclusion build — API routes, + middleware, and dynamic marketing pages are moved aside during the build). +- **Local data**: single `local_api(method, path, body)` Rust command → + `router/` dispatcher mirroring the FastAPI `/api/v1/*` JSON contracts → + SQLCipher (`entries` table stores opaque docs; E2E crypto stays in the + webview — Rust never sees plaintext vault data). +- **Cloud**: `remote_api` command (reqwest + persistent cookie jar in + SQLCipher) calls FastAPI directly — no CORS, no backend changes. Cloud + sign-in happens in the system browser (`/login?desktop=1`) and hands a + Firebase custom token back via the `mydevtools://auth` deep link. +- **Sync**: TS engine (`apps/web/src/lib/desktop/sync-engine.ts`) reconciles + the local personal workspace against the remote one — push dirty rows + first, then pull with last-write-wins on `updatedAt`. Toggle in Settings. +- **Live DB tools**: native Rust drivers (tokio-postgres, mysql_async, + mongodb, redis) behind the same JSON contracts as the web API routes. +- **api-client**: `http_request` / `http_request_stream` commands (reqwest) + replace the `/api/proxy` and `/api/proxy-stream` routes. + +## Dev workflow + +```sh +nvm use 20.19.5 # Node >= 20.19 required (older 20.x breaks the export) +pnpm dev:desktop # tauri dev against next dev :3000 +pnpm build:desktop # static export → release build → .app + .dmg +``` + +Artifacts land in `apps/desktop/src-tauri/target/release/bundle/`. + +The `.dmg` is unsigned: on first launch, **right-click the app → Open → +Open** to pass Gatekeeper (or `xattr -dr com.apple.quarantine +/Applications/MyDevTools.app`). + +Rust tests: `cargo test` (unit) and `cargo test -- --ignored` (integration — +needs local `mongod` on 27017, `redis-server` on 6379, and network for the +httpbin proxy test). + +## Not in v1 (planned v1.1) + +- gRPC / NTLM / SPNEGO proxies and the mock server (api-client advanced) +- Redis MONITOR + pub/sub live subscribe (SSE panes show an error) +- CSP hardening in `tauri.conf.json` (currently null) +- Signing/notarization, auto-update, user-preferences sync + +## Regression matrix + +Offline pass (Wi-Fi off): 41 pure-client tools · master-vault +create/unlock/relaunch · password-manager, api-keys, environment-manager, +snippets, bookmarks, notes (text), to-do, api-client +collections/history/environments, DB connection vaults · gitignore-generator +(bundled templates) · sql-client/database-explorer/redis-commander against +local servers. + +Online pass: cloud sign-in via browser handoff · session survives relaunch · +shared workspace round-trip with web · sync toggle on → offline edits appear +on web, web edits propagate back, deletes propagate both ways · online-only +gates (s3-drive, url-shortener, dns-lookup, email-validator) unlock. diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 00000000..999f0a97 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,14 @@ +{ + "name": "@mydevtools/desktop", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "tauri dev", + "build": "tauri build --bundles app && bash scripts/make-dmg.sh", + "build:bundle": "tauri build", + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} diff --git a/apps/desktop/scripts/make-dmg.sh b/apps/desktop/scripts/make-dmg.sh new file mode 100755 index 00000000..089b92d6 --- /dev/null +++ b/apps/desktop/scripts/make-dmg.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Wrap the built .app into an unsigned .dmg via hdiutil. +# +# Tauri's own dmg bundler (bundle_dmg.sh) drives Finder through AppleScript to +# style the window and flakes in headless / permission-restricted sessions. +# hdiutil is deterministic and needs no Finder — good enough for an unsigned +# local build. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +APP="$ROOT/src-tauri/target/release/bundle/macos/MyDevTools.app" +OUT_DIR="$ROOT/src-tauri/target/release/bundle/dmg" +DMG="$OUT_DIR/MyDevTools.dmg" +STAGE="$(mktemp -d)" + +if [ ! -d "$APP" ]; then + echo "make-dmg: $APP not found — run 'tauri build' first." >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" +cp -R "$APP" "$STAGE/" +ln -s /Applications "$STAGE/Applications" +rm -f "$DMG" +hdiutil create -volname "MyDevTools" -srcfolder "$STAGE" -ov -format UDZO "$DMG" +rm -rf "$STAGE" +echo "make-dmg: created $DMG" diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore new file mode 100644 index 00000000..1bea80ce --- /dev/null +++ b/apps/desktop/src-tauri/.gitignore @@ -0,0 +1,2 @@ +/target/ +/gen/schemas/ diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 00000000..bec40035 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,6860 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64 0.22.1", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.14.0", + "js-sys", + "once_cell", + "rand 0.9.4", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "btoi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.2", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.2", + "resolv-conf", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry 0.6.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.4", + "widestring", + "windows-registry 0.6.1", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.118", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyed_priority_queue" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" +dependencies = [ + "indexmap 2.14.0", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8426a875ded61430d4a811dbfda7633b6b8af0225c547fc6c28b8b0aa7d79a13" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.6+1.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851fac73f7fe22f6a3ab87f720ce509cae7c9fd08e7dd27866cc232dee07ccf4" + +[[package]] +name = "mongodb" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b814038f367d212f55de0a630cb35102a9b8ca23785a86955d62c0087c93846d" +dependencies = [ + "base64 0.22.1", + "bitflags 2.13.0", + "bson", + "derive-where", + "derive_more", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-net", + "hickory-proto", + "hickory-resolver", + "hmac", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand 0.9.4", + "rustc_version_runtime", + "rustls", + "serde", + "serde_bytes", + "serde_with", + "sha1 0.11.0", + "sha2 0.11.0", + "socket2 0.6.4", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots 1.0.8", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f736d2fbc56e0011a341fbb9172bd822fda75c5f93b82fae1c7aab1e2613c810" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "mydevtools-desktop" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "cookie_store 0.21.1", + "dashmap", + "futures-util", + "keyring", + "mongodb", + "mysql_async", + "native-tls", + "postgres-native-tls", + "rand 0.8.6", + "redis", + "reqwest 0.12.28", + "reqwest_cookie_store", + "rusqlite", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-plugin-opener", + "tauri-plugin-window-state", + "thiserror 2.0.18", + "tokio", + "tokio-postgres", + "uuid", +] + +[[package]] +name = "mysql-common-derive" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4db8a44120571277accfaa3f3d91e7d3989d601d817c2fc01a9391b86135666" +dependencies = [ + "darling", + "heck 0.5.0", + "manyhow", + "num-bigint", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "termcolor", + "thiserror 2.0.18", +] + +[[package]] +name = "mysql_async" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1d9585dc9058886ff3a1f48a23024dd1d054264dee7c5ae0e4bd640c953bee5" +dependencies = [ + "bytes", + "crossbeam-queue", + "flate2", + "futures-core", + "futures-sink", + "futures-util", + "keyed_priority_queue", + "lru", + "mysql_common", + "pem", + "percent-encoding", + "rand 0.9.4", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "socket2 0.5.10", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "twox-hash", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "mysql_common" +version = "0.35.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb9f371618ce723f095c61fbcdc36e8936956d2b62832f9c7648689b338e052" +dependencies = [ + "base64 0.22.1", + "bitflags 2.13.0", + "btoi", + "byteorder", + "bytes", + "crc32fast", + "flate2", + "getrandom 0.3.4", + "mysql-common-derive", + "num-bigint", + "num-traits", + "regex", + "saturating", + "serde", + "serde_json", + "sha1 0.10.6", + "sha2 0.10.9", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "open" +version = "5.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postgres-native-tls" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef4de47bb81477e0c3deaf153a1b10ae176484713ff1640969f4cb96b653ebc" +dependencies = [ + "native-tls", + "tokio", + "tokio-native-tls", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac", + "md-5", + "memchr", + "rand 0.10.2", + "sha2 0.11.0", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redis" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f" +dependencies = [ + "arc-swap", + "bytes", + "combine", + "futures-util", + "itoa", + "native-tls", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-native-tls", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "cookie", + "cookie_store 0.22.1", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "reqwest_cookie_store" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2314c325724fea278d44c13a525ebf60074e33c05f13b4345c076eb65b2446b3" +dependencies = [ + "bytes", + "cookie_store 0.21.1", + "reqwest 0.12.28", + "url", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.0", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "saturating" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "2.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa" +dependencies = [ + "dunce", + "plist", + "rust-ini", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "tracing", + "url", + "windows-registry 0.5.3", + "windows-result 0.3.4", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.10.2", + "socket2 0.6.4", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..1d59d359 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "mydevtools-desktop" +version = "0.1.0" +description = "MyDevTools desktop app" +edition = "2021" + +[lib] +name = "mydevtools_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.32", features = ["bundled-sqlcipher-vendored-openssl"] } +keyring = { version = "3", features = ["apple-native"] } +uuid = { version = "1", features = ["v4"] } +chrono = "0.4" +rand = "0.8" +thiserror = "2" +reqwest = { version = "0.12", features = ["json", "gzip", "multipart", "stream"] } +base64 = "0.22" +reqwest_cookie_store = "0.8" +cookie_store = "0.21" +tauri-plugin-deep-link = "2" +tauri-plugin-opener = "2" +tauri-plugin-window-state = "2" +tokio = { version = "1", features = ["time", "sync", "net", "io-util"] } +tokio-postgres = "0.7" +postgres-native-tls = "0.5" +native-tls = "0.2" +mysql_async = { version = "0.36", default-features = false, features = ["default-rustls-ring"] } +mongodb = "3" +redis = { version = "0.29", features = ["tokio-comp", "tokio-native-tls-comp"] } +futures-util = "0.3" +dashmap = "6" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[profile.release] +strip = true +lto = true diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..a964e7fb --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for the main window", + "windows": ["main"], + "permissions": ["core:default", "deep-link:default", "opener:default"] +} diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png new file mode 100644 index 00000000..0efa2374 Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..d9933d0f Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png new file mode 100644 index 00000000..df773d02 Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png new file mode 100644 index 00000000..aa45d966 Binary files /dev/null and b/apps/desktop/src-tauri/icons/64x64.png differ diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..b8c868ac Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..0fc9415c Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..9c8ab671 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..3d1cdb06 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..d952200c Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..c5fc351b Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..b927f18e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..4bde60a9 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..d6714e00 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..0e5c935c Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..55aadae8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico new file mode 100644 index 00000000..f74a0be7 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 00000000..28de56a8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/src/db/device_key.rs b/apps/desktop/src-tauri/src/db/device_key.rs new file mode 100644 index 00000000..fd6a259c --- /dev/null +++ b/apps/desktop/src-tauri/src/db/device_key.rs @@ -0,0 +1,24 @@ +use keyring::Entry; +use rand::RngCore; + +use crate::error::Result; + +const SERVICE: &str = "tech.mydevtools.desktop"; +const ACCOUNT: &str = "db-key"; + +/// Returns the 32-byte SQLCipher key as lowercase hex, creating and storing it +/// in the macOS Keychain on first run. +pub fn get_or_create() -> Result { + let entry = Entry::new(SERVICE, ACCOUNT)?; + match entry.get_password() { + Ok(key) => Ok(key), + Err(keyring::Error::NoEntry) => { + let mut bytes = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut bytes); + let key: String = bytes.iter().map(|b| format!("{b:02x}")).collect(); + entry.set_password(&key)?; + Ok(key) + } + Err(e) => Err(e.into()), + } +} diff --git a/apps/desktop/src-tauri/src/db/migrations.rs b/apps/desktop/src-tauri/src/db/migrations.rs new file mode 100644 index 00000000..cab02b51 --- /dev/null +++ b/apps/desktop/src-tauri/src/db/migrations.rs @@ -0,0 +1,68 @@ +use rusqlite::Connection; + +use crate::error::Result; + +const MIGRATIONS: &[&str] = &[ + // v1 — initial schema + " + CREATE TABLE entries ( + tool_kind TEXT NOT NULL, + workspace_id TEXT NOT NULL, + id TEXT NOT NULL, + encrypted_data TEXT, + iv TEXT, + meta_json TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + deleted_at INTEGER, + dirty INTEGER NOT NULL DEFAULT 1, + last_synced_at INTEGER, + PRIMARY KEY (tool_kind, workspace_id, id) + ); + CREATE INDEX idx_entries_dirty ON entries(dirty) WHERE dirty = 1; + CREATE INDEX idx_entries_list ON entries(tool_kind, workspace_id, deleted_at); + + CREATE TABLE kv ( + k TEXT PRIMARY KEY, + v TEXT NOT NULL + ); + + CREATE TABLE sync_state ( + workspace_id TEXT NOT NULL, + tool_kind TEXT NOT NULL, + last_pull_at INTEGER, + PRIMARY KEY (workspace_id, tool_kind) + ); + ", + // v2 — conflict archive. When two devices edit the same row while offline, + // sync keeps applying last-writer-wins but stores the LOSING version here so + // nothing is ever silently destroyed. `loser_doc` is the raw doc as stored + // (opaque {encryptedData, iv} for credential tools — Rust never decrypts). + " + CREATE TABLE conflicts ( + id TEXT PRIMARY KEY, + tool_kind TEXT NOT NULL, + workspace_id TEXT NOT NULL, + entry_id TEXT NOT NULL, + loser_side TEXT NOT NULL, -- 'local' | 'remote' + loser_doc TEXT NOT NULL, + loser_updated_at INTEGER NOT NULL, + winner_updated_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + resolved_at INTEGER -- set when the user dismisses/keeps + ); + CREATE INDEX idx_conflicts_open ON conflicts(created_at) WHERE resolved_at IS NULL; + ", +]; + +pub fn run(conn: &Connection) -> Result<()> { + let version: i64 = conn.query_row("PRAGMA user_version", [], |r| r.get(0))?; + for (i, sql) in MIGRATIONS.iter().enumerate() { + let target = (i + 1) as i64; + if version < target { + conn.execute_batch(sql)?; + conn.pragma_update(None, "user_version", target)?; + } + } + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/db/mod.rs b/apps/desktop/src-tauri/src/db/mod.rs new file mode 100644 index 00000000..c7319909 --- /dev/null +++ b/apps/desktop/src-tauri/src/db/mod.rs @@ -0,0 +1,30 @@ +pub mod device_key; +pub mod migrations; + +use std::path::Path; + +use rusqlite::Connection; + +use crate::error::Result; + +/// Opens (creating if needed) the SQLCipher database keyed by the +/// Keychain-stored device key. +pub fn open(path: &Path) -> Result { + let key = device_key::get_or_create()?; + let conn = Connection::open(path)?; + // Raw-key form: PRAGMA key = "x'<64 hex chars>'" + conn.execute_batch(&format!("PRAGMA key = \"x'{key}'\";"))?; + // Fail fast if the key is wrong / file is not a valid encrypted DB. + conn.query_row("SELECT count(*) FROM sqlite_master", [], |_| Ok(()))?; + conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;")?; + migrations::run(&conn)?; + Ok(conn) +} + +/// Current time as epoch milliseconds (matches the API's createdAt/updatedAt). +pub fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0) +} diff --git a/apps/desktop/src-tauri/src/dbtools/mod.rs b/apps/desktop/src-tauri/src/dbtools/mod.rs new file mode 100644 index 00000000..1fcf6981 --- /dev/null +++ b/apps/desktop/src-tauri/src/dbtools/mod.rs @@ -0,0 +1,189 @@ +//! Native drivers for the live-DB tools (sql-client, database-explorer, +//! redis-commander), mirroring the Next.js API route contracts under +//! `/api/sql-client/*`, `/api/nosql/*`, `/api/redis-commander/*`. +//! +//! Async (unlike the sync local router) — dispatched separately from lib.rs. + +pub mod mongo; +pub mod redis_tool; +pub mod sql; +pub mod sqlsplit; + +use crate::router::ApiResponse; +use serde_json::Value; + +pub fn err(status: u16, msg: &str) -> ApiResponse { + ApiResponse { + status, + body: serde_json::json!({ "error": msg }).to_string(), + } +} + +pub fn ok(value: &Value) -> ApiResponse { + ApiResponse { status: 200, body: value.to_string() } +} + +pub fn parse_body(body: Option<&str>) -> Result { + serde_json::from_str(body.unwrap_or("{}")).map_err(|_| err(400, "Invalid JSON body")) +} + +pub fn is_dbtool_path(path: &str) -> bool { + let p = path.split('?').next().unwrap_or(path); + p.starts_with("/api/sql-client/") + || p.starts_with("/api/nosql/") + || p.starts_with("/api/redis-commander/") +} + +pub async fn route(method: &str, full_path: &str, body: Option<&str>) -> ApiResponse { + let (path, _query) = match full_path.split_once('?') { + Some((p, q)) => (p, q), + None => (full_path, ""), + }; + let path = path.trim_end_matches('/'); + + let result = if let Some(rest) = path.strip_prefix("/api/sql-client") { + sql::handle(method, rest, body).await + } else if let Some(rest) = path.strip_prefix("/api/nosql") { + mongo::handle(method, rest, body).await + } else if let Some(rest) = path.strip_prefix("/api/redis-commander") { + redis_tool::handle(method, rest, body).await + } else { + Ok(err(404, "Not found")) + }; + + result.unwrap_or_else(|e| err(500, &e)) +} + +// Integration tests against local services (run with `cargo test -- --ignored` +// while mongod is on :27017 and redis on :6379). +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn body_json(r: &ApiResponse) -> Value { + serde_json::from_str(&r.body).unwrap() + } + + #[tokio::test] + #[ignore] + async fn mongo_roundtrip() { + let conn = json!({"connectionString": "mongodb://localhost:27017"}); + let mk = |extra: Value| { + let mut b = conn.clone(); + for (k, v) in extra.as_object().unwrap() { + b[k] = v.clone(); + } + Some(b.to_string()) + }; + + let r = route("POST", "/api/nosql/connect", mk(json!({})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + assert_eq!(body_json(&r)["success"], true); + + // insert + let r = route("POST", "/api/nosql/documents", + mk(json!({"dbName":"mdt_test","collectionName":"t1","document":{"name":"a","n":1}})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + let inserted_id = body_json(&r)["result"]["insertedId"].as_str().unwrap().to_string(); + + // query + let r = route("POST", "/api/nosql/documents/query", + mk(json!({"dbName":"mdt_test","collectionName":"t1","query":"{}","limit":10,"skip":0})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + let v = body_json(&r); + assert!(v["total"].as_i64().unwrap() >= 1); + assert!(v["documents"][0]["_id"].is_string()); // node-style hex string + + // aggregation pipeline + let r = route("POST", "/api/nosql/documents/query", + mk(json!({"dbName":"mdt_test","collectionName":"t1","query":[{"$match":{"name":"a"}}],"limit":10})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + assert!(body_json(&r)["total"].as_i64().unwrap() >= 1); + + // blocked stage + let r = route("POST", "/api/nosql/documents/query", + mk(json!({"dbName":"mdt_test","collectionName":"t1","query":[{"$where":"1"}]})).as_deref()).await; + assert_eq!(r.status, 400); + + // update + delete by id + let r = route("PUT", "/api/nosql/documents", + mk(json!({"dbName":"mdt_test","collectionName":"t1","documentId":inserted_id,"update":{"n":2}})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + let r = route("DELETE", "/api/nosql/documents", + mk(json!({"dbName":"mdt_test","collectionName":"t1","documentId":inserted_id})).as_deref()).await; + assert_eq!(body_json(&r)["result"]["deletedCount"], 1); + + // schema + collections + drop db + route("POST", "/api/nosql/documents", + mk(json!({"dbName":"mdt_test","collectionName":"t1","document":{"name":"b"}})).as_deref()).await; + let r = route("POST", "/api/nosql/schema", + mk(json!({"dbName":"mdt_test","collectionName":"t1"})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + assert_eq!(body_json(&r)["fields"][0]["name"], "_id"); + let r = route("POST", "/api/nosql/collections", mk(json!({"dbName":"mdt_test"})).as_deref()).await; + assert!(body_json(&r)["collections"].as_array().unwrap().len() >= 1); + let r = route("POST", "/api/nosql/database/drop", mk(json!({"dbName":"mdt_test"})).as_deref()).await; + assert_eq!(body_json(&r)["success"], true); + } + + #[tokio::test] + #[ignore] + async fn redis_roundtrip() { + let base = json!({"redisUrl": "redis://localhost:6379", "db": 15}); + let mk = |extra: Value| { + let mut b = base.clone(); + for (k, v) in extra.as_object().unwrap() { + b[k] = v.clone(); + } + Some(b.to_string()) + }; + + let r = route("POST", "/api/redis-commander/connect", mk(json!({})).as_deref()).await; + assert_eq!(r.status, 200, "{}", r.body); + assert_eq!(body_json(&r)["pong"], "PONG"); + + // set via execute, read via key, list via keys (scan) + let r = route("POST", "/api/redis-commander/execute", + mk(json!({"command":["SET","mdt:test:k1","v1"]})).as_deref()).await; + assert_eq!(body_json(&r)["result"], "OK"); + let r = route("POST", "/api/redis-commander/key", mk(json!({"key":"mdt:test:k1"})).as_deref()).await; + let v = body_json(&r); + assert_eq!(v["type"], "string"); + assert_eq!(v["value"], "v1"); + let r = route("POST", "/api/redis-commander/keys", + mk(json!({"pattern":"mdt:test:*","cursor":"0","count":100})).as_deref()).await; + assert!(body_json(&r)["keys"].as_array().unwrap().len() >= 1); + + // hash PUT roundtrip + let r = route("PUT", "/api/redis-commander/key", + mk(json!({"key":"mdt:test:h1","type":"hash","value":{"f":"x"},"ttl":60})).as_deref()).await; + assert_eq!(body_json(&r)["success"], true); + let r = route("POST", "/api/redis-commander/key", mk(json!({"key":"mdt:test:h1"})).as_deref()).await; + let v = body_json(&r); + assert_eq!(v["value"]["f"], "x"); + assert!(v["ttl"].as_i64().unwrap() > 0); + + // dangerous command blocked + let r = route("POST", "/api/redis-commander/execute", + mk(json!({"command":["FLUSHALL"]})).as_deref()).await; + assert_eq!(r.status, 403); + + // rename + copy conflict semantics + route("POST", "/api/redis-commander/execute", mk(json!({"command":["SET","mdt:test:k2","v2"]})).as_deref()).await; + let r = route("POST", "/api/redis-commander/key/rename", + mk(json!({"key":"mdt:test:k2","newKey":"mdt:test:k1"})).as_deref()).await; + assert_eq!(r.status, 409); + + // info + bulk-delete cleanup + let r = route("POST", "/api/redis-commander/info", mk(json!({})).as_deref()).await; + assert!(body_json(&r)["sections"]["server"]["redis_version"].is_string()); + let r = route("POST", "/api/redis-commander/bulk-delete", + mk(json!({"pattern":"mdt:test:*","dryRun":false})).as_deref()).await; + assert!(body_json(&r)["affected"].as_u64().unwrap() >= 3); + + // monitor deferred + let r = route("GET", "/api/redis-commander/monitor?redisUrl=redis://localhost:6379", None).await; + assert_eq!(r.status, 501); + } +} diff --git a/apps/desktop/src-tauri/src/dbtools/mongo.rs b/apps/desktop/src-tauri/src/dbtools/mongo.rs new file mode 100644 index 00000000..a9db6538 --- /dev/null +++ b/apps/desktop/src-tauri/src/dbtools/mongo.rs @@ -0,0 +1,609 @@ +//! /api/nosql/* — MongoDB via the official Rust driver. Clients are cached +//! per connection string (handshakes to Atlas are slow); no host blocking — +//! localhost databases are the point of a desktop app. + +use dashmap::DashMap; +use futures_util::TryStreamExt; +use mongodb::bson::{doc, oid::ObjectId, Bson, Document}; +use mongodb::options::ClientOptions; +use mongodb::Client; +use serde_json::{json, Map, Value}; +use std::sync::OnceLock; +use std::time::Duration; + +use super::{err, ok, parse_body}; +use crate::router::ApiResponse; + +type HandlerResult = Result; + +static CLIENTS: OnceLock> = OnceLock::new(); + +fn clients() -> &'static DashMap { + CLIENTS.get_or_init(DashMap::new) +} + +/// Scrub connection strings / credentials from error messages +/// (mirrors lib/nosql-error-sanitizer.ts intent). +fn sanitize(msg: &str, conn: &str) -> String { + let mut out = msg.replace(conn, ""); + if let Some(at) = out.find('@') { + if let Some(scheme_end) = out.find("://") { + if scheme_end < at { + out.replace_range(scheme_end + 3..at, ""); + } + } + } + out +} + +async fn get_client(conn_str: &str) -> Result { + if let Some(c) = clients().get(conn_str) { + return Ok(c.clone()); + } + let mut opts = ClientOptions::parse(conn_str) + .await + .map_err(|e| sanitize(&e.to_string(), conn_str))?; + opts.max_pool_size = Some(5); + opts.min_pool_size = Some(1); + opts.server_selection_timeout = Some(Duration::from_secs(5)); + opts.connect_timeout = Some(Duration::from_secs(5)); + let client = Client::with_options(opts).map_err(|e| sanitize(&e.to_string(), conn_str))?; + clients().insert(conn_str.to_string(), client.clone()); + Ok(client) +} + +// ── JSON ⇄ BSON with node-driver-compatible serialization ────────────────── + +/// Bson → JSON matching Node's JSON.stringify behavior: ObjectId → hex +/// string, DateTime → ISO string. +fn bson_to_json(b: &Bson) -> Value { + match b { + Bson::ObjectId(o) => json!(o.to_hex()), + Bson::DateTime(d) => json!(d.try_to_rfc3339_string().unwrap_or_default()), + Bson::Document(d) => { + let mut m = Map::new(); + for (k, v) in d { + m.insert(k.clone(), bson_to_json(v)); + } + Value::Object(m) + } + Bson::Array(a) => Value::Array(a.iter().map(bson_to_json).collect()), + Bson::Double(f) => json!(f), + Bson::Int32(i) => json!(i), + Bson::Int64(i) => json!(i), + Bson::String(s) => json!(s), + Bson::Boolean(v) => json!(v), + Bson::Null => Value::Null, + Bson::Decimal128(d) => json!(d.to_string()), + Bson::Timestamp(t) => json!({ "t": t.time, "i": t.increment }), + Bson::Binary(bin) => json!({ "$binary": mongodb::bson::binary::Binary::from(bin.clone()).bytes.len() }), + Bson::RegularExpression(r) => json!({ "$regex": r.pattern.clone(), "$options": r.options.clone() }), + other => json!(other.to_string()), + } +} + +fn doc_to_json(d: &Document) -> Value { + bson_to_json(&Bson::Document(d.clone())) +} + +/// JSON → BSON, recursively converting `_id` strings that are valid ObjectIds +/// (mirrors the web routes' convertObjectIds). +fn json_to_bson(v: &Value, under_id: bool) -> Bson { + match v { + Value::Null => Bson::Null, + Value::Bool(b) => Bson::Boolean(*b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + if i >= i32::MIN as i64 && i <= i32::MAX as i64 { + Bson::Int32(i as i32) + } else { + Bson::Int64(i) + } + } else { + Bson::Double(n.as_f64().unwrap_or(0.0)) + } + } + Value::String(s) => { + if under_id { + if let Ok(oid) = ObjectId::parse_str(s) { + return Bson::ObjectId(oid); + } + } + Bson::String(s.clone()) + } + Value::Array(a) => Bson::Array(a.iter().map(|x| json_to_bson(x, under_id)).collect()), + Value::Object(o) => { + let mut d = Document::new(); + for (k, val) in o { + // `_id` (or operators nested under it, e.g. {$in: [...]}) get + // ObjectId conversion for plausible hex strings. + let next_under = k == "_id" || (under_id && k.starts_with('$')); + d.insert(k.clone(), json_to_bson(val, next_under)); + } + Bson::Document(d) + } + } +} + +fn json_to_doc(v: &Value) -> Result { + match json_to_bson(v, false) { + Bson::Document(d) => Ok(d), + _ => Err("Expected a JSON object".into()), + } +} + +fn id_filter(document_id: &str) -> Document { + match ObjectId::parse_str(document_id) { + Ok(oid) => doc! { "_id": oid }, + Err(_) => doc! { "_id": document_id }, + } +} + +fn required<'a>(req: &'a Value, keys: &[&str]) -> Result, ApiResponse> { + let mut out = Vec::new(); + for k in keys { + match req[*k].as_str() { + Some(s) if !s.is_empty() => out.push(s), + _ => return Err(err(400, "Missing required fields")), + } + } + Ok(out) +} + +const BLOCKED_STAGES: &[&str] = &["$where", "$function", "$accumulator", "$out", "$merge"]; + +fn validate_pipeline(stages: &[Value]) -> Result<(), String> { + if stages.len() > 100 { + return Err("Aggregation pipeline too long (max 100 stages)".into()); + } + let raw = serde_json::to_string(stages).unwrap_or_default(); + for blocked in BLOCKED_STAGES { + if raw.contains(blocked) { + return Err(format!("Aggregation stage {blocked} is not allowed")); + } + } + Ok(()) +} + +// ── Dispatch ──────────────────────────────────────────────────────────────── + +pub async fn handle(method: &str, rest: &str, body: Option<&str>) -> HandlerResult { + let req = match parse_body(body) { + Ok(v) => v, + Err(r) => return Ok(r), + }; + let conn_str = req["connectionString"].as_str().unwrap_or(""); + if conn_str.is_empty() { + return Ok(err(400, "Connection string is required")); + } + if !conn_str.starts_with("mongodb://") && !conn_str.starts_with("mongodb+srv://") { + return Ok(err(400, "Invalid MongoDB connection string")); + } + + let result = dispatch(method, rest, &req, conn_str).await; + match result { + Ok(r) => Ok(r), + Err(e) => Ok(err(500, &sanitize(&e, conn_str))), + } +} + +async fn dispatch(method: &str, rest: &str, req: &Value, conn_str: &str) -> HandlerResult { + let client = get_client(conn_str).await?; + match (method, rest) { + ("POST", "/connect") => { + let dbs = client.list_databases().await.map_err(|e| e.to_string())?; + let list: Vec = dbs + .iter() + .map(|d| { + json!({ "name": d.name, "sizeOnDisk": d.size_on_disk, "empty": d.empty }) + }) + .collect(); + Ok(ok(&json!({ "success": true, "message": "Connected successfully", "databases": list }))) + } + ("POST", "/collections") => { + let db_name = match required(req, &["dbName"]) { + Ok(v) => v[0].to_string(), + Err(r) => return Ok(r), + }; + let db = client.database(&db_name); + let specs = db.list_collections().await.map_err(|e| e.to_string())?; + let specs: Vec<_> = specs.try_collect().await.map_err(|e| e.to_string())?; + let mut out = Vec::new(); + for spec in specs { + let count = db + .collection::(&spec.name) + .estimated_document_count() + .await + .ok(); + out.push(json!({ + "name": spec.name, + "type": format!("{:?}", spec.collection_type).to_lowercase(), + "documentCount": count, + })); + } + Ok(ok(&json!({ "collections": out }))) + } + ("POST", "/documents") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let document = json_to_doc(&req["document"])?; + let res = client + .database(&db_name) + .collection::(&coll) + .insert_one(document) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "result": { "acknowledged": true, "insertedId": bson_to_json(&res.inserted_id) } }))) + } + ("PUT", "/documents") => { + let (db_name, coll, doc_id) = + match required(req, &["dbName", "collectionName", "documentId"]) { + Ok(v) => (v[0].to_string(), v[1].to_string(), v[2].to_string()), + Err(r) => return Ok(r), + }; + let mut update = json_to_doc(&req["update"])?; + update.remove("_id"); + let res = client + .database(&db_name) + .collection::(&coll) + .update_one(id_filter(&doc_id), doc! { "$set": update }) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "result": { + "acknowledged": true, + "matchedCount": res.matched_count, + "modifiedCount": res.modified_count, + "upsertedId": res.upserted_id.as_ref().map(bson_to_json), + }}))) + } + ("DELETE", "/documents") => { + let (db_name, coll, doc_id) = + match required(req, &["dbName", "collectionName", "documentId"]) { + Ok(v) => (v[0].to_string(), v[1].to_string(), v[2].to_string()), + Err(r) => return Ok(r), + }; + let res = client + .database(&db_name) + .collection::(&coll) + .delete_one(id_filter(&doc_id)) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "result": { "acknowledged": true, "deletedCount": res.deleted_count } }))) + } + ("POST", "/documents/query") => query_documents(&client, req).await, + ("POST", "/bulk-delete") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let Some(ids) = req["documentIds"].as_array() else { + return Ok(err(400, "Missing required parameters")); + }; + if ids.is_empty() { + return Ok(err(400, "No document ids provided")); + } + if ids.len() > 1000 { + return Ok(err(400, "Bulk delete limited to 1000 documents per request")); + } + let oids: Vec = ids + .iter() + .filter_map(|v| v.as_str()) + .filter_map(|s| ObjectId::parse_str(s).ok().map(Bson::ObjectId)) + .collect(); + let res = client + .database(&db_name) + .collection::(&coll) + .delete_many(doc! { "_id": { "$in": oids } }) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "deletedCount": res.deleted_count }))) + } + ("POST", "/import") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let Some(docs) = req["documents"].as_array() else { + return Ok(err(400, "documents array is required")); + }; + if docs.is_empty() { + return Ok(err(400, "documents array is required")); + } + if docs.len() > 10000 { + return Ok(err(400, "Maximum 10,000 documents per import")); + } + let bson_docs: Vec = + docs.iter().map(json_to_doc).collect::>()?; + let res = client + .database(&db_name) + .collection::(&coll) + .insert_many(bson_docs) + .with_options(mongodb::options::InsertManyOptions::builder().ordered(false).build()) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "insertedCount": res.inserted_ids.len() }))) + } + ("POST", "/schema") => schema(&client, req).await, + ("POST", "/indexes") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let keys = json_to_doc(&req["keys"]).map_err(|_| "Missing required parameters".to_string())?; + let options = if req["options"].is_object() { + Some( + mongodb::bson::from_bson::(json_to_bson(&req["options"], false)) + .map_err(|e| e.to_string())?, + ) + } else { + None + }; + let mut index = mongodb::IndexModel::builder().keys(keys).build(); + index.options = options; + let res = client + .database(&db_name) + .collection::(&coll) + .create_index(index) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "indexName": res.index_name }))) + } + ("DELETE", "/indexes") => { + let (db_name, coll, index_name) = + match required(req, &["dbName", "collectionName", "indexName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string(), v[2].to_string()), + Err(r) => return Ok(r), + }; + client + .database(&db_name) + .collection::(&coll) + .drop_index(index_name) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/indexes/list") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let cursor = client + .database(&db_name) + .collection::(&coll) + .list_indexes() + .await + .map_err(|e| e.to_string())?; + let indexes: Vec<_> = cursor.try_collect().await.map_err(|e| e.to_string())?; + let list: Vec = indexes + .iter() + .map(|i| { + let mut v = json!({ "key": bson_to_json(&Bson::Document(i.keys.clone())) }); + if let Some(opts) = &i.options { + if let Some(name) = &opts.name { + v["name"] = json!(name); + } + if opts.unique == Some(true) { + v["unique"] = json!(true); + } + if opts.sparse == Some(true) { + v["sparse"] = json!(true); + } + } + v + }) + .collect(); + Ok(ok(&json!({ "indexes": list, "totalIndexSize": Value::Null }))) + } + ("POST", "/collection/drop") => { + let (db_name, coll) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + client + .database(&db_name) + .collection::(&coll) + .drop() + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/collection/rename") => { + let (db_name, coll, new_name) = + match required(req, &["dbName", "collectionName", "newCollectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string(), v[2].to_string()), + Err(r) => return Ok(r), + }; + client + .database("admin") + .run_command(doc! { + "renameCollection": format!("{db_name}.{coll}"), + "to": format!("{db_name}.{new_name}"), + }) + .await + .map_err(|e| e.to_string())?; + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/database/drop") => { + let db_name = match required(req, &["dbName"]) { + Ok(v) => v[0].to_string(), + Err(r) => return Ok(r), + }; + client.database(&db_name).drop().await.map_err(|e| e.to_string())?; + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/database/rename") => { + let (old_db, new_db) = match required(req, &["oldDbName", "newDbName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let old = client.database(&old_db); + let specs = old.list_collection_names().await.map_err(|e| e.to_string())?; + for coll in specs { + if coll.starts_with("system.") { + continue; + } + client + .database("admin") + .run_command(doc! { + "renameCollection": format!("{old_db}.{coll}"), + "to": format!("{new_db}.{coll}"), + }) + .await + .map_err(|e| e.to_string())?; + } + Ok(ok(&json!({ "success": true }))) + } + _ => Ok(err(404, "Not found")), + } +} + +async fn query_documents(client: &Client, req: &Value) -> HandlerResult { + let (db_name, coll_name) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let limit = req["limit"].as_i64().unwrap_or(20).clamp(1, 500); + let skip = req["skip"].as_i64().unwrap_or(0).max(0) as u64; + let sort_field = req["sortField"].as_str().unwrap_or(""); + let sort_dir: i32 = if req["sortDirection"].as_str() == Some("desc") { -1 } else { 1 }; + + let raw_query = &req["query"]; + let parsed: Value = match raw_query { + Value::String(s) => { + let s = if s.trim().is_empty() { "{}" } else { s }; + match serde_json::from_str(s) { + Ok(v) => v, + Err(_) => return Ok(err(400, "Invalid JSON in query parameter")), + } + } + Value::Null => json!({}), + other => other.clone(), + }; + + let coll = client.database(&db_name).collection::(&coll_name); + + if let Value::Array(stages_json) = &parsed { + if let Err(e) = validate_pipeline(stages_json) { + return Ok(err(400, &e)); + } + let stages: Vec = stages_json + .iter() + .map(json_to_doc) + .collect::>() + .map_err(|_| "Invalid aggregation stage".to_string())?; + + // Total via appended $count + let mut count_stages = stages.clone(); + count_stages.push(doc! { "$count": "total" }); + let count_cursor = coll.aggregate(count_stages).await.map_err(|e| e.to_string())?; + let count_docs: Vec = count_cursor.try_collect().await.map_err(|e| e.to_string())?; + let total = count_docs + .first() + .and_then(|d| d.get("total").and_then(|b| b.as_i64().or(b.as_i32().map(i64::from)))) + .unwrap_or(0); + + let mut page_stages = stages; + if !sort_field.is_empty() { + page_stages.push(doc! { "$sort": { sort_field: sort_dir } }); + } + page_stages.push(doc! { "$skip": skip as i64 }); + page_stages.push(doc! { "$limit": limit }); + let cursor = coll.aggregate(page_stages).await.map_err(|e| e.to_string())?; + let docs: Vec = cursor.try_collect().await.map_err(|e| e.to_string())?; + let documents: Vec = docs.iter().map(doc_to_json).collect(); + return Ok(ok(&json!({ "documents": documents, "total": total }))); + } + + let filter = json_to_doc(&parsed)?; + let total = coll.count_documents(filter.clone()).await.map_err(|e| e.to_string())?; + let mut find = coll.find(filter).skip(skip).limit(limit); + if !sort_field.is_empty() { + find = find.sort(doc! { sort_field: sort_dir }); + } + let cursor = find.await.map_err(|e| e.to_string())?; + let docs: Vec = cursor.try_collect().await.map_err(|e| e.to_string())?; + let documents: Vec = docs.iter().map(doc_to_json).collect(); + Ok(ok(&json!({ "documents": documents, "total": total }))) +} + +fn bucket_type(b: &Bson) -> String { + match b { + Bson::Null => "null".into(), + Bson::ObjectId(_) => "ObjectId".into(), + Bson::DateTime(_) => "Date".into(), + Bson::Array(_) => "Array".into(), + Bson::Document(_) => "Object".into(), + Bson::String(_) => "string".into(), + Bson::Boolean(_) => "boolean".into(), + Bson::Int32(_) | Bson::Int64(_) | Bson::Double(_) | Bson::Decimal128(_) => "number".into(), + other => format!("{other:?}").split('(').next().unwrap_or("unknown").to_string(), + } +} + +async fn schema(client: &Client, req: &Value) -> HandlerResult { + let (db_name, coll_name) = match required(req, &["dbName", "collectionName"]) { + Ok(v) => (v[0].to_string(), v[1].to_string()), + Err(r) => return Ok(r), + }; + let sample_size = req["sampleSize"].as_i64().unwrap_or(200).min(1000); + let coll = client.database(&db_name).collection::(&coll_name); + let cursor = coll + .aggregate(vec![doc! { "$sample": { "size": sample_size } }]) + .await + .map_err(|e| e.to_string())?; + let docs: Vec = cursor.try_collect().await.map_err(|e| e.to_string())?; + if docs.is_empty() { + return Ok(ok(&json!({ "fields": [], "sampleSize": 0 }))); + } + + struct FieldStat { + types: std::collections::BTreeMap, + null_count: u64, + count: u64, + examples: Vec, + } + let mut fields: std::collections::BTreeMap = Default::default(); + for d in &docs { + for (k, v) in d { + let stat = fields.entry(k.clone()).or_insert_with(|| FieldStat { + types: Default::default(), + null_count: 0, + count: 0, + examples: Vec::new(), + }); + stat.count += 1; + let t = bucket_type(v); + if t == "null" { + stat.null_count += 1; + } + *stat.types.entry(t).or_insert(0) += 1; + if stat.examples.len() < 2 { + stat.examples.push(bson_to_json(v)); + } + } + } + let total = docs.len() as u64; + let mut out: Vec = fields + .into_iter() + .map(|(name, s)| { + json!({ + "name": name, + "types": s.types, + "nullCount": s.null_count, + "count": s.count, + "coverage": (s.count * 100) / total, + "examples": s.examples, + }) + }) + .collect(); + out.sort_by(|a, b| { + let a_id = a["name"] == "_id"; + let b_id = b["name"] == "_id"; + b_id.cmp(&a_id) + .then(b["coverage"].as_u64().cmp(&a["coverage"].as_u64())) + .then(a["name"].as_str().cmp(&b["name"].as_str())) + }); + Ok(ok(&json!({ "fields": out, "sampleSize": total }))) +} diff --git a/apps/desktop/src-tauri/src/dbtools/redis_tool.rs b/apps/desktop/src-tauri/src/dbtools/redis_tool.rs new file mode 100644 index 00000000..5b083374 --- /dev/null +++ b/apps/desktop/src-tauri/src/dbtools/redis_tool.rs @@ -0,0 +1,845 @@ +//! /api/redis-commander/* — Redis via redis-rs, one client per request +//! (mirrors the web routes, which have no pool). MONITOR and pub/sub +//! subscribe are SSE on the web and deferred to v1.1 on desktop. + +use redis::aio::MultiplexedConnection; +use redis::Value as RV; +use serde_json::{json, Map, Value}; + +use super::{err, ok, parse_body}; +use crate::router::ApiResponse; + +type HandlerResult = Result; + +const DANGEROUS_COMMANDS: &[&str] = &[ + "FLUSHALL", "FLUSHDB", "CONFIG", "DEBUG", "SHUTDOWN", "SLAVEOF", "REPLICAOF", + "BGREWRITEAOF", "BGSAVE", "SAVE", +]; +const MAX_SCAN_LIMIT: usize = 100_000; +const MAX_EXPORT_KEYS: usize = 10_000; +const MAX_SAMPLE: i64 = 5_000; + +async fn connect(req: &Value) -> Result { + let url = req["redisUrl"].as_str().unwrap_or(""); + if !url.starts_with("redis://") && !url.starts_with("rediss://") { + return Err("redisUrl must start with redis:// or rediss://".into()); + } + let client = redis::Client::open(url).map_err(|e| e.to_string())?; + let mut conn = tokio::time::timeout( + std::time::Duration::from_secs(10), + client.get_multiplexed_async_connection(), + ) + .await + .map_err(|_| "Connection timed out".to_string())? + .map_err(|e| e.to_string())?; + if let Some(db) = req["db"].as_i64() { + let _: RV = redis::cmd("SELECT").arg(db).query_async(&mut conn).await.map_err(|e| e.to_string())?; + } + Ok(conn) +} + +fn rv_to_json(v: &RV) -> Value { + match v { + RV::Nil => Value::Null, + RV::Int(i) => json!(i), + RV::Double(d) => json!(d), + RV::Boolean(b) => json!(b), + RV::BulkString(b) => json!(String::from_utf8_lossy(b)), + RV::SimpleString(s) => json!(s), + RV::Okay => json!("OK"), + RV::Array(a) | RV::Set(a) => Value::Array(a.iter().map(rv_to_json).collect()), + RV::Map(m) => { + let mut obj = Map::new(); + for (k, val) in m { + let key = match rv_to_json(k) { + Value::String(s) => s, + other => other.to_string(), + }; + obj.insert(key, rv_to_json(val)); + } + Value::Object(obj) + } + RV::VerbatimString { text, .. } => json!(text), + RV::BigNumber(n) => json!(n.to_string()), + other => json!(format!("{other:?}")), + } +} + +async fn cmd(conn: &mut MultiplexedConnection, parts: &[&str]) -> Result { + let mut c = redis::cmd(parts[0]); + for p in &parts[1..] { + c.arg(*p); + } + c.query_async(conn).await.map_err(|e| e.to_string()) +} + +async fn cmd_json(conn: &mut MultiplexedConnection, parts: &[&str]) -> Result { + Ok(rv_to_json(&cmd(conn, parts).await?)) +} + +fn as_str_vec(v: &Value) -> Vec { + match v { + Value::Array(a) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect(), + _ => Vec::new(), + } +} + +/// SCAN one page → (next_cursor, keys) +async fn scan_page( + conn: &mut MultiplexedConnection, + cursor: &str, + pattern: &str, + count: usize, +) -> Result<(String, Vec), String> { + let v = cmd(conn, &["SCAN", cursor, "MATCH", pattern, "COUNT", &count.to_string()]).await?; + if let RV::Array(items) = v { + let next = match items.first() { + Some(RV::BulkString(b)) => String::from_utf8_lossy(b).to_string(), + Some(RV::SimpleString(s)) => s.clone(), + Some(RV::Int(i)) => i.to_string(), + _ => "0".into(), + }; + let keys = match items.get(1) { + Some(RV::Array(ks)) => ks + .iter() + .filter_map(|k| match k { + RV::BulkString(b) => Some(String::from_utf8_lossy(b).to_string()), + RV::SimpleString(s) => Some(s.clone()), + _ => None, + }) + .collect(), + _ => Vec::new(), + }; + Ok((next, keys)) + } else { + Err("Unexpected SCAN reply".into()) + } +} + +async fn scan_all( + conn: &mut MultiplexedConnection, + pattern: &str, + page: usize, + max: usize, +) -> Result<(Vec, usize, bool), String> { + let mut cursor = "0".to_string(); + let mut keys = Vec::new(); + let mut scanned = 0usize; + let mut truncated = false; + loop { + let (next, batch) = scan_page(conn, &cursor, pattern, page).await?; + scanned += batch.len(); + keys.extend(batch); + cursor = next; + if keys.len() >= max { + truncated = cursor != "0"; + keys.truncate(max); + break; + } + if cursor == "0" { + break; + } + } + Ok((keys, scanned, truncated)) +} + +/// Read a key's value by type (shared by key GET / export). +async fn read_typed_value( + conn: &mut MultiplexedConnection, + key: &str, +) -> Result<(String, i64, Value), String> { + let ty = match cmd_json(conn, &["TYPE", key]).await? { + Value::String(s) => s, + _ => "none".into(), + }; + let ttl = cmd_json(conn, &["TTL", key]).await?.as_i64().unwrap_or(-1); + let value = match ty.as_str() { + "string" => cmd_json(conn, &["GET", key]).await?, + "list" => cmd_json(conn, &["LRANGE", key, "0", "-1"]).await?, + "set" => cmd_json(conn, &["SMEMBERS", key]).await?, + "zset" => { + let raw = cmd_json(conn, &["ZRANGE", key, "0", "-1", "WITHSCORES"]).await?; + let flat = raw.as_array().cloned().unwrap_or_default(); + let mut out = Vec::new(); + let mut i = 0; + while i + 1 < flat.len() { + let member = flat[i].clone(); + let score = flat[i + 1] + .as_str() + .and_then(|s| s.parse::().ok()) + .or(flat[i + 1].as_f64()) + .unwrap_or(0.0); + out.push(json!({ "member": member, "score": score })); + i += 2; + } + Value::Array(out) + } + "hash" => { + // RESP2 returns a flat [field, value, ...] array; RESP3 a map. + match cmd_json(conn, &["HGETALL", key]).await? { + Value::Array(flat) => { + let mut obj = Map::new(); + let mut i = 0; + while i + 1 < flat.len() { + if let Some(f) = flat[i].as_str() { + obj.insert(f.to_string(), flat[i + 1].clone()); + } + i += 2; + } + Value::Object(obj) + } + other => other, + } + } + _ => Value::Null, + }; + Ok((ty, ttl, value)) +} + +/// Recreate a key from an exported {type, value} (shared by key PUT / import). +async fn write_typed_value( + conn: &mut MultiplexedConnection, + key: &str, + ty: &str, + value: &Value, + ttl: i64, +) -> Result<(), String> { + let _ = cmd(conn, &["DEL", key]).await?; + match ty { + "string" => { + let s = value.as_str().map(str::to_string).unwrap_or_else(|| value.to_string()); + cmd(conn, &["SET", key, &s]).await?; + } + "list" => { + for item in value.as_array().cloned().unwrap_or_default() { + let s = item.as_str().map(str::to_string).unwrap_or_else(|| item.to_string()); + cmd(conn, &["RPUSH", key, &s]).await?; + } + } + "set" => { + for item in value.as_array().cloned().unwrap_or_default() { + let s = item.as_str().map(str::to_string).unwrap_or_else(|| item.to_string()); + cmd(conn, &["SADD", key, &s]).await?; + } + } + "zset" => { + for item in value.as_array().cloned().unwrap_or_default() { + let member = item["member"].as_str().unwrap_or_default().to_string(); + let score = item["score"].as_f64().unwrap_or(0.0).to_string(); + cmd(conn, &["ZADD", key, &score, &member]).await?; + } + } + "hash" => { + if let Some(obj) = value.as_object() { + for (f, v) in obj { + let s = v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()); + cmd(conn, &["HSET", key, f, &s]).await?; + } + } + } + _ => return Err(format!("Unknown type: {ty}")), + } + if ttl > 0 { + cmd(conn, &["EXPIRE", key, &ttl.to_string()]).await?; + } else if ttl == -1 { + cmd(conn, &["PERSIST", key]).await?; + } + Ok(()) +} + +pub async fn handle(method: &str, rest: &str, body: Option<&str>) -> HandlerResult { + // Streaming routes (SSE on web) are not available on desktop v1. + if rest == "/monitor" || (rest == "/pubsub" && method == "GET") { + return Ok(err(501, "Live monitor/subscribe is not available in the desktop app yet")); + } + let req = match parse_body(body) { + Ok(v) => v, + Err(r) => return Ok(r), + }; + if req["redisUrl"].as_str().unwrap_or("").is_empty() { + return Ok(err(400, "redisUrl is required")); + } + let mut conn = connect(&req).await?; + match (method, rest) { + ("POST", "/connect") => { + let info = cmd_json(&mut conn, &["INFO", "server"]).await?; + let version = info + .as_str() + .and_then(|s| { + s.lines() + .find(|l| l.starts_with("redis_version:")) + .map(|l| l.trim_start_matches("redis_version:").trim().to_string()) + }) + .unwrap_or_default(); + let pong = cmd_json(&mut conn, &["PING"]).await?; + Ok(ok(&json!({ "success": true, "version": version, "pong": pong }))) + } + ("POST", "/execute") => { + let parts = as_str_vec(&req["command"]); + if parts.is_empty() { + return Ok(err(400, "command array is required")); + } + let upper = parts[0].to_uppercase(); + if DANGEROUS_COMMANDS.contains(&upper.as_str()) { + return Ok(err(403, &format!("Command {upper} is disabled"))); + } + let started = std::time::Instant::now(); + let refs: Vec<&str> = parts.iter().map(String::as_str).collect(); + let result = cmd_json(&mut conn, &refs).await?; + Ok(ok(&json!({ "result": result, "executionTime": started.elapsed().as_millis() as u64 }))) + } + ("POST", "/keys") => { + let pattern = req["pattern"].as_str().unwrap_or("*"); + let cursor = match &req["cursor"] { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + _ => "0".into(), + }; + let count = req["count"].as_u64().unwrap_or(100) as usize; + let (next, keys) = scan_page(&mut conn, &cursor, pattern, count).await?; + let mut out = Vec::new(); + for key in keys { + let ty = cmd_json(&mut conn, &["TYPE", &key]).await?; + let ttl = cmd_json(&mut conn, &["TTL", &key]).await?; + out.push(json!({ "key": key, "type": ty, "ttl": ttl })); + } + let dbsize = cmd_json(&mut conn, &["DBSIZE"]).await?; + Ok(ok(&json!({ "cursor": next, "keys": out, "dbSize": dbsize }))) + } + (m, "/key") => { + let key = req["key"].as_str().unwrap_or(""); + if key.is_empty() { + return Ok(err(400, "key is required")); + } + match m { + "POST" => { + let (ty, ttl, value) = read_typed_value(&mut conn, key).await?; + Ok(ok(&json!({ "key": key, "type": ty, "ttl": ttl, "value": value }))) + } + "PUT" => { + let ty = req["type"].as_str().unwrap_or(""); + let ttl = req["ttl"].as_i64().unwrap_or(0); + write_typed_value(&mut conn, key, ty, &req["value"], ttl) + .await + .map_err(|e| e)?; + Ok(ok(&json!({ "success": true }))) + } + "DELETE" => { + let deleted = cmd_json(&mut conn, &["DEL", key]).await?; + Ok(ok(&json!({ "deleted": deleted }))) + } + _ => Ok(err(405, "Method not allowed")), + } + } + ("POST", "/key/copy") => { + let key = req["key"].as_str().unwrap_or(""); + let dest = req["destination"].as_str().unwrap_or(""); + if key.is_empty() || dest.is_empty() { + return Ok(err(400, "key and destination are required")); + } + let mut parts: Vec = vec!["COPY".into(), key.into(), dest.into()]; + if let Some(ddb) = req["destinationDb"].as_i64() { + parts.push("DB".into()); + parts.push(ddb.to_string()); + } + if req["overwrite"].as_bool() == Some(true) { + parts.push("REPLACE".into()); + } + let refs: Vec<&str> = parts.iter().map(String::as_str).collect(); + let result = cmd_json(&mut conn, &refs).await?; + if result.as_i64() == Some(0) { + return Ok(err(409, &format!("Destination '{dest}' already exists. Enable overwrite to replace it."))); + } + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/key/rename") => { + let key = req["key"].as_str().unwrap_or(""); + let new_key = req["newKey"].as_str().unwrap_or(""); + if key.is_empty() || new_key.is_empty() { + return Ok(err(400, "key and newKey are required")); + } + if key == new_key { + return Ok(err(400, "New name must be different")); + } + if req["overwrite"].as_bool() == Some(true) { + cmd(&mut conn, &["RENAME", key, new_key]).await?; + } else { + let r = cmd_json(&mut conn, &["RENAMENX", key, new_key]).await?; + if r.as_i64() == Some(0) { + return Ok(err(409, &format!("Key '{new_key}' already exists. Enable overwrite to replace it."))); + } + } + Ok(ok(&json!({ "success": true }))) + } + ("POST", "/bulk-delete") => { + let pattern = req["pattern"].as_str().unwrap_or(""); + if pattern.is_empty() || pattern == "*" { + return Ok(err(400, "Refusing to bulk-delete all keys — use Flush instead")); + } + let dry_run = req["dryRun"].as_bool().unwrap_or(false); + let (keys, scanned, truncated) = scan_all(&mut conn, pattern, 500, MAX_SCAN_LIMIT).await?; + let sample: Vec<&String> = keys.iter().take(20).collect(); + let affected = keys.len(); + if !dry_run { + for chunk in keys.chunks(500) { + let mut parts: Vec<&str> = vec!["DEL"]; + parts.extend(chunk.iter().map(String::as_str)); + cmd(&mut conn, &parts).await?; + } + } + Ok(ok(&json!({ "dryRun": dry_run, "affected": affected, "scanned": scanned, "sample": sample, "truncated": truncated }))) + } + ("POST", "/flush") => { + let pattern = req["pattern"].as_str().unwrap_or(""); + if pattern.is_empty() || pattern == "*" { + return Ok(err(403, "Full flush is disabled")); + } + let (keys, ..) = scan_all(&mut conn, pattern, 100, MAX_SCAN_LIMIT).await?; + let deleted = keys.len(); + for chunk in keys.chunks(500) { + let mut parts: Vec<&str> = vec!["DEL"]; + parts.extend(chunk.iter().map(String::as_str)); + cmd(&mut conn, &parts).await?; + } + Ok(ok(&json!({ "success": true, "deleted": deleted }))) + } + ("POST", "/export") => { + let pattern = req["pattern"].as_str().unwrap_or("*"); + let (keys, _scanned, truncated) = scan_all(&mut conn, pattern, 500, MAX_EXPORT_KEYS).await?; + let mut out = Vec::new(); + for key in &keys { + let (ty, ttl, value) = read_typed_value(&mut conn, key).await?; + out.push(json!({ "key": key, "type": ty, "ttl": ttl, "value": value })); + } + Ok(ok(&json!({ + "exportedAt": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0), + "pattern": pattern, + "count": out.len(), + "truncated": truncated, + "keys": out, + }))) + } + ("POST", "/import") => { + let Some(keys) = req["keys"].as_array() else { + return Ok(err(400, "keys array is required")); + }; + if keys.is_empty() { + return Ok(ok(&json!({ "imported": 0, "skipped": 0 }))); + } + let overwrite = req["overwrite"].as_bool().unwrap_or(false); + let mut imported = 0u64; + let mut skipped = 0u64; + let mut errors: Vec = Vec::new(); + for entry in keys { + let key = entry["key"].as_str().unwrap_or(""); + let ty = entry["type"].as_str().unwrap_or(""); + if key.is_empty() || ty.is_empty() { + continue; + } + if !overwrite { + let exists = cmd_json(&mut conn, &["EXISTS", key]).await?; + if exists.as_i64() == Some(1) { + skipped += 1; + continue; + } + } + let ttl = entry["ttl"].as_i64().unwrap_or(-1); + match write_typed_value(&mut conn, key, ty, &entry["value"], ttl).await { + Ok(()) => imported += 1, + Err(e) => errors.push(format!("{key}: {e}")), + } + } + Ok(ok(&json!({ "imported": imported, "skipped": skipped, "errors": errors }))) + } + ("POST", "/info") => { + let raw = cmd_json(&mut conn, &["INFO"]).await?; + let mut sections: Map = Map::new(); + let mut current = String::new(); + for line in raw.as_str().unwrap_or("").lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + if let Some(name) = line.strip_prefix("# ") { + current = name.to_lowercase(); + sections.insert(current.clone(), json!({})); + } else if let Some((k, v)) = line.split_once(':') { + if let Some(Value::Object(sec)) = sections.get_mut(¤t) { + sec.insert(k.to_string(), json!(v)); + } + } + } + let dbsize = cmd_json(&mut conn, &["DBSIZE"]).await?; + Ok(ok(&json!({ "sections": sections, "dbsize": dbsize }))) + } + ("POST", "/clients") => { + match req["action"].as_str().unwrap_or("list") { + "kill" => { + let Some(client_id) = req["clientId"].as_str().map(str::to_string).or_else(|| req["clientId"].as_i64().map(|i| i.to_string())) else { + return Ok(err(400, "clientId is required")); + }; + let killed = cmd_json(&mut conn, &["CLIENT", "KILL", "ID", &client_id]).await?; + Ok(ok(&json!({ "killed": killed }))) + } + _ => { + let raw = cmd_json(&mut conn, &["CLIENT", "LIST"]).await?; + let clients: Vec = raw + .as_str() + .unwrap_or("") + .lines() + .filter(|l| !l.trim().is_empty()) + .map(|line| { + let mut fields = Map::new(); + for pair in line.split(' ') { + if let Some((k, v)) = pair.split_once('=') { + fields.insert(k.to_string(), json!(v)); + } + } + let g = |k: &str| fields.get(k).cloned().unwrap_or(Value::Null); + json!({ + "id": g("id"), "addr": g("addr"), "name": g("name"), + "age": g("age"), "idle": g("idle"), "db": g("db"), + "cmd": g("cmd"), "user": g("user"), "raw": fields, + }) + }) + .collect(); + Ok(ok(&json!({ "clients": clients }))) + } + } + } + ("POST", "/slowlog") => { + match req["action"].as_str().unwrap_or("get") { + "reset" => { + cmd(&mut conn, &["SLOWLOG", "RESET"]).await?; + Ok(ok(&json!({ "success": true }))) + } + "len" => { + let len = cmd_json(&mut conn, &["SLOWLOG", "LEN"]).await?; + Ok(ok(&json!({ "length": len }))) + } + _ => { + let count = req["count"].as_u64().unwrap_or(50).to_string(); + let raw = cmd_json(&mut conn, &["SLOWLOG", "GET", &count]).await?; + let entries: Vec = raw + .as_array() + .cloned() + .unwrap_or_default() + .iter() + .map(|e| { + let a = e.as_array().cloned().unwrap_or_default(); + json!({ + "id": a.first().cloned().unwrap_or(Value::Null), + "ts": a.get(1).cloned().unwrap_or(Value::Null), + "durationMicros": a.get(2).cloned().unwrap_or(Value::Null), + "command": a.get(3).cloned().unwrap_or(json!([])), + "clientAddr": a.get(4).cloned(), + "clientName": a.get(5).cloned(), + }) + }) + .collect(); + Ok(ok(&json!({ "entries": entries }))) + } + } + } + ("POST", "/scanner") => { + let pattern = req["pattern"].as_str().unwrap_or("*"); + let sample_size = req["sampleSize"].as_i64().unwrap_or(1000).min(MAX_SAMPLE) as usize; + let (keys, scanned, truncated) = scan_all(&mut conn, pattern, 500, sample_size).await?; + let mut total_bytes: u64 = 0; + let mut by_type: Map = Map::new(); + let mut detail: Vec<(String, String, u64, i64)> = Vec::new(); + let mut no_ttl = 0u64; + let mut expiring = 0u64; + for key in &keys { + let ty = match cmd_json(&mut conn, &["TYPE", key]).await? { + Value::String(s) => s, + _ => "unknown".into(), + }; + let bytes = cmd_json(&mut conn, &["MEMORY", "USAGE", key, "SAMPLES", "0"]) + .await + .ok() + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let ttl = cmd_json(&mut conn, &["TTL", key]).await?.as_i64().unwrap_or(-1); + total_bytes += bytes; + if ttl == -1 { + no_ttl += 1; + } else if ttl > 0 { + expiring += 1; + } + let entry = by_type.entry(ty.clone()).or_insert_with(|| json!({ "count": 0, "bytes": 0 })); + entry["count"] = json!(entry["count"].as_u64().unwrap_or(0) + 1); + entry["bytes"] = json!(entry["bytes"].as_u64().unwrap_or(0) + bytes); + detail.push((key.clone(), ty, bytes, ttl)); + } + detail.sort_by(|a, b| b.2.cmp(&a.2)); + let top: Vec = detail + .iter() + .take(50) + .map(|(key, ty, bytes, ttl)| json!({ "key": key, "type": ty, "bytes": bytes, "ttl": ttl })) + .collect(); + Ok(ok(&json!({ + "pattern": pattern, "sampledKeys": keys.len(), "truncated": truncated, + "scanned": scanned, "totalBytes": total_bytes, "byType": by_type, + "topByBytes": top, + "ttlSummary": { "noTtl": no_ttl, "expiring": expiring, "expired": 0 }, + }))) + } + ("POST", "/pubsub") => { + let channel = req["channel"].as_str().unwrap_or(""); + if channel.is_empty() { + return Ok(err(400, "channel is required")); + } + let message = req["message"].as_str().map(str::to_string).unwrap_or_else(|| req["message"].to_string()); + let receivers = cmd_json(&mut conn, &["PUBLISH", channel, &message]).await?; + Ok(ok(&json!({ "receivers": receivers }))) + } + (m, "/json") => { + if m == "GET" { + let raw = cmd_json(&mut conn, &["MODULE", "LIST"]).await?; + let names: Vec = raw + .as_array() + .cloned() + .unwrap_or_default() + .iter() + .filter_map(|module| { + let v = if module.is_object() { + module.get("name").cloned() + } else { + let a = module.as_array()?; + a.iter().position(|x| x.as_str() == Some("name")).and_then(|i| a.get(i + 1).cloned()) + }; + v + }) + .collect(); + return Ok(ok(&json!({ "modules": names }))); + } + let action = req["action"].as_str().unwrap_or(""); + if action == "modules" { + let raw = cmd_json(&mut conn, &["MODULE", "LIST"]).await?; + return Ok(ok(&json!({ "modules": raw }))); + } + let key = req["key"].as_str().unwrap_or(""); + if key.is_empty() { + return Ok(err(400, "key is required")); + } + let path = req["path"].as_str().unwrap_or("$"); + match action { + "get" => { + let value = cmd_json(&mut conn, &["JSON.GET", key, path]).await?; + Ok(ok(&json!({ "value": value }))) + } + "set" => { + if req["value"].is_null() { + return Ok(err(400, "value is required")); + } + let payload = req["value"].to_string(); + let result = cmd_json(&mut conn, &["JSON.SET", key, path, &payload]).await?; + Ok(ok(&json!({ "result": result }))) + } + "del" => { + let deleted = cmd_json(&mut conn, &["JSON.DEL", key, path]).await?; + Ok(ok(&json!({ "deleted": deleted }))) + } + "type" => { + let ty = cmd_json(&mut conn, &["JSON.TYPE", key, path]).await?; + Ok(ok(&json!({ "type": ty }))) + } + _ => Ok(err(400, "Unknown action")), + } + } + ("POST", "/search") => { + match req["action"].as_str().unwrap_or("list") { + "list" => { + let indexes = cmd_json(&mut conn, &["FT._LIST"]).await?; + Ok(ok(&json!({ "indexes": indexes }))) + } + "info" => { + let index = req["index"].as_str().unwrap_or(""); + if index.is_empty() { + return Ok(err(400, "index is required")); + } + let info = cmd_json(&mut conn, &["FT.INFO", index]).await?; + Ok(ok(&json!({ "info": info }))) + } + "explain" => { + let index = req["index"].as_str().unwrap_or(""); + let query = req["query"].as_str().unwrap_or(""); + if index.is_empty() || query.is_empty() { + return Ok(err(400, "index and query are required")); + } + let explain = cmd_json(&mut conn, &["FT.EXPLAIN", index, query]).await?; + Ok(ok(&json!({ "explain": explain }))) + } + "search" => { + let index = req["index"].as_str().unwrap_or(""); + let query = req["query"].as_str().unwrap_or(""); + if index.is_empty() || query.is_empty() { + return Ok(err(400, "index and query are required")); + } + let offset = req["offset"].as_u64().unwrap_or(0).to_string(); + let limit = req["limit"].as_u64().unwrap_or(25).to_string(); + let raw = cmd_json(&mut conn, &["FT.SEARCH", index, query, "LIMIT", &offset, &limit]).await?; + let arr = raw.as_array().cloned().unwrap_or_default(); + let total = arr.first().and_then(Value::as_i64).unwrap_or(0); + let mut results = Vec::new(); + let mut i = 1; + while i + 1 < arr.len() + 1 && i < arr.len() { + let key = arr[i].clone(); + let fields_arr = arr.get(i + 1).and_then(Value::as_array).cloned().unwrap_or_default(); + let mut fields = Map::new(); + let mut j = 0; + while j + 1 < fields_arr.len() { + if let Some(name) = fields_arr[j].as_str() { + fields.insert(name.to_string(), fields_arr[j + 1].clone()); + } + j += 2; + } + results.push(json!({ "key": key, "fields": fields })); + i += 2; + } + Ok(ok(&json!({ "total": total, "results": results }))) + } + _ => Ok(err(400, "Unknown action")), + } + } + ("POST", "/streams") => streams(&mut conn, &req).await, + ("POST", "/timeseries") => { + let key = req["key"].as_str().unwrap_or(""); + if key.is_empty() { + return Ok(err(400, "key is required")); + } + match req["action"].as_str().unwrap_or("range") { + "info" => { + let info = cmd_json(&mut conn, &["TS.INFO", key]).await?; + Ok(ok(&json!({ "info": info }))) + } + _ => { + let from = req["from"].as_str().unwrap_or("-").to_string(); + let to = req["to"].as_str().unwrap_or("+").to_string(); + let count = req["count"].as_u64().unwrap_or(500).to_string(); + let mut parts: Vec = vec!["TS.RANGE".into(), key.into(), from, to]; + if let (Some(agg), Some(bucket)) = (req["aggregation"].as_str(), req["bucketMs"].as_u64()) { + parts.push("AGGREGATION".into()); + parts.push(agg.to_string()); + parts.push(bucket.to_string()); + } + parts.push("COUNT".into()); + parts.push(count); + let refs: Vec<&str> = parts.iter().map(String::as_str).collect(); + let raw = cmd_json(&mut conn, &refs).await?; + let samples: Vec = raw + .as_array() + .cloned() + .unwrap_or_default() + .iter() + .filter_map(|s| { + let a = s.as_array()?; + let ts = a.first()?.as_i64()?; + let value = a.get(1)?.as_str().and_then(|v| v.parse::().ok()).or(a.get(1)?.as_f64())?; + Some(json!({ "ts": ts, "value": value })) + }) + .collect(); + Ok(ok(&json!({ "samples": samples }))) + } + } + } + _ => Ok(err(404, "Not found")), + } +} + +async fn streams(conn: &mut MultiplexedConnection, req: &Value) -> HandlerResult { + let key = req["key"].as_str().unwrap_or(""); + let action = req["action"].as_str().unwrap_or(""); + if key.is_empty() || action.is_empty() { + return Ok(err(400, "key and action are required")); + } + let entries_from = |raw: Value| -> Vec { + raw.as_array() + .cloned() + .unwrap_or_default() + .iter() + .filter_map(|e| { + let a = e.as_array()?; + let id = a.first()?.clone(); + let flat = a.get(1).and_then(Value::as_array).cloned().unwrap_or_default(); + let mut fields = Map::new(); + let mut i = 0; + while i + 1 < flat.len() { + if let Some(name) = flat[i].as_str() { + fields.insert(name.to_string(), flat[i + 1].clone()); + } + i += 2; + } + Some(json!({ "id": id, "fields": fields })) + }) + .collect() + }; + match action { + "info" => { + let info = cmd_json(conn, &["XINFO", "STREAM", key]).await?; + Ok(ok(&json!({ "info": info }))) + } + "range" => { + let start = req["start"].as_str().unwrap_or("-").to_string(); + let end = req["end"].as_str().unwrap_or("+").to_string(); + let count = req["count"].as_u64().unwrap_or(50).to_string(); + let raw = cmd_json(conn, &["XRANGE", key, &start, &end, "COUNT", &count]).await?; + Ok(ok(&json!({ "entries": entries_from(raw) }))) + } + "add" => { + let Some(fields) = req["fields"].as_object() else { + return Ok(err(400, "fields object is required")); + }; + let id = req["id"].as_str().unwrap_or("*").to_string(); + let mut parts: Vec = vec!["XADD".into(), key.into(), id]; + for (f, v) in fields { + parts.push(f.clone()); + parts.push(v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string())); + } + let refs: Vec<&str> = parts.iter().map(String::as_str).collect(); + let new_id = cmd_json(conn, &refs).await?; + Ok(ok(&json!({ "id": new_id }))) + } + "del" => { + let ids = as_str_vec(&req["ids"]); + let mut parts: Vec<&str> = vec!["XDEL", key]; + parts.extend(ids.iter().map(String::as_str)); + let deleted = cmd_json(conn, &parts).await?; + Ok(ok(&json!({ "deleted": deleted }))) + } + "trim" => { + let maxlen = req["maxlen"].as_u64().unwrap_or(0).to_string(); + let trimmed = cmd_json(conn, &["XTRIM", key, "MAXLEN", "~", &maxlen]).await?; + Ok(ok(&json!({ "trimmed": trimmed }))) + } + "groups" => { + let groups = cmd_json(conn, &["XINFO", "GROUPS", key]).await?; + Ok(ok(&json!({ "groups": groups }))) + } + "pending" => { + let group = req["group"].as_str().unwrap_or(""); + let pending = cmd_json(conn, &["XPENDING", key, group]).await?; + Ok(ok(&json!({ "pending": pending }))) + } + "ack" => { + let group = req["group"].as_str().unwrap_or(""); + let ids = as_str_vec(&req["ids"]); + let mut parts: Vec<&str> = vec!["XACK", key, group]; + parts.extend(ids.iter().map(String::as_str)); + let acked = cmd_json(conn, &parts).await?; + Ok(ok(&json!({ "acked": acked }))) + } + "claim" => { + let group = req["group"].as_str().unwrap_or(""); + let consumer = req["consumer"].as_str().unwrap_or(""); + let min_idle = req["minIdle"].as_u64().unwrap_or(0).to_string(); + let ids = as_str_vec(&req["ids"]); + let mut parts: Vec<&str> = vec!["XCLAIM", key, group, consumer, &min_idle]; + parts.extend(ids.iter().map(String::as_str)); + let raw = cmd_json(conn, &parts).await?; + Ok(ok(&json!({ "claimed": entries_from(raw) }))) + } + _ => Ok(err(400, "Unknown action")), + } +} diff --git a/apps/desktop/src-tauri/src/dbtools/sql.rs b/apps/desktop/src-tauri/src/dbtools/sql.rs new file mode 100644 index 00000000..a9ff71a6 --- /dev/null +++ b/apps/desktop/src-tauri/src/dbtools/sql.rs @@ -0,0 +1,354 @@ +//! /api/sql-client/* — PostgreSQL (tokio-postgres, text protocol) and +//! MySQL/MariaDB (mysql_async). Connections are per-request (single local +//! user; avoids pool lifetime management). Credentials arrive per request, +//! decrypted client-side, exactly like the web routes. + +use serde_json::{json, Map, Value}; + +use super::sqlsplit::split_sql_statements; +use super::{err, ok, parse_body}; +use crate::router::ApiResponse; + +const MAX_ROWS: usize = 5000; +const DEFAULT_ROWS: usize = 500; + +type HandlerResult = Result; + +struct SqlConfig { + ty: String, + host: String, + port: u16, + database: String, + username: String, + password: String, + ssl: bool, +} + +fn config_from(body: &Value, require_db: bool) -> Result { + let ty = body["type"].as_str().unwrap_or("").to_string(); + let host = body["host"].as_str().unwrap_or("").to_string(); + let database = body["database"].as_str().unwrap_or("").to_string(); + if ty.is_empty() || host.is_empty() || (require_db && database.is_empty()) { + return Err(err(400, "type, host, and database are required")); + } + if !matches!(ty.as_str(), "postgresql" | "mysql" | "mariadb") { + return Err(err(400, &format!("Unsupported database type: {ty}"))); + } + let default_port = if ty == "postgresql" { 5432 } else { 3306 }; + let port = body["port"].as_u64().filter(|p| *p > 0).unwrap_or(default_port) as u16; + Ok(SqlConfig { + ty, + host, + port, + database, + username: body["username"].as_str().unwrap_or("").to_string(), + password: body["password"].as_str().unwrap_or("").to_string(), + ssl: body["ssl"].as_bool().unwrap_or(false), + }) +} + +// ── Postgres ──────────────────────────────────────────────────────────────── + +async fn pg_connect(cfg: &SqlConfig) -> Result { + let mut pg = tokio_postgres::Config::new(); + pg.host(&cfg.host) + .port(cfg.port) + .dbname(&cfg.database) + .user(&cfg.username) + .password(&cfg.password) + .connect_timeout(std::time::Duration::from_secs(10)) + .options("-c statement_timeout=30000"); + if cfg.ssl { + let tls = native_tls::TlsConnector::builder() + .danger_accept_invalid_certs(true) + .danger_accept_invalid_hostnames(true) + .build() + .map_err(|e| e.to_string())?; + let (client, conn) = pg + .connect(postgres_native_tls::MakeTlsConnector::new(tls)) + .await + .map_err(|e| e.to_string())?; + tauri::async_runtime::spawn(async move { + let _ = conn.await; + }); + Ok(client) + } else { + let (client, conn) = pg.connect(tokio_postgres::NoTls).await.map_err(|e| e.to_string())?; + tauri::async_runtime::spawn(async move { + let _ = conn.await; + }); + Ok(client) + } +} + +/// Run one statement over the text protocol; values come back as strings +/// (numbers/booleans lose their JSON type — display-equivalent for the UI). +async fn pg_statement( + client: &tokio_postgres::Client, + stmt: &str, + row_limit: usize, +) -> Result<(Vec, Vec, u64), String> { + use tokio_postgres::SimpleQueryMessage; + let messages = client.simple_query(stmt).await.map_err(|e| e.to_string())?; + let mut rows: Vec = Vec::new(); + let mut columns: Vec = Vec::new(); + let mut row_count: u64 = 0; + for msg in messages { + match msg { + SimpleQueryMessage::Row(row) => { + if columns.is_empty() { + columns = row.columns().iter().map(|c| c.name().to_string()).collect(); + } + row_count += 1; + if rows.len() < row_limit { + let mut obj = Map::new(); + for (i, col) in columns.iter().enumerate() { + obj.insert( + col.clone(), + row.get(i).map(|v| json!(v)).unwrap_or(Value::Null), + ); + } + rows.push(Value::Object(obj)); + } + } + SimpleQueryMessage::CommandComplete(n) => { + if row_count == 0 { + row_count = n; + } + } + _ => {} + } + } + Ok((rows, columns, row_count)) +} + +// ── MySQL ─────────────────────────────────────────────────────────────────── + +async fn mysql_connect(cfg: &SqlConfig) -> Result { + let mut opts = mysql_async::OptsBuilder::default() + .ip_or_hostname(cfg.host.clone()) + .tcp_port(cfg.port) + .user(Some(cfg.username.clone())) + .pass(Some(cfg.password.clone())); + if !cfg.database.is_empty() { + opts = opts.db_name(Some(cfg.database.clone())); + } + if cfg.ssl { + opts = opts.ssl_opts(Some( + mysql_async::SslOpts::default() + .with_danger_accept_invalid_certs(true) + .with_danger_skip_domain_validation(true), + )); + } + mysql_async::Conn::new(opts).await.map_err(|e| e.to_string()) +} + +fn mysql_value_to_json(v: &mysql_async::Value) -> Value { + use mysql_async::Value as V; + match v { + V::NULL => Value::Null, + V::Bytes(b) => json!(String::from_utf8_lossy(b)), + V::Int(i) => json!(i), + V::UInt(u) => json!(u), + V::Float(f) => json!(f), + V::Double(d) => json!(d), + V::Date(y, m, d, h, min, s, _us) => { + json!(format!("{y:04}-{m:02}-{d:02} {h:02}:{min:02}:{s:02}")) + } + V::Time(neg, days, h, m, s, _us) => { + let sign = if *neg { "-" } else { "" }; + json!(format!("{sign}{}:{m:02}:{s:02}", (*days as u64) * 24 + *h as u64)) + } + } +} + +async fn mysql_statement( + conn: &mut mysql_async::Conn, + stmt: &str, + row_limit: usize, +) -> Result<(Vec, Vec, u64), String> { + use mysql_async::prelude::Queryable; + let result: Vec = conn.query(stmt).await.map_err(|e| e.to_string())?; + let columns: Vec = result + .first() + .map(|r| r.columns_ref().iter().map(|c| c.name_str().to_string()).collect()) + .unwrap_or_default(); + let row_count = result.len() as u64; + let rows: Vec = result + .iter() + .take(row_limit) + .map(|row| { + let mut obj = Map::new(); + for (i, col) in columns.iter().enumerate() { + let v = row.as_ref(i).map(mysql_value_to_json).unwrap_or(Value::Null); + obj.insert(col.clone(), v); + } + Value::Object(obj) + }) + .collect(); + Ok((rows, columns, row_count)) +} + +// ── Route handlers ────────────────────────────────────────────────────────── + +pub async fn handle(method: &str, rest: &str, body: Option<&str>) -> HandlerResult { + if method != "POST" { + return Ok(err(405, "Method not allowed")); + } + let req = match parse_body(body) { + Ok(v) => v, + Err(r) => return Ok(r), + }; + match rest { + "/connect" => connect(&req).await, + "/query" => query(&req).await, + "/tables" => tables(&req).await, + _ => Ok(err(404, "Not found")), + } +} + +async fn connect(req: &Value) -> HandlerResult { + let cfg = match config_from(req, true) { + Ok(c) => c, + Err(r) => return Ok(r), + }; + let version = if cfg.ty == "postgresql" { + let client = pg_connect(&cfg).await?; + let (rows, ..) = pg_statement(&client, "SELECT version()", 1).await?; + rows.first() + .and_then(|r| r.get("version")) + .cloned() + .unwrap_or(Value::Null) + } else { + let mut conn = mysql_connect(&cfg).await?; + let (rows, ..) = mysql_statement(&mut conn, "SELECT VERSION() as version", 1).await?; + let _ = conn.disconnect().await; + rows.first().and_then(|r| r.get("version")).cloned().unwrap_or(Value::Null) + }; + Ok(ok(&json!({ "success": true, "version": version }))) +} + +async fn query(req: &Value) -> HandlerResult { + let cfg = match config_from(req, true) { + Ok(c) => c, + Err(r) => return Ok(r), + }; + let sql = req["query"].as_str().unwrap_or(""); + if sql.is_empty() { + return Ok(err(400, "type, host, and database are required")); + } + let row_limit = req["limit"] + .as_u64() + .map(|l| l as usize) + .filter(|l| *l > 0) + .unwrap_or(DEFAULT_ROWS) + .min(MAX_ROWS); + let statements = split_sql_statements(sql); + if statements.is_empty() { + return Ok(err(400, "No SQL statement provided")); + } + + let started = std::time::Instant::now(); + let mut results: Vec = Vec::new(); + + if cfg.ty == "postgresql" { + let client = pg_connect(&cfg).await?; + for stmt in &statements { + let (rows, columns, row_count) = pg_statement(&client, stmt, row_limit).await?; + results.push(json!({ "rows": rows, "columns": columns, "rowCount": row_count })); + } + } else { + let mut conn = mysql_connect(&cfg).await?; + for stmt in &statements { + match mysql_statement(&mut conn, stmt, row_limit).await { + Ok((rows, columns, row_count)) => { + results.push(json!({ "rows": rows, "columns": columns, "rowCount": row_count })); + } + Err(e) => { + let _ = conn.disconnect().await; + return Err(e); + } + } + } + let _ = conn.disconnect().await; + } + + let execution_time = started.elapsed().as_millis() as u64; + let last = results.last().cloned().unwrap_or(json!({"rows": [], "columns": [], "rowCount": 0})); + Ok(ok(&json!({ + "rows": last["rows"], + "columns": last["columns"], + "rowCount": last["rowCount"], + "executionTime": execution_time, + "results": results, + }))) +} + +fn parse_numeric_fields(rows: &mut [Value], fields: &[&str]) { + for row in rows { + if let Some(obj) = row.as_object_mut() { + for f in fields { + if let Some(Value::String(s)) = obj.get(*f) { + if let Ok(n) = s.parse::() { + obj.insert((*f).to_string(), json!(n)); + } + } + } + } + } +} + +async fn tables(req: &Value) -> HandlerResult { + let cfg = match config_from(req, true) { + Ok(c) => c, + Err(r) => return Ok(r), + }; + if cfg.ty == "postgresql" { + let client = pg_connect(&cfg).await?; + let (mut tables, ..) = pg_statement( + &client, + "SELECT t.table_schema AS schema, t.table_name AS name, t.table_type AS type, + (SELECT count(*) FROM information_schema.columns c + WHERE c.table_schema = t.table_schema AND c.table_name = t.table_name) AS column_count + FROM information_schema.tables t + WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY t.table_schema, t.table_name", + usize::MAX, + ) + .await?; + let (mut columns, ..) = pg_statement( + &client, + "SELECT table_schema AS schema, table_name, column_name, data_type, + is_nullable, column_default, ordinal_position + FROM information_schema.columns + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + ORDER BY table_schema, table_name, ordinal_position", + usize::MAX, + ) + .await?; + parse_numeric_fields(&mut tables, &["column_count"]); + parse_numeric_fields(&mut columns, &["ordinal_position"]); + Ok(ok(&json!({ "tables": tables, "columns": columns }))) + } else { + let mut conn = mysql_connect(&cfg).await?; + let (tables, ..) = mysql_statement( + &mut conn, + "SELECT table_schema AS `schema`, table_name AS `name`, table_type AS `type`, table_rows AS row_estimate + FROM information_schema.tables WHERE table_schema = DATABASE() + ORDER BY table_name", + usize::MAX, + ) + .await?; + let (columns, ..) = mysql_statement( + &mut conn, + "SELECT table_schema AS `schema`, table_name, column_name, data_type, + is_nullable, column_default, ordinal_position + FROM information_schema.columns WHERE table_schema = DATABASE() + ORDER BY table_name, ordinal_position", + usize::MAX, + ) + .await?; + let _ = conn.disconnect().await; + Ok(ok(&json!({ "tables": tables, "columns": columns }))) + } +} diff --git a/apps/desktop/src-tauri/src/dbtools/sqlsplit.rs b/apps/desktop/src-tauri/src/dbtools/sqlsplit.rs new file mode 100644 index 00000000..d7c3e9a9 --- /dev/null +++ b/apps/desktop/src-tauri/src/dbtools/sqlsplit.rs @@ -0,0 +1,158 @@ +//! Port of apps/web/src/lib/sql-split.ts — split SQL on top-level `;` only, +//! honoring quotes, comments, and Postgres dollar-quoted bodies. + +pub fn split_sql_statements(sql: &str) -> Vec { + let chars: Vec = sql.chars().collect(); + let mut statements = Vec::new(); + let mut current = String::new(); + let mut i = 0; + + #[derive(PartialEq)] + enum Mode { + Normal, + Single, // '...' + Double, // "..." + Backtick, // `...` + LineComment, + BlockComment, + Dollar(String), // $tag$ ... $tag$ + } + let mut mode = Mode::Normal; + + while i < chars.len() { + let c = chars[i]; + let next = chars.get(i + 1).copied(); + match &mode { + Mode::Normal => match c { + '\'' => { + mode = Mode::Single; + current.push(c); + } + '"' => { + mode = Mode::Double; + current.push(c); + } + '`' => { + mode = Mode::Backtick; + current.push(c); + } + '-' if next == Some('-') => { + mode = Mode::LineComment; + current.push(c); + } + '/' if next == Some('*') => { + mode = Mode::BlockComment; + current.push(c); + } + '$' => { + // Possible dollar-quote open: $tag$ where tag is [A-Za-z0-9_]* + let mut j = i + 1; + while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') { + j += 1; + } + if j < chars.len() && chars[j] == '$' { + let tag: String = chars[i..=j].iter().collect(); + for k in i..=j { + current.push(chars[k]); + } + i = j; + mode = Mode::Dollar(tag); + } else { + current.push(c); + } + } + ';' => { + let stmt = current.trim().to_string(); + if !stmt.is_empty() { + statements.push(stmt); + } + current = String::new(); + } + _ => current.push(c), + }, + Mode::Single => { + current.push(c); + if c == '\'' { + // '' escape stays inside the string + if next == Some('\'') { + current.push('\''); + i += 1; + } else { + mode = Mode::Normal; + } + } + } + Mode::Double => { + current.push(c); + if c == '"' { + mode = Mode::Normal; + } + } + Mode::Backtick => { + current.push(c); + if c == '`' { + mode = Mode::Normal; + } + } + Mode::LineComment => { + current.push(c); + if c == '\n' { + mode = Mode::Normal; + } + } + Mode::BlockComment => { + current.push(c); + if c == '*' && next == Some('/') { + current.push('/'); + i += 1; + mode = Mode::Normal; + } + } + Mode::Dollar(tag) => { + if c == '$' { + let remaining: String = chars[i..].iter().take(tag.len()).collect(); + if remaining == *tag { + for k in 0..tag.len() { + current.push(chars[i + k]); + } + i += tag.len() - 1; + mode = Mode::Normal; + } else { + current.push(c); + } + } else { + current.push(c); + } + } + } + i += 1; + } + let stmt = current.trim().to_string(); + if !stmt.is_empty() { + statements.push(stmt); + } + statements +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_on_top_level_semicolons_only() { + let sql = "SELECT 'a;b'; -- c;\nSELECT $$x;y$$; SELECT 1"; + let parts = split_sql_statements(sql); + assert_eq!(parts.len(), 3); + assert_eq!(parts[0], "SELECT 'a;b'"); + assert!(parts[1].contains("$$x;y$$")); + assert_eq!(parts[2], "SELECT 1"); + } + + #[test] + fn dollar_tag_quotes() { + let sql = "CREATE FUNCTION f() RETURNS int AS $fn$ BEGIN RETURN 1; END $fn$ LANGUAGE plpgsql; SELECT 2"; + let parts = split_sql_statements(sql); + assert_eq!(parts.len(), 2); + assert!(parts[0].contains("RETURN 1;")); + } +} diff --git a/apps/desktop/src-tauri/src/error.rs b/apps/desktop/src-tauri/src/error.rs new file mode 100644 index 00000000..e244ccfa --- /dev/null +++ b/apps/desktop/src-tauri/src/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + #[error("database error: {0}")] + Db(#[from] rusqlite::Error), + #[error("keychain error: {0}")] + Keychain(#[from] keyring::Error), + #[error("serialization error: {0}")] + Json(#[from] serde_json::Error), + #[error("io error: {0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + Other(String), +} + +pub type Result = std::result::Result; diff --git a/apps/desktop/src-tauri/src/http/auth_server.rs b/apps/desktop/src-tauri/src/http/auth_server.rs new file mode 100644 index 00000000..5f00bbcc --- /dev/null +++ b/apps/desktop/src-tauri/src/http/auth_server.rs @@ -0,0 +1,144 @@ +//! Loopback OAuth callback server. +//! +//! Native-app auth pattern: bind an ephemeral localhost port, open the system +//! browser to the web login (passing the port), and let the browser redirect +//! the minted token back to `http://127.0.0.1:/callback?token=...`. Works +//! identically in `tauri dev` and the packaged app — no URL-scheme / Info.plist +//! registration needed (unlike the mydevtools:// deep link, which macOS only +//! routes to a bundled .app). + +use std::time::Duration; + +use tauri::ipc::Channel; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpListener; + +const AUTH_TIMEOUT: Duration = Duration::from_secs(300); + +fn parse_token(request: &str) -> Option { + // First line: "GET /callback?token=XXX HTTP/1.1" + let line = request.lines().next()?; + let path = line.split_whitespace().nth(1)?; + let query = path.split_once('?')?.1; + for pair in query.split('&') { + if let Some(v) = pair.strip_prefix("token=") { + return Some(urldecode(v)); + } + } + None +} + +fn urldecode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'%' if i + 2 < bytes.len() => { + let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok(); + if let Some(b) = hex.and_then(|h| u8::from_str_radix(h, 16).ok()) { + out.push(b); + i += 3; + continue; + } + out.push(bytes[i]); + i += 1; + } + b'+' => { + out.push(b' '); + i += 1; + } + b => { + out.push(b); + i += 1; + } + } + } + String::from_utf8_lossy(&out).into_owned() +} + +const DONE_HTML: &str = "MyDevTools

Signed in ✓

You can close this tab and return to the MyDevTools app.

"; + +/// Bind a loopback listener, report its port on `port_channel`, then await the +/// browser's `/callback?token=...` request and return the token. +pub async fn await_browser_auth(port_channel: Channel) -> Result { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .map_err(|e| format!("failed to bind auth callback port: {e}"))?; + let port = listener.local_addr().map_err(|e| e.to_string())?.port(); + port_channel + .send(serde_json::json!({ "port": port })) + .map_err(|e| e.to_string())?; + + match tokio::time::timeout(AUTH_TIMEOUT, accept_callback(&listener)).await { + Ok(result) => result, + Err(_) => Err("Sign-in timed out — no callback received".into()), + } +} + +/// Await the browser's `/callback?token=...` on an already-bound listener. +/// Non-callback requests (favicon, preflight) get a 204 and are ignored. +async fn accept_callback(listener: &TcpListener) -> Result { + loop { + let (mut socket, _) = listener.accept().await.map_err(|e| e.to_string())?; + let mut buf = vec![0u8; 8192]; + let n = socket.read(&mut buf).await.map_err(|e| e.to_string())?; + let request = String::from_utf8_lossy(&buf[..n]); + if let Some(token) = parse_token(&request) { + let resp = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + DONE_HTML.len(), + DONE_HTML + ); + let _ = socket.write_all(resp.as_bytes()).await; + let _ = socket.flush().await; + return Ok(token); + } + let _ = socket + .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") + .await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_token_from_request() { + let req = "GET /callback?token=abc.def-ghi%3D&x=1 HTTP/1.1\r\nHost: localhost\r\n\r\n"; + assert_eq!(parse_token(req).as_deref(), Some("abc.def-ghi=")); + } + + #[test] + fn no_token_returns_none() { + assert_eq!(parse_token("GET /favicon.ico HTTP/1.1\r\n\r\n"), None); + } + + #[tokio::test] + async fn loopback_callback_roundtrip() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = tokio::spawn(async move { accept_callback(&listener).await }); + + // Simulate the browser: a favicon probe (ignored) then the real callback. + let mut ignored = tokio::net::TcpStream::connect(("127.0.0.1", port)).await.unwrap(); + ignored + .write_all(b"GET /favicon.ico HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let mut sink = Vec::new(); + let _ = ignored.read_to_end(&mut sink).await; + + let mut cb = tokio::net::TcpStream::connect(("127.0.0.1", port)).await.unwrap(); + cb.write_all(b"GET /callback?token=tok-123%3D HTTP/1.1\r\nHost: localhost\r\n\r\n") + .await + .unwrap(); + let mut resp = Vec::new(); + cb.read_to_end(&mut resp).await.unwrap(); + assert!(String::from_utf8_lossy(&resp).contains("Signed in")); + + let token = server.await.unwrap().unwrap(); + assert_eq!(token, "tok-123="); + } +} diff --git a/apps/desktop/src-tauri/src/http/mod.rs b/apps/desktop/src-tauri/src/http/mod.rs new file mode 100644 index 00000000..8f96e6ed --- /dev/null +++ b/apps/desktop/src-tauri/src/http/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_server; +pub mod proxy; +pub mod remote; diff --git a/apps/desktop/src-tauri/src/http/proxy.rs b/apps/desktop/src-tauri/src/http/proxy.rs new file mode 100644 index 00000000..f1efba46 --- /dev/null +++ b/apps/desktop/src-tauri/src/http/proxy.rs @@ -0,0 +1,428 @@ +//! api-client HTTP engine: mirrors the web `/api/proxy` envelope +//! (`{status, statusText, headers, setCookies, redirectChain, body, isBase64, +//! time, size, error?}`) and the `/api/proxy-stream` SSE relay. +//! +//! Rust-side HTTP has no CORS. Unlike the web proxy, localhost/private +//! targets are allowed (it's the user's own machine); only cloud metadata +//! endpoints stay blocked. + +use base64::Engine; +use dashmap::DashMap; +use futures_util::StreamExt; +use serde_json::{json, Map, Value}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::OnceLock; +use tauri::ipc::Channel; + +const MAX_REDIRECTS: usize = 5; +const DEFAULT_TIMEOUT_MS: u64 = 30_000; +const MAX_TIMEOUT_MS: u64 = 600_000; + +const METADATA_HOSTS: &[&str] = &[ + "169.254.169.254", + "metadata.google.internal", + "metadata.google", + "100.100.100.200", +]; + +static STREAMS: OnceLock>> = OnceLock::new(); +static STREAM_ID: AtomicU64 = AtomicU64::new(1); + +fn streams() -> &'static DashMap> { + STREAMS.get_or_init(DashMap::new) +} + +fn envelope_error(status: u16, status_text: &str, msg: &str) -> Value { + json!({ + "status": status, "statusText": status_text, "headers": {}, + "body": msg, "time": 0, "size": 0, "error": msg, + }) +} + +fn assert_hop_allowed(url: &reqwest::Url) -> Result<(), String> { + match url.scheme() { + "http" | "https" => {} + s => return Err(format!("Blocked scheme: {s}:")), + } + let host = url.host_str().unwrap_or("").to_lowercase(); + if METADATA_HOSTS.contains(&host.as_str()) { + return Err(format!("Blocked target: {host}")); + } + Ok(()) +} + +fn is_textual_content_type(ct: &str) -> bool { + let l = ct.to_lowercase(); + l.starts_with("text/") + || ["json", "xml", "javascript", "ecmascript", "html", "yaml", "csv", "urlencoded", "graphql", "x-ndjson"] + .iter() + .any(|t| l.contains(t)) +} + +fn plain_client() -> Result { + reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .user_agent("mydevtools-desktop") + .build() + .map_err(|e| e.to_string()) +} + +fn build_body(body: &Value) -> Result, String> { + if body.is_null() { + return Ok(None); + } + if let Some(s) = body.as_str() { + return Ok(Some(BodyKind::Text(s.to_string()))); + } + if body["mode"].as_str() == Some("form-data") { + return Ok(Some(BodyKind::Multipart(body["entries"].as_array().cloned().unwrap_or_default()))); + } + Ok(Some(BodyKind::Text(body.to_string()))) +} + +enum BodyKind { + Text(String), + Multipart(Vec), +} + +impl BodyKind { + fn apply(&self, mut req: reqwest::RequestBuilder) -> Result { + match self { + BodyKind::Text(s) => Ok(req.body(s.clone())), + BodyKind::Multipart(entries) => { + let mut form = reqwest::multipart::Form::new(); + for entry in entries { + let Some(key) = entry["key"].as_str().filter(|k| !k.is_empty()) else { + continue; + }; + if entry["type"].as_str() == Some("file") { + let Some(b64) = entry["fileContentBase64"].as_str() else { continue }; + let bytes = base64::engine::general_purpose::STANDARD + .decode(b64) + .map_err(|e| e.to_string())?; + let part = reqwest::multipart::Part::bytes(bytes) + .file_name(entry["fileName"].as_str().unwrap_or("upload.bin").to_string()) + .mime_str(entry["fileType"].as_str().unwrap_or("application/octet-stream")) + .map_err(|e| e.to_string())?; + form = form.part(key.to_string(), part); + } else { + form = form.text(key.to_string(), entry["value"].as_str().unwrap_or("").to_string()); + } + } + req = req.multipart(form); + Ok(req) + } + } + } +} + +struct Walked { + response: reqwest::Response, + chain: Vec, +} + +async fn fetch_following_redirects( + client: &reqwest::Client, + initial_url: &str, + method: &str, + headers: &Map, + body: &Option, + timeout: std::time::Duration, + is_multipart: bool, +) -> Result { + let mut current_url = initial_url.to_string(); + let mut current_method = method.to_uppercase(); + let mut drop_body = false; + let mut chain: Vec = Vec::new(); + + for _hop in 0..=MAX_REDIRECTS { + let url = reqwest::Url::parse(¤t_url).map_err(|_| "Invalid URL format".to_string())?; + assert_hop_allowed(&url)?; + + let m = reqwest::Method::from_bytes(current_method.as_bytes()).map_err(|e| e.to_string())?; + let mut req = client.request(m, url).timeout(timeout); + for (k, v) in headers { + // Let reqwest set the multipart boundary itself. + if is_multipart && k.to_lowercase() == "content-type" { + continue; + } + if let Some(s) = v.as_str() { + req = req.header(k, s); + } + } + let use_body = !drop_body && current_method != "GET" && current_method != "HEAD"; + if use_body { + if let Some(b) = body { + req = b.apply(req)?; + } + } + + let response = req.send().await.map_err(|e| e.to_string())?; + let status = response.status().as_u16(); + let is_redirect = (300..400).contains(&status) && status != 304; + if !is_redirect { + return Ok(Walked { response, chain }); + } + let Some(location) = response + .headers() + .get("location") + .and_then(|l| l.to_str().ok()) + .map(str::to_string) + else { + return Ok(Walked { response, chain }); + }; + chain.push(json!({ "url": current_url, "status": status })); + let next = reqwest::Url::parse(¤t_url) + .and_then(|base| base.join(&location)) + .map_err(|e| e.to_string())?; + if status == 303 || ((status == 301 || status == 302) && current_method != "GET" && current_method != "HEAD") { + current_method = "GET".into(); + drop_body = true; + } + current_url = next.to_string(); + } + Err(format!("Too many redirects (>{MAX_REDIRECTS})")) +} + +/// Non-streaming request. Returns the `/api/proxy` envelope as JSON. +pub async fn http_request(input: Value) -> Value { + let url = input["url"].as_str().unwrap_or(""); + if url.is_empty() { + return envelope_error(400, "Bad Request", "URL is required"); + } + let parsed = match reqwest::Url::parse(url) { + Ok(u) => u, + Err(_) => return envelope_error(400, "Bad Request", "Invalid URL format"), + }; + if let Err(e) = assert_hop_allowed(&parsed) { + return envelope_error(403, "Forbidden", &e); + } + let method = input["method"].as_str().unwrap_or("GET"); + let headers = input["headers"].as_object().cloned().unwrap_or_default(); + let timeout_ms = input["timeoutMs"].as_u64().filter(|t| *t > 0).unwrap_or(DEFAULT_TIMEOUT_MS).min(MAX_TIMEOUT_MS); + let body = match build_body(&input["body"]) { + Ok(b) => b, + Err(e) => return envelope_error(0, "Error", &e), + }; + let is_multipart = matches!(body, Some(BodyKind::Multipart(_))); + + let client = match plain_client() { + Ok(c) => c, + Err(e) => return envelope_error(0, "Error", &e), + }; + let started = std::time::Instant::now(); + let walked = match fetch_following_redirects( + &client, + url, + method, + &headers, + &body, + std::time::Duration::from_millis(timeout_ms), + is_multipart, + ) + .await + { + Ok(w) => w, + Err(e) => { + let status = if e.starts_with("Blocked") { 403 } else { 0 }; + return envelope_error(status, if status == 403 { "Forbidden" } else { "Error" }, &e); + } + }; + let time = started.elapsed().as_millis() as u64; + + let response = walked.response; + let status = response.status().as_u16(); + let status_text = response.status().canonical_reason().unwrap_or("").to_string(); + let mut resp_headers = Map::new(); + let mut set_cookies: Vec = Vec::new(); + for (k, v) in response.headers() { + let val = String::from_utf8_lossy(v.as_bytes()).to_string(); + if k.as_str().eq_ignore_ascii_case("set-cookie") { + set_cookies.push(val.clone()); + } + resp_headers.insert(k.as_str().to_string(), json!(val)); + } + let declared_len = response.content_length().unwrap_or(0); + let content_type = resp_headers + .get("content-type") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + + let bytes = match response.bytes().await { + Ok(b) => b, + Err(e) => return envelope_error(0, "Error", &e.to_string()), + }; + let (body_str, is_base64) = if is_textual_content_type(&content_type) { + (String::from_utf8_lossy(&bytes).to_string(), false) + } else { + (base64::engine::general_purpose::STANDARD.encode(&bytes), true) + }; + let size = if declared_len > 0 { declared_len } else { bytes.len() as u64 }; + + json!({ + "status": status, + "statusText": status_text, + "headers": resp_headers, + "setCookies": set_cookies, + "redirectChain": walked.chain, + "body": body_str, + "isBase64": is_base64, + "time": time, + "size": size, + }) +} + +/// Streaming request (SSE relay). Emits events over the channel: +/// `{kind:"meta", status, statusText, headers}` → `{kind:"chunk", data}`* → +/// `{kind:"end"}` | `{kind:"error", message}`. Returns a stream id usable +/// with `http_request_stream_cancel`. +pub async fn http_request_stream(input: Value, channel: Channel) -> Result { + let url = input["url"].as_str().unwrap_or("").to_string(); + if url.is_empty() { + return Err("URL is required".into()); + } + let parsed = reqwest::Url::parse(&url).map_err(|_| "Invalid URL format".to_string())?; + assert_hop_allowed(&parsed)?; + let method = input["method"].as_str().unwrap_or("GET").to_string(); + let headers = input["headers"].as_object().cloned().unwrap_or_default(); + let body = input["body"].as_str().map(str::to_string); + + let id = STREAM_ID.fetch_add(1, Ordering::Relaxed); + let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); + streams().insert(id, cancel_tx); + + tauri::async_runtime::spawn(async move { + let emit = |v: Value| { + let _ = channel.send(v); + }; + let client = match plain_client() { + Ok(c) => c, + Err(e) => { + emit(json!({ "kind": "error", "message": e })); + return; + } + }; + let m = match reqwest::Method::from_bytes(method.as_bytes()) { + Ok(m) => m, + Err(e) => { + emit(json!({ "kind": "error", "message": e.to_string() })); + return; + } + }; + let mut req = client.request(m, &url); + for (k, v) in &headers { + if let Some(s) = v.as_str() { + req = req.header(k, s); + } + } + if let Some(b) = body { + req = req.body(b); + } + let response = match req.send().await { + Ok(r) => r, + Err(e) => { + emit(json!({ "kind": "error", "message": e.to_string() })); + streams().remove(&id); + return; + } + }; + let mut resp_headers = Map::new(); + for (k, v) in response.headers() { + resp_headers.insert(k.as_str().to_string(), json!(String::from_utf8_lossy(v.as_bytes()))); + } + emit(json!({ + "kind": "meta", + "status": response.status().as_u16(), + "statusText": response.status().canonical_reason().unwrap_or(""), + "headers": resp_headers, + })); + let mut stream = response.bytes_stream(); + loop { + tokio::select! { + _ = cancel_rx.changed() => break, + chunk = stream.next() => match chunk { + Some(Ok(bytes)) => emit(json!({ "kind": "chunk", "data": String::from_utf8_lossy(&bytes) })), + Some(Err(e)) => { + emit(json!({ "kind": "error", "message": e.to_string() })); + break; + } + None => break, + }, + } + } + emit(json!({ "kind": "end" })); + streams().remove(&id); + }); + + Ok(id) +} + +pub fn cancel_stream(id: u64) { + if let Some((_, tx)) = streams().remove(&id) { + let _ = tx.send(true); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn textual_content_type_detection() { + assert!(is_textual_content_type("application/json; charset=utf-8")); + assert!(is_textual_content_type("text/html")); + assert!(!is_textual_content_type("application/octet-stream")); + assert!(!is_textual_content_type("")); + } + + #[tokio::test] + #[ignore] // network + async fn httpbin_get_and_redirect() { + let r = http_request(json!({ + "url": "https://httpbin.org/get?x=1", + "method": "GET", + "headers": { "X-Test": "yes" }, + })) + .await; + assert_eq!(r["status"], 200, "{r}"); + assert_eq!(r["isBase64"], false); + let body: Value = serde_json::from_str(r["body"].as_str().unwrap()).unwrap(); + assert_eq!(body["args"]["x"], "1"); + assert_eq!(body["headers"]["X-Test"], "yes"); + + // POST json + let r = http_request(json!({ + "url": "https://httpbin.org/post", + "method": "POST", + "headers": { "Content-Type": "application/json" }, + "body": "{\"a\":1}", + })) + .await; + assert_eq!(r["status"], 200, "{r}"); + + // redirect chain recorded; 302 flips POST→GET + let r = http_request(json!({ + "url": "https://httpbin.org/redirect-to?url=%2Fget&status_code=302", + "method": "POST", + "headers": {}, + "body": "x", + })) + .await; + assert_eq!(r["status"], 200, "{r}"); + assert_eq!(r["redirectChain"].as_array().unwrap().len(), 1); + + // binary → base64 + let r = http_request(json!({ + "url": "https://httpbin.org/bytes/16", + "method": "GET", + "headers": {}, + })) + .await; + assert_eq!(r["isBase64"], true); + + // metadata host blocked + let r = http_request(json!({ "url": "http://169.254.169.254/latest", "method": "GET" })).await; + assert_eq!(r["status"], 403); + } +} diff --git a/apps/desktop/src-tauri/src/http/remote.rs b/apps/desktop/src-tauri/src/http/remote.rs new file mode 100644 index 00000000..1c6adf31 --- /dev/null +++ b/apps/desktop/src-tauri/src/http/remote.rs @@ -0,0 +1,96 @@ +//! Remote bridge: Rust-side HTTP to the FastAPI backend with a persistent +//! cookie jar (HttpOnly JWT cookies live here, never in the webview). +//! +//! Rust HTTP has no CORS, so the existing backend cookie flow works with zero +//! backend changes. The jar is serialized into the SQLCipher kv table so the +//! session survives restarts. + +use std::io::Cursor; +use std::sync::Arc; + +use reqwest_cookie_store::CookieStoreMutex; +use rusqlite::OptionalExtension; +use serde::Serialize; + +use crate::error::{AppError, Result}; +use crate::state::AppState; + +const KV_JAR: &str = "cookie_jar"; + +#[derive(Serialize)] +pub struct RemoteResponse { + pub status: u16, + pub body: String, +} + +pub struct RemoteHttp { + pub client: reqwest::Client, + pub jar: Arc, +} + +impl RemoteHttp { + pub fn init(db: &rusqlite::Connection) -> Result { + let raw: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [KV_JAR], |r| r.get(0)) + .optional()?; + let store = match raw { + Some(json) => cookie_store::serde::json::load(Cursor::new(json.into_bytes())) + .unwrap_or_default(), + None => cookie_store::CookieStore::default(), + }; + let jar = Arc::new(CookieStoreMutex::new(store)); + let client = reqwest::Client::builder() + .cookie_provider(jar.clone()) + .user_agent("mydevtools-desktop") + .build() + .map_err(|e| AppError::Other(e.to_string()))?; + Ok(Self { client, jar }) + } + + pub fn persist_jar(&self, db: &rusqlite::Connection) -> Result<()> { + let mut buf = Vec::new(); + { + let store = self.jar.lock().unwrap(); + cookie_store::serde::json::save_incl_expired_and_nonpersistent(&store, &mut buf) + .map_err(|e| AppError::Other(e.to_string()))?; + } + let json = String::from_utf8_lossy(&buf).to_string(); + db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", + [KV_JAR, &json], + )?; + Ok(()) + } + + pub fn clear_jar(&self, db: &rusqlite::Connection) -> Result<()> { + self.jar.lock().unwrap().clear(); + db.execute("DELETE FROM kv WHERE k = ?1", [KV_JAR])?; + Ok(()) + } +} + +/// Perform an HTTP request against the backend with the persistent jar. +/// `url` is the full URL (base comes from the frontend's baked env config). +pub async fn request( + state: &AppState, + method: &str, + url: &str, + body: Option, +) -> Result { + let method = reqwest::Method::from_bytes(method.as_bytes()) + .map_err(|_| AppError::Other(format!("bad method {method}")))?; + let mut req = state.http.client.request(method, url); + if let Some(b) = body { + req = req.header("Content-Type", "application/json").body(b); + } + let res = req.send().await.map_err(|e| AppError::Other(format!("network error: {e}")))?; + let status = res.status().as_u16(); + let body = res.text().await.unwrap_or_default(); + + // Persist cookies after every call — refresh rotations must survive restart. + { + let db = state.db.lock().unwrap(); + state.http.persist_jar(&db)?; + } + Ok(RemoteResponse { status, body }) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 00000000..05ffff07 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,93 @@ +mod db; +mod dbtools; +mod error; +mod http; +mod router; +mod state; + +use http::remote::RemoteResponse; +use router::ApiResponse; +use state::AppState; +use tauri::Manager; + +#[tauri::command] +async fn local_api( + state: tauri::State<'_, AppState>, + method: String, + path: String, + body: Option, +) -> Result { + if dbtools::is_dbtool_path(&path) { + return Ok(dbtools::route(&method, &path, body.as_deref()).await); + } + router::route(&state, &method, &path, body.as_deref()).map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn remote_api( + state: tauri::State<'_, AppState>, + method: String, + url: String, + body: Option, +) -> Result { + http::remote::request(&state, &method, &url, body) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn clear_remote_session(state: tauri::State<'_, AppState>) -> Result<(), String> { + let db = state.db.lock().unwrap(); + state.http.clear_jar(&db).map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn http_request(input: serde_json::Value) -> Result { + Ok(http::proxy::http_request(input).await) +} + +#[tauri::command] +async fn http_request_stream( + input: serde_json::Value, + channel: tauri::ipc::Channel, +) -> Result { + http::proxy::http_request_stream(input, channel).await +} + +#[tauri::command] +fn http_request_stream_cancel(id: u64) { + http::proxy::cancel_stream(id); +} + +#[tauri::command] +async fn await_browser_auth( + port_channel: tauri::ipc::Channel, +) -> Result { + http::auth_server::await_browser_auth(port_channel).await +} + +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_window_state::Builder::default().build()) + .setup(|app| { + let dir = app.path().app_data_dir()?; + std::fs::create_dir_all(&dir)?; + let state = AppState::init(dir.join("mydevtools.db")) + .map_err(|e| format!("failed to open local database: {e}"))?; + app.manage(state); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + local_api, + remote_api, + clear_remote_session, + http_request, + http_request_stream, + http_request_stream_cancel, + await_browser_auth + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..0be81d2a --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + mydevtools_desktop_lib::run() +} diff --git a/apps/desktop/src-tauri/src/router/api_client.rs b/apps/desktop/src-tauri/src/router/api_client.rs new file mode 100644 index 00000000..0a2fb847 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/api_client.rs @@ -0,0 +1,278 @@ +//! Local mirror of `/api/v1/api-client/*`: collections (incl. apply-delta), +//! environments, history, workspaces, public-mocks. All plaintext docs. + +use serde_json::{json, Value}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::*; +use crate::router::ApiResponse; +use crate::state::AppState; + +const COLLECTIONS: &str = "api_client_collections"; +const ENVIRONMENTS: &str = "api_client_environments"; +const HISTORY: &str = "api_client_history"; +const WORKSPACES: &str = "api_client_workspaces"; +const PUBLIC_MOCKS: &str = "api_client_public_mocks"; + +// ── Collections item-tree helpers (for apply-delta) ──────────────────────── + +/// Remove item by id anywhere in the tree; returns the removed item. +fn remove_item(items: &mut Vec, item_id: &str) -> Option { + if let Some(pos) = items.iter().position(|i| i["id"].as_str() == Some(item_id)) { + return Some(items.remove(pos)); + } + for item in items.iter_mut() { + if let Some(children) = item.get_mut("items").and_then(Value::as_array_mut) { + let mut vec = std::mem::take(children); + let removed = remove_item(&mut vec, item_id); + *children = vec; + if removed.is_some() { + return removed; + } + } + } + None +} + +/// Find the index path to an item anywhere in the tree. +fn find_path(items: &[Value], item_id: &str, path: &mut Vec) -> bool { + for (i, item) in items.iter().enumerate() { + if item["id"].as_str() == Some(item_id) { + path.push(i); + return true; + } + if let Some(children) = item.get("items").and_then(Value::as_array) { + path.push(i); + if find_path(children, item_id, path) { + return true; + } + path.pop(); + } + } + false +} + +fn find_item_mut<'a>(items: &'a mut Vec, item_id: &str) -> Option<&'a mut Value> { + let mut path = Vec::new(); + if !find_path(items, item_id, &mut path) { + return None; + } + let (&last, parents) = path.split_last()?; + let mut current: &mut Vec = items; + for &idx in parents { + current = current.get_mut(idx)?.get_mut("items")?.as_array_mut()?; + } + current.get_mut(last) +} + +/// Insert `item` under `parent_id` (empty/null = root) at `position` (or end). +fn insert_item(items: &mut Vec, parent_id: Option<&str>, item: Value, position: Option) -> bool { + match parent_id { + None | Some("") => { + let pos = position.unwrap_or(items.len()).min(items.len()); + items.insert(pos, item); + true + } + Some(pid) => { + if let Some(parent) = find_item_mut(items, pid) { + let children = parent + .as_object_mut() + .unwrap() + .entry("items") + .or_insert_with(|| json!([])); + if let Some(arr) = children.as_array_mut() { + let pos = position.unwrap_or(arr.len()).min(arr.len()); + arr.insert(pos, item); + return true; + } + } + false + } + } +} + +fn apply_delta(collection: &mut Value, ops: &[Value]) { + let Some(items) = collection.get_mut("items").and_then(Value::as_array_mut) else { + return; + }; + let mut items_vec = std::mem::take(items); + for op in ops { + match op["type"].as_str() { + Some("add") => { + let parent = op["parent_id"].as_str(); + let position = op["position"].as_u64().map(|p| p as usize); + insert_item(&mut items_vec, parent, op["item"].clone(), position); + } + Some("update") => { + if let Some(item_id) = op["item_id"].as_str() { + if let Some(item) = find_item_mut(&mut items_vec, item_id) { + merge_patch(item, &op["patch"]); + } + } + } + Some("delete") => { + if let Some(item_id) = op["item_id"].as_str() { + remove_item(&mut items_vec, item_id); + } + } + Some("move") => { + if let Some(item_id) = op["item_id"].as_str() { + if let Some(item) = remove_item(&mut items_vec, item_id) { + let parent = op["new_parent_id"].as_str(); + let index = op["new_index"].as_u64().map(|i| i as usize); + insert_item(&mut items_vec, parent, item, index); + } + } + } + _ => {} + } + } + *collection.get_mut("items").unwrap() = Value::Array(items_vec); +} + +// ── Generic named-doc CRUD (collections / environments / workspaces) ─────── + +fn crud( + db: &rusqlite::Connection, + ws: &str, + kind: &str, + defaults: Value, + method: &str, + rest: &str, + body: Option<&str>, +) -> Result { + match (method, rest) { + ("GET", "") => ApiResponse::ok(&list_docs(db, kind, ws)?), + ("POST", "") => { + let req = parse_body(body)?; + let mut doc = defaults; + doc["id"] = json!(new_id()); + merge_patch(&mut doc, &req); + insert_doc(db, kind, ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("PATCH", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let Some(mut doc) = get_doc(db, kind, ws, id)? else { + return Ok(ApiResponse::detail(404, "Not found")); + }; + merge_patch(&mut doc, &parse_body(body)?); + save_doc(db, kind, ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with('/') => { + if tombstone(db, kind, ws, &rest[1..])? { + Ok(ApiResponse::empty(204)) + } else { + Ok(ApiResponse::detail(404, "Not found")) + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} + +pub fn handle( + state: &AppState, + method: &str, + rest: &str, // path after "/api-client" (e.g. "/collections/abc") + query: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + + if let Some(tail) = rest.strip_prefix("/collections") { + // apply-delta: POST /collections/{id}/items:apply-delta + if method == "POST" { + if let Some(id) = tail + .strip_prefix('/') + .and_then(|t| t.strip_suffix("/items:apply-delta")) + { + let Some(mut doc) = get_doc(&db, COLLECTIONS, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Collection not found")); + }; + let req = parse_body(body)?; + let ops = req["ops"].as_array().cloned().unwrap_or_default(); + apply_delta(&mut doc, &ops); + save_doc(&db, COLLECTIONS, &ws, id, &doc)?; + return ApiResponse::ok(&json!({ "collection": doc })); + } + } + return crud(&db, &ws, COLLECTIONS, json!({ "name": "", "items": [] }), method, tail, body); + } + if let Some(tail) = rest.strip_prefix("/environments") { + return crud( + &db, + &ws, + ENVIRONMENTS, + json!({ "name": "", "variables": [] }), + method, + tail, + body, + ); + } + if let Some(tail) = rest.strip_prefix("/history") { + return match (method, tail) { + ("GET", "") => { + let mut docs = list_docs(&db, HISTORY, &ws)?; + docs.sort_by_key(|d| -d["timestamp"].as_i64().unwrap_or(0)); + let limit: usize = + query_param(query, "limit").and_then(|v| v.parse().ok()).unwrap_or(100); + docs.truncate(limit); + ApiResponse::ok(&docs) + } + ("POST", "") => { + let req = parse_body(body)?; + let mut doc = json!({ + "id": new_id(), + "params": [], + "headers": [], + "body": {}, + "auth": {}, + "name": "", + "timestamp": now_ms(), + }); + merge_patch(&mut doc, &req); + insert_doc(&db, HISTORY, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", "/clear") => { + for doc in list_docs(&db, HISTORY, &ws)? { + if let Some(id) = doc["id"].as_str() { + tombstone(&db, HISTORY, &ws, id)?; + } + } + Ok(ApiResponse::empty(204)) + } + ("DELETE", _) if tail.starts_with('/') => { + tombstone(&db, HISTORY, &ws, &tail[1..])?; + Ok(ApiResponse::empty(204)) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + }; + } + if let Some(tail) = rest.strip_prefix("/workspaces") { + return crud( + &db, + &ws, + WORKSPACES, + json!({ "name": "", "created_at": now_ms() }), + method, + tail, + body, + ); + } + if let Some(tail) = rest.strip_prefix("/public-mocks") { + return crud( + &db, + &ws, + PUBLIC_MOCKS, + json!({ "name": "", "items": [], "created_at": now_ms() }), + method, + tail, + body, + ); + } + Ok(ApiResponse::detail(404, "Not found")) +} diff --git a/apps/desktop/src-tauri/src/router/backup.rs b/apps/desktop/src-tauri/src/router/backup.rs new file mode 100644 index 00000000..db1f05e6 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/backup.rs @@ -0,0 +1,150 @@ +//! Logical backup export/import (`/desktop/backup/*`). +//! +//! A raw SQLCipher backup is useless off this machine — the DB key lives only in +//! this Mac's keychain and can't be exported — so we export the DECODED contents +//! (all `entries` rows + a whitelist of portable `kv` singletons) as JSON. The +//! webview encrypts that under a user backup passphrase before it ever touches +//! disk, and hands the same JSON back on restore. Secret payloads stay the exact +//! opaque `{encryptedData, iv}` blobs Rust already stores — nothing is decrypted +//! here. This is the only recovery path that survives losing the device key. + +use rusqlite::{params, OptionalExtension}; +use serde_json::{json, Value}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::parse_body; +use crate::router::ApiResponse; +use crate::state::AppState; + +/// kv singletons that are portable across machines. Excludes session cookies +/// (`cookie_jar`), per-workspace sync flags (`sync_enabled:*`), and the +/// device-local active-workspace pointer. +const PORTABLE_KV: &[&str] = &["master_vault", "backup_codes", "user_preferences"]; + +const BACKUP_VERSION: i64 = 1; + +pub fn handle( + state: &AppState, + method: &str, + rest: &str, // path after "/desktop/backup" + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + match (method, rest) { + // Full logical dump for the webview to encrypt + download. + ("GET", "/export") => { + let mut stmt = db.prepare( + "SELECT tool_kind, workspace_id, id, encrypted_data, iv, meta_json, + created_at, updated_at, deleted_at + FROM entries", + )?; + let entries: Vec = stmt + .query_map([], |r| { + Ok(json!({ + "tool_kind": r.get::<_, String>(0)?, + "workspace_id": r.get::<_, String>(1)?, + "id": r.get::<_, String>(2)?, + "encrypted_data": r.get::<_, Option>(3)?, + "iv": r.get::<_, Option>(4)?, + "meta_json": r.get::<_, Option>(5)?, + "created_at": r.get::<_, i64>(6)?, + "updated_at": r.get::<_, i64>(7)?, + "deleted_at": r.get::<_, Option>(8)?, + })) + })? + .collect::>()?; + + let mut kv = serde_json::Map::new(); + for &k in PORTABLE_KV { + let v: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [k], |r| r.get(0)) + .optional()?; + if let Some(v) = v { + kv.insert(k.to_string(), Value::String(v)); + } + } + + ApiResponse::ok(&json!({ + "version": BACKUP_VERSION, + "created_at": now_ms(), + "entries": entries, + "kv": Value::Object(kv), + })) + } + // Merge a decrypted dump back into the local store. + ("POST", "/import") => { + let dump = parse_body(body)?; + if dump["version"].as_i64() != Some(BACKUP_VERSION) { + return Ok(ApiResponse::detail(422, "Unsupported backup version")); + } + + let entries = dump["entries"].as_array().cloned().unwrap_or_default(); + let mut imported = 0usize; + for e in &entries { + let (kind, ws, id) = ( + e["tool_kind"].as_str().unwrap_or(""), + e["workspace_id"].as_str().unwrap_or(""), + e["id"].as_str().unwrap_or(""), + ); + if kind.is_empty() || ws.is_empty() || id.is_empty() { + continue; + } + let created = e["created_at"].as_i64().unwrap_or_else(now_ms); + let updated = e["updated_at"].as_i64().unwrap_or(created); + // Restored rows are marked dirty + unsynced so they re-push if the + // user later enables Cloud Sync. Existing rows only yield to the + // backup when the backup's copy is at least as new (LWW guard). + // execute() returns 0 when the LWW guard skips an older backup row. + imported += db.execute( + "INSERT INTO entries + (tool_kind, workspace_id, id, encrypted_data, iv, meta_json, + created_at, updated_at, deleted_at, dirty, last_synced_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1, NULL) + ON CONFLICT(tool_kind, workspace_id, id) DO UPDATE SET + encrypted_data = excluded.encrypted_data, + iv = excluded.iv, + meta_json = excluded.meta_json, + created_at = MIN(entries.created_at, excluded.created_at), + updated_at = excluded.updated_at, + deleted_at = excluded.deleted_at, + dirty = 1, + last_synced_at = NULL + WHERE excluded.updated_at >= entries.updated_at", + params![ + kind, ws, id, + e["encrypted_data"].as_str(), + e["iv"].as_str(), + e["meta_json"].as_str(), + created, updated, + e["deleted_at"].as_i64(), + ], + )?; + } + + // kv singletons are inserted only when ABSENT — never clobber an + // already-configured master_vault (its salt keys the existing local + // entries; overwriting it would strand them). Restore is meant for a + // fresh/empty vault; the webview warns before importing into a + // configured one. + let mut kv_imported = 0usize; + if let Some(kv) = dump["kv"].as_object() { + for &k in PORTABLE_KV { + if let Some(Value::String(v)) = kv.get(k) { + let n = db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO NOTHING", + params![k, v], + )?; + kv_imported += n; + } + } + } + + ApiResponse::ok(&json!({ + "imported": imported, + "kv_imported": kv_imported, + })) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/backup_codes.rs b/apps/desktop/src-tauri/src/router/backup_codes.rs new file mode 100644 index 00000000..e08e344b --- /dev/null +++ b/apps/desktop/src-tauri/src/router/backup_codes.rs @@ -0,0 +1,92 @@ +//! Local mirror of FastAPI `/api/v1/auth/backup-codes` (+ /lookup, /use). +//! Codes are stored as the client sends them: each entry re-encrypts the +//! master password under a code-derived key — opaque to Rust. + +use serde::{Deserialize, Serialize}; + +use crate::error::Result; +use crate::router::ApiResponse; +use crate::state::AppState; + +const KV_KEY: &str = "backup_codes"; + +#[derive(Serialize, Deserialize, Clone)] +struct BackupCodeEntry { + #[serde(rename = "codeId")] + code_id: String, + #[serde(rename = "codeSalt")] + code_salt: String, + encrypted: String, + iv: String, + #[serde(default)] + used: bool, +} + +fn load(db: &rusqlite::Connection) -> Result> { + let raw: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [KV_KEY], |r| r.get(0)) + .map(Some) + .or_else(|e| match e { + rusqlite::Error::QueryReturnedNoRows => Ok(None), + other => Err(other), + })?; + Ok(match raw { + Some(v) => serde_json::from_str(&v)?, + None => Vec::new(), + }) +} + +fn save(db: &rusqlite::Connection, codes: &[BackupCodeEntry]) -> Result<()> { + let json = serde_json::to_string(codes)?; + db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", + [KV_KEY, &json], + )?; + Ok(()) +} + +#[derive(Deserialize)] +struct StoreRequest { + codes: Vec, +} + +#[derive(Deserialize)] +struct CodeIdRequest { + #[serde(rename = "codeId")] + code_id: String, +} + +pub fn handle(state: &AppState, method: &str, path: &str, body: Option<&str>) -> Result { + let db = state.db.lock().unwrap(); + match (method, path) { + ("POST", "/api/v1/auth/backup-codes") => { + let req: StoreRequest = serde_json::from_str(body.unwrap_or(""))?; + save(&db, &req.codes)?; + Ok(ApiResponse::detail(200, "ok")) + } + ("POST", "/api/v1/auth/backup-codes/lookup") => { + let req: CodeIdRequest = serde_json::from_str(body.unwrap_or(""))?; + let codes = load(&db)?; + match codes.iter().find(|c| c.code_id == req.code_id && !c.used) { + Some(c) => ApiResponse::ok(&serde_json::json!({ + "codeSalt": c.code_salt, + "encrypted": c.encrypted, + "iv": c.iv, + })), + None => Ok(ApiResponse::detail(404, "Invalid or already-used backup code")), + } + } + ("POST", "/api/v1/auth/backup-codes/use") => { + let req: CodeIdRequest = serde_json::from_str(body.unwrap_or(""))?; + let mut codes = load(&db)?; + for c in codes.iter_mut() { + if c.code_id == req.code_id { + c.used = true; + } + } + save(&db, &codes)?; + Ok(ApiResponse::detail(200, "ok")) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/bookmarks.rs b/apps/desktop/src-tauri/src/router/bookmarks.rs new file mode 100644 index 00000000..605f4d90 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/bookmarks.rs @@ -0,0 +1,148 @@ +//! Local mirror of `/api/v1/bookmarks` + `/api/v1/bookmark-folders` (plaintext). + +use serde_json::json; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::*; +use crate::router::ApiResponse; +use crate::state::AppState; + +const BOOKMARKS: &str = "bookmarks"; +const FOLDERS: &str = "bookmark_folders"; + +pub fn handle_bookmarks( + state: &AppState, + method: &str, + rest: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "/snapshot") => ApiResponse::ok(&json!({ + "bookmarks": list_docs(&db, BOOKMARKS, &ws)?, + "folders": list_docs(&db, FOLDERS, &ws)?, + })), + ("POST", "") => { + let req = parse_body(body)?; + let now = now_ms(); + let mut doc = json!({ + "id": new_id(), + "tags": [], + "folderId": null, + "createdAt": now, + "updatedAt": now, + }); + merge_patch(&mut doc, &req); + insert_doc(&db, BOOKMARKS, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("POST", "/import") => { + let req = parse_body(body)?; + let now = now_ms(); + for b in req["bookmarks"].as_array().unwrap_or(&Vec::new()) { + let mut doc = json!({ "id": new_id(), "tags": [], "folderId": null, "createdAt": now, "updatedAt": now }); + merge_patch(&mut doc, b); + insert_doc(&db, BOOKMARKS, &ws, doc["id"].as_str().unwrap(), &doc)?; + } + for f in req["folders"].as_array().unwrap_or(&Vec::new()) { + let mut doc = json!({ "id": new_id(), "parentId": null, "isExpanded": true, "createdAt": now }); + merge_patch(&mut doc, f); + insert_doc(&db, FOLDERS, &ws, doc["id"].as_str().unwrap(), &doc)?; + } + Ok(ApiResponse::detail(200, "ok")) + } + ("POST", "/clear-all") => { + for kind in [BOOKMARKS, FOLDERS] { + for doc in list_docs(&db, kind, &ws)? { + if let Some(id) = doc["id"].as_str() { + tombstone(&db, kind, &ws, id)?; + } + } + } + Ok(ApiResponse::detail(200, "ok")) + } + (m, r) if r.starts_with('/') => { + let tail = &r[1..]; + let (id, is_move) = match tail.strip_suffix("/move") { + Some(id) => (id, true), + None => (tail, false), + }; + let Some(mut doc) = get_doc(&db, BOOKMARKS, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Bookmark not found")); + }; + match m { + "PATCH" => { + let patch = parse_body(body)?; + if is_move { + merge_patch(&mut doc, &json!({ "folderId": patch["folderId"] })); + } else { + merge_patch(&mut doc, &patch); + } + doc["updatedAt"] = json!(now_ms()); + save_doc(&db, BOOKMARKS, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + "DELETE" => { + tombstone(&db, BOOKMARKS, &ws, id)?; + Ok(ApiResponse::empty(204)) + } + _ => Ok(ApiResponse::detail(405, "Method not allowed")), + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} + +pub fn handle_folders( + state: &AppState, + method: &str, + rest: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("POST", "") => { + let req = parse_body(body)?; + let mut doc = json!({ + "id": new_id(), + "parentId": null, + "isExpanded": true, + "createdAt": now_ms(), + }); + merge_patch(&mut doc, &req); + insert_doc(&db, FOLDERS, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + (m, r) if r.starts_with('/') => { + let tail = &r[1..]; + let (id, is_expanded) = match tail.strip_suffix("/expanded") { + Some(id) => (id, true), + None => (tail, false), + }; + let Some(mut doc) = get_doc(&db, FOLDERS, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Folder not found")); + }; + match m { + "PATCH" => { + let patch = parse_body(body)?; + if is_expanded { + merge_patch(&mut doc, &json!({ "isExpanded": patch["isExpanded"] })); + } else { + merge_patch(&mut doc, &patch); + } + save_doc(&db, FOLDERS, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + "DELETE" => { + tombstone(&db, FOLDERS, &ws, id)?; + Ok(ApiResponse::empty(204)) + } + _ => Ok(ApiResponse::detail(405, "Method not allowed")), + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/entries.rs b/apps/desktop/src-tauri/src/router/entries.rs new file mode 100644 index 00000000..e4de6a8f --- /dev/null +++ b/apps/desktop/src-tauri/src/router/entries.rs @@ -0,0 +1,231 @@ +//! Generic local document store over the `entries` table, plus the two +//! contract families that are pure envelope CRUD: +//! +//! * envelope entries — password-manager / api-keys / environment-manager +//! (`{id, encryptedData, iv, createdAt, updatedAt}`) +//! * connection vaults — sql-client / redis-commander / nosql connections +//! (`{id, userId, encryptedData, iv, name, [type], createdAt, lastUsedAt}`) +//! +//! Every row stores the full response-shaped JSON doc in `meta_json`; the +//! `updated_at` column always carries epoch-ms for LWW sync regardless of the +//! doc's own timestamp format. Rust never sees plaintext secrets — encrypted +//! tools ship opaque `{encryptedData, iv}` blobs produced in the webview. + +use rusqlite::{params, Connection, OptionalExtension}; +use serde_json::{json, Value}; +use uuid::Uuid; + +use crate::db::now_ms; +use crate::error::{AppError, Result}; +use crate::router::ApiResponse; +use crate::state::AppState; + +// ── Workspace + doc-store helpers (used by all tool handlers) ────────────── + +pub fn active_workspace(db: &Connection) -> String { + db.query_row("SELECT v FROM kv WHERE k = 'active_workspace'", [], |r| r.get(0)) + .optional() + .ok() + .flatten() + .unwrap_or_else(|| crate::router::workspaces::LOCAL_WORKSPACE_ID.to_string()) +} + +pub fn parse_body(body: Option<&str>) -> Result { + serde_json::from_str(body.unwrap_or("{}")).map_err(AppError::from) +} + +pub fn new_id() -> String { + Uuid::new_v4().to_string() +} + +pub fn list_docs(db: &Connection, kind: &str, ws: &str) -> Result> { + let mut stmt = db.prepare( + "SELECT meta_json FROM entries + WHERE tool_kind = ?1 AND workspace_id = ?2 AND deleted_at IS NULL + ORDER BY created_at ASC", + )?; + let rows = stmt.query_map(params![kind, ws], |r| r.get::<_, String>(0))?; + let mut docs = Vec::new(); + for row in rows { + docs.push(serde_json::from_str(&row?)?); + } + Ok(docs) +} + +pub fn get_doc(db: &Connection, kind: &str, ws: &str, id: &str) -> Result> { + let raw: Option = db + .query_row( + "SELECT meta_json FROM entries + WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3 AND deleted_at IS NULL", + params![kind, ws, id], + |r| r.get(0), + ) + .optional()?; + Ok(match raw { + Some(s) => Some(serde_json::from_str(&s)?), + None => None, + }) +} + +pub fn insert_doc(db: &Connection, kind: &str, ws: &str, id: &str, doc: &Value) -> Result<()> { + let now = now_ms(); + db.execute( + "INSERT INTO entries (tool_kind, workspace_id, id, meta_json, created_at, updated_at, dirty) + VALUES (?1, ?2, ?3, ?4, ?5, ?5, 1) + ON CONFLICT(tool_kind, workspace_id, id) DO UPDATE SET + meta_json = excluded.meta_json, updated_at = excluded.updated_at, + deleted_at = NULL, dirty = 1", + params![kind, ws, id, serde_json::to_string(doc)?, now], + )?; + Ok(()) +} + +pub fn save_doc(db: &Connection, kind: &str, ws: &str, id: &str, doc: &Value) -> Result<()> { + db.execute( + "UPDATE entries SET meta_json = ?4, updated_at = ?5, dirty = 1 + WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3", + params![kind, ws, id, serde_json::to_string(doc)?, now_ms()], + )?; + Ok(()) +} + +/// Tombstone (soft-delete) a doc. Returns false when it didn't exist. +pub fn tombstone(db: &Connection, kind: &str, ws: &str, id: &str) -> Result { + let n = db.execute( + "UPDATE entries SET deleted_at = ?4, updated_at = ?4, dirty = 1 + WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3 AND deleted_at IS NULL", + params![kind, ws, id, now_ms()], + )?; + Ok(n > 0) +} + +/// Shallow-merge `patch` object fields into `doc`. +pub fn merge_patch(doc: &mut Value, patch: &Value) { + if let (Some(doc_map), Some(patch_map)) = (doc.as_object_mut(), patch.as_object()) { + for (k, v) in patch_map { + doc_map.insert(k.clone(), v.clone()); + } + } +} + +pub fn query_param<'a>(query: &'a str, key: &str) -> Option<&'a str> { + query.split('&').find_map(|pair| { + let (k, v) = pair.split_once('=')?; + (k == key).then_some(v) + }) +} + +// ── Envelope entries: password-manager / api-keys / environment-manager ──── + +/// Handles `/{prefix}/entries[/{id}]` for the encrypted-envelope tools. +pub fn handle_envelope( + state: &AppState, + kind: &str, + method: &str, + rest: &str, // path after `/{prefix}` (starts with "/entries") + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "/entries") => ApiResponse::ok(&list_docs(&db, kind, &ws)?), + ("POST", "/entries") => { + let req = parse_body(body)?; + let now = now_ms(); + let doc = json!({ + "id": new_id(), + "encryptedData": req["encryptedData"], + "iv": req["iv"], + "createdAt": req.get("createdAt").and_then(Value::as_i64).unwrap_or(now), + "updatedAt": req.get("updatedAt").and_then(Value::as_i64).unwrap_or(now), + }); + insert_doc(&db, kind, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("PATCH", _) if rest.starts_with("/entries/") => { + let id = &rest["/entries/".len()..]; + let Some(mut doc) = get_doc(&db, kind, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Entry not found")); + }; + let mut patch = parse_body(body)?; + if patch.get("updatedAt").and_then(Value::as_i64).is_none() { + merge_patch(&mut patch, &json!({ "updatedAt": now_ms() })); + } + merge_patch(&mut doc, &patch); + save_doc(&db, kind, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with("/entries/") => { + let id = &rest["/entries/".len()..]; + if tombstone(&db, kind, &ws, id)? { + Ok(ApiResponse::empty(204)) + } else { + Ok(ApiResponse::detail(404, "Entry not found")) + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} + +// ── Connection vaults: sql-client / redis-commander / nosql ──────────────── + +/// Handles `/{prefix}/connections[/{id}[/touch]]`. +pub fn handle_connections( + state: &AppState, + kind: &str, + method: &str, + rest: &str, // path after `/{prefix}` (starts with "/connections") + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "/connections") => ApiResponse::ok(&list_docs(&db, kind, &ws)?), + ("POST", "/connections") => { + let req = parse_body(body)?; + let now = now_ms(); + let mut doc = json!({ + "id": new_id(), + "userId": "desktop-local", + "createdAt": now, + "lastUsedAt": now, + "updatedAt": now, + }); + merge_patch(&mut doc, &req); // name, encryptedData, iv, type… + insert_doc(&db, kind, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + (m, r) if r.starts_with("/connections/") => { + let tail = &r["/connections/".len()..]; + let (id, is_touch) = match tail.strip_suffix("/touch") { + Some(id) => (id, true), + None => (tail, false), + }; + let Some(mut doc) = get_doc(&db, kind, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Connection not found")); + }; + match (m, is_touch) { + ("POST", true) => { + merge_patch(&mut doc, &json!({ "lastUsedAt": now_ms() })); + save_doc(&db, kind, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("PATCH", false) => { + merge_patch(&mut doc, &parse_body(body)?); + // updatedAt is the sync LWW clock — a content edit must + // advance it (mirrors the backend). /touch bumps only + // lastUsedAt above, deliberately leaving updatedAt alone. + merge_patch(&mut doc, &json!({ "updatedAt": now_ms() })); + save_doc(&db, kind, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", false) => { + tombstone(&db, kind, &ws, id)?; + Ok(ApiResponse::empty(204)) + } + _ => Ok(ApiResponse::detail(405, "Method not allowed")), + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/master_vault.rs b/apps/desktop/src-tauri/src/router/master_vault.rs new file mode 100644 index 00000000..d987c446 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/master_vault.rs @@ -0,0 +1,74 @@ +//! Local mirror of FastAPI `/api/v1/auth/master-vault`. +//! +//! Stores only `{salt, verifier, createdAt}` — the PBKDF2 salt and an +//! AES-GCM verifier blob. The master password / derived key never reach Rust; +//! verification happens client-side in the webview (lib/encryption.ts). + +use serde::{Deserialize, Serialize}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::ApiResponse; +use crate::state::AppState; + +const KV_KEY: &str = "master_vault"; + +#[derive(Serialize, Deserialize)] +struct KeyVerifier { + encrypted: String, + iv: String, +} + +#[derive(Serialize, Deserialize)] +struct MasterVault { + salt: String, + verifier: KeyVerifier, + #[serde(rename = "createdAt")] + created_at: i64, +} + +#[derive(Deserialize)] +struct SetupRequest { + salt: String, + verifier: KeyVerifier, +} + +pub fn handle(state: &AppState, method: &str, path: &str, body: Option<&str>) -> Result { + if path != "/api/v1/auth/master-vault" { + return Ok(ApiResponse::detail(404, "Not found")); + } + let db = state.db.lock().unwrap(); + match method { + "GET" => { + let existing: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [KV_KEY], |r| r.get(0)) + .map(Some) + .or_else(|e| match e { + rusqlite::Error::QueryReturnedNoRows => Ok(None), + other => Err(other), + })?; + match existing { + Some(v) => Ok(ApiResponse { status: 200, body: v }), + None => Ok(ApiResponse::detail(404, "Master vault not configured")), + } + } + "POST" => { + let req: SetupRequest = + serde_json::from_str(body.unwrap_or("")).map_err(crate::error::AppError::from)?; + let already: i64 = + db.query_row("SELECT count(*) FROM kv WHERE k = ?1", [KV_KEY], |r| r.get(0))?; + if already > 0 { + return Ok(ApiResponse::detail(409, "Master vault already configured")); + } + let vault = MasterVault { + salt: req.salt, + verifier: req.verifier, + created_at: now_ms(), + }; + let json = serde_json::to_string(&vault)?; + db.execute("INSERT INTO kv (k, v) VALUES (?1, ?2)", [KV_KEY, &json])?; + Ok(ApiResponse { status: 200, body: json }) + } + _ => Ok(ApiResponse::detail(405, "Method not allowed")), + } +} diff --git a/apps/desktop/src-tauri/src/router/mod.rs b/apps/desktop/src-tauri/src/router/mod.rs new file mode 100644 index 00000000..bfebabf2 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/mod.rs @@ -0,0 +1,582 @@ +pub mod api_client; +pub mod backup; +pub mod backup_codes; +pub mod bookmarks; +pub mod entries; +pub mod master_vault; +pub mod notes; +pub mod preferences; +pub mod snippets; +pub mod stubs; +pub mod sync; +pub mod tasks; +pub mod workspaces; + +use serde::Serialize; + +use crate::error::Result; +use crate::state::AppState; + +#[derive(Serialize)] +pub struct ApiResponse { + pub status: u16, + pub body: String, +} + +impl ApiResponse { + pub fn json(status: u16, value: &impl Serialize) -> Result { + Ok(Self { status, body: serde_json::to_string(value)? }) + } + + pub fn ok(value: &impl Serialize) -> Result { + Self::json(200, value) + } + + pub fn empty(status: u16) -> Self { + Self { status, body: String::new() } + } + + pub fn detail(status: u16, msg: &str) -> Self { + Self { + status, + body: serde_json::json!({ "detail": msg }).to_string(), + } + } +} + +/// Remote-only path prefixes (relative to /api/v1): tools that require the +/// cloud even on desktop. Rejected with 503 offline; Phase 4 routes them to +/// the remote bridge instead. +const REMOTE_ONLY: &[&str] = &["/url-shortener", "/s3-drive", "/game-scores", "/dns-lookup"]; + +/// Dispatch a request against the local store. Paths are normalized FastAPI +/// paths (`/api/v1/...`); query strings are split off here and passed along. +pub fn route(state: &AppState, method: &str, full_path: &str, body: Option<&str>) -> Result { + let (path, query) = match full_path.split_once('?') { + Some((p, q)) => (p, q), + None => (full_path, ""), + }; + let path = path.trim_end_matches('/'); + + // Session endpoints: the local store is authorized by OS login + master + // vault, so session checks always succeed offline. + match (method, path) { + ("GET", "/api/v1/auth/session/check") => return Ok(ApiResponse::detail(200, "ok")), + ("POST", "/api/v1/auth/refresh") => return Ok(ApiResponse::detail(200, "ok")), + ("POST", "/api/v1/auth/logout") => return Ok(ApiResponse::detail(200, "ok")), + _ => {} + } + + if let Some(rest) = path.strip_prefix("/desktop/sync") { + return sync::handle(state, method, rest, query, body); + } + if let Some(rest) = path.strip_prefix("/desktop/backup") { + return backup::handle(state, method, rest, body); + } + + if path.starts_with("/api/v1/auth/master-vault") { + return master_vault::handle(state, method, path, body); + } + if path.starts_with("/api/v1/auth/backup-codes") { + return backup_codes::handle(state, method, path, body); + } + if path.starts_with("/api/v1/workspaces-api/") { + return workspaces::handle(state, method, path, body); + } + + let Some(rel) = path.strip_prefix("/api/v1") else { + return stubs::handle(method, path); + }; + + // Encrypted-envelope vault tools + for (prefix, kind) in [ + ("/password-manager", "password_entries"), + ("/api-keys", "api_key_entries"), + ("/environment-manager", "environment_entries"), + ] { + if let Some(rest) = rel.strip_prefix(prefix) { + return entries::handle_envelope(state, kind, method, rest, body); + } + } + + // DB connection vaults + for (prefix, kind) in [ + ("/sql-client/connections", "sql_connections"), + ("/redis-commander/connections", "redis_connections"), + ("/nosql/connections", "nosql_connections"), + ] { + if rel.starts_with(prefix) { + // rest starts at "/connections..." + let rest = &rel[prefix.len() - "/connections".len()..]; + return entries::handle_connections(state, kind, method, rest, body); + } + } + + if let Some(rest) = rel.strip_prefix("/code-snippets") { + return snippets::handle(state, method, rest, body); + } + if let Some(rest) = rel.strip_prefix("/bookmark-folders") { + return bookmarks::handle_folders(state, method, rest, body); + } + if let Some(rest) = rel.strip_prefix("/bookmarks") { + return bookmarks::handle_bookmarks(state, method, rest, body); + } + if let Some(rest) = rel.strip_prefix("/notes") { + return notes::handle(state, method, rest, query, body); + } + if let Some(rest) = rel.strip_prefix("/tasks") { + return tasks::handle_tasks(state, method, rest, query, body); + } + if let Some(rest) = rel.strip_prefix("/projects") { + return tasks::handle_projects(state, method, rest, body); + } + if let Some(rest) = rel.strip_prefix("/api-client") { + return api_client::handle(state, method, rest, query, body); + } + if let Some(rest) = rel.strip_prefix("/user-preferences") { + return preferences::handle(state, method, rest, query, body); + } + + if REMOTE_ONLY.iter().any(|p| rel.starts_with(p)) { + return Ok(ApiResponse::detail( + 503, + "This tool requires a network connection and cloud sign-in", + )); + } + + stubs::handle(method, path) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn body_json(r: &ApiResponse) -> serde_json::Value { + serde_json::from_str(&r.body).unwrap() + } + + #[test] + fn master_vault_setup_and_get() { + let state = AppState::in_memory(); + // Not configured yet + let r = route(&state, "GET", "/api/v1/auth/master-vault", None).unwrap(); + assert_eq!(r.status, 404); + // Setup + let setup = r#"{"salt":"c2FsdA==","verifier":{"encrypted":"ZW5j","iv":"aXY="}}"#; + let r = route(&state, "POST", "/api/v1/auth/master-vault", Some(setup)).unwrap(); + assert_eq!(r.status, 200); + // Second setup rejected + let r = route(&state, "POST", "/api/v1/auth/master-vault", Some(setup)).unwrap(); + assert_eq!(r.status, 409); + // Get returns stored vault + let r = route(&state, "GET", "/api/v1/auth/master-vault", None).unwrap(); + assert_eq!(r.status, 200); + let v = body_json(&r); + assert_eq!(v["salt"], "c2FsdA=="); + assert_eq!(v["verifier"]["iv"], "aXY="); + assert!(v["createdAt"].as_i64().unwrap() > 0); + } + + #[test] + fn backup_codes_roundtrip() { + let state = AppState::in_memory(); + let store = r#"{"codes":[{"codeId":"c1","codeSalt":"s","encrypted":"e","iv":"i"}]}"#; + let r = route(&state, "POST", "/api/v1/auth/backup-codes", Some(store)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "POST", "/api/v1/auth/backup-codes/lookup", Some(r#"{"codeId":"c1"}"#)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "POST", "/api/v1/auth/backup-codes/use", Some(r#"{"codeId":"c1"}"#)).unwrap(); + assert_eq!(r.status, 200); + // Used codes no longer resolve + let r = route(&state, "POST", "/api/v1/auth/backup-codes/lookup", Some(r#"{"codeId":"c1"}"#)).unwrap(); + assert_eq!(r.status, 404); + } + + #[test] + fn workspaces_synthetic_personal() { + let state = AppState::in_memory(); + let r = route(&state, "GET", "/api/v1/workspaces-api/workspaces", None).unwrap(); + assert_eq!(r.status, 200); + let v = body_json(&r); + assert_eq!(v[0]["is_personal"], true); + assert_eq!(v[0]["kind"], "personal"); + let r = route( + &state, + "POST", + "/api/v1/workspaces-api/workspaces/active", + Some(r#"{"workspace_id":"local-personal"}"#), + ) + .unwrap(); + assert_eq!(r.status, 200); + } + + #[test] + fn unmapped_route_is_loud_501() { + let state = AppState::in_memory(); + let r = route(&state, "GET", "/api/v1/nonexistent", None).unwrap(); + assert_eq!(r.status, 501); + assert!(r.body.contains("nonexistent")); + } + + #[test] + fn session_check_ok_offline() { + let state = AppState::in_memory(); + let r = route(&state, "GET", "/api/v1/auth/session/check", None).unwrap(); + assert_eq!(r.status, 200); + } + + #[test] + fn envelope_entries_crud() { + let state = AppState::in_memory(); + for prefix in ["password-manager", "api-keys", "environment-manager"] { + let base = format!("/api/v1/{prefix}/entries"); + let r = route(&state, "POST", &base, Some(r#"{"encryptedData":"AAA","iv":"BBB"}"#)).unwrap(); + assert_eq!(r.status, 200, "{prefix} create"); + let created = body_json(&r); + let id = created["id"].as_str().unwrap().to_string(); + assert_eq!(created["encryptedData"], "AAA"); + assert!(created["createdAt"].as_i64().unwrap() > 0); + + let r = route(&state, "GET", &format!("{base}?offset=0&limit=50"), None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 1); + + let r = route(&state, "PATCH", &format!("{base}/{id}"), Some(r#"{"encryptedData":"CCC","iv":"DDD"}"#)).unwrap(); + assert_eq!(body_json(&r)["encryptedData"], "CCC"); + + let r = route(&state, "DELETE", &format!("{base}/{id}"), None).unwrap(); + assert_eq!(r.status, 204); + let r = route(&state, "GET", &base, None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + } + } + + #[test] + fn connection_vault_crud_and_touch() { + let state = AppState::in_memory(); + let base = "/api/v1/sql-client/connections"; + let r = route(&state, "POST", base, Some(r#"{"encryptedData":"E","iv":"I","name":"pg","type":"postgresql"}"#)).unwrap(); + let created = body_json(&r); + let id = created["id"].as_str().unwrap().to_string(); + assert_eq!(created["type"], "postgresql"); + assert!(created["lastUsedAt"].as_i64().unwrap() > 0); + let r = route(&state, "POST", &format!("{base}/{id}/touch"), None).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "DELETE", &format!("{base}/{id}"), None).unwrap(); + assert_eq!(r.status, 204); + } + + #[test] + fn connection_patch_bumps_updated_at_touch_does_not() { + let state = AppState::in_memory(); + let base = "/api/v1/redis-commander/connections"; + let r = route(&state, "POST", base, Some(r#"{"encryptedData":"E","iv":"I","name":"r"}"#)).unwrap(); + let created = body_json(&r); + let id = created["id"].as_str().unwrap().to_string(); + let created_updated = created["updatedAt"].as_i64().unwrap(); + assert!(created_updated > 0, "create sets updatedAt"); + + // A content edit advances updatedAt (the LWW clock). + let r = route(&state, "PATCH", &format!("{base}/{id}"), Some(r#"{"name":"renamed"}"#)).unwrap(); + let edited = body_json(&r); + assert_eq!(edited["name"], "renamed"); + assert!(edited["updatedAt"].as_i64().unwrap() >= created_updated); + + // A bare touch bumps lastUsedAt but MUST NOT move updatedAt. + let after_edit = edited["updatedAt"].as_i64().unwrap(); + let r = route(&state, "POST", &format!("{base}/{id}/touch"), None).unwrap(); + let touched = body_json(&r); + assert!(touched["lastUsedAt"].as_i64().unwrap() >= after_edit); + assert_eq!(touched["updatedAt"].as_i64().unwrap(), after_edit, "touch leaves updatedAt untouched"); + } + + #[test] + fn snippets_dup_id_409() { + let state = AppState::in_memory(); + let body = r#"{"id":"s1","title":"t","language":"rust","code":"x"}"#; + let r = route(&state, "POST", "/api/v1/code-snippets", Some(body)).unwrap(); + assert_eq!(r.status, 200); + let created = body_json(&r); + assert_eq!(created["tags"], serde_json::json!([])); + assert_eq!(created["pinned"], false); + let r = route(&state, "POST", "/api/v1/code-snippets", Some(body)).unwrap(); + assert_eq!(r.status, 409); + } + + #[test] + fn bookmarks_snapshot_move_clear() { + let state = AppState::in_memory(); + let r = route(&state, "POST", "/api/v1/bookmark-folders", Some(r#"{"id":"f1","name":"Dev","parentId":null,"isExpanded":true}"#)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "POST", "/api/v1/bookmarks", Some(r#"{"id":"b1","title":"x","url":"https://x.dev","tags":[],"folderId":null}"#)).unwrap(); + assert_eq!(body_json(&r)["id"], "b1"); + let r = route(&state, "PATCH", "/api/v1/bookmarks/b1/move", Some(r#"{"folderId":"f1"}"#)).unwrap(); + assert_eq!(body_json(&r)["folderId"], "f1"); + let r = route(&state, "GET", "/api/v1/bookmarks/snapshot", None).unwrap(); + let snap = body_json(&r); + assert_eq!(snap["bookmarks"].as_array().unwrap().len(), 1); + assert_eq!(snap["folders"].as_array().unwrap().len(), 1); + let r = route(&state, "POST", "/api/v1/bookmarks/clear-all", None).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "GET", "/api/v1/bookmarks/snapshot", None).unwrap(); + assert_eq!(body_json(&r)["bookmarks"].as_array().unwrap().len(), 0); + } + + #[test] + fn notes_recursive_delete_iso_timestamps() { + let state = AppState::in_memory(); + let r = route(&state, "POST", "/api/v1/notes", Some(r#"{"title":"root","content":{"a":1},"parentId":null}"#)).unwrap(); + let root = body_json(&r); + assert!(root["createdAt"].as_str().unwrap().contains('T')); + let root_id = root["id"].as_str().unwrap().to_string(); + let r = route(&state, "POST", "/api/v1/notes", Some(&format!(r#"{{"title":"child","content":null,"parentId":"{root_id}"}}"#))).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "DELETE", &format!("/api/v1/notes/{root_id}?recursive=true"), None).unwrap(); + assert_eq!(r.status, 204); + let r = route(&state, "GET", "/api/v1/notes", None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + } + + #[test] + fn tasks_wrapped_list_stats_and_filters() { + let state = AppState::in_memory(); + let r = route(&state, "POST", "/api/v1/tasks", Some(r#"{"text":"do it","projectId":"p1"}"#)).unwrap(); + let task = body_json(&r); + assert_eq!(task["status"], "not-started"); + assert_eq!(task["statusOrder"], 0); + let id = task["id"].as_str().unwrap().to_string(); + let r = route(&state, "PATCH", &format!("/api/v1/tasks/{id}"), Some(r#"{"status":"completed"}"#)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "GET", "/api/v1/tasks?status=completed&page=1&pageSize=10", None).unwrap(); + let list = body_json(&r); + assert_eq!(list["items"].as_array().unwrap().len(), 1); + assert_eq!(list["total"], 1); + let r = route(&state, "GET", "/api/v1/tasks/stats", None).unwrap(); + let stats = body_json(&r); + assert_eq!(stats["completed"], 1); + assert_eq!(stats["total"], 1); + } + + #[test] + fn api_client_collections_apply_delta() { + let state = AppState::in_memory(); + let r = route(&state, "POST", "/api/v1/api-client/collections", Some(r#"{"name":"My APIs"}"#)).unwrap(); + let col = body_json(&r); + let id = col["id"].as_str().unwrap().to_string(); + assert_eq!(col["items"], serde_json::json!([])); + // add folder at root, add request into folder, then move request to root + let ops = r#"{"ops":[ + {"type":"add","parent_id":null,"item":{"id":"fold1","name":"F","items":[]}}, + {"type":"add","parent_id":"fold1","item":{"id":"req1","name":"R","method":"GET"}}, + {"type":"update","item_id":"req1","patch":{"name":"R2"}}, + {"type":"move","item_id":"req1","new_parent_id":null,"new_index":0} + ]}"#; + let r = route(&state, "POST", &format!("/api/v1/api-client/collections/{id}/items:apply-delta"), Some(ops)).unwrap(); + assert_eq!(r.status, 200); + let items = &body_json(&r)["collection"]["items"]; + assert_eq!(items[0]["id"], "req1"); + assert_eq!(items[0]["name"], "R2"); + assert_eq!(items[1]["id"], "fold1"); + assert_eq!(items[1]["items"].as_array().unwrap().len(), 0); + } + + #[test] + fn history_limit_and_clear() { + let state = AppState::in_memory(); + for i in 0..3 { + let body = format!(r#"{{"method":"GET","url":"https://x/{i}","name":"","timestamp":{}}}"#, 1000 + i); + let r = route(&state, "POST", "/api/v1/api-client/history", Some(&body)).unwrap(); + assert_eq!(r.status, 200); + } + let r = route(&state, "GET", "/api/v1/api-client/history?limit=2", None).unwrap(); + let list = body_json(&r); + assert_eq!(list.as_array().unwrap().len(), 2); + assert_eq!(list[0]["url"], "https://x/2"); // newest first + let r = route(&state, "DELETE", "/api/v1/api-client/history/clear", None).unwrap(); + assert_eq!(r.status, 204); + } + + #[test] + fn preferences_defaults_and_tool_usage() { + let state = AppState::in_memory(); + let r = route(&state, "GET", "/api/v1/user-preferences", None).unwrap(); + let prefs = body_json(&r); + assert_eq!(prefs["theme"], "system"); + assert_eq!(prefs["accentColor"], "blue"); + assert!(prefs["enabledTools"].as_array().unwrap().len() > 30); + let r = route(&state, "POST", "/api/v1/user-preferences/tool-usage", Some(r#"{"toolId":"/app/json-formatter"}"#)).unwrap(); + assert_eq!(body_json(&r)["ok"], true); + let r = route(&state, "GET", "/api/v1/user-preferences", None).unwrap(); + assert_eq!(body_json(&r)["toolStats"]["/app/json-formatter"]["usageCount"], 1); + let r = route(&state, "PATCH", "/api/v1/user-preferences", Some(r#"{"theme":"dark"}"#)).unwrap(); + assert_eq!(body_json(&r)["theme"], "dark"); + } + + #[test] + fn sync_rows_resolve_and_detach() { + let state = AppState::in_memory(); + // Create an entry, verify it shows up dirty with no last_synced_at. + let r = route(&state, "POST", "/api/v1/password-manager/entries", Some(r#"{"encryptedData":"X","iv":"Y"}"#)).unwrap(); + let id = body_json(&r)["id"].as_str().unwrap().to_string(); + let r = route(&state, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + let rows = body_json(&r); + assert_eq!(rows[0]["dirty"], true); + assert!(rows[0]["last_synced_at"].is_null()); + + // mark-synced with re-key to a server id. + let body = format!(r#"{{"workspace_id":"local-personal","tool_kind":"password_entries","id":"{id}","action":"mark-synced","new_id":"srv-1"}}"#); + let r = route(&state, "POST", "/desktop/sync/resolve", Some(&body)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + let rows = body_json(&r); + assert_eq!(rows[0]["id"], "srv-1"); + assert_eq!(rows[0]["dirty"], false); + assert!(rows[0]["last_synced_at"].as_i64().is_some()); + + // Tombstone + mark-synced removes the row entirely. + let r = route(&state, "DELETE", "/api/v1/password-manager/entries/srv-1", None).unwrap(); + assert_eq!(r.status, 204); + let body = r#"{"workspace_id":"local-personal","tool_kind":"password_entries","id":"srv-1","action":"mark-synced"}"#; + route(&state, "POST", "/desktop/sync/resolve", Some(body)).unwrap(); + let r = route(&state, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + + // apply-remote inserts a clean row. + let body = r#"{"workspace_id":"local-personal","tool_kind":"password_entries","id":"srv-2","action":"apply-remote","doc":{"id":"srv-2","encryptedData":"R","iv":"I","updatedAt":123},"updated_at":123}"#; + route(&state, "POST", "/desktop/sync/resolve", Some(body)).unwrap(); + let r = route(&state, "GET", "/api/v1/password-manager/entries", None).unwrap(); + assert_eq!(body_json(&r)[0]["id"], "srv-2"); + + // Toggle sync off → last_synced_at cleared (fresh merge on re-enable). + let r = route(&state, "POST", "/desktop/sync/settings", Some(r#"{"workspace_id":"local-personal","enabled":true}"#)).unwrap(); + assert_eq!(r.status, 200); + route(&state, "POST", "/desktop/sync/settings", Some(r#"{"workspace_id":"local-personal","enabled":false}"#)).unwrap(); + let r = route(&state, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + assert!(body_json(&r)[0]["last_synced_at"].is_null()); + let r = route(&state, "GET", "/desktop/sync/settings?workspace_id=local-personal", None).unwrap(); + assert_eq!(body_json(&r)["enabled"], false); + } + + #[test] + fn sync_conflict_archive_list_dismiss() { + let state = AppState::in_memory(); + // No conflicts initially. + let r = route(&state, "GET", "/desktop/sync/conflicts?workspace_id=local-personal", None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + + // Archive a losing remote version. + let body = r#"{"workspace_id":"local-personal","tool_kind":"password_entries","entry_id":"e1", + "loser_side":"remote","loser_doc":{"id":"e1","encryptedData":"OLD","iv":"IV"}, + "loser_updated_at":100,"winner_updated_at":200}"#; + let r = route(&state, "POST", "/desktop/sync/conflict", Some(body)).unwrap(); + assert_eq!(r.status, 200); + let cid = body_json(&r)["id"].as_str().unwrap().to_string(); + + // It shows up in the open list with the loser doc intact. + let r = route(&state, "GET", "/desktop/sync/conflicts?workspace_id=local-personal", None).unwrap(); + let list = body_json(&r); + assert_eq!(list.as_array().unwrap().len(), 1); + assert_eq!(list[0]["entry_id"], "e1"); + assert_eq!(list[0]["loser_side"], "remote"); + assert_eq!(list[0]["loser_doc"]["encryptedData"], "OLD"); + assert_eq!(list[0]["winner_updated_at"], 200); + + // Bad side is rejected. + let bad = r#"{"workspace_id":"local-personal","tool_kind":"x","entry_id":"e2","loser_side":"nope","loser_doc":{}}"#; + assert_eq!(route(&state, "POST", "/desktop/sync/conflict", Some(bad)).unwrap().status, 422); + + // Dismiss removes it from the open list. + let dismiss = format!(r#"{{"id":"{cid}"}}"#); + assert_eq!(route(&state, "POST", "/desktop/sync/conflicts/dismiss", Some(&dismiss)).unwrap().status, 200); + let r = route(&state, "GET", "/desktop/sync/conflicts?workspace_id=local-personal", None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + // Dismissing again is a 404. + assert_eq!(route(&state, "POST", "/desktop/sync/conflicts/dismiss", Some(&dismiss)).unwrap().status, 404); + } + + #[test] + fn sync_conflict_restore_reapplies_loser() { + let state = AppState::in_memory(); + // Seed a synced entry (the current "winner"). + let body = r#"{"workspace_id":"local-personal","tool_kind":"password_entries","id":"e1","action":"apply-remote","doc":{"id":"e1","encryptedData":"WINNER","iv":"IV","updatedAt":500},"updated_at":500}"#; + route(&state, "POST", "/desktop/sync/resolve", Some(body)).unwrap(); + + // Archive the losing local version. + let conflict = r#"{"workspace_id":"local-personal","tool_kind":"password_entries","entry_id":"e1", + "loser_side":"local","loser_doc":{"id":"e1","encryptedData":"LOSER","iv":"IV","updatedAt":400}, + "loser_updated_at":400,"winner_updated_at":500}"#; + let r = route(&state, "POST", "/desktop/sync/conflict", Some(conflict)).unwrap(); + let cid = body_json(&r)["id"].as_str().unwrap().to_string(); + + // Restore: the loser becomes the live doc, dirty and re-clocked. + let restore = format!(r#"{{"id":"{cid}"}}"#); + let r = route(&state, "POST", "/desktop/sync/conflicts/restore", Some(&restore)).unwrap(); + assert_eq!(r.status, 200); + let r = route(&state, "GET", "/api/v1/password-manager/entries", None).unwrap(); + let entry = &body_json(&r)[0]; + assert_eq!(entry["encryptedData"], "LOSER"); + assert!(entry["updatedAt"].as_i64().unwrap() > 500, "clock bumped forward"); + + // Row is dirty again so it re-syncs, and the conflict is resolved. + let r = route(&state, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + assert_eq!(body_json(&r)[0]["dirty"], true); + let r = route(&state, "GET", "/desktop/sync/conflicts?workspace_id=local-personal", None).unwrap(); + assert_eq!(body_json(&r).as_array().unwrap().len(), 0); + // Restoring a resolved conflict is a 404. + assert_eq!(route(&state, "POST", "/desktop/sync/conflicts/restore", Some(&restore)).unwrap().status, 404); + } + + #[test] + fn backup_export_import_roundtrip() { + let src = AppState::in_memory(); + // Seed an entry + a configured master vault. + let r = route(&src, "POST", "/api/v1/password-manager/entries", Some(r#"{"encryptedData":"SECRET","iv":"IV"}"#)).unwrap(); + let id = body_json(&r)["id"].as_str().unwrap().to_string(); + let vault = r#"{"salt":"c2FsdA==","verifier":{"encrypted":"ZW5j","iv":"aXY="}}"#; + route(&src, "POST", "/api/v1/auth/master-vault", Some(vault)).unwrap(); + + // Export the dump. + let r = route(&src, "GET", "/desktop/backup/export", None).unwrap(); + assert_eq!(r.status, 200); + let dump = body_json(&r); + assert_eq!(dump["version"], 1); + assert_eq!(dump["entries"].as_array().unwrap().len(), 1); + // Envelope tools keep the secret in meta_json (encrypted_data column is unused). + assert!(dump["entries"][0]["meta_json"].as_str().unwrap().contains("SECRET")); + assert!(dump["kv"]["master_vault"].is_string()); + let dump_str = r.body; + + // Import into a fresh machine → entry + master vault land. + let dst = AppState::in_memory(); + let r = route(&dst, "POST", "/desktop/backup/import", Some(&dump_str)).unwrap(); + assert_eq!(r.status, 200); + assert_eq!(body_json(&r)["imported"], 1); + assert!(body_json(&r)["kv_imported"].as_u64().unwrap() >= 1); + let r = route(&dst, "GET", "/api/v1/password-manager/entries", None).unwrap(); + assert_eq!(body_json(&r)[0]["id"], id); + assert_eq!(body_json(&r)[0]["encryptedData"], "SECRET"); + // Restored row is dirty (re-syncs if sync is later enabled). + let r = route(&dst, "GET", "/desktop/sync/rows?workspace_id=local-personal&tool_kind=password_entries", None).unwrap(); + assert_eq!(body_json(&r)[0]["dirty"], true); + let r = route(&dst, "GET", "/api/v1/auth/master-vault", None).unwrap(); + assert_eq!(body_json(&r)["salt"], "c2FsdA=="); + + // Re-importing must NOT clobber an existing (different) master vault. + let other = r#"{"version":1,"created_at":0,"entries":[],"kv":{"master_vault":"{\"salt\":\"T1RIRVI=\"}"}}"#; + route(&dst, "POST", "/desktop/backup/import", Some(other)).unwrap(); + let r = route(&dst, "GET", "/api/v1/auth/master-vault", None).unwrap(); + assert_eq!(body_json(&r)["salt"], "c2FsdA==", "existing vault preserved"); + + // Bad version rejected. + let r = route(&dst, "POST", "/desktop/backup/import", Some(r#"{"version":99,"entries":[]}"#)).unwrap(); + assert_eq!(r.status, 422); + } + + #[test] + fn remote_only_prefix_rejected_503() { + let state = AppState::in_memory(); + let r = route(&state, "GET", "/api/v1/url-shortener?skip=0", None).unwrap(); + assert_eq!(r.status, 503); + let r = route(&state, "GET", "/api/v1/s3-drive/connections", None).unwrap(); + assert_eq!(r.status, 503); + } +} diff --git a/apps/desktop/src-tauri/src/router/notes.rs b/apps/desktop/src-tauri/src/router/notes.rs new file mode 100644 index 00000000..58441131 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/notes.rs @@ -0,0 +1,91 @@ +//! Local mirror of `/api/v1/notes` (plaintext docs, ISO-8601 timestamps). + +use serde_json::{json, Value}; + +use crate::error::Result; +use crate::router::entries::*; +use crate::router::ApiResponse; +use crate::state::AppState; + +const KIND: &str = "notes"; + +pub fn now_iso() -> String { + chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true) +} + +pub fn handle( + state: &AppState, + method: &str, + rest: &str, + query: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "") => { + let docs = list_docs(&db, KIND, &ws)?; + let skip: usize = query_param(query, "skip").and_then(|v| v.parse().ok()).unwrap_or(0); + let limit: usize = query_param(query, "limit") + .and_then(|v| v.parse().ok()) + .unwrap_or(usize::MAX); + let page: Vec<&Value> = docs.iter().skip(skip).take(limit).collect(); + ApiResponse::ok(&page) + } + ("POST", "") => { + let req = parse_body(body)?; + let now = now_iso(); + let mut doc = json!({ + "id": new_id(), + "title": "", + "content": null, + "parentId": null, + "pinned": false, + "tags": [], + "userId": "desktop-local", + "createdAt": now, + "updatedAt": now, + }); + merge_patch(&mut doc, &req); + insert_doc(&db, KIND, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("GET", _) if rest.starts_with('/') => match get_doc(&db, KIND, &ws, &rest[1..])? { + Some(doc) => ApiResponse::ok(&doc), + None => Ok(ApiResponse::detail(404, "Note not found")), + }, + ("PATCH", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let Some(mut doc) = get_doc(&db, KIND, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Note not found")); + }; + merge_patch(&mut doc, &parse_body(body)?); + doc["updatedAt"] = json!(now_iso()); + save_doc(&db, KIND, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let recursive = query_param(query, "recursive") == Some("true"); + if !tombstone(&db, KIND, &ws, id)? { + return Ok(ApiResponse::detail(404, "Note not found")); + } + if recursive { + // Tombstone all descendants (parentId chains). + let mut frontier = vec![id.to_string()]; + while let Some(parent) = frontier.pop() { + for doc in list_docs(&db, KIND, &ws)? { + if doc["parentId"].as_str() == Some(parent.as_str()) { + if let Some(child_id) = doc["id"].as_str() { + tombstone(&db, KIND, &ws, child_id)?; + frontier.push(child_id.to_string()); + } + } + } + } + } + Ok(ApiResponse::empty(204)) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/preferences.rs b/apps/desktop/src-tauri/src/router/preferences.rs new file mode 100644 index 00000000..8ad257a1 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/preferences.rs @@ -0,0 +1,165 @@ +//! Local mirror of `/api/v1/user-preferences` (per-user singleton in `kv`, +//! defaults synced with apps/backend/app/api/routes/user_preferences/schema.py). + +use rusqlite::OptionalExtension; +use serde_json::{json, Value}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::{merge_patch, parse_body, query_param}; +use crate::router::notes::now_iso; +use crate::router::ApiResponse; +use crate::state::AppState; + +const KV_KEY: &str = "user_preferences"; +const MAX_NOSQL_HISTORY_QUERIES: usize = 10; + +// Keep in sync with backend DEFAULT_ENABLED_TOOLS (user_preferences/schema.py) +const DEFAULT_ENABLED_TOOLS: &[&str] = &[ + "/app/to-do", + "/app/notes", + "/app/bookmarks", + "/app/snippet-manager", + "/app/password-manager", + "/app/environment-manager", + "/app/email-validator", + "/app/jwt-decoder", + "/app/encryption-playground", + "/app/certificate-pem-decoder", + "/app/json-formatter", + "/app/json-schema-generator", + "/app/sql-formatter", + "/app/graphql-formatter", + "/app/diff-checker", + "/app/regex-tester", + "/app/timestamp-converter", + "/app/cron-builder", + "/app/number-base-converter", + "/app/csv-excel-json", + "/app/api-client", + "/app/http-status-codes", + "/app/nosql-explorer", + "/app/url-encode", + "/app/uuid-generator", + "/app/secret-api-key-generator", + "/app/qr-code-generator", + "/app/ip-subnet-calculator", + "/app/hash-generator", + "/app/hmac-generator", + "/app/totp-generator", + "/app/lorem-ipsum", + "/app/color-picker", + "/app/contrast-checker", + "/app/markdown-preview-html", +]; + +fn defaults() -> Value { + json!({ + "theme": "system", + "accentColor": "blue", + "locale": "en", + "enabledTools": DEFAULT_ENABLED_TOOLS, + "toolFavorites": [], + "pinnedToolsByWorkspace": {}, + "toolStats": {}, + // updatedAt=0 marks an unconfigured (never-persisted) baseline so cloud + // sync (LWW) prefers the remote prefs until the user changes one here. + "createdAt": 0, + "updatedAt": 0, + }) +} + +fn load(db: &rusqlite::Connection) -> Result { + let raw: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [KV_KEY], |r| r.get(0)) + .optional()?; + Ok(match raw { + Some(v) => serde_json::from_str(&v)?, + None => defaults(), + }) +} + +fn save(db: &rusqlite::Connection, prefs: &Value) -> Result<()> { + let jsonv = serde_json::to_string(prefs)?; + db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", + [KV_KEY, &jsonv], + )?; + Ok(()) +} + +fn nosql_history_key(query_or_body: &Value) -> String { + format!( + "nosql_query_history:{}|{}|{}", + query_or_body["connectionName"].as_str().unwrap_or(""), + query_or_body["dbName"].as_str().unwrap_or(""), + query_or_body["collectionName"].as_str().unwrap_or(""), + ) +} + +pub fn handle( + state: &AppState, + method: &str, + rest: &str, // path after "/user-preferences" + query: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + match (method, rest) { + ("GET", "") => ApiResponse::ok(&load(&db)?), + ("PATCH", "") => { + let mut prefs = load(&db)?; + merge_patch(&mut prefs, &parse_body(body)?); + prefs["updatedAt"] = json!(now_ms()); + save(&db, &prefs)?; + ApiResponse::ok(&prefs) + } + ("POST", "/tool-usage") => { + let req = parse_body(body)?; + let Some(tool_id) = req["toolId"].as_str() else { + return Ok(ApiResponse::detail(422, "toolId required")); + }; + let mut prefs = load(&db)?; + let count = prefs["toolStats"][tool_id]["usageCount"].as_i64().unwrap_or(0); + prefs["toolStats"][tool_id] = json!({ "usageCount": count + 1, "lastUsed": now_iso() }); + prefs["updatedAt"] = json!(now_ms()); + save(&db, &prefs)?; + ApiResponse::ok(&json!({ "ok": true })) + } + ("GET", "/nosql-query-history") => { + let params = json!({ + "connectionName": query_param(query, "connectionName").unwrap_or(""), + "dbName": query_param(query, "dbName").unwrap_or(""), + "collectionName": query_param(query, "collectionName").unwrap_or(""), + }); + let key = nosql_history_key(¶ms); + let raw: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [&key], |r| r.get(0)) + .optional()?; + match raw { + Some(v) => Ok(ApiResponse { status: 200, body: v }), + None => { + let mut empty = params; + empty["queries"] = json!([]); + empty["updatedAt"] = json!(now_ms()); + ApiResponse::ok(&empty) + } + } + } + ("PUT", "/nosql-query-history") => { + let mut req = parse_body(body)?; + if let Some(queries) = req["queries"].as_array_mut() { + queries.truncate(MAX_NOSQL_HISTORY_QUERIES); + } + req["updatedAt"] = json!(now_ms()); + let key = nosql_history_key(&req); + let jsonv = serde_json::to_string(&req)?; + db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", + [&key, &jsonv], + )?; + ApiResponse::ok(&req) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/snippets.rs b/apps/desktop/src-tauri/src/router/snippets.rs new file mode 100644 index 00000000..7f55f05a --- /dev/null +++ b/apps/desktop/src-tauri/src/router/snippets.rs @@ -0,0 +1,64 @@ +//! Local mirror of `/api/v1/code-snippets` (plaintext docs). + +use serde_json::{json, Value}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::*; +use crate::router::ApiResponse; +use crate::state::AppState; + +const KIND: &str = "code_snippets"; + +pub fn handle(state: &AppState, method: &str, rest: &str, body: Option<&str>) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "") => ApiResponse::ok(&list_docs(&db, KIND, &ws)?), + ("POST", "") => { + let req = parse_body(body)?; + let now = now_ms(); + let id = req + .get("id") + .and_then(Value::as_str) + .map(str::to_string) + .unwrap_or_else(new_id); + if get_doc(&db, KIND, &ws, &id)?.is_some() { + return Ok(ApiResponse::detail(409, "Snippet with this id already exists")); + } + let mut doc = json!({ + "id": id, + "title": "", + "language": "", + "code": "", + "tags": [], + "pinned": false, + "createdAt": now, + "updatedAt": now, + }); + merge_patch(&mut doc, &req); + doc["id"] = json!(doc["id"].as_str().unwrap_or_default()); + insert_doc(&db, KIND, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("PATCH", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let Some(mut doc) = get_doc(&db, KIND, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Snippet not found")); + }; + merge_patch(&mut doc, &parse_body(body)?); + doc["updatedAt"] = json!(now_ms()); + save_doc(&db, KIND, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with('/') => { + let id = &rest[1..]; + if tombstone(&db, KIND, &ws, id)? { + Ok(ApiResponse::empty(204)) + } else { + Ok(ApiResponse::detail(404, "Snippet not found")) + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/stubs.rs b/apps/desktop/src-tauri/src/router/stubs.rs new file mode 100644 index 00000000..e1f1fd97 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/stubs.rs @@ -0,0 +1,12 @@ +//! Loud 501 for any unmapped path — gaps must be visible, never silent. + +use crate::error::Result; +use crate::router::ApiResponse; + +pub fn handle(method: &str, path: &str) -> Result { + eprintln!("[local_api] 501 unmapped route: {method} {path}"); + Ok(ApiResponse::detail( + 501, + &format!("Local route not implemented: {method} {path}"), + )) +} diff --git a/apps/desktop/src-tauri/src/router/sync.rs b/apps/desktop/src-tauri/src/router/sync.rs new file mode 100644 index 00000000..a41add6a --- /dev/null +++ b/apps/desktop/src-tauri/src/router/sync.rs @@ -0,0 +1,251 @@ +//! Internal sync endpoints (`/desktop/sync/*`) — NOT FastAPI mirrors. +//! +//! The sync engine runs in the webview (TS owns the REST adapters and any +//! crypto context); Rust exposes raw row state: dirty rows, tombstones, and +//! resolution actions (mark-synced / apply-remote / remove-local). + +use rusqlite::{params, OptionalExtension}; +use serde_json::{json, Value}; + +use crate::db::now_ms; +use crate::error::Result; +use crate::router::entries::{new_id, parse_body, query_param}; +use crate::router::ApiResponse; +use crate::state::AppState; + +pub fn handle( + state: &AppState, + method: &str, + rest: &str, // path after "/desktop/sync" + query: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + match (method, rest) { + ("GET", "/settings") => { + let ws = query_param(query, "workspace_id").unwrap_or("local-personal"); + let key = format!("sync_enabled:{ws}"); + let enabled: Option = db + .query_row("SELECT v FROM kv WHERE k = ?1", [&key], |r| r.get(0)) + .optional()?; + ApiResponse::ok(&json!({ "enabled": enabled.as_deref() == Some("1") })) + } + ("POST", "/settings") => { + let req = parse_body(body)?; + let ws = req["workspace_id"].as_str().unwrap_or("local-personal"); + let enabled = req["enabled"].as_bool().unwrap_or(false); + let key = format!("sync_enabled:{ws}"); + db.execute( + "INSERT INTO kv (k, v) VALUES (?1, ?2) ON CONFLICT(k) DO UPDATE SET v = excluded.v", + params![key, if enabled { "1" } else { "0" }], + )?; + if !enabled { + // Detach: forget sync state so a later re-enable does a fresh + // additive merge instead of inferring remote deletions. + db.execute( + "UPDATE entries SET last_synced_at = NULL WHERE workspace_id = ?1", + [ws], + )?; + db.execute("DELETE FROM sync_state WHERE workspace_id = ?1", [ws])?; + } + ApiResponse::ok(&json!({ "enabled": enabled })) + } + ("GET", "/rows") => { + let ws = query_param(query, "workspace_id").unwrap_or("local-personal"); + let kind = query_param(query, "tool_kind").unwrap_or(""); + let mut stmt = db.prepare( + "SELECT id, meta_json, updated_at, deleted_at, dirty, last_synced_at + FROM entries WHERE workspace_id = ?1 AND tool_kind = ?2", + )?; + let rows: Vec = stmt + .query_map(params![ws, kind], |r| { + let doc_raw: String = r.get(1)?; + Ok(json!({ + "id": r.get::<_, String>(0)?, + "doc": serde_json::from_str::(&doc_raw).unwrap_or(Value::Null), + "updated_at": r.get::<_, i64>(2)?, + "deleted_at": r.get::<_, Option>(3)?, + "dirty": r.get::<_, i64>(4)? == 1, + "last_synced_at": r.get::<_, Option>(5)?, + })) + })? + .collect::>()?; + ApiResponse::ok(&rows) + } + // Archive the losing side of a concurrent edit. Called by the engine + // right before it overwrites the loser, so the discarded version stays + // recoverable. `loser_doc` is stored verbatim (ciphertext for vaults). + ("POST", "/conflict") => { + let req = parse_body(body)?; + let ws = req["workspace_id"].as_str().unwrap_or("local-personal"); + let kind = req["tool_kind"].as_str().unwrap_or(""); + let entry_id = req["entry_id"].as_str().unwrap_or(""); + let side = match req["loser_side"].as_str() { + Some(s @ ("local" | "remote")) => s, + _ => return Ok(ApiResponse::detail(422, "loser_side must be 'local' or 'remote'")), + }; + let loser_doc = serde_json::to_string(&req["loser_doc"])?; + let loser_ts = req["loser_updated_at"].as_i64().unwrap_or(0); + let winner_ts = req["winner_updated_at"].as_i64().unwrap_or(0); + let cid = new_id(); + db.execute( + "INSERT INTO conflicts + (id, tool_kind, workspace_id, entry_id, loser_side, loser_doc, + loser_updated_at, winner_updated_at, created_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![cid, kind, ws, entry_id, side, loser_doc, loser_ts, winner_ts, now_ms()], + )?; + ApiResponse::ok(&json!({ "id": cid })) + } + // List unresolved conflicts (newest first) for a review UI. + ("GET", "/conflicts") => { + let ws = query_param(query, "workspace_id").unwrap_or("local-personal"); + let mut stmt = db.prepare( + "SELECT id, tool_kind, entry_id, loser_side, loser_doc, + loser_updated_at, winner_updated_at, created_at + FROM conflicts WHERE workspace_id = ?1 AND resolved_at IS NULL + ORDER BY created_at DESC", + )?; + let rows: Vec = stmt + .query_map([ws], |r| { + let doc_raw: String = r.get(4)?; + Ok(json!({ + "id": r.get::<_, String>(0)?, + "tool_kind": r.get::<_, String>(1)?, + "entry_id": r.get::<_, String>(2)?, + "loser_side": r.get::<_, String>(3)?, + "loser_doc": serde_json::from_str::(&doc_raw).unwrap_or(Value::Null), + "loser_updated_at": r.get::<_, i64>(5)?, + "winner_updated_at": r.get::<_, i64>(6)?, + "created_at": r.get::<_, i64>(7)?, + })) + })? + .collect::>()?; + ApiResponse::ok(&rows) + } + // Dismiss one conflict (the user reviewed it) — keeps it for audit but + // hides it from the open list. + ("POST", "/conflicts/dismiss") => { + let req = parse_body(body)?; + let id = req["id"].as_str().unwrap_or(""); + let n = db.execute( + "UPDATE conflicts SET resolved_at = ?2 WHERE id = ?1 AND resolved_at IS NULL", + params![id, now_ms()], + )?; + if n == 0 { + return Ok(ApiResponse::detail(404, "Conflict not found")); + } + ApiResponse::ok(&json!({ "ok": true })) + } + // Restore a discarded version: re-apply the loser as the live doc with a + // fresh clock so it wins the next sync round, then mark the conflict + // resolved. Keeps last_synced_at intact so the push updates (not + // recreates) the existing remote row. + ("POST", "/conflicts/restore") => { + let req = parse_body(body)?; + let id = req["id"].as_str().unwrap_or(""); + let found: Option<(String, String, String, String)> = db + .query_row( + "SELECT tool_kind, workspace_id, entry_id, loser_doc + FROM conflicts WHERE id = ?1 AND resolved_at IS NULL", + [id], + |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)), + ) + .optional()?; + let Some((kind, ws, entry_id, loser_raw)) = found else { + return Ok(ApiResponse::detail(404, "Conflict not found")); + }; + let now = now_ms(); + // Bump the doc's own numeric clock fields so the pushed update carries + // a fresh timestamp (envelope tools send updatedAt on PATCH). + let mut doc: Value = serde_json::from_str(&loser_raw).unwrap_or(Value::Null); + if let Some(obj) = doc.as_object_mut() { + for k in ["updatedAt", "lastUsedAt"] { + if obj.get(k).is_some_and(Value::is_number) { + obj.insert(k.to_string(), json!(now)); + } + } + } + let doc_str = serde_json::to_string(&doc)?; + db.execute( + "INSERT INTO entries + (tool_kind, workspace_id, id, meta_json, created_at, updated_at, deleted_at, dirty) + VALUES (?1, ?2, ?3, ?4, ?5, ?5, NULL, 1) + ON CONFLICT(tool_kind, workspace_id, id) DO UPDATE SET + meta_json = excluded.meta_json, updated_at = excluded.updated_at, + deleted_at = NULL, dirty = 1", + params![kind, ws, entry_id, doc_str, now], + )?; + db.execute( + "UPDATE conflicts SET resolved_at = ?2 WHERE id = ?1", + params![id, now], + )?; + ApiResponse::ok(&json!({ "ok": true, "tool_kind": kind, "entry_id": entry_id })) + } + ("POST", "/resolve") => { + let req = parse_body(body)?; + let ws = req["workspace_id"].as_str().unwrap_or("local-personal"); + let kind = req["tool_kind"].as_str().unwrap_or(""); + let id = req["id"].as_str().unwrap_or(""); + match req["action"].as_str() { + Some("mark-synced") => { + let tombstoned: Option> = db + .query_row( + "SELECT deleted_at FROM entries + WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3", + params![kind, ws, id], + |r| r.get(0), + ) + .optional()?; + match tombstoned { + Some(Some(_)) => { + // Tombstone acknowledged remotely — row can go. + db.execute( + "DELETE FROM entries WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3", + params![kind, ws, id], + )?; + } + Some(None) => { + let new_id = req["new_id"].as_str().unwrap_or(id); + let new_doc = if req["new_doc"].is_object() { + Some(serde_json::to_string(&req["new_doc"])?) + } else { + None + }; + db.execute( + "UPDATE entries SET id = ?4, meta_json = COALESCE(?5, meta_json), + dirty = 0, last_synced_at = ?6 + WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3", + params![kind, ws, id, new_id, new_doc, now_ms()], + )?; + } + None => return Ok(ApiResponse::detail(404, "Row not found")), + } + ApiResponse::ok(&json!({ "ok": true })) + } + Some("apply-remote") => { + let doc = serde_json::to_string(&req["doc"])?; + let updated_at = req["updated_at"].as_i64().unwrap_or_else(now_ms); + db.execute( + "INSERT INTO entries (tool_kind, workspace_id, id, meta_json, created_at, updated_at, deleted_at, dirty, last_synced_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?5, NULL, 0, ?6) + ON CONFLICT(tool_kind, workspace_id, id) DO UPDATE SET + meta_json = excluded.meta_json, updated_at = excluded.updated_at, + deleted_at = NULL, dirty = 0, last_synced_at = excluded.last_synced_at", + params![kind, ws, id, doc, updated_at, now_ms()], + )?; + ApiResponse::ok(&json!({ "ok": true })) + } + Some("remove-local") => { + db.execute( + "DELETE FROM entries WHERE tool_kind = ?1 AND workspace_id = ?2 AND id = ?3", + params![kind, ws, id], + )?; + ApiResponse::ok(&json!({ "ok": true })) + } + _ => Ok(ApiResponse::detail(422, "Unknown action")), + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/tasks.rs b/apps/desktop/src-tauri/src/router/tasks.rs new file mode 100644 index 00000000..85529209 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/tasks.rs @@ -0,0 +1,201 @@ +//! Local mirror of `/api/v1/tasks` + `/api/v1/projects` (plaintext, ISO timestamps). + +use serde_json::{json, Value}; + +use crate::error::Result; +use crate::router::entries::*; +use crate::router::notes::now_iso; +use crate::router::ApiResponse; +use crate::state::AppState; + +const TASKS: &str = "tasks"; +const PROJECTS: &str = "projects"; + +fn matches_filter(doc: &Value, query: &str) -> bool { + let status = query_param(query, "status").unwrap_or("all"); + if status != "all" && !status.is_empty() && doc["status"].as_str() != Some(status) { + return false; + } + let project = query_param(query, "projectId").unwrap_or("all"); + if project != "all" && !project.is_empty() && doc["projectId"].as_str() != Some(project) { + return false; + } + let assignee = query_param(query, "assignee").unwrap_or("all"); + match assignee { + "all" | "" => {} + "unassigned" => { + if doc["assigneeUid"].as_str().map(|s| !s.is_empty()).unwrap_or(false) { + return false; + } + } + // "me" and explicit uids both resolve to the local user on desktop. + _ => { + let uid = doc["assigneeUid"].as_str().unwrap_or(""); + if assignee != "me" && uid != assignee { + return false; + } + } + } + let archived = query_param(query, "archived").unwrap_or("false") == "true"; + let doc_archived = doc["archived"].as_bool().unwrap_or(false); + if archived != doc_archived { + return false; + } + true +} + +pub fn handle_tasks( + state: &AppState, + method: &str, + rest: &str, + query: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "") => { + let docs = list_docs(&db, TASKS, &ws)?; + let filtered: Vec<&Value> = docs.iter().filter(|d| matches_filter(d, query)).collect(); + let page: usize = query_param(query, "page").and_then(|v| v.parse().ok()).unwrap_or(1); + let page_size: usize = query_param(query, "pageSize") + .and_then(|v| v.parse().ok()) + .unwrap_or(50); + let total = filtered.len(); + let total_pages = total.div_ceil(page_size).max(1); + let items: Vec<&&Value> = + filtered.iter().skip((page.max(1) - 1) * page_size).take(page_size).collect(); + ApiResponse::ok(&json!({ + "items": items, + "total": total, + "total_pages": total_pages, + "page": page, + "page_size": page_size, + })) + } + ("GET", "/stats") => { + let docs = list_docs(&db, TASKS, &ws)?; + let active: Vec<&Value> = docs + .iter() + .filter(|d| !d["archived"].as_bool().unwrap_or(false)) + .collect(); + let count = |s: &str| active.iter().filter(|d| d["status"].as_str() == Some(s)).count(); + ApiResponse::ok(&json!({ + "total": active.len(), + "completed": count("completed"), + "ongoing": count("ongoing"), + "notStarted": count("not-started"), + })) + } + ("GET", "/export") => { + let docs = list_docs(&db, TASKS, &ws)?; + let filtered: Vec<&Value> = docs.iter().filter(|d| matches_filter(d, query)).collect(); + let skip: usize = query_param(query, "skip").and_then(|v| v.parse().ok()).unwrap_or(0); + let limit: usize = query_param(query, "limit") + .and_then(|v| v.parse().ok()) + .unwrap_or(usize::MAX); + let page: Vec<&&Value> = filtered.iter().skip(skip).take(limit).collect(); + ApiResponse::ok(&page) + } + ("POST", "") => { + let req = parse_body(body)?; + let docs = list_docs(&db, TASKS, &ws)?; + let max_order = docs + .iter() + .filter(|d| d["status"].as_str() == Some("not-started")) + .filter_map(|d| d["statusOrder"].as_i64()) + .max() + .unwrap_or(-1); + let mut doc = json!({ + "id": new_id(), + "text": "", + "status": "not-started", + "statusOrder": max_order + 1, + "tags": [], + "subTasks": [], + "archived": false, + "createdAt": now_iso(), + "created_by": "desktop-local", + }); + merge_patch(&mut doc, &req); + insert_doc(&db, TASKS, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("POST", "/import") => { + let req = parse_body(body)?; + for t in req["tasks"].as_array().unwrap_or(&Vec::new()) { + let mut doc = json!({ + "id": new_id(), + "status": "not-started", + "statusOrder": 0, + "archived": false, + "createdAt": now_iso(), + "created_by": "desktop-local", + }); + merge_patch(&mut doc, t); + insert_doc(&db, TASKS, &ws, doc["id"].as_str().unwrap(), &doc)?; + } + Ok(ApiResponse::detail(200, "ok")) + } + ("PATCH", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let Some(mut doc) = get_doc(&db, TASKS, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Task not found")); + }; + merge_patch(&mut doc, &parse_body(body)?); + save_doc(&db, TASKS, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with('/') => { + if tombstone(&db, TASKS, &ws, &rest[1..])? { + Ok(ApiResponse::empty(204)) + } else { + Ok(ApiResponse::detail(404, "Task not found")) + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} + +pub fn handle_projects( + state: &AppState, + method: &str, + rest: &str, + body: Option<&str>, +) -> Result { + let db = state.db.lock().unwrap(); + let ws = active_workspace(&db); + match (method, rest) { + ("GET", "") => ApiResponse::ok(&list_docs(&db, PROJECTS, &ws)?), + ("POST", "") => { + let req = parse_body(body)?; + let mut doc = json!({ + "id": new_id(), + "name": "", + "color": "", + "created_by": "desktop-local", + "createdAt": now_iso(), + }); + merge_patch(&mut doc, &req); + insert_doc(&db, PROJECTS, &ws, doc["id"].as_str().unwrap(), &doc)?; + ApiResponse::ok(&doc) + } + ("PATCH", _) if rest.starts_with('/') => { + let id = &rest[1..]; + let Some(mut doc) = get_doc(&db, PROJECTS, &ws, id)? else { + return Ok(ApiResponse::detail(404, "Project not found")); + }; + merge_patch(&mut doc, &parse_body(body)?); + save_doc(&db, PROJECTS, &ws, id, &doc)?; + ApiResponse::ok(&doc) + } + ("DELETE", _) if rest.starts_with('/') => { + if tombstone(&db, PROJECTS, &ws, &rest[1..])? { + Ok(ApiResponse::empty(204)) + } else { + Ok(ApiResponse::detail(404, "Project not found")) + } + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/router/workspaces.rs b/apps/desktop/src-tauri/src/router/workspaces.rs new file mode 100644 index 00000000..b0a8e078 --- /dev/null +++ b/apps/desktop/src-tauri/src/router/workspaces.rs @@ -0,0 +1,63 @@ +//! Local mirror of FastAPI `/api/v1/workspaces-api/*`. +//! +//! Offline there is exactly one org and one personal workspace, synthesized to +//! match the shapes in apps/web/src/lib/workspace-api.ts. + +use serde_json::json; + +use crate::error::Result; +use crate::router::ApiResponse; +use crate::state::AppState; + +pub const LOCAL_ORG_ID: &str = "local-org"; +pub const LOCAL_WORKSPACE_ID: &str = "local-personal"; + +fn local_org() -> serde_json::Value { + json!({ + "id": LOCAL_ORG_ID, + "name": "This Mac", + "slug": "local", + "kind": "user", + "org_role": "owner", + }) +} + +fn local_workspace() -> serde_json::Value { + json!({ + "id": LOCAL_WORKSPACE_ID, + "org_id": LOCAL_ORG_ID, + "name": "Personal", + "slug": "personal-local", + "is_personal": true, + "kind": "personal", + "ws_role": "admin", + "settings": { "encryption": null }, + }) +} + +pub fn handle(state: &AppState, method: &str, path: &str, body: Option<&str>) -> Result { + match (method, path) { + ("GET", "/api/v1/workspaces-api/orgs") => ApiResponse::ok(&json!([local_org()])), + ("GET", "/api/v1/workspaces-api/workspaces") => { + ApiResponse::ok(&json!([local_workspace()])) + } + ("POST", "/api/v1/workspaces-api/workspaces/active") => { + #[derive(serde::Deserialize)] + struct Req { + workspace_id: String, + } + let req: Req = serde_json::from_str(body.unwrap_or(""))?; + let db = state.db.lock().unwrap(); + db.execute( + "INSERT INTO kv (k, v) VALUES ('active_workspace', ?1) + ON CONFLICT(k) DO UPDATE SET v = excluded.v", + [&req.workspace_id], + )?; + Ok(ApiResponse::detail(200, "ok")) + } + ("GET", p) if p.starts_with("/api/v1/workspaces-api/workspaces/") => { + ApiResponse::ok(&local_workspace()) + } + _ => Ok(ApiResponse::detail(404, "Not found")), + } +} diff --git a/apps/desktop/src-tauri/src/state.rs b/apps/desktop/src-tauri/src/state.rs new file mode 100644 index 00000000..93634ace --- /dev/null +++ b/apps/desktop/src-tauri/src/state.rs @@ -0,0 +1,28 @@ +use std::path::PathBuf; +use std::sync::Mutex; + +use crate::db; +use crate::error::Result; +use crate::http::remote::RemoteHttp; + +pub struct AppState { + pub db: Mutex, + pub http: RemoteHttp, +} + +impl AppState { + pub fn init(db_path: PathBuf) -> Result { + let conn = db::open(&db_path)?; + let http = RemoteHttp::init(&conn)?; + Ok(Self { db: Mutex::new(conn), http }) + } + + /// In-memory state for tests (no Keychain, no SQLCipher key). + #[cfg(test)] + pub fn in_memory() -> Self { + let conn = rusqlite::Connection::open_in_memory().unwrap(); + db::migrations::run(&conn).unwrap(); + let http = RemoteHttp::init(&conn).unwrap(); + Self { db: Mutex::new(conn), http } + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..1c4c40a3 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "MyDevTools", + "version": "0.1.0", + "identifier": "tech.mydevtools.desktop", + "build": { + "beforeDevCommand": "pnpm --filter @mydevtools/web dev:tauri", + "devUrl": "http://localhost:3000", + "beforeBuildCommand": "node ../web/scripts/build-tauri.mjs", + "frontendDist": "../../web/out" + }, + "app": { + "windows": [ + { + "title": "MyDevTools", + "width": 1440, + "height": 900, + "minWidth": 1024, + "minHeight": 700 + } + ], + "security": { + "csp": null + } + }, + "plugins": { + "deep-link": { + "desktop": { + "schemes": ["mydevtools"] + } + } + }, + "bundle": { + "active": true, + "targets": ["app", "dmg"], + "icon": ["icons/icon.icns"], + "macOS": { + "minimumSystemVersion": "12.0" + } + } +} diff --git a/apps/extension/background.js b/apps/extension/background.js deleted file mode 100644 index bd56e893..00000000 --- a/apps/extension/background.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * mydevtools companion service worker. - * - * Listens for `webRequest.onBeforeSendHeaders` + `onCompleted` and stages the - * request+response pair. Popup picks one and forwards to the mydevtools tab. - */ - -const RECENT_CAP = 50 -const recent = [] - -chrome.webRequest.onBeforeSendHeaders.addListener( - (details) => { - if (details.type === "main_frame") return - if (!details.url.startsWith("http")) return - const headers = {} - for (const h of details.requestHeaders ?? []) headers[h.name] = h.value - recent.unshift({ - id: details.requestId, - method: details.method, - url: details.url, - headers, - timestamp: Date.now(), - }) - recent.length = Math.min(recent.length, RECENT_CAP) - }, - { urls: [""] }, - ["requestHeaders", "extraHeaders"], -) - -chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg?.type === "list-recent") { - sendResponse({ recent }) - return true - } - if (msg?.type === "send-to-mydevtools") { - forwardToMydevtools(msg.payload).then((ok) => sendResponse({ ok })) - return true - } - return false -}) - -async function forwardToMydevtools(payload) { - const tabs = await chrome.tabs.query({ url: ["https://mydevtools.tech/app/api-client*", "http://localhost:*/app/api-client*"] }) - if (tabs.length === 0) { - await chrome.tabs.create({ url: "https://mydevtools.tech/app/api-client" }) - return false - } - const tab = tabs[0] - await chrome.tabs.sendMessage(tab.id, { type: "mdt-extension-import", payload }) - await chrome.tabs.update(tab.id, { active: true }) - return true -} diff --git a/apps/extension/content-bridge.js b/apps/extension/content-bridge.js deleted file mode 100644 index c8fe49d0..00000000 --- a/apps/extension/content-bridge.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Content script injected into mydevtools.tech tabs. - * Relays extension imports to the page via window.postMessage. The web app - * listens for `kind: "mdt-extension-import"` and stages a new tab. - */ - -chrome.runtime.onMessage.addListener((msg) => { - if (msg?.type !== "mdt-extension-import") return - window.postMessage({ kind: "mdt-extension-import", payload: msg.payload }, window.location.origin) -}) diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json deleted file mode 100644 index 4c996ed4..00000000 --- a/apps/extension/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "manifest_version": 3, - "name": "mydevtools API client companion", - "version": "0.1.0", - "description": "Capture network requests in any tab and ship them to mydevtools.tech/app/api-client.", - "permissions": ["activeTab", "scripting", "storage", "webRequest"], - "host_permissions": [""], - "action": { - "default_title": "Capture this request", - "default_popup": "popup.html" - }, - "background": { - "service_worker": "background.js", - "type": "module" - }, - "content_scripts": [ - { - "matches": ["https://mydevtools.tech/*", "https://*.mydevtools.tech/*", "http://localhost:*/*"], - "js": ["content-bridge.js"], - "run_at": "document_idle" - } - ] -} diff --git a/apps/extension/popup.html b/apps/extension/popup.html deleted file mode 100644 index 615c5f84..00000000 --- a/apps/extension/popup.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Capture requests - - - -

Recent requests

-
Loading…
- - - diff --git a/apps/extension/popup.js b/apps/extension/popup.js deleted file mode 100644 index c6f693d5..00000000 --- a/apps/extension/popup.js +++ /dev/null @@ -1,23 +0,0 @@ -chrome.runtime.sendMessage({ type: "list-recent" }, (resp) => { - const list = document.getElementById("list") - list.innerHTML = "" - if (!resp?.recent || resp.recent.length === 0) { - list.textContent = "No captured requests yet. Trigger one in the page." - return - } - for (const r of resp.recent) { - const row = document.createElement("div") - row.className = "row" - row.innerHTML = ` - ${r.method} - ${r.url} - - ` - row.querySelector("button").addEventListener("click", () => { - chrome.runtime.sendMessage({ type: "send-to-mydevtools", payload: r }, (resp) => { - if (resp?.ok) window.close() - }) - }) - list.appendChild(row) - } -}) diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 86b97de7..ff0845d1 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -14,20 +14,29 @@ function getAppBuildId(): string { ); } +/** Tauri desktop build: static export loaded from the app bundle (see scripts/build-tauri.mjs). */ +const isTauriBuild = process.env.TAURI_BUILD === '1'; + const nextConfig: NextConfig = { + ...(isTauriBuild ? { output: 'export' as const } : {}), env: { NEXT_PUBLIC_APP_BUILD_ID: getAppBuildId(), }, - async redirects() { - return [ - { - source: '/app/certificate-pem', - destination: '/app/certificate-pem-decoder', - permanent: true, - }, - ]; - }, + ...(isTauriBuild + ? {} + : { + async redirects() { + return [ + { + source: '/app/certificate-pem', + destination: '/app/certificate-pem-decoder', + permanent: true, + }, + ]; + }, + }), images: { + ...(isTauriBuild ? { unoptimized: true } : {}), remotePatterns: [ { protocol: 'https', diff --git a/apps/web/package.json b/apps/web/package.json index 515a02ed..ce430704 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,7 +13,9 @@ "lint": "eslint .", "test": "jest", "test:watch": "jest --watch", - "clean-install": "rimraf node_modules && pnpm install --filter @mydevtools/web..." + "clean-install": "rimraf node_modules && pnpm install --filter @mydevtools/web...", + "build:tauri": "node scripts/build-tauri.mjs", + "dev:tauri": "NEXT_PUBLIC_TAURI=1 next dev" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -52,6 +54,9 @@ "@tabler/icons-react": "^3.35.0", "@tanstack/react-query": "^5", "@tanstack/react-virtual": "^3.14.3", + "@tauri-apps/api": "^2.11.1", + "@tauri-apps/plugin-deep-link": "^2.4.9", + "@tauri-apps/plugin-opener": "^2.5.4", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", "bcryptjs": "^3.0.3", diff --git a/apps/web/public/gitignore-templates.json b/apps/web/public/gitignore-templates.json new file mode 100644 index 00000000..bf8fc071 --- /dev/null +++ b/apps/web/public/gitignore-templates.json @@ -0,0 +1 @@ +{"list":["1c","1c-bitrix","a-frame","actionscript","ada","adobe","advancedinstaller","adventuregamestudio","agda","al","alteraquartusii","altium","amplify","android","androidstudio","angular","anjuta","ansible","ansibletower","apachecordova","apachehadoop","appbuilder","appceleratortitanium","appcode","appcode+all","appcode+iml","appengine","aptanastudio","arcanist","archive","archives","archlinuxpackages","asdf","aspnetcore","assembler","astro","ate","atmelstudio","ats","audio","autohotkey","automationstudio","autotools","autotools+strict","awr","azurefunctions","azurite","backup","ballerina","basercms","basic","batch","bazaar","bazel","bitrise","bitrix","bittorrent","blackbox","blender","bloop","bluej","bookdown","bower","bricxcc","buck","c","c++","cake","cakephp","cakephp2","cakephp3","calabash","carthage","certificates","ceylon","cfwheels","chefcookbook","chocolatey","circuitpython","clean","clion","clion+all","clion+iml","clojure","cloud9","cmake","cocoapods","cocos2dx","cocoscreator","codeblocks","codecomposerstudio","codeigniter","codeio","codekit","codesniffer","coffeescript","commonlisp","compodoc","composer","compressed","compressedarchive","compression","conan","concrete5","coq","cordova","craftcms","crashlytics","crbasic","crossbar","crystal","cs-cart","csharp","cuda","cvs","cypressio","d","dart","darteditor","data","database","datarecovery","dbeaver","dbt","defold","delphi","deno","dframe","diff","direnv","diskimage","django","dm","docfx","docpress","docusaurus","docz","dotenv","dotfilessh","dotnetcore","dotsettings","doxygen","dreamweaver","dropbox","drupal","drupal7","drupal8","e2studio","eagle","easybook","eclipse","eiffelstudio","elasticbeanstalk","elisp","elixir","elm","emacs","ember","ensime","episerver","erlang","espresso","executable","exercism","expressionengine","extjs","fancy","fastlane","finale","firebase","fish","flashbuilder","flask","flatpak","flex","flexbuilder","floobits","flutter","font","fontforge","forcedotcom","forgegradle","fortran","freecad","freepascal","fsharp","fuelphp","fusetools","games","gatsby","gcov","genero4gl","geth","ggts","gis","git","gitbook","go","godot","goland","goland+all","goland+iml","goodsync","gpg","gradle","grails","greenfoot","groovy","grunt","gwt","haskell","helm","hexo","hol","homeassistant","homebrew","hsp","hugo","hyperledgercomposer","iar","iar_ewarm","iarembeddedworkbench","idapro","idris","igorpro","images","infer","inforcms","inforcrm","intellij","intellij+all","intellij+iml","ionic3","jabref","janet","java","jboss","jboss-4-2-3-ga","jboss-6-x","jboss4","jboss6","jdeveloper","jekyll","jenv","jetbrains","jetbrains+all","jetbrains+iml","jgiven","jigsaw","jmeter","joe","joomla","jsonnet","jspm","julia","jupyternotebooks","justcode","kaldi","kate","kdevelop4","kdiff3","keil","kentico","keystonejs","kicad","kirby2","kirby3","kobalt","kohana","komodoedit","konyvisualizer","kotlin","labview","labviewnxg","lamp","laravel","latex","lazarus","leiningen","lemonstand","less","liberosoc","librarian-chef","libreoffice","lighthouseci","lilypond","linux","lithium","localstack","logtalk","lsspice","ltspice","lua","lyx","macos","magento","magento1","magento2","magic-xpa","matlab","maven","mavensmate","mdbook","mean","mercurial","mercury","meson","metals","metalsmith","metaprogrammingsystem","meteor","meteorjs","microsoftoffice","mikroc","mill","moban","modelsim","modx","momentics","monodevelop","mplabx","mule","nanoc","nativescript","ncrunch","nesc","netbeans","nette","nextjs","nikola","nim","ninja","node","nodechakratimetraveldebug","nohup","notepadpp","nova","now","nuxtjs","nwjs","objective-c","obsidian","ocaml","octave","octobercms","opa","opencart","opencv","openfoam","openframeworks","openframeworks+visualstudio","oracleforms","orcad","osx","otto","oxideshop","oxygenxmleditor","packer","pants","particle","patch","pawn","perl","perl6","ph7cms","phalcon","phoenix","php-cs-fixer","phpcodesniffer","phpstorm","phpstorm+all","phpstorm+iml","phpunit","pico8","pimcore","pimcore4","pimcore5","pinegrow","platformio","playframework","plone","polymer","powershell","premake-gmake","prepros","prestashop","processing","progressabl","psoccreator","pulumi","pulumi+stacks","puppet","puppet-librarian","purebasic","purescript","putty","pvs","pycharm","pycharm+all","pycharm+iml","pydev","python","pythonvanilla","qml","qooxdoo","qt","qtcreator","r","racket","rails","react","reactnative","reasonml","red","redcar","redis","remix","remix+arc","remix+cloudflarepages","remix+cloudflareworkers","remix+netlify","remix+vercel","renpy","replit","retool","rhodesrhomobile","rider","robotframework","root","ros","ros2","ruby","rubymine","rubymine+all","rubymine+iml","rust","rust-analyzer","salesforce","salesforcedx","sam","sam+config","sas","sass","sbt","scala","scheme","scons","scrivener","sdcc","seamgen","senchatouch","serverless","shopware","silverstripe","sketchup","slickedit","smalltalk","snap","snapcraft","snyk","solidity","soliditytruffle","sonar","sonarqube","sourcepawn","spark","specflow","splunk","spreadsheet","ssh","standardml","stata","stdlib","stella","stellar","storybookjs","strapi","stylus","sublimetext","sugarcrm","svelte","svn","swift","swiftpackagemanager","swiftpm","symfony","symphonycms","synology","synopsysvcs","tags","tarmainstallmate","terraform","terragrunt","test","testcomplete","testinfra","tex","text","textmate","textpattern","theos-tweak","thinkphp","tla+","toit","tortoisegit","tower","turbo","turbogears2","twincat3","tye","typings","typo3","typo3-composer","umbraco","unity","unrealengine","vaadin","vagrant","valgrind","vapor","vcpkg","venv","vercel","vertx","video","vim","virtualenv","virtuoso","visualbasic","visualstudio","visualstudiocode","vivado","vlab","vrealizeorchestrator","vs","vue","vuejs","vvvv","waf","wakanda","web","webmethods","webstorm","webstorm+all","webstorm+iml","werckercli","windows","wintersmith","wordpress","wyam","xamarinstudio","xcode","xcodeinjection","xilinx","xilinxise","xilinxvivado","xill","xmake","xojo","xtext","y86","yalc","yarn","yeoman","yii","yii2","zendframework","zephir","zig","zsh","zukencr8000"],"templates":{"1c":"# Created by https://www.toptal.com/developers/gitignore/api/1c\n# Edit at https://www.toptal.com/developers/gitignore?templates=1c\n\n### 1C ###\n# 1C external data processors on unmanaged formes\n\n**/Form/**/form\nsrc/**/und/*\nsrc/**/maps.txt\nsrc/**/renames.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/1c\n","1c-bitrix":"# Created by https://www.toptal.com/developers/gitignore/api/1c-bitrix\n# Edit at https://www.toptal.com/developers/gitignore?templates=1c-bitrix\n\n### 1C-Bitrix ###\n# core\n/bitrix/\n\n# uploaded files\n/upload/\n\n# other files\ntest.php\n*.zip\n*.gzip\n*.gz\n*.tgz\n*.tar\n*.gz.[0-9]\n*.sql\n*.log\n*.mp3\n*.exe\n*.db\n*.csv\n*.xml\n*.psd\n*.[oa]\n*~\n\n# End of https://www.toptal.com/developers/gitignore/api/1c-bitrix\n","a-frame":"# Created by https://www.toptal.com/developers/gitignore/api/a-frame\n# Edit at https://www.toptal.com/developers/gitignore?templates=a-frame\n\n### A-Frame ###\n.DS_Store\n*.sw[mnop]\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules\njspm_packages\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\n# End of https://www.toptal.com/developers/gitignore/api/a-frame\n","actionscript":"# Created by https://www.toptal.com/developers/gitignore/api/actionscript\n# Edit at https://www.toptal.com/developers/gitignore?templates=actionscript\n\n### Actionscript ###\n# Build and Release Folders\nbin-debug/\nbin-release/\n[Oo]bj/\n[Bb]in/\n\n# Other files and folders\n.settings/\n\n# Executables\n*.swf\n*.air\n*.ipa\n*.apk\n\n# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`\n# should NOT be excluded as they contain compiler settings and other important\n# information for Eclipse / Flash Builder.\n\n# End of https://www.toptal.com/developers/gitignore/api/actionscript\n","ada":"# Created by https://www.toptal.com/developers/gitignore/api/ada\n# Edit at https://www.toptal.com/developers/gitignore?templates=ada\n\n### Ada ###\n# Object file\n*.o\n\n# Ada Library Information\n*.ali\n\n# End of https://www.toptal.com/developers/gitignore/api/ada\n","adobe":"# Created by https://www.toptal.com/developers/gitignore/api/adobe\n# Edit at https://www.toptal.com/developers/gitignore?templates=adobe\n\n### Adobe ###\n# Adobe Source Files\n*.psd\n*.ai\n*.indd\n*.prproj\n*.aep\n\n# End of https://www.toptal.com/developers/gitignore/api/adobe\n","advancedinstaller":"# Created by https://www.toptal.com/developers/gitignore/api/advancedinstaller\n# Edit at https://www.toptal.com/developers/gitignore?templates=advancedinstaller\n\n### AdvancedInstaller ###\n# Advanced Installer caches and default build output\n*-cache/\n*-SetupFiles/\nPrerequisites/\n\n# End of https://www.toptal.com/developers/gitignore/api/advancedinstaller\n","adventuregamestudio":"# Created by https://www.toptal.com/developers/gitignore/api/adventuregamestudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=adventuregamestudio\n\n### AdventureGameStudio ###\n# Built things\n_Debug/\nCompiled/\n\n# AudioCache can be rebuilt from sources\nAudioCache/\n\n# Lockfile\n_OpenInEditor.lock\n\n# User settings\nGame.agf.user\n*.crm.user\n\n# Backups\nGame.agf.bak\nbackup_acsprset.spr\n\n# Memory dumps\n*.dmp\n\n# Temporary files\n# temporarily created during sprite or room background compression\n~aclzw.tmp\n# temporary, main game data, before getting packed into exe\ngame28.dta\n# temporary build of the game before being moved to Compiled/ folder\n*.exe\n\n# Log files\nwarnings.log\n\n# End of https://www.toptal.com/developers/gitignore/api/adventuregamestudio\n","agda":"# Created by https://www.toptal.com/developers/gitignore/api/agda\n# Edit at https://www.toptal.com/developers/gitignore?templates=agda\n\n### Agda ###\n*.agdai\nMAlonzo/**\n\n# End of https://www.toptal.com/developers/gitignore/api/agda\n","al":"# Created by https://www.toptal.com/developers/gitignore/api/al\n# Edit at https://www.toptal.com/developers/gitignore?templates=al\n\n### AL ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n*.app\n.snapshots/*\n\n# End of https://www.toptal.com/developers/gitignore/api/al\n","alteraquartusii":"# Created by https://www.toptal.com/developers/gitignore/api/alteraquartusii\n# Edit at https://www.toptal.com/developers/gitignore?templates=alteraquartusii\n\n### AlteraQuartusII ###\n##list taken from http://www.alterawiki.com/wiki/Version_Control (01.10.2015)\n\n######### Quartus II source files\n\n# project files:\n### project_name.qpf Quartus II project file\n### project_name.qsf Quartus constraint file (lists the hardware constraints defined for a project, from the used chip and pinout to timing constraints)\n### project_name.qws Quartus Window Settings ? (the configuration of the Quartus gui for the project, may be omitted)\n\n# top level source files:\n### project_name.bdf Block diagram / Schematic file (top level schematic file, there may be many nested files)\n### project_name.vhd VHDL file (top level VHDL file)\n### project_name.v Verilog file (top level Verilog file)\n\n# component source files:\n### component_name.bsf Block Symbol file (component symbol file)\n### component_name.vhd VHDL file (top level VHDL file)\n### component_name.v Verilog file (top level Verilog file)\n\n# SOPC builder project source files (SOPC builder creates many VHDL or Verilog files, that you do not need to store)\n### sopc_project_name.ptf the list and configuration of components selected in the SOPC gui\n### sopc_project_name.bsf Block Symbol file (SOPC component symbol file, especially if you modified it)\n\n# Board Description (if you created your own board, the list is incomplete!)\n### board_name/class.ptf\n\n# software source files:\n### tbd\n\n######## Quartus II binary files\n\n# hardware binary files\n### project_name.sof SRAM Object File\n\n# software binary files\n\n/*\n!/*.gitignore\n##\n!*.qpf\n!*.qsf\n!*.qws\n!*.bdf\n!*.vhd\n!*.v\n!*.ptf\n!*.bsf\n!**/class.ptf\n!*.tbd\n!*.sof\n## tbd\n\n!*.tcl\n\n# End of https://www.toptal.com/developers/gitignore/api/alteraquartusii\n","altium":"# Created by https://www.toptal.com/developers/gitignore/api/altium\n# Edit at https://www.toptal.com/developers/gitignore?templates=altium\n\n### Altium ###\n# Previews Folders\n**/__Previews/\n\n# History Folders\n**/History/*\n\n# Project Logs\nProject\\ Logs*/\n\n# Project Outputs\nProject\\ Outputs*/\n\n# Auto-conversion notices\n*.PcbDoc.htm\n\n# Access lock file for dbLib sources\n**/*.ldb\n\n\n# End of https://www.toptal.com/developers/gitignore/api/altium\n","amplify":"# Created by https://www.toptal.com/developers/gitignore/api/amplify\n# Edit at https://www.toptal.com/developers/gitignore?templates=amplify\n\n### Amplify ###\n# AWS Amplify\namplify/\\#current-cloud-backend\namplify/.config/local-*\namplify/mock-data\namplify/backend/amplify-meta.json\namplify/backend/awscloudformation\nbuild/\ndist/\nnode_modules/\naws-exports.js\nawsconfiguration.json\namplifyconfiguration.json\namplify-build-config.json\namplify-gradle-config.json\namplifyxc.config\n\n# End of https://www.toptal.com/developers/gitignore/api/amplify\n","android":"# Created by https://www.toptal.com/developers/gitignore/api/android\n# Edit at https://www.toptal.com/developers/gitignore?templates=android\n\n### Android ###\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Log/OS Files\n*.log\n\n# Android Studio generated files and folders\ncaptures/\n.externalNativeBuild/\n.cxx/\n*.apk\noutput.json\n\n# IntelliJ\n*.iml\n.idea/\nmisc.xml\ndeploymentTargetDropDown.xml\nrender.experimental.xml\n\n# Keystore files\n*.jks\n*.keystore\n\n# Google Services (e.g. APIs or Firebase)\ngoogle-services.json\n\n# Android Profiling\n*.hprof\n\n### Android Patch ###\ngen-external-apklibs\n\n# Replacement of .externalNativeBuild directories introduced\n# with Android Studio 3.5.\n\n# End of https://www.toptal.com/developers/gitignore/api/android\n","androidstudio":"# Created by https://www.toptal.com/developers/gitignore/api/androidstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio\n\n### AndroidStudio ###\n# Covers files to be ignored for android development using Android Studio.\n\n# Built application files\n*.apk\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n\n# Gradle files\n.gradle\n.gradle/\nbuild/\n\n# Signing files\n.signing/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio\n/*/build/\n/*/local.properties\n/*/out\n/*/*/build\n/*/*/production\ncaptures/\n.navigation/\n*.ipr\n*~\n*.swp\n\n# Keystore files\n*.jks\n*.keystore\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Android Patch\ngen-external-apklibs\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n\n# NDK\nobj/\n\n# IntelliJ IDEA\n*.iml\n*.iws\n/out/\n\n# User-specific configurations\n.idea/caches/\n.idea/libraries/\n.idea/shelf/\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/.name\n.idea/compiler.xml\n.idea/copyright/profiles_settings.xml\n.idea/encodings.xml\n.idea/misc.xml\n.idea/modules.xml\n.idea/scopes/scope_settings.xml\n.idea/dictionaries\n.idea/vcs.xml\n.idea/jsLibraryMappings.xml\n.idea/datasources.xml\n.idea/dataSources.ids\n.idea/sqlDataSources.xml\n.idea/dynamic.xml\n.idea/uiDesigner.xml\n.idea/assetWizardSettings.xml\n.idea/gradle.xml\n.idea/jarRepositories.xml\n.idea/navEditor.xml\n\n# Legacy Eclipse project files\n.classpath\n.project\n.cproject\n.settings/\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.war\n*.ear\n\n# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)\nhs_err_pid*\n\n## Plugin-specific files:\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Mongo Explorer plugin\n.idea/mongoSettings.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n### AndroidStudio Patch ###\n\n!/gradle/wrapper/gradle-wrapper.jar\n\n# End of https://www.toptal.com/developers/gitignore/api/androidstudio\n","angular":"# Created by https://www.toptal.com/developers/gitignore/api/angular\n# Edit at https://www.toptal.com/developers/gitignore?templates=angular\n\n### Angular ###\n## Angular ##\n# compiled output\ndist/\ntmp/\napp/**/*.js\napp/**/*.js.map\n\n# dependencies\nnode_modules/\nbower_components/\n\n# IDEs and editors\n.idea/\n\n# misc\n.sass-cache/\nconnect.lock/\ncoverage/\nlibpeerconnection.log/\nnpm-debug.log\ntestem.log\ntypings/\n.angular/\n\n# e2e\ne2e/*.js\ne2e/*.map\n\n# System Files\n.DS_Store/\n\n# End of https://www.toptal.com/developers/gitignore/api/angular\n","anjuta":"# Created by https://www.toptal.com/developers/gitignore/api/anjuta\n# Edit at https://www.toptal.com/developers/gitignore?templates=anjuta\n\n### Anjuta ###\n# Local configuration folder and symbol database\n/.anjuta/\n/.anjuta_sym_db.db\n\n# End of https://www.toptal.com/developers/gitignore/api/anjuta\n","ansible":"# Created by https://www.toptal.com/developers/gitignore/api/ansible\n# Edit at https://www.toptal.com/developers/gitignore?templates=ansible\n\n### Ansible ###\n*.retry\n\n# End of https://www.toptal.com/developers/gitignore/api/ansible\n","ansibletower":"# Created by https://www.toptal.com/developers/gitignore/api/ansibletower\n# Edit at https://www.toptal.com/developers/gitignore?templates=ansibletower\n\n### AnsibleTower ###\n# Ansible runtime and backups\n*.original\n*.tmp\n*.bkp\n*.retry\n*.*~\n\n# Tower runtime roles\nroles/**\n!roles/requirements.yml\n\n# Avoid plain-text passwords\n*pwd*\n*pass*\n*password*\n*.txt\n\n# Exclude all binaries\n*.bin\n*.jar\n*.tar\n*.zip\n*.gzip\n*.tgz\n\n\n# End of https://www.toptal.com/developers/gitignore/api/ansibletower\n","apachecordova":"# Created by https://www.toptal.com/developers/gitignore/api/apachecordova\n# Edit at https://www.toptal.com/developers/gitignore?templates=apachecordova\n\n### ApacheCordova ###\n# Apache Cordova generated files and directories\nbin/*\n!/plugins\n!/plugins/android.json\n!/plugins/fetch.json\nplugins/*\nplatforms/*\n\n# End of https://www.toptal.com/developers/gitignore/api/apachecordova\n","apachehadoop":"# Created by https://www.toptal.com/developers/gitignore/api/apachehadoop\n# Edit at https://www.toptal.com/developers/gitignore?templates=apachehadoop\n\n### ApacheHadoop ###\n*.iml\n*.ipr\n*.iws\n*.orig\n*.rej\n.idea\n.svn\n.classpath\n.project\n.settings\ntarget\nhadoop-common-project/hadoop-kms/downloads/\nhadoop-hdfs-project/hadoop-hdfs/downloads\nhadoop-hdfs-project/hadoop-hdfs-httpfs/downloads\nhadoop-common-project/hadoop-common/src/test/resources/contract-test-options.xml\nhadoop-tools/hadoop-openstack/src/test/resources/contract-test-options.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/apachehadoop\n","appbuilder":"# Created by https://www.toptal.com/developers/gitignore/api/appbuilder\n# Edit at https://www.toptal.com/developers/gitignore?templates=appbuilder\n\n### AppBuilder ###\n.ab/\n\n# End of https://www.toptal.com/developers/gitignore/api/appbuilder\n","appceleratortitanium":"# Created by https://www.toptal.com/developers/gitignore/api/appceleratortitanium\n# Edit at https://www.toptal.com/developers/gitignore?templates=appceleratortitanium\n\n### AppceleratorTitanium ###\n# Build folder and log file\nbuild/\nbuild.log\n\n# End of https://www.toptal.com/developers/gitignore/api/appceleratortitanium\n","appcode":"# Created by https://www.toptal.com/developers/gitignore/api/appcode\n# Edit at https://www.toptal.com/developers/gitignore?templates=appcode\n\n### AppCode ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### AppCode Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/appcode\n","appcode+all":"# Created by https://www.toptal.com/developers/gitignore/api/appcode+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=appcode+all\n\n### AppCode+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### AppCode+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/appcode+all\n","appcode+iml":"# Created by https://www.toptal.com/developers/gitignore/api/appcode+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=appcode+iml\n\n### AppCode+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### AppCode+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/appcode+iml\n","appengine":"# Created by https://www.toptal.com/developers/gitignore/api/appengine\n# Edit at https://www.toptal.com/developers/gitignore?templates=appengine\n\n### AppEngine ###\n# Google App Engine generated folder\nappengine-generated/\n\n# End of https://www.toptal.com/developers/gitignore/api/appengine\n","aptanastudio":"# Created by https://www.toptal.com/developers/gitignore/api/aptanastudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=aptanastudio\n\n### AptanaStudio ###\n.metadata\nbin/\ntmp/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.settings/\n.loadpath\n.recommenders\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# PyDev specific (Python IDE for Eclipse)\n*.pydevproject\n\n# CDT-specific (C/C++ Development Tooling)\n.cproject\n\n# CDT- autotools\n.autotools\n\n# Java annotation processor (APT)\n.factorypath\n\n# PDT-specific (PHP Development Tools)\n.buildpath\n\n# sbteclipse plugin\n.target\n\n# Tern plugin\n.tern-project\n\n# TeXlipse plugin\n.texlipse\n\n# STS (Spring Tool Suite)\n.springBeans\n\n# Code Recommenders\n.recommenders/\n\n# Annotation Processing\n.apt_generated/\n.apt_generated_test/\n\n# Scala IDE specific (Scala & Java development for Eclipse)\n.cache-main\n.scala_dependencies\n.worksheet\n\n# Uncomment this line if you wish to ignore the project description file.\n# Typically, this file would be tracked if it contains build/dependency configurations:\n#.project\n\n# End of https://www.toptal.com/developers/gitignore/api/aptanastudio\n","arcanist":"# Created by https://www.toptal.com/developers/gitignore/api/arcanist\n# Edit at https://www.toptal.com/developers/gitignore?templates=arcanist\n\n### Arcanist ###\n.phutil_module_cache\n.divinercache/\n\n# End of https://www.toptal.com/developers/gitignore/api/arcanist\n","archive":"# Created by https://www.toptal.com/developers/gitignore/api/archive\n# Edit at https://www.toptal.com/developers/gitignore?templates=archive\n\n### Archive ###\n\n### Mostly from https://en.wikipedia.org/wiki/List_of_archive_formats\n\n## Archiving only\n# The traditional archive format on Unix-like systems, now used mainly for the creation of static libraries.\n*.a\n*.ar\n# RPM files consist of metadata concatenated with (usually) a cpio archive. Newer RPM systems also support other archives, as cpio is becoming obsolete. cpio is also used with initramfs.\n*.cpio\n\n# A self-extracting archive that uses the Bourne shell (sh).\n*.shar\n# A system for storing multiple files. LBR archives typically contained files processed by SQ, or the archive itself was compressed with SQ. LBR archives that were compressed with SQ ended with the extension .LQR\t\n*.LBR\n# An archive format originally used mainly for archiving and distribution of the exact, nearly-exact, or custom-modified contents of an optical storage medium such as a CD-ROM or DVD-ROM. However, it can be used to archive the contents of other storage media, selected partitions, folders, and/or files. The resulting archive is typically optimized for convenient rendering to (re-)writable CD or DVD media.\n*.iso\n# A library format used primarily on the Commodore 64 and 128 lines of computers. This bears no resemblance to the DOS LBR format. While library files were quick to implement (a number of programs exist to work with them) they are crippled in that they cannot grow with use: once a file has been created it cannot be amended (files added, changed or deleted) without recreating the entire file.\n*.lbr\n# An archive format used by Mozilla for storing binary diffs. Used in conjunction with bzip2.\n*.mar\n# A common archive format used on Unix-like systems. Generally used in conjunction with compressors such as gzip, bzip2, compress or xz to create .tar.gz, .tar.bz2, .tar.Z or tar.xz files.\n*.tar\n\n# Package managers\n# Red Hat Package Manager\n*.rpm\n# Debian package\n*.deb\n# MicroSoft Installer\n*.msi\n*.msm\n*.msp\n# Mozilla package installer\n*.xpi\n# Ruby Package\n*.gem\n\n\n# End of https://www.toptal.com/developers/gitignore/api/archive\n","archives":"# Created by https://www.toptal.com/developers/gitignore/api/archives\n# Edit at https://www.toptal.com/developers/gitignore?templates=archives\n\n### Archives ###\n# It's better to unpack these files and commit the raw source because\n# git has its own built in compression methods.\n*.7z\n*.jar\n*.rar\n*.zip\n*.gz\n*.gzip\n*.tgz\n*.bzip\n*.bzip2\n*.bz2\n*.xz\n*.lzma\n*.cab\n*.xar\n\n# Packing-only formats\n*.iso\n*.tar\n\n# Package management formats\n*.dmg\n*.xpi\n*.gem\n*.egg\n*.deb\n*.rpm\n*.msi\n*.msm\n*.msp\n*.txz\n\n# End of https://www.toptal.com/developers/gitignore/api/archives\n","archlinuxpackages":"# Created by https://www.toptal.com/developers/gitignore/api/archlinuxpackages\n# Edit at https://www.toptal.com/developers/gitignore?templates=archlinuxpackages\n\n### ArchLinuxPackages ###\n*.tar\n*.tar.*\n*.jar\n*.exe\n*.msi\n*.zip\n*.tgz\n*.log\n*.log.*\n*.sig\n\npkg/\nsrc/\n\n# End of https://www.toptal.com/developers/gitignore/api/archlinuxpackages\n","asdf":"# Created by https://www.toptal.com/developers/gitignore/api/asdf\n# Edit at https://www.toptal.com/developers/gitignore?templates=asdf\n\n### asdf ###\n/.tool-versions\n\n# End of https://www.toptal.com/developers/gitignore/api/asdf\n","aspnetcore":"# Created by https://www.toptal.com/developers/gitignore/api/aspnetcore\n# Edit at https://www.toptal.com/developers/gitignore?templates=aspnetcore\n\n### ASPNETCore ###\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush\n.cr/\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/\n\n# End of https://www.toptal.com/developers/gitignore/api/aspnetcore\n","assembler":"# Created by https://www.toptal.com/developers/gitignore/api/assembler\n# Edit at https://www.toptal.com/developers/gitignore?templates=assembler\n\n### Assembler ###\n*.exe\n*.o\n*.obj\n*.bc\n\n# End of https://www.toptal.com/developers/gitignore/api/assembler\n","astro":"# Created by https://www.toptal.com/developers/gitignore/api/astro\n# Edit at https://www.toptal.com/developers/gitignore?templates=astro\n\n### Astro ###\n# Gitignore template for Astro projects\n# Ignore content collection generated files\n.astro\n\n# End of https://www.toptal.com/developers/gitignore/api/astro\n","ate":"# Created by https://www.toptal.com/developers/gitignore/api/ate\n# Edit at https://www.toptal.com/developers/gitignore?templates=ate\n\n### ATE ###\n\n### For C++ language related files\n# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n\n\n### For D10/DX Platform\n\n*.pat\n*/**/*rto/*\n*/**/*rtol/*\n*.make\n*.properties\n*.vpj\n*.vpw\n*.scan\n*.patl\n*.bakvpj\n*.bakvpw\n*.vpwhistu\n*.vtg\n*.pvs\n\n\n### For Project Management Related files\n.bak*\n*.xml\n*.zip\n*.tar\n*.gz\n\n# End of https://www.toptal.com/developers/gitignore/api/ate\n","atmelstudio":"# Created by https://www.toptal.com/developers/gitignore/api/atmelstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=atmelstudio\n\n### AtmelStudio ###\n## Ignore Atmel Studio temporary files and build results\n# https://www.microchip.com/mplab/avr-support/atmel-studio-7\n\n# Atmel Studio is powered by an older version of Visual Studio,\n# so most of the project and solution files are the same as VS files,\n# only prefixed by an `at`.\n\n#Build Directories\n[Dd]ebug/\n[Rr]elease/\n\n#Build Results\n*.o\n*.d\n*.eep\n*.elf\n*.hex\n*.map\n*.srec\n\n#User Specific Files\n*.atsuo\n\n# End of https://www.toptal.com/developers/gitignore/api/atmelstudio\n","ats":"# Created by https://www.toptal.com/developers/gitignore/api/ats\n# Edit at https://www.toptal.com/developers/gitignore?templates=ats\n\n### ATS ###\n# Compiled C Files\n*~\n*_?ats.c\n*_?ats.o\n\n# Compiled Javascript Files\n*_?ats.js\n\n# End of https://www.toptal.com/developers/gitignore/api/ats\n","audio":"# Created by https://www.toptal.com/developers/gitignore/api/audio\n# Edit at https://www.toptal.com/developers/gitignore?templates=audio\n\n### Audio ###\n*.aif\n*.iff\n*.m3u\n*.m4a\n*.mid\n*.mp3\n*.mpa\n*.ra\n*.wav\n*.wma\n*.ogg\n*.flac\n\n# End of https://www.toptal.com/developers/gitignore/api/audio\n","autohotkey":"# Created by https://www.toptal.com/developers/gitignore/api/autohotkey\n# Edit at https://www.toptal.com/developers/gitignore?templates=autohotkey\n\n### Autohotkey ###\n# Windows image file caches\nThumbs.db\nehthumbs.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# =========================\n# Operating System Files\n# =========================\n\n# OSX\n# =========================\n\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# End of https://www.toptal.com/developers/gitignore/api/autohotkey\n","automationstudio":"# Created by https://www.toptal.com/developers/gitignore/api/automationstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=automationstudio\n\n### AutomationStudio ###\n## Automation Studio : ignore temporary files and build results (.gitignore parallel to *.apj)\n# Ignore AR versions as those are not considered part of the project\n/AS/System/\n# Ignore build results\n/Binaries/\n# Ignore debugging configurations (Watch,...)\n/Diagnosis/\n# Ignore Temporary Safety files\n/Physical/*/*/*/DLFiles/\n# Ignore standard temp folder\n/Temp/\n# Ignore upgrades as those are not considered part of the project source code\n/Upgrades/\n# Ignore user settings file\n/*.set\n# Ignore is open indication\n/*.isopen\n# Ignore debug logs\n/debug.log\n# Ignore Conversion logs (AS2 to AS3)\n/ConvertLog.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/automationstudio\n","autotools":"# Created by https://www.toptal.com/developers/gitignore/api/autotools\n# Edit at https://www.toptal.com/developers/gitignore?templates=autotools\n\n### Autotools ###\n# http://www.gnu.org/software/automake\n\nMakefile.in\n/ar-lib\n/mdate-sh\n/py-compile\n/test-driver\n/ylwrap\n.deps/\n.dirstamp\n\n# http://www.gnu.org/software/autoconf\n\nautom4te.cache\n/autoscan.log\n/autoscan-*.log\n/aclocal.m4\n/compile\n/config.cache\n/config.guess\n/config.h.in\n/config.log\n/config.status\n/config.sub\n/configure\n/configure.scan\n/depcomp\n/install-sh\n/missing\n/stamp-h1\n\n# https://www.gnu.org/software/libtool/\n\n/ltmain.sh\n\n# http://www.gnu.org/software/texinfo\n\n/texinfo.tex\n\n# http://www.gnu.org/software/m4/\n\nm4/libtool.m4\nm4/ltoptions.m4\nm4/ltsugar.m4\nm4/ltversion.m4\nm4/lt~obsolete.m4\n\n# Generated Makefile\n# (meta build system like autotools,\n# can automatically generate from config.status script\n# (which is called by configure script))\nMakefile\n\n### Autotools Patch ###\n\n# End of https://www.toptal.com/developers/gitignore/api/autotools\n","autotools+strict":"# Created by https://www.toptal.com/developers/gitignore/api/autotools+strict\n# Edit at https://www.toptal.com/developers/gitignore?templates=autotools+strict\n\n### Autotools+strict ###\n# http://www.gnu.org/software/automake\n\nMakefile.in\n/ar-lib\n/mdate-sh\n/py-compile\n/test-driver\n/ylwrap\n.deps/\n.dirstamp\n\n# http://www.gnu.org/software/autoconf\n\nautom4te.cache\n/autoscan.log\n/autoscan-*.log\n/aclocal.m4\n/compile\n/config.cache\n/config.guess\n/config.h.in\n/config.log\n/config.status\n/config.sub\n/configure\n/configure.scan\n/depcomp\n/install-sh\n/missing\n/stamp-h1\n\n# https://www.gnu.org/software/libtool/\n\n/ltmain.sh\n\n# http://www.gnu.org/software/texinfo\n\n/texinfo.tex\n\n# http://www.gnu.org/software/m4/\n\nm4/libtool.m4\nm4/ltoptions.m4\nm4/ltsugar.m4\nm4/ltversion.m4\nm4/lt~obsolete.m4\n\n# Generated Makefile\n# (meta build system like autotools,\n# can automatically generate from config.status script\n# (which is called by configure script))\nMakefile\n\n### Autotools+strict Patch ###\n# Generated source files\nconfigure\n\n# End of https://www.toptal.com/developers/gitignore/api/autotools+strict\n","awr":"# Created by https://www.toptal.com/developers/gitignore/api/awr\n# Edit at https://www.toptal.com/developers/gitignore?templates=awr\n\n### AWR ###\n### National Instruments AWR ###\n*.dsf\n*.dsf.ext\n*.dsf.ext*\n*.emi\n*.opt\n*.stderr.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/awr\n","azurefunctions":"# Created by https://www.toptal.com/developers/gitignore/api/azurefunctions\n# Edit at https://www.toptal.com/developers/gitignore?templates=azurefunctions\n\n### AzureFunctions ###\n# Azure Functions localsettings file\nlocal.settings.json\n\n# End of https://www.toptal.com/developers/gitignore/api/azurefunctions\n","azurite":"# Created by https://www.toptal.com/developers/gitignore/api/azurite\n# Edit at https://www.toptal.com/developers/gitignore?templates=azurite\n\n### Azurite ###\n# Azurite queue\n__queuestorage__\n__azurite_db_queue__.json\n__azurite_db_queue_extent__.json\n\n# Azurite blob\n__blobstorage__\n__azurite_db_blob__.json\n__azurite_db_blob_extent__.json\n\n# Azurite table\n__azurite_db_table__.json\n__azurite_db_table_extent__.json\n\n# End of https://www.toptal.com/developers/gitignore/api/azurite\n","backup":"# Created by https://www.toptal.com/developers/gitignore/api/backup\n# Edit at https://www.toptal.com/developers/gitignore?templates=backup\n\n### Backup ###\n*.bak\n*.gho\n*.ori\n*.orig\n*.tmp\n\n# End of https://www.toptal.com/developers/gitignore/api/backup\n","ballerina":"# Created by https://www.toptal.com/developers/gitignore/api/ballerina\n# Edit at https://www.toptal.com/developers/gitignore?templates=ballerina\n\n### Ballerina ###\n*.class\n*.log\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.ear\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n.idea\n*.iml\n*.ipr\n*.iws\n\n# generated files\ntarget\nresults\n.ballerina\n/gen\n/compiler/ballerina-lang/src/main/resources/grammar/BallerinaLexer.tokens\n/tool-plugins/intellij/src/main/antlr/org/ballerinalang/plugins/idea/grammar/BallerinaLexer.tokens\n/composer/modules/web/dist-electron/\nvelocity.log\n/composer/modules/web/dist-electron\n\n# gradle\n.gradle\nbuild/\ngradle-app.setting\n!gradle-wrapper.jar\n.gradletasknamecache\n\n# mac\n.DS_Store\n\n.classpath\n.project\n.settings\n.vscode\n\n# End of https://www.toptal.com/developers/gitignore/api/ballerina\n","basercms":"# Created by https://www.toptal.com/developers/gitignore/api/basercms\n# Edit at https://www.toptal.com/developers/gitignore?templates=basercms\n\n### baserCMS ###\n## Configuration and Temporary files\napp/Config/database.php\napp/Config/install.php\napp/tmp/*\n!app/tmp/empty\napp/db/*\n!app/db/empty\n!app/Plugin/empty\napp/webroot/files/*\n!app/webroot/files/empty\nfiles/*\n!files/empty\n\n## IDE files and folders\n.cache\n.project\n.settings\nnbproject\n.idea\n.DS_Store\n_notes\n.svn\n\n## Allow 'empty' files\n!empty\n\n## Theme\n!app/webroot/theme/empty\ntheme/demo/*\n!theme/demo/empty\n\n## Project\n# The PROJECT of character,\n# please rewrite on a project\n# that you are participating.\n\ntheme/PROJECT/Pages/*\n!theme/PROJECT/Pages/empty\napp/webroot/theme/PROJECT/Pages/*\napp/webroot/theme/PROJECT/Pages/empty\n\n# End of https://www.toptal.com/developers/gitignore/api/basercms\n","basic":"# Created by https://www.toptal.com/developers/gitignore/api/basic\n# Edit at https://www.toptal.com/developers/gitignore?templates=basic\n\n### Basic ###\n# Apples Build\n*.build\n*.apples\n\n# Initialized files\n*.exe\n*.ini\n*.basic\n\n# End of https://www.toptal.com/developers/gitignore/api/basic\n","batch":"# Created by https://www.toptal.com/developers/gitignore/api/batch\n# Edit at https://www.toptal.com/developers/gitignore?templates=batch\n\n### Batch ###\n# BatchFiles\n*.bat\n*.cmd\n*.btm\n\n# End of https://www.toptal.com/developers/gitignore/api/batch\n","bazaar":"# Created by https://www.toptal.com/developers/gitignore/api/bazaar\n# Edit at https://www.toptal.com/developers/gitignore?templates=bazaar\n\n### Bazaar ###\n.bzr/\n.bzrignore\n\n# End of https://www.toptal.com/developers/gitignore/api/bazaar\n","bazel":"# Created by https://www.toptal.com/developers/gitignore/api/bazel\n# Edit at https://www.toptal.com/developers/gitignore?templates=bazel\n\n### Bazel ###\n# gitignore template for Bazel build system\n# website: https://bazel.build/\n\n# Ignore all bazel-* symlinks. There is no full list since this can change\n# based on the name of the directory bazel is cloned into.\n/bazel-*\n\n# Directories for the Bazel IntelliJ plugin containing the generated\n# IntelliJ project files and plugin configuration. Seperate directories are\n# for the IntelliJ, Android Studio and CLion versions of the plugin.\n/.ijwb/\n/.aswb/\n/.clwb/\n\n# End of https://www.toptal.com/developers/gitignore/api/bazel\n","bitrise":"# Created by https://www.toptal.com/developers/gitignore/api/bitrise\n# Edit at https://www.toptal.com/developers/gitignore?templates=bitrise\n\n### Bitrise ###\n.bitrise.secrets.yml\n\n# End of https://www.toptal.com/developers/gitignore/api/bitrise\n","bitrix":"# Created by https://www.toptal.com/developers/gitignore/api/bitrix\n# Edit at https://www.toptal.com/developers/gitignore?templates=bitrix\n\n### Bitrix ###\n# gitignore template for 1C-Bitrix, a PHP-based CMS\n# website: https://www.1c-bitrix.ru\n\n#Exclude all of core files\n/bitrix/*\n\n#But not the templates and non bitrix components\n!/bitrix/templates\n!/bitrix/components\n/bitrix/components/bitrix\n\n#Exclude bitrix gadgets\n!/bitrix/gadgets\n/bitrix/gadgets/bitrix\n\n#User can use that directory to store some stuff, but it's not really recommended, just use /local instead of this\n!/bitrix/php_interface/\n\n#Exclude database configs\n/bitrix/php_interface/dbconn.php\n\n#Exclude default file storage directory\n/upload/\n\n# End of https://www.toptal.com/developers/gitignore/api/bitrix\n","bittorrent":"# Created by https://www.toptal.com/developers/gitignore/api/bittorrent\n# Edit at https://www.toptal.com/developers/gitignore?templates=bittorrent\n\n### BitTorrent ###\n*.torrent\n\n# End of https://www.toptal.com/developers/gitignore/api/bittorrent\n","blackbox":"# Created by https://www.toptal.com/developers/gitignore/api/blackbox\n# Edit at https://www.toptal.com/developers/gitignore?templates=blackbox\n\n### Blackbox ###\n# https://github.com/StackExchange/blackbox\n/keyrings/live/pubring.gpg~\n/keyrings/live/pubring.kbx~\n/keyrings/live/secring.gpg\n\n# End of https://www.toptal.com/developers/gitignore/api/blackbox\n","blender":"# Created by https://www.toptal.com/developers/gitignore/api/blender\n# Edit at https://www.toptal.com/developers/gitignore?templates=blender\n\n### Blender ###\n# Ignore temporary Blender files.\n*.blend[0-9]*\n\n# End of https://www.toptal.com/developers/gitignore/api/blender\n","bloop":"# Created by https://www.toptal.com/developers/gitignore/api/bloop\n# Edit at https://www.toptal.com/developers/gitignore?templates=bloop\n\n### Bloop ###\n.bloop/\n\n# End of https://www.toptal.com/developers/gitignore/api/bloop\n","bluej":"# Created by https://www.toptal.com/developers/gitignore/api/bluej\n# Edit at https://www.toptal.com/developers/gitignore?templates=bluej\n\n### bluej ###\n# BlueJ: A Java IDE for programming beginners. http://bluej.org\n\n# Editor preferences for a class etc, see http://lists.bluej.org/pipermail/bluej-discuss/2003-May/002351.html\n*.ctxt\n\n# Duplicate of project file, see http://www.hs-owl.de/fb5/labor/it/de/if1/vl/IF1JavaOhneBluej.pdf, page 4\nbluej.pkh\n\n# End of https://www.toptal.com/developers/gitignore/api/bluej\n","bookdown":"# Created by https://www.toptal.com/developers/gitignore/api/bookdown\n# Edit at https://www.toptal.com/developers/gitignore?templates=bookdown\n\n### Bookdown ###\n# R package: bookdown caching files\n/*_files/\n\n# End of https://www.toptal.com/developers/gitignore/api/bookdown\n","bower":"# Created by https://www.toptal.com/developers/gitignore/api/bower\n# Edit at https://www.toptal.com/developers/gitignore?templates=bower\n\n### Bower ###\nbower_components\n.bower-cache\n.bower-registry\n.bower-tmp\n\n# End of https://www.toptal.com/developers/gitignore/api/bower\n","bricxcc":"# Created by https://www.toptal.com/developers/gitignore/api/bricxcc\n# Edit at https://www.toptal.com/developers/gitignore?templates=bricxcc\n\n### BricxCC ###\n# Bricx Command Center IDE\n# http://bricxcc.sourceforge.net\n*.bak\n*.sym\n\n# End of https://www.toptal.com/developers/gitignore/api/bricxcc\n","buck":"# Created by https://www.toptal.com/developers/gitignore/api/buck\n# Edit at https://www.toptal.com/developers/gitignore?templates=buck\n\n### Buck ###\nbuck-out/\n.buckconfig.local\n.buckd/\n.buckversion\n.fakebuckversion\n\n# End of https://www.toptal.com/developers/gitignore/api/buck\n","c":"# Created by https://www.toptal.com/developers/gitignore/api/c\n# Edit at https://www.toptal.com/developers/gitignore?templates=c\n\n### C ###\n# Prerequisites\n*.d\n\n# Object files\n*.o\n*.ko\n*.obj\n*.elf\n\n# Linker output\n*.ilk\n*.map\n*.exp\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Libraries\n*.lib\n*.a\n*.la\n*.lo\n\n# Shared objects (inc. Windows DLLs)\n*.dll\n*.so\n*.so.*\n*.dylib\n\n# Executables\n*.exe\n*.out\n*.app\n*.i*86\n*.x86_64\n*.hex\n\n# Debug files\n*.dSYM/\n*.su\n*.idb\n*.pdb\n\n# Kernel Module Compile Results\n*.mod*\n*.cmd\n.tmp_versions/\nmodules.order\nModule.symvers\nMkfile.old\ndkms.conf\n\n# End of https://www.toptal.com/developers/gitignore/api/c\n","c++":"# Created by https://www.toptal.com/developers/gitignore/api/c++\n# Edit at https://www.toptal.com/developers/gitignore?templates=c++\n\n### C++ ###\n# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n\n# End of https://www.toptal.com/developers/gitignore/api/c++\n","cake":"# Created by https://www.toptal.com/developers/gitignore/api/cake\n# Edit at https://www.toptal.com/developers/gitignore?templates=cake\n\n### Cake ###\ntools/*\n!tools/packages.config\n\n# End of https://www.toptal.com/developers/gitignore/api/cake\n","cakephp":"# Created by https://www.toptal.com/developers/gitignore/api/cakephp\n# Edit at https://www.toptal.com/developers/gitignore?templates=cakephp\n\n### CakePHP ###\n# CakePHP 3\n\n/vendor/*\n/config/app.php\n\n/tmp/cache/models/*\n!/tmp/cache/models/empty\n/tmp/cache/persistent/*\n!/tmp/cache/persistent/empty\n/tmp/cache/views/*\n!/tmp/cache/views/empty\n/tmp/sessions/*\n!/tmp/sessions/empty\n/tmp/tests/*\n!/tmp/tests/empty\n\n/logs/*\n!/logs/empty\n\n# CakePHP 2\n\n/app/tmp/*\n/app/Config/core.php\n/app/Config/database.php\n/vendors/*\n\n# End of https://www.toptal.com/developers/gitignore/api/cakephp\n","cakephp2":"# Created by https://www.toptal.com/developers/gitignore/api/cakephp2\n# Edit at https://www.toptal.com/developers/gitignore?templates=cakephp2\n\n### CakePHP2 ###\n# CakePHP 2\n\n/app/tmp/*\n/app/Config/core.php\n/app/Config/database.php\n/vendors/*\n\n# End of https://www.toptal.com/developers/gitignore/api/cakephp2\n","cakephp3":"# Created by https://www.toptal.com/developers/gitignore/api/cakephp3\n# Edit at https://www.toptal.com/developers/gitignore?templates=cakephp3\n\n### CakePHP3 ###\n# CakePHP 3\n\n/config/app.php\n/config/.env\n/logs/*\n/tmp/*\n/vendor/*\n\n# End of https://www.toptal.com/developers/gitignore/api/cakephp3\n","calabash":"# Created by https://www.toptal.com/developers/gitignore/api/calabash\n# Edit at https://www.toptal.com/developers/gitignore?templates=calabash\n\n### Calabash ###\n# Calabash / Cucumber\nrerun/\nreports/\nscreenshots/\nscreenshot*.png\ntest-servers/\n\n# bundler\n.bundle\nvendor\n\n# End of https://www.toptal.com/developers/gitignore/api/calabash\n","carthage":"# Created by https://www.toptal.com/developers/gitignore/api/carthage\n# Edit at https://www.toptal.com/developers/gitignore?templates=carthage\n\n### Carthage ###\n# Carthage\n#\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build\n\n# End of https://www.toptal.com/developers/gitignore/api/carthage\n","certificates":"# Created by https://www.toptal.com/developers/gitignore/api/certificates\n# Edit at https://www.toptal.com/developers/gitignore?templates=certificates\n\n### certificates ###\n*.pem\n*.key\n*.crt\n*.cer\n*.der\n*.priv\n\n# End of https://www.toptal.com/developers/gitignore/api/certificates\n","ceylon":"# Created by https://www.toptal.com/developers/gitignore/api/ceylon\n# Edit at https://www.toptal.com/developers/gitignore?templates=ceylon\n\n### Ceylon ###\n# build\nmodules\ntest-modules\n\n# fat-jar build\n*.jar\n\n# Auto backup setting file\n.ceylon/config.old\n\n# End of https://www.toptal.com/developers/gitignore/api/ceylon\n","cfwheels":"# Created by https://www.toptal.com/developers/gitignore/api/cfwheels\n# Edit at https://www.toptal.com/developers/gitignore?templates=cfwheels\n\n### CFWheels ###\n# unpacked plugin folders\nplugins/**/*\n\n# files directory where uploads go\nfiles\n\n# DBMigrate plugin: generated SQL\ndb/sql\n\n# AssetBundler plugin: generated bundles\njavascripts/bundles\nstylesheets/bundles\n\n# End of https://www.toptal.com/developers/gitignore/api/cfwheels\n","chefcookbook":"# Created by https://www.toptal.com/developers/gitignore/api/chefcookbook\n# Edit at https://www.toptal.com/developers/gitignore?templates=chefcookbook\n\n### ChefCookbook ###\n.vagrant\n/cookbooks\n\n# Bundler\nbin/*\n.bundle/*\n\n.kitchen/\n.kitchen.local.yml\n\n# End of https://www.toptal.com/developers/gitignore/api/chefcookbook\n","chocolatey":"# Created by https://www.toptal.com/developers/gitignore/api/chocolatey\n# Edit at https://www.toptal.com/developers/gitignore?templates=chocolatey\n\n### Chocolatey ###\n# Exclude packaged modules\n*.nupkg\n\n# End of https://www.toptal.com/developers/gitignore/api/chocolatey\n","circuitpython":"# Created by https://www.toptal.com/developers/gitignore/api/circuitpython\n# Edit at https://www.toptal.com/developers/gitignore?templates=circuitpython\n\n### CircuitPython ###\n.Trashes\n.metadata_never_index\n.fseventsd/\nboot_out.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/circuitpython\n","clean":"# Created by https://www.toptal.com/developers/gitignore/api/clean\n# Edit at https://www.toptal.com/developers/gitignore?templates=clean\n\n### Clean ###\n# Binaries\n*.exe\n*.out\n\n# Project files\n*.prj\n\n# Directory used to store object files, abc files and assembly files\nClean System Files/\n\n# iTasks environment extra data\n*-data/\n*-www/\n*-sapl/\n\n# End of https://www.toptal.com/developers/gitignore/api/clean\n","clion":"# Created by https://www.toptal.com/developers/gitignore/api/clion\n# Edit at https://www.toptal.com/developers/gitignore?templates=clion\n\n### CLion ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### CLion Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/clion\n","clion+all":"# Created by https://www.toptal.com/developers/gitignore/api/clion+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=clion+all\n\n### CLion+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### CLion+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/clion+all\n","clion+iml":"# Created by https://www.toptal.com/developers/gitignore/api/clion+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=clion+iml\n\n### CLion+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### CLion+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/clion+iml\n","clojure":"# Created by https://www.toptal.com/developers/gitignore/api/clojure\n# Edit at https://www.toptal.com/developers/gitignore?templates=clojure\n\n### Clojure ###\npom.xml\npom.xml.asc\n*.jar\n*.class\n/lib/\n/classes/\n/target/\n/checkouts/\n.lein-deps-sum\n.lein-repl-history\n.lein-plugins/\n.lein-failures\n.nrepl-port\n.cpcache/\n\n# End of https://www.toptal.com/developers/gitignore/api/clojure\n","cloud9":"# Created by https://www.toptal.com/developers/gitignore/api/cloud9\n# Edit at https://www.toptal.com/developers/gitignore?templates=cloud9\n\n### Cloud9 ###\n# Cloud9 IDE - http://c9.io\n.c9revisions\n.c9\n\n# End of https://www.toptal.com/developers/gitignore/api/cloud9\n","cmake":"# Created by https://www.toptal.com/developers/gitignore/api/cmake\n# Edit at https://www.toptal.com/developers/gitignore?templates=cmake\n\n### CMake ###\nCMakeLists.txt.user\nCMakeCache.txt\nCMakeFiles\nCMakeScripts\nTesting\nMakefile\ncmake_install.cmake\ninstall_manifest.txt\ncompile_commands.json\nCTestTestfile.cmake\n_deps\n\n### CMake Patch ###\nCMakeUserPresets.json\n\n# External projects\n*-prefix/\n\n# End of https://www.toptal.com/developers/gitignore/api/cmake\n","cocoapods":"# Created by https://www.toptal.com/developers/gitignore/api/cocoapods\n# Edit at https://www.toptal.com/developers/gitignore?templates=cocoapods\n\n### CocoaPods ###\n## CocoaPods GitIgnore Template\n\n# CocoaPods - Only use to conserve bandwidth / Save time on Pushing\n# - Also handy if you have a large number of dependant pods\n# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE\nPods/\n\n# End of https://www.toptal.com/developers/gitignore/api/cocoapods\n","cocos2dx":"# Created by https://www.toptal.com/developers/gitignore/api/cocos2dx\n# Edit at https://www.toptal.com/developers/gitignore?templates=cocos2dx\n\n### Cocos2dx ###\n# Ignore thumbnails created by windows\nThumbs.db\n\n# Ignore files build by Visual Studio\n*.obj\n*.exe\n*.pdb\n*.aps\n*.vcproj.*.user\n*.vcxproj.user\n*.csproj.user\n*.vspscc\n*_i.c\n*.i\n*.icf\n*_p.c\n*.ncb\n*.suo\n*.tlb\n*.tlh\n*.bak\n*.cache\n*.ilk\n*.log\n[Bb]in\n[Dd]ebug/\n[Dd]ebug.win32/\n*.sbr\n*.sdf\nobj/\n[Rr]elease/\n[Rr]elease.win32/\n_ReSharper*/\n[Tt]est[Rr]esult*\nipch/\n*.opensdf\n*.opendb\nSubmissionInfo\nGenerated Files\nAppPackages\nBundleArtifacts\n.vs/\n\n# Ignore files build by ndk and eclipse\nlibs/\nbin/\ngen/\nassets/\nlocal.properties\n\n# Ignore python compiled files\n*.pyc\n\n# Ignore files build by airplay and marmalade\nbuild_*_xcode/\nbuild_*_vc10/\n\n# Ignore files build by xcode\n*.mode*v*\n*.pbxuser\n*.xcbkptlist\n*.xcworkspacedata\n*.xcuserstate\n*.xccheckout\nxcschememanagement.plist\n.DS_Store\n._.*\nxcuserdata/\nDerivedData/\n\n# Ignore files built by AppCode\n.idea/\n\n# Ignore files built by bada\n.Simulator-Debug/\n.Target-Debug/\n.Target-Release/\n\n# Ignore files built by blackberry\nSimulator/\nDevice-Debug/\nDevice-Release/\n\n# Ignore vim swaps\n*.swp\n*.swo\n\n# Ignore files created by create_project.py\nprojects/\n\n# Ignore config files in javascript bindings generator\ntools/tojs/user.cfg\n# ... userconf.ini generated if running from tools/tojs\ntools/tojs/userconf.ini\ntools/tolua/userconf.ini\n# ... userconf.ini generated if running from tools/jenkins_scripts/mac/android/\ntools/jenkins_scripts/mac/android/userconf.ini\n\n# CTags\ntags\n\n# ignore files, created with make-all-linux-project script\n/lib\n/build/linux-build\n\n# Cmake files\nCMakeCache.txt\nCMakeFiles\nMakefile\ncmake_install.cmake\nCMakeLists.txt.user\n\n# Ignore files generated by console\nbuild/build/\ncocos/scripting/lua-bindings/proj.ios_mac/build/\ntests/*/runtime/\ntests/*/publish/\ntests/*/project/proj.android-studio/app/build.xml\ntests/*/project/proj.android-studio/app/proguard-project.txt\ntests/*/proj.android-studio/app/build.xml\ntests/*/proj.android-studio/app/proguard-project.txt\n\n# Android\nproject.properties\n*.iml\n\n# Ignore prebuilt libraries folder\n/external/*\n!/external/config.json\n/templates/lua-template-runtime/runtime\n/v*-deps-*.zip\n/v*-lua-runtime-*.zip\n/v*-console-*.zip\n/tools/fbx-conv/\ntests/cpp-tests/Resources/audio\n/tests/lua-empty-test/src/cocos/\n/tests/lua-game-controller-test/src/cocos/\n/tests/lua-tests/src/cocos/\n/tests/js-tests/res/\n/tools/framework-compile/bin/proj_modifier/plutil-win32/\n!/tools/framework-compile/bin/\n\n# generated by framework-compile\n/templates/*-template-binary/\n/prebuilt/\n/*/prebuilt-mk/Android.mk\n/*/*/prebuilt-mk/Android.mk\n/*/*/*/prebuilt-mk/Android.mk\n/*/*/*/*/prebuilt-mk/Android.mk\n*.xcscmblueprint\n\n# End of https://www.toptal.com/developers/gitignore/api/cocos2dx\n","cocoscreator":"# Created by https://www.toptal.com/developers/gitignore/api/cocoscreator\n# Edit at https://www.toptal.com/developers/gitignore?templates=cocoscreator\n\n### CocosCreator ###\n#/////////////////////////////////////////////////////////////////////////////\n# Cocos Creator Projects\n\nbuild/\nlibrary/\ntemp/\nlocal/\njsb-default/\njsb-binary/\njsb-link/\nweb-desktop/\nweb-mobile/\nruntime/\njsb/\nexport/\n\n# Logs and databases\n\n*.log\n*.sql\n*.sqlite\n\n# files for debugger\n\n*.sln\n*.csproj\n*.pidb\n*.unityproj\n*.suo\n\n# OS generated files\n\n.DS_Store\nehthumbs.db\nThumbs.db\n\n# exvim files\n\n*UnityVS.meta\n*.err\n*.err.meta\n*.exvim\n*.exvim.meta\n*.vimentry\n*.vimentry.meta\n*.vimproject\n*.vimproject.meta\n.vimfiles.*/\n.exvim.*/\nquick_gen_project_*_autogen.bat\nquick_gen_project_*_autogen.bat.meta\nquick_gen_project_*_autogen.sh\nquick_gen_project_*_autogen.sh.meta\n.exvim.app\n\n# webstorm files\n\n.idea/\n\n# VSCode files\n\ncreator.d.ts\n.vscode/\n\n# End of https://www.toptal.com/developers/gitignore/api/cocoscreator\n","codeblocks":"# Created by https://www.toptal.com/developers/gitignore/api/codeblocks\n# Edit at https://www.toptal.com/developers/gitignore?templates=codeblocks\n\n### CodeBlocks ###\n# specific to CodeBlocks IDE\n*.layout\n*.depend\n# generated directories\nbin/\nobj/\n\n# End of https://www.toptal.com/developers/gitignore/api/codeblocks\n","codecomposerstudio":"# Created by https://www.toptal.com/developers/gitignore/api/codecomposerstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=codecomposerstudio\n\n### CodeComposerStudio ###\n\ntmp/\n*.tmp\n*.bak\n# Generated build artifact directories\nDebug/\nRelease/\nFlash/\nRAM/\n# Generated build artifacts\nmakefile\n*.map\n*.mk\n*.opt\n*.pp\n*.xml\n# Generated directories\n.config/\n.launches/\n# Generated file used to help display error messages in the problems view\n.xdchelp\n# Uniflash session files\n*.uniflashsession\n\n# End of https://www.toptal.com/developers/gitignore/api/codecomposerstudio\n","codeigniter":"# Created by https://www.toptal.com/developers/gitignore/api/codeigniter\n# Edit at https://www.toptal.com/developers/gitignore?templates=codeigniter\n\n### CodeIgniter ###\n*/config/development\n*/logs/log-*.php\n!*/logs/index.html\n*/cache/*\n!system/cache/*\n!*/cache/index.html\n!*/cache/.htaccess\n\nuser_guide_src/build/*\nuser_guide_src/cilexer/build/*\nuser_guide_src/cilexer/dist/*\nuser_guide_src/cilexer/pycilexer.egg-info/*\n\n#codeigniter 3\napplication/logs/*\n!application/logs/index.html\n!application/logs/.htaccess\n/vendor/\n\n### CodeIgniter Patch ###\n\n# End of https://www.toptal.com/developers/gitignore/api/codeigniter\n","codeio":"# Created by https://www.toptal.com/developers/gitignore/api/codeio\n# Edit at https://www.toptal.com/developers/gitignore?templates=codeio\n\n### Codeio ###\n.codio\n\n# End of https://www.toptal.com/developers/gitignore/api/codeio\n","codekit":"# Created by https://www.toptal.com/developers/gitignore/api/codekit\n# Edit at https://www.toptal.com/developers/gitignore?templates=codekit\n\n### CodeKit ###\n# General CodeKit files to ignore\nconfig.codekit\nconfig.codekit3\n/min\n\n### CodeKit Patch ###\n*/codekit-config.json\n.codekit-cache\n\n# End of https://www.toptal.com/developers/gitignore/api/codekit\n","codesniffer":"# Created by https://www.toptal.com/developers/gitignore/api/codesniffer\n# Edit at https://www.toptal.com/developers/gitignore?templates=codesniffer\n\n### CodeSniffer ###\n# gitignore for the PHP Codesniffer framework\n# website: https://github.com/squizlabs/PHP_CodeSniffer\n#\n# Recommended template: PHP.gitignore\n\n/wpcs/*\n\n# End of https://www.toptal.com/developers/gitignore/api/codesniffer\n","coffeescript":"# Created by https://www.toptal.com/developers/gitignore/api/coffeescript\n# Edit at https://www.toptal.com/developers/gitignore?templates=coffeescript\n\n### CoffeeScript ###\n*.js\n\n# End of https://www.toptal.com/developers/gitignore/api/coffeescript\n","commonlisp":"# Created by https://www.toptal.com/developers/gitignore/api/commonlisp\n# Edit at https://www.toptal.com/developers/gitignore?templates=commonlisp\n\n### CommonLisp ###\n*.FASL\n*.fasl\n*.lisp-temp\n*.dfsl\n*.pfsl\n*.d64fsl\n*.p64fsl\n*.lx64fsl\n*.lx32fsl\n*.dx64fsl\n*.dx32fsl\n*.fx64fsl\n*.fx32fsl\n*.sx64fsl\n*.sx32fsl\n*.wx64fsl\n*.wx32fsl\n\n# End of https://www.toptal.com/developers/gitignore/api/commonlisp\n","compodoc":"# Created by https://www.toptal.com/developers/gitignore/api/compodoc\n# Edit at https://www.toptal.com/developers/gitignore?templates=compodoc\n\n### Compodoc ###\n# gitignore template for the Compodoc, UI guide for front apps\n# website: https://compodoc.app/\n\ndocumentation/\n\n# End of https://www.toptal.com/developers/gitignore/api/compodoc\n","composer":"# Created by https://www.toptal.com/developers/gitignore/api/composer\n# Edit at https://www.toptal.com/developers/gitignore?templates=composer\n\n### Composer ###\ncomposer.phar\n/vendor/\n\n# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control\n# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file\n# composer.lock\n\n# End of https://www.toptal.com/developers/gitignore/api/composer\n","compressed":"# Created by https://www.toptal.com/developers/gitignore/api/compressed\n# Edit at https://www.toptal.com/developers/gitignore?templates=compressed\n\n### Compressed ###\n*.7z\n*.deb\n*.gz\n*.pkg\n*.rar\n*.rpm\n*.sit\n*.sitx\n*.tar\n*.zip\n*.zipx\n*.tgz\n\n# End of https://www.toptal.com/developers/gitignore/api/compressed\n","compressedarchive":"# Created by https://www.toptal.com/developers/gitignore/api/compressedarchive\n# Edit at https://www.toptal.com/developers/gitignore?templates=compressedarchive\n\n### CompressedArchive ###\n\n### Mostly from https://en.wikipedia.org/wiki/List_of_archive_formats\n\n## Archiving and compression\n# Open source file format. Used by 7-Zip.\n*.7z\n# Mac OS X, restoration on different platforms is possible although not immediate \tYes \tBased on 7z. Preserves Spotlight metadata, resource forks, owner/group information, dates and other data which would be otherwise lost with compression.\n*.s7z\n# Old archive versions only \tProprietary format\n*.ace\n# A format that compresses and doubly encrypt the data (AES256 and CAS256) avoiding brute force attacks, also hide files in an AFA file. It has two ways to safeguard data integrity and subsequent repair of the file if has an error (repair with AstroA2P (online) or Astrotite (offline)).\n*.afa\n# A mainly Korean format designed for very large archives.\n*.alz\n# Android application package (variant of JAR file format).\n*.apk\n# ??\n*.arc\n# Originally DOS, now multiple\n*.arj\n# Open archive format, used by B1 Free Archiver (http://dev.b1.org/standard/archive-format.html)\n*.b1\n# Binary Archive with external header\n*.ba\n# Proprietary format from the ZipTV Compression Components\n*.bh\n# The Microsoft Windows native archive format, which is also used by many commercial installers such as InstallShield and WISE.\n*.cab\n# Originally DOS, now DOS and Windows \tCreated by Yaakov Gringeler; released last in 2003 (Compressia 1.0.0.1 beta), now apparently defunct. Free trial of 30 days lets user create and extract archives; after that it is possible to extract, but not to create.\n*.car\n# Open source file format.\n*.cfs\n# Compact Pro archive, a common archiver used on Mac platforms until about Mac OS 7.5.x. Competed with StuffIt; now obsolete.\n*.cpt\n# Windows, Unix-like, Mac OS X Open source file format. Files are compressed individually with either gzip, bzip2 or lzo.\n*.dar\n# DiskDoubler \tMac OS \t\t\tobsolete\n*.dd\n# ??\n*.dgc\n# Apple Disk Image upports \"Internet-enabled\" disk images, which, once downloaded, are automatically decompressed, mounted, have the contents extracted, and thrown away. Currently, Safari is the only browser that supports this form of extraction; however, the images can be manually extracted as well. This format can also be password-protected or encrypted with 128-bit or 256-bit AES encryption.\n*.dmg\n# Enterprise Java Archive archive\n*.ear\n# ETSoft compressed archive\n*.egg\n# The predecessor of DGCA.\n*.gca\n# Originally DOS \tYes, but may be covered by patents \tDOS era format; uses arithmetic/Markov coding\n*.ha\n# MS Windows \tHKI\n*.hki\n# Produced by ICEOWS program. Excels at text file compression.\n*.ice\n# Java archive, compatible with ZIP files\n*.jar\n# Open sourced archiver with compression using the PAQ family of algorithms and optional encryption.\n*.kgb\n# Originally DOS, now multiple \tMultiple \tYes \tThe standard format on Amiga.\n*.lzh\n*.lha\n# Archiver originally used on The Amiga. Now copied by Microsoft to use in their .cab and .chm files.\n*.lzx\n# file format from NoGate Consultings, a rival from ARC-Compressor.\n*.pak\n# A disk image archive format that supports several compression methods as well as splitting the archive into smaller pieces.\n*.partimg\n# An experimental open source packager (http://mattmahoney.net/dc)\n*.paq*\n# Open source archiver supporting authenticated encryption, volume spanning, customizable object level and volume level integrity checks (form CRCs to SHA-512 and Whirlpool hashes), fast deflate based compression\n*.pea\n# The format from the PIM - a freeware compression tool by Ilia Muraviev. It uses an LZP-based compression algorithm with set of filters for executable, image and audio files.\n*.pim\n# PackIt \tMac OS \t\t\tobsolete\n*.pit\n# Used for data in games written using the Quadruple D library for Delphi. Uses byte pair compression.\n*.qda\n# A proprietary archive format, second in popularity to .zip files.\n*.rar\n# The format from a commercial archiving package. Odd among commercial packages in that they focus on incorporating experimental algorithms with the highest possible compression (at the expense of speed and memory), such as PAQ, PPMD and PPMZ (PPMD with unlimited-length strings), as well as a proprietary algorithms.\n*.rk\n# Self Dissolving ARChive \tCommodore 64, Commodore 128 \tCommodore 64, Commodore 128 \tYes \tSDAs refer to Self Dissolving ARC files, and are based on the Commodore 64 and Commodore 128 versions of ARC, originally written by Chris Smeets. While the files share the same extension, they are not compatible between platforms. That is, an SDA created on a Commodore 64 but run on a Commodore 128 in Commodore 128 mode will crash the machine, and vice versa. The intended successor to SDA is SFX.\n*.sda\n# A pre-Mac OS X Self-Extracting Archive format. StuffIt, Compact Pro, Disk Doubler and others could create .sea files, though the StuffIt versions were the most common.\n*.sea\n# Scifer Archive with internal header\n*.sen\n# Commodore 64, Commodore 128 \tSFX is a Self Extracting Archive which uses the LHArc compression algorithm. It was originally developed by Chris Smeets on the Commodore platform, and runs primarily using the CS-DOS extension for the Commodore 128. Unlike its predecessor SDA, SFX files will run on both the Commodore 64 and Commodore 128 regardless of which machine they were created on.\n*.sfx\n# An archive format designed for the Apple II series of computers. The canonical implementation is ShrinkIt, which can operate on disk images as well as files. Preferred compression algorithm is a combination of RLE and 12-bit LZW. Archives can be manipulated with the command-line NuLib tool, or the Windows-based CiderPress.\n*.shk\n# A compression format common on Apple Macintosh computers. The free StuffIt Expander is available for Windows and OS X.\n*.sit\n# The replacement for the .sit format that supports more compression methods, UNIX file permissions, long file names, very large files, more encryption options, data specific compressors (JPEG, Zip, PDF, 24-bit image, MP3). The free StuffIt Expander is available for Windows and OS X.\n*.sitx\n# A royalty-free compressing format\n*.sqx\n# The \"tarball\" format combines tar archives with a file-based compression scheme (usually gzip). Commonly used for source and binary distribution on Unix-like platforms, widely available elsewhere.\n*.tar.gz\n*.tgz\n*.tar.Z\n*.tar.bz2\n*.tbz2\n*.tar.lzma\n*.tlz\n# UltraCompressor 2.3 was developed to act as an alternative to the then popular PKZIP application. The main feature of the application is its ability to create large archives. This means that compressed archives with the UC2 file extension can hold almost 1 million files.\n*.uc\n*.uc0\n*.uc2\n*.ucn\n*.ur2\n*.ue2\n# Based on PAQ, RZM, CSC, CCM, and 7zip. The format consists of a PAQ, RZM, CSC, or CCM compressed file and a manifest with compression settings stored in a 7z archive.\n*.uca\n# A high compression rate archive format originally for DOS.\n*.uha\n# Web Application archive (Java-based web app)\n*.war\n# File-based disk image format developed to deploy Microsoft Windows.\n*.wim\n# XAR\n*.xar\n# Native format of the Open Source KiriKiri Visual Novel engine. Uses combination of block splitting and zlib compression. The filenames and pathes are stored in UTF-16 format. For integrity check, the Adler-32 hashsum is used. For many commercial games, the files are encrypted (and decoded on runtime) via so-called \"cxdec\" module, which implements xor-based encryption.\n*.xp3\n# Yamazaki zipper archive. Compression format used in DeepFreezer archiver utility created by Yamazaki Satoshi. Read and write support exists in TUGZip, IZArc and ZipZag\n*.yz1\n# The most widely used compression format on Microsoft Windows. Commonly used on Macintosh and Unix systems as well.\n*.zip\n*.zipx\n# application/x-zoo \tzoo \tMultiple \tMultiple \tYes \t\n*.zoo\n# Journaling (append-only) archive format with rollback capability. Supports deduplication and incremental update based on last-modified dates. Multi-threaded. Compresses in LZ77, BWT, and context mixing formats. Open source.\n*.zpaq\n# Archiver with a compression algorithm based on the Burrows-Wheeler transform method.\n*.zz\n\n\n# End of https://www.toptal.com/developers/gitignore/api/compressedarchive\n","compression":"# Created by https://www.toptal.com/developers/gitignore/api/compression\n# Edit at https://www.toptal.com/developers/gitignore?templates=compression\n\n### Compression ###\n\n### From https://en.wikipedia.org/wiki/List_of_archive_formats\n\n## Compression only\n# An open source, patent- and royalty-free compression format. The compression algorithm is a Burrows-Wheeler transform followed by a move-to-front transform and finally Huffman coding\n*.bz2\n# Old compressor for QNX4 OS. The compression algorithm is a modified LZSS, with an adaptive Huffman coding.\n*.F\n# GNU Zip, the primary compression format used by Unix-like systems. The compression algorithm is DEFLATE.\n*.gz\n# An alternate LZMA algorithm implementation, with support for checksums and ident bytes.\n*.lz\n# The LZMA compression algorithm as used by 7-Zip\n*.lzma\n# An implementation of the LZO data compression algorithm\n*.lzo\n# A compression program designed to do particularly well on very large files containing long distance redundancy.\n*.rz\n# Windows compress/decompress- Linux and Mac OS X decompress only \tA compression program designed to do high compression on SF2 files (SoundFont)\n*.sfark\n# A compression format invented by Google and open-sourced in 2011. Snappy aims for very high speeds, reasonable compression, and maximum stability rather than maximum compression or compatibility with any other compression library.\n*.sz\n# Squeeze: A program which compressed files. A file which was \"squeezed\" had the middle initial of the name changed to \"Q\", so that a squeezed text file would end with .TQT, a squeezed executable would end with .CQM or .EQE. Typically used with .LBR archives, either by storing the squeezed files in the archive, or by storing the files decompressed and then compressing the archive, which would have a name ending in \".LQR\".\n*.?Q?\n# A compression program written by Steven Greenberg implementing the LZW algorithm. For several years in the CP/M world when no implementation was available of ARC, CRUNCHed files stored in .LBR archives were very popular. CRUNCH's implementation of LZW had a somewhat unique feature of modifying and occasionally clearing the code table in memory when it became full, resulting in a few percent better compression on many files.\n*.?Z?\n# A compression format using LZMA2 to yield very high compression ratios.\n*.xz\n# The traditional Huffman coding compression format.\n*.z\n# The traditional LZW compression format.\n*.Z\n# Joke compression program, actually increasing file size\n*.infl\n# Compression format(s) used by some DOS and Windows install programs. MS-DOS includes expand.exe to decompress its install files. The compressed files are created with a matching compress.exe command. The compression algorithm is LZSS.\n*.??_\n\n\n# End of https://www.toptal.com/developers/gitignore/api/compression\n","conan":"# Created by https://www.toptal.com/developers/gitignore/api/conan\n# Edit at https://www.toptal.com/developers/gitignore?templates=conan\n\n### Conan ###\n# Conan build information\nconan.lock\nconanbuildinfo.*\nconaninfo.txt\ngraph_info.json\n\n## Various built-in generators\n# cmake_find_package generator https://docs.conan.io/en/latest/reference/generators/cmake_find_package.html\nFind*.cmake\n\n# cmake_paths generator https://docs.conan.io/en/1.4/integrations/cmake/cmake_paths_generator.html\nconan_paths.*\n\n# Environment activation scripts produced by https://docs.conan.io/en/latest/mastering/virtualenv.html#virtualenv-generator\nactivate_run.ps1\nactivate_run.sh\ndeactivate_run.ps1\ndeactivate_run.sh\nenvironment_run.ps1.env\nenvironment_run.sh.env\n\n# End of https://www.toptal.com/developers/gitignore/api/conan\n","concrete5":"# Created by https://www.toptal.com/developers/gitignore/api/concrete5\n# Edit at https://www.toptal.com/developers/gitignore?templates=concrete5\n\n### Concrete5 ###\n# ignore the error log and .htaccess and others\nerror_log\n.htaccess\n\n# concrete5 5.6 specific\n\nconfig/site.php\nfiles/cache/*\nfiles/tmp/*\n\n# concrete5 5.7 specific\n\n# ignore everything but the index.html\n/application/files/*\n!/application/files/index.html\n\n# ignore updates folder\n/updates/*\n\n# ignore sitemap.xml\n/sitemap.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/concrete5\n","coq":"# Created by https://www.toptal.com/developers/gitignore/api/coq\n# Edit at https://www.toptal.com/developers/gitignore?templates=coq\n\n### Coq ###\n.*.aux\n.*.d\n*.a\n*.cma\n*.cmi\n*.cmo\n*.cmx\n*.cmxa\n*.cmxs\n*.glob\n*.ml.d\n*.ml4.d\n*.mlg.d\n*.mli.d\n*.mllib.d\n*.mlpack.d\n*.native\n*.o\n*.v.d\n*.vio\n*.vo\n*.vok\n*.vos\n.coq-native\n.csdp.cache\n.lia.cache\n.nia.cache\n.nlia.cache\n.nra.cache\ncsdp.cache\nlia.cache\nnia.cache\nnlia.cache\nnra.cache\nnative_compute_profile_*.data\n\n# generated timing files\n*.timing.diff\n*.v.after-timing\n*.v.before-timing\n*.v.timing\ntime-of-build-after.log\ntime-of-build-before.log\ntime-of-build-both.log\ntime-of-build-pretty.log\n\n# End of https://www.toptal.com/developers/gitignore/api/coq\n","cordova":"# Created by https://www.toptal.com/developers/gitignore/api/cordova\n# Edit at https://www.toptal.com/developers/gitignore?templates=cordova\n\n### Cordova ###\n# gitignore template for the Cordova framework\n# website: https://cordova.apache.org/\n#\n# Recommended template: Node.gitignore\n\n# App platform binaries and built files\n/platforms\n\n# Optional to ignore plugin Git clones\n#/plugins\n\n# End of https://www.toptal.com/developers/gitignore/api/cordova\n","craftcms":"# Created by https://www.toptal.com/developers/gitignore/api/craftcms\n# Edit at https://www.toptal.com/developers/gitignore?templates=craftcms\n\n### CraftCMS ###\n# Craft 2 Storage (https://craftcms.com/support/craft-storage-gitignore)\n# not necessary for Craft 3 (https://github.com/craftcms/craft/issues/26)\n/craft/storage/*\n!/craft/storage/rebrand\n\n# End of https://www.toptal.com/developers/gitignore/api/craftcms\n","crashlytics":"# Created by https://www.toptal.com/developers/gitignore/api/crashlytics\n# Edit at https://www.toptal.com/developers/gitignore?templates=crashlytics\n\n### Crashlytics ###\n#Crashlytics\ncrashlytics-build.properties\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\nfabric.properties\n\n# End of https://www.toptal.com/developers/gitignore/api/crashlytics\n","crbasic":"# Created by https://www.toptal.com/developers/gitignore/api/crbasic\n# Edit at https://www.toptal.com/developers/gitignore?templates=crbasic\n\n### CRBasic ###\n# optional table definintions created at compile time\n*.tdf\n\n# any encrypted files\n*_Enc.*\n\n# End of https://www.toptal.com/developers/gitignore/api/crbasic\n","crossbar":"# Created by https://www.toptal.com/developers/gitignore/api/crossbar\n# Edit at https://www.toptal.com/developers/gitignore?templates=crossbar\n\n### Crossbar ###\n.crossbar/node.pid\n\n# End of https://www.toptal.com/developers/gitignore/api/crossbar\n","crystal":"# Created by https://www.toptal.com/developers/gitignore/api/crystal\n# Edit at https://www.toptal.com/developers/gitignore?templates=crystal\n\n### Crystal ###\n/docs/\n/lib/\n/bin/\n/.shards/\n*.dwarf\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application that uses them\n#/shard.lock\n\n# End of https://www.toptal.com/developers/gitignore/api/crystal\n","cs-cart":"# Created by https://www.toptal.com/developers/gitignore/api/cs-cart\n# Edit at https://www.toptal.com/developers/gitignore?templates=cs-cart\n\n### CS-Cart ###\n# CS-Cart and CS-Cart Multi-Vendor, a PHP-based CMS\n## Maintainer https://simtechdev.com\n\njs/core/node_modules\njs/tygh/core.js\njs/tygh/notifications_center.js\njs/tygh/bottom_panel.js\njs/core/npm-debug.log\n\napp/lib/composer.phar\ndesign/backend/css/twitterbootstrap.css\nimages/*\nusers/*\nvar/*\n!var/database\n!var/backups/.htaccess\n!var/restore/.htaccess\n!var/resources\n!var/langs\n!var/conf\nvar/langs/*\n!var/langs/en\n!var/.htaccess\n!var/index.php\n!var/builds\n!images/promo\n\nupgrades/*\n!upgrades/source_restore.php\n\nlocal_conf.php\ninstall/\nsph*.php\nget_addon.php\nget_addon/\n\nerror_log*\n\n_tools/selenium_WebDriver/tests/share/config/sessions.local.php\n_tools/selenium_WebDriver/tests/share/storage/local.php\n_tools/selenium_WebDriver/tests/share/storage/screens/*.png\n\n_tools/migration/composer.lock\n_tools/migration/conf.php\n_tools/migration/vendor\n\n*.log\n*.sql\n*.bak\n*.swp\n*.orig\n*.tmp\n\n# End of https://www.toptal.com/developers/gitignore/api/cs-cart\n","csharp":"# Created by https://www.toptal.com/developers/gitignore/api/csharp\n# Edit at https://www.toptal.com/developers/gitignore?templates=csharp\n\n### Csharp ###\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n\n# End of https://www.toptal.com/developers/gitignore/api/csharp\n","cuda":"# Created by https://www.toptal.com/developers/gitignore/api/cuda\n# Edit at https://www.toptal.com/developers/gitignore?templates=cuda\n\n### CUDA ###\n*.i\n*.ii\n*.gpu\n*.ptx\n*.cubin\n*.fatbin\n\n# End of https://www.toptal.com/developers/gitignore/api/cuda\n","cvs":"# Created by https://www.toptal.com/developers/gitignore/api/cvs\n# Edit at https://www.toptal.com/developers/gitignore?templates=cvs\n\n### CVS ###\n/CVS/*\n**/CVS/*\n.cvsignore\n*/.cvsignore\n\n# End of https://www.toptal.com/developers/gitignore/api/cvs\n","cypressio":"# Created by https://www.toptal.com/developers/gitignore/api/cypressio\n# Edit at https://www.toptal.com/developers/gitignore?templates=cypressio\n\n### CypressIO ###\n# gitignore template for the CypressIO, browser test framework\n# website: https://www.cypress.io/\n\ncypress/results/*\ncypress/reports/*\ncypress/screenshots/*\ncypress/videos/*\n\n# End of https://www.toptal.com/developers/gitignore/api/cypressio\n","d":"# Created by https://www.toptal.com/developers/gitignore/api/d\n# Edit at https://www.toptal.com/developers/gitignore?templates=d\n\n### D ###\n# Compiled Object files\n*.o\n*.obj\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Compiled Static libraries\n*.a\n*.lib\n\n# Executables\n*.exe\n\n# DUB\n.dub\ndocs.json\n__dummy.html\ndocs/\n\n# Code coverage\n*.lst\n\n### D Patch ###\n# Test Executables\n*-test-*\n\n# Comment to allow dub lock file to be version controlled as well\ndub.selections.json\n\n# End of https://www.toptal.com/developers/gitignore/api/d\n","dart":"# Created by https://www.toptal.com/developers/gitignore/api/dart\n# Edit at https://www.toptal.com/developers/gitignore?templates=dart\n\n### Dart ###\n# See https://www.dartlang.org/guides/libraries/private-files\n\n# Files and directories created by pub\n.dart_tool/\n.packages\nbuild/\n# If you're building an application, you may want to check-in your pubspec.lock\npubspec.lock\n\n# Directory created by dartdoc\n# If you don't generate documentation locally you can remove this line.\ndoc/api/\n\n# dotenv environment variables file\n.env*\n\n# Avoid committing generated Javascript files:\n*.dart.js\n*.info.json # Produced by the --dump-info flag.\n*.js # When generated by dart2js. Don't specify *.js if your\n # project includes source files written in JavaScript.\n*.js_\n*.js.deps\n*.js.map\n\n.flutter-plugins\n.flutter-plugins-dependencies\n\n### Dart Patch ###\n# dotenv environment variables file\n.env\n\n# End of https://www.toptal.com/developers/gitignore/api/dart\n","darteditor":"# Created by https://www.toptal.com/developers/gitignore/api/darteditor\n# Edit at https://www.toptal.com/developers/gitignore?templates=darteditor\n\n### DartEditor ###\n.project\n.buildlog\n\n# End of https://www.toptal.com/developers/gitignore/api/darteditor\n","data":"# Created by https://www.toptal.com/developers/gitignore/api/data\n# Edit at https://www.toptal.com/developers/gitignore?templates=data\n\n### Data ###\n*.csv\n*.dat\n*.efx\n*.gbr\n*.key\n*.pps\n*.ppt\n*.pptx\n*.sdf\n*.tax2010\n*.vcf\n*.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/data\n","database":"# Created by https://www.toptal.com/developers/gitignore/api/database\n# Edit at https://www.toptal.com/developers/gitignore?templates=database\n\n### Database ###\n*.accdb\n*.db\n*.dbf\n*.mdb\n*.pdb\n*.sqlite3\n*.db-shm\n*.db-wal\n\n# End of https://www.toptal.com/developers/gitignore/api/database\n","datarecovery":"# Created by https://www.toptal.com/developers/gitignore/api/datarecovery\n# Edit at https://www.toptal.com/developers/gitignore?templates=datarecovery\n\n### DataRecovery ###\n\n### From https://en.wikipedia.org/wiki/List_of_archive_formats\n\n## Data recovery\n# File format used by dvdisaster to be used for data recovery when discs become damaged or partially unreadable.\n*.ecc\n# File format used in conjunction with any archive format to provide redundancy and data recovery, most often in newsgroup distribution of binary files.\n*.par\n*.par2\n\n\n# End of https://www.toptal.com/developers/gitignore/api/datarecovery\n","dbeaver":"# Created by https://www.toptal.com/developers/gitignore/api/dbeaver\n# Edit at https://www.toptal.com/developers/gitignore?templates=dbeaver\n\n### DBeaver ###\n# ide config file\n.dbeaver-data-sources*.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/dbeaver\n","dbt":"# Created by https://www.toptal.com/developers/gitignore/api/dbt\n# Edit at https://www.toptal.com/developers/gitignore?templates=dbt\n\n### dbt ###\ntarget/\ndbt_modules/\ndbt_packages/\nlogs/\n\n# End of https://www.toptal.com/developers/gitignore/api/dbt\n","defold":"# Created by https://www.toptal.com/developers/gitignore/api/defold\n# Edit at https://www.toptal.com/developers/gitignore?templates=defold\n\n### Defold ###\n/.internal\n/build\n.externalToolBuilders\n.DS_Store\nThumbs.db\n.lock-wscript\n*.pyc\n.project\n.cproject\nbuiltins\n\n# End of https://www.toptal.com/developers/gitignore/api/defold\n","delphi":"# Created by https://www.toptal.com/developers/gitignore/api/delphi\n# Edit at https://www.toptal.com/developers/gitignore?templates=delphi\n\n### Delphi ###\n# Uncomment these types if you want even more clean repository. But be careful.\n# It can make harm to an existing project source. Read explanations below.\n#\n# Resource files are binaries containing manifest, project icon and version info.\n# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files.\n#*.res\n# Type library file (binary). In old Delphi versions it should be stored.\n# Since Delphi 2009 it is produced from .ridl file and can safely be ignored.\n#*.tlb\n# Diagram Portfolio file. Used by the diagram editor up to Delphi 7.\n# Uncomment this if you are not using diagrams or use newer Delphi version.\n#*.ddp\n# Visual LiveBindings file. Added in Delphi XE2.\n# Uncomment this if you are not using LiveBindings Designer.\n#*.vlb\n# Deployment Manager configuration file for your project. Added in Delphi XE2.\n# Uncomment this if it is not mobile development and you do not use remote debug feature.\n#*.deployproj\n# C++ object files produced when C/C++ Output file generation is configured.\n# Uncomment this if you are not using external objects (zlib library for example).\n#*.obj\n\n# Delphi compiler-generated binaries (safe to delete)\n*.exe\n*.dll\n*.bpl\n*.bpi\n*.dcp\n*.so\n*.apk\n*.drc\n*.map\n*.dres\n*.rsm\n*.tds\n*.dcu\n*.lib\n*.a\n*.o\n*.ocx\n\n# Delphi autogenerated files (duplicated info)\n*.cfg\n*.hpp\n*Resource.rc\n\n# Delphi local files (user-specific info)\n*.local\n*.identcache\n*.projdata\n*.tvsconfig\n*.dsk\n\n# Delphi history and backups\n__history/\n__recovery/\n*.~*\n\n# Castalia statistics file (since XE7 Castalia is distributed with Delphi)\n*.stat\n\n# Boss dependency manager vendor folder https://github.com/HashLoad/boss\nmodules/\n\n# End of https://www.toptal.com/developers/gitignore/api/delphi\n","deno":"# Created by https://www.toptal.com/developers/gitignore/api/deno\n# Edit at https://www.toptal.com/developers/gitignore?templates=deno\n\n### Deno ###\n/.idea/\n/.vscode/\n\n/node_modules\n\n.env\n*.orig\n*.pyc\n*.swp\n\n# End of https://www.toptal.com/developers/gitignore/api/deno\n","dframe":"# Created by https://www.toptal.com/developers/gitignore/api/dframe\n# Edit at https://www.toptal.com/developers/gitignore?templates=dframe\n\n### Dframe ###\n# Ignore files that are auto-generated\napp/View/templates_c/*\napp/View/cache/*\napp/View/logs/*\nweb/assets/*\nvendor\n\n# Force include\n!app/View/templates_c/.gitkeep\n!app/View/cache/.gitkeep\n!app/View/logs/.gitkeep\n!web/assets/.gitkeep\n\n# Ignore IDE\n.project\n.vscode/\n\n# End of https://www.toptal.com/developers/gitignore/api/dframe\n","diff":"# Created by https://www.toptal.com/developers/gitignore/api/diff\n# Edit at https://www.toptal.com/developers/gitignore?templates=diff\n\n### Diff ###\n*.patch\n*.diff\n\n# End of https://www.toptal.com/developers/gitignore/api/diff\n","direnv":"# Created by https://www.toptal.com/developers/gitignore/api/direnv\n# Edit at https://www.toptal.com/developers/gitignore?templates=direnv\n\n### direnv ###\n.direnv\n.envrc\n\n# End of https://www.toptal.com/developers/gitignore/api/direnv\n","diskimage":"# Created by https://www.toptal.com/developers/gitignore/api/diskimage\n# Edit at https://www.toptal.com/developers/gitignore?templates=diskimage\n\n### DiskImage ###\n*.dmg\n*.iso\n*.toast\n*.vcd\n\n# End of https://www.toptal.com/developers/gitignore/api/diskimage\n","django":"# Created by https://www.toptal.com/developers/gitignore/api/django\n# Edit at https://www.toptal.com/developers/gitignore?templates=django\n\n### Django ###\n*.log\n*.pot\n*.pyc\n__pycache__/\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\nmedia\n\n# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/\n# in your Git repository. Update and uncomment the following line accordingly.\n# /staticfiles/\n\n### Django.Python Stack ###\n# Byte-compiled / optimized / DLL files\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n\n# Django stuff:\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n# This is especially recommended for binary packages to ensure reproducibility, and is more\n# commonly ignored for libraries.\n# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n# in version control.\n# https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n# JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n# and can be added to the global gitignore or merged into this file. For a more nuclear\n# option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n# End of https://www.toptal.com/developers/gitignore/api/django\n","dm":"# Created by https://www.toptal.com/developers/gitignore/api/dm\n# Edit at https://www.toptal.com/developers/gitignore?templates=dm\n\n### DM ###\n*.dmb\n*.rsc\n*.int\n*.lk\n*.zip\n\n# End of https://www.toptal.com/developers/gitignore/api/dm\n","docfx":"# Created by https://www.toptal.com/developers/gitignore/api/docfx\n# Edit at https://www.toptal.com/developers/gitignore?templates=docfx\n\n### DocFx ###\n.cache\n/**/_site/\n\n# End of https://www.toptal.com/developers/gitignore/api/docfx\n","docpress":"# Created by https://www.toptal.com/developers/gitignore/api/docpress\n# Edit at https://www.toptal.com/developers/gitignore?templates=docpress\n\n### Docpress ###\n# docpress documentation generator: https://docpress.github.io/index.html\n\n_docpress/\n\n# End of https://www.toptal.com/developers/gitignore/api/docpress\n","docusaurus":"# Created by https://www.toptal.com/developers/gitignore/api/docusaurus\n# Edit at https://www.toptal.com/developers/gitignore?templates=docusaurus\n\n### Docusaurus ###\n# Docusaurus cache and generated files\n.docusaurus\n\n# Docusaurus Build Directory\n/build/\n\n### Docusaurus.Node Stack ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n\n# Docusaurus cache and generated files\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# End of https://www.toptal.com/developers/gitignore/api/docusaurus\n","docz":"# Created by https://www.toptal.com/developers/gitignore/api/docz\n# Edit at https://www.toptal.com/developers/gitignore?templates=docz\n\n### Docz ###\n.docz\n\n\n# End of https://www.toptal.com/developers/gitignore/api/docz\n","dotenv":"# Created by https://www.toptal.com/developers/gitignore/api/dotenv\n# Edit at https://www.toptal.com/developers/gitignore?templates=dotenv\n\n### dotenv ###\n.env\n\n# End of https://www.toptal.com/developers/gitignore/api/dotenv\n","dotfilessh":"# Created by https://www.toptal.com/developers/gitignore/api/dotfilessh\n# Edit at https://www.toptal.com/developers/gitignore?templates=dotfilessh\n\n### DotfilesSh ###\nlocal-patch\npatched-src\n\n# End of https://www.toptal.com/developers/gitignore/api/dotfilessh\n","dotnetcore":"# Created by https://www.toptal.com/developers/gitignore/api/dotnetcore\n# Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore\n\n### DotnetCore ###\n# .NET Core build folders\nbin/\nobj/\n\n# Common node modules locations\n/node_modules\n/wwwroot/node_modules\n\n# End of https://www.toptal.com/developers/gitignore/api/dotnetcore\n","dotsettings":"# Created by https://www.toptal.com/developers/gitignore/api/dotsettings\n# Edit at https://www.toptal.com/developers/gitignore?templates=dotsettings\n\n### DotSettings ###\n*.DotSettings\n\n# End of https://www.toptal.com/developers/gitignore/api/dotsettings\n","doxygen":"# Created by https://www.toptal.com/developers/gitignore/api/doxygen\n# Edit at https://www.toptal.com/developers/gitignore?templates=doxygen\n\n### Doxygen ###\nhtml\nlatex\n\n# End of https://www.toptal.com/developers/gitignore/api/doxygen\n","dreamweaver":"# Created by https://www.toptal.com/developers/gitignore/api/dreamweaver\n# Edit at https://www.toptal.com/developers/gitignore?templates=dreamweaver\n\n### Dreamweaver ###\n# DW Dreamweaver added files\n_notes\n_compareTemp\nconfigs/\ndwsync.xml\ndw_php_codehinting.config\n*.mno\n\n# End of https://www.toptal.com/developers/gitignore/api/dreamweaver\n","dropbox":"# Created by https://www.toptal.com/developers/gitignore/api/dropbox\n# Edit at https://www.toptal.com/developers/gitignore?templates=dropbox\n\n### Dropbox ###\n# Dropbox settings and caches\n.dropbox\n.dropbox.attr\n.dropbox.cache\n\n# End of https://www.toptal.com/developers/gitignore/api/dropbox\n","drupal":"# Created by https://www.toptal.com/developers/gitignore/api/drupal\n# Edit at https://www.toptal.com/developers/gitignore?templates=drupal\n\n### Drupal ###\n# gitignore template for Drupal 8 projects\n#\n# earlier versions of Drupal are tracked in `community/PHP/`\n# follows official upstream conventions:\n# https://www.drupal.org/docs/develop/using-composer\n\n# Ignore configuration files that may contain sensitive information\n/web/sites/*/*settings*.php\n/web/sites/*/*services*.yml\n\n# Ignore paths that may contain user-generated content\n/web/sites/*/files\n/web/sites/*/public\n/web/sites/*/private\n/web/sites/*/files-public\n/web/sites/*/files-private\n\n# Ignore paths that may contain temporary files\n/web/sites/*/translations\n/web/sites/*/tmp\n/web/sites/*/cache\n\n# Ignore drupal core (if not versioning drupal sources)\n/web/vendor\n/web/core\n/web/modules/README.txt\n/web/profiles/README.txt\n/web/sites/development.services.yml\n/web/sites/example.settings.local.php\n/web/sites/example.sites.php\n/web/sites/README.txt\n/web/themes/README.txt\n/web/.csslintrc\n/web/.editorconfig\n/web/.eslintignore\n/web/.eslintrc.json\n/web/.gitattributes\n/web/.htaccess\n/web/.ht.router.php\n/web/autoload.php\n/web/composer.json\n/web/composer.lock\n/web/example.gitignore\n/web/index.php\n/web/INSTALL.txt\n/web/LICENSE.txt\n/web/README.txt\n/web/robots.txt\n/web/update.php\n/web/web.config\n\n# Ignore vendor dependencies and scripts\n/vendor\n/composer.phar\n/composer\n/robo.phar\n/robo\n/drush.phar\n/drush\n/drupal.phar\n/drupal\n\n# End of https://www.toptal.com/developers/gitignore/api/drupal\n","drupal7":"# Created by https://www.toptal.com/developers/gitignore/api/drupal7\n# Edit at https://www.toptal.com/developers/gitignore?templates=drupal7\n\n### Drupal7 ###\n# gitignore template for Drupal 7 projects\n#\n# It is recommended that you use `Drupal.gitignore` as this is the latest version\n\n# Ignore configuration files that may contain sensitive information.\nsites/*/*settings*.php\nsites/example.sites.php\n\n# Ignore paths that contain generated content.\nfiles/\nsites/*/files\nsites/*/private\nsites/*/translations\n\n# Ignore default text files\nrobots.txt\n/CHANGELOG.txt\n/COPYRIGHT.txt\n/INSTALL*.txt\n/LICENSE.txt\n/MAINTAINERS.txt\n/UPGRADE.txt\n/README.txt\nsites/README.txt\nsites/all/libraries/README.txt\nsites/all/modules/README.txt\nsites/all/themes/README.txt\n\n# Ignore everything but the \"sites\" folder ( for non core developer )\n.htaccess\nweb.config\nauthorize.php\ncron.php\nindex.php\ninstall.php\nupdate.php\nxmlrpc.php\n/includes\n/misc\n/modules\n/profiles\n/scripts\n/themes\n\n# End of https://www.toptal.com/developers/gitignore/api/drupal7\n","drupal8":"# Created by https://www.toptal.com/developers/gitignore/api/drupal8\n# Edit at https://www.toptal.com/developers/gitignore?templates=drupal8\n\n### Drupal8 ###\n# gitignore template for Drupal 8 projects\n#\n# earlier versions of Drupal are tracked in `community/PHP/`\n# follows official upstream conventions:\n# https://www.drupal.org/docs/develop/using-composer\n\n# Ignore configuration files that may contain sensitive information\n/web/sites/*/*settings*.php\n/web/sites/*/*services*.yml\n\n# Ignore paths that may contain user-generated content\n/web/sites/*/files\n/web/sites/*/public\n/web/sites/*/private\n/web/sites/*/files-public\n/web/sites/*/files-private\n\n# Ignore paths that may contain temporary files\n/web/sites/*/translations\n/web/sites/*/tmp\n/web/sites/*/cache\n\n# Ignore drupal core (if not versioning drupal sources)\n/web/vendor\n/web/core\n/web/modules/README.txt\n/web/profiles/README.txt\n/web/sites/development.services.yml\n/web/sites/example.settings.local.php\n/web/sites/example.sites.php\n/web/sites/README.txt\n/web/themes/README.txt\n/web/.csslintrc\n/web/.editorconfig\n/web/.eslintignore\n/web/.eslintrc.json\n/web/.gitattributes\n/web/.htaccess\n/web/.ht.router.php\n/web/autoload.php\n/web/composer.json\n/web/composer.lock\n/web/example.gitignore\n/web/index.php\n/web/INSTALL.txt\n/web/LICENSE.txt\n/web/README.txt\n/web/robots.txt\n/web/update.php\n/web/web.config\n\n# Ignore vendor dependencies and scripts\n/vendor\n/composer.phar\n/composer\n/robo.phar\n/robo\n/drush.phar\n/drush\n/drupal.phar\n/drupal\n\n# End of https://www.toptal.com/developers/gitignore/api/drupal8\n","e2studio":"# Created by https://www.toptal.com/developers/gitignore/api/e2studio\n# Edit at https://www.toptal.com/developers/gitignore?templates=e2studio\n\n### e2studio ###\n# Modified gitignore for Renesas E2Studio (Built on eclipse): maintain all data needed by eclipse to setting up a workspace.\n\n.history\n*.tmp\n*.obj\n*.log\nhistory.index\n\n# E2 Studio Specific\nHardwareDebug/src\n\n\n# End of https://www.toptal.com/developers/gitignore/api/e2studio\n","eagle":"# Created by https://www.toptal.com/developers/gitignore/api/eagle\n# Edit at https://www.toptal.com/developers/gitignore?templates=eagle\n\n### Eagle ###\n# Ignore list for Eagle, a PCB layout tool\n\n# Backup files\n*.s#?\n*.b#?\n*.l#?\n*.b$?\n*.s$?\n*.l$?\n\n# Eagle project file\n# It contains a serial number and references to the file structure\n# on your computer.\n# comment the following line if you want to have your project file included.\neagle.epf\n\n# Autorouter files\n*.pro\n*.job\n\n# CAM files\n*.$$$\n*.cmp\n*.ly2\n*.l15\n*.sol\n*.plc\n*.stc\n*.sts\n*.crc\n*.crs\n\n*.dri\n*.drl\n*.gpi\n*.pls\n*.ger\n*.xln\n\n*.drd\n*.drd.*\n\n*.s#*\n*.b#*\n\n*.info\n\n*.eps\n\n# file locks introduced since 7.x\n*.lck\n\n# End of https://www.toptal.com/developers/gitignore/api/eagle\n","easybook":"# Created by https://www.toptal.com/developers/gitignore/api/easybook\n# Edit at https://www.toptal.com/developers/gitignore?templates=easybook\n\n### easybook ###\n# Destination directory of the various publications\nOutput\n\n# End of https://www.toptal.com/developers/gitignore/api/easybook\n","eclipse":"# Created by https://www.toptal.com/developers/gitignore/api/eclipse\n# Edit at https://www.toptal.com/developers/gitignore?templates=eclipse\n\n### Eclipse ###\n.metadata\nbin/\ntmp/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.settings/\n.loadpath\n.recommenders\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# PyDev specific (Python IDE for Eclipse)\n*.pydevproject\n\n# CDT-specific (C/C++ Development Tooling)\n.cproject\n\n# CDT- autotools\n.autotools\n\n# Java annotation processor (APT)\n.factorypath\n\n# PDT-specific (PHP Development Tools)\n.buildpath\n\n# sbteclipse plugin\n.target\n\n# Tern plugin\n.tern-project\n\n# TeXlipse plugin\n.texlipse\n\n# STS (Spring Tool Suite)\n.springBeans\n\n# Code Recommenders\n.recommenders/\n\n# Annotation Processing\n.apt_generated/\n.apt_generated_test/\n\n# Scala IDE specific (Scala & Java development for Eclipse)\n.cache-main\n.scala_dependencies\n.worksheet\n\n# Uncomment this line if you wish to ignore the project description file.\n# Typically, this file would be tracked if it contains build/dependency configurations:\n#.project\n\n### Eclipse Patch ###\n# Spring Boot Tooling\n.sts4-cache/\n\n# End of https://www.toptal.com/developers/gitignore/api/eclipse\n","eiffelstudio":"# Created by https://www.toptal.com/developers/gitignore/api/eiffelstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=eiffelstudio\n\n### EiffelStudio ###\n# The compilation directory\nEIFGENs\n\n# End of https://www.toptal.com/developers/gitignore/api/eiffelstudio\n","elasticbeanstalk":"# Created by https://www.toptal.com/developers/gitignore/api/elasticbeanstalk\n# Edit at https://www.toptal.com/developers/gitignore?templates=elasticbeanstalk\n\n### ElasticBeanstalk ###\n.elasticbeanstalk/\n\n# End of https://www.toptal.com/developers/gitignore/api/elasticbeanstalk\n","elisp":"# Created by https://www.toptal.com/developers/gitignore/api/elisp\n# Edit at https://www.toptal.com/developers/gitignore?templates=elisp\n\n### Elisp ###\n# Compiled\n*.elc\n\n# Packaging\n.cask\n\n# Backup files\n*~\n\n# Undo-tree save-files\n*.~undo-tree\n\n# End of https://www.toptal.com/developers/gitignore/api/elisp\n","elixir":"# Created by https://www.toptal.com/developers/gitignore/api/elixir\n# Edit at https://www.toptal.com/developers/gitignore?templates=elixir\n\n### Elixir ###\n/_build\n/cover\n/deps\n/doc\n/.fetch\nerl_crash.dump\n*.ez\n*.beam\n/config/*.secret.exs\n.elixir_ls/\n\n### Elixir Patch ###\n\n# End of https://www.toptal.com/developers/gitignore/api/elixir\n","elm":"# Created by https://www.toptal.com/developers/gitignore/api/elm\n# Edit at https://www.toptal.com/developers/gitignore?templates=elm\n\n### Elm ###\n# elm-package generated files\nelm-stuff\n# elm-repl generated files\nrepl-temp-*\n\n# End of https://www.toptal.com/developers/gitignore/api/elm\n","emacs":"# Created by https://www.toptal.com/developers/gitignore/api/emacs\n# Edit at https://www.toptal.com/developers/gitignore?templates=emacs\n\n### Emacs ###\n# -*- mode: gitignore; -*-\n*~\n\\#*\\#\n/.emacs.desktop\n/.emacs.desktop.lock\n*.elc\nauto-save-list\ntramp\n.\\#*\n\n# Org-mode\n.org-id-locations\n*_archive\n\n# flymake-mode\n*_flymake.*\n\n# eshell files\n/eshell/history\n/eshell/lastdir\n\n# elpa packages\n/elpa/\n\n# reftex files\n*.rel\n\n# AUCTeX auto folder\n/auto/\n\n# cask packages\n.cask/\ndist/\n\n# Flycheck\nflycheck_*.el\n\n# server auth directory\n/server/\n\n# projectiles files\n.projectile\n\n# directory configuration\n.dir-locals.el\n\n# network security\n/network-security.data\n\n\n# End of https://www.toptal.com/developers/gitignore/api/emacs\n","ember":"# Created by https://www.toptal.com/developers/gitignore/api/ember\n# Edit at https://www.toptal.com/developers/gitignore?templates=ember\n\n### Ember ###\n# see https://github.com/ember-cli/ember-cli/blob/master/blueprints/app/files/gitignore\n# compiled output\n/dist\n/tmp\n\n# dependencies\n/node_modules\n/bower_components\n\n# misc\n/.sass-cache\n/connect.lock\n/coverage/*\n/libpeerconnection.log\nnpm-debug.log\ntestem.log\n\n# End of https://www.toptal.com/developers/gitignore/api/ember\n","ensime":"# Created by https://www.toptal.com/developers/gitignore/api/ensime\n# Edit at https://www.toptal.com/developers/gitignore?templates=ensime\n\n### Ensime ###\n# Ensime specific\n.ensime\n.ensime_cache/\n.ensime_lucene/\n\n# End of https://www.toptal.com/developers/gitignore/api/ensime\n","episerver":"# Created by https://www.toptal.com/developers/gitignore/api/episerver\n# Edit at https://www.toptal.com/developers/gitignore?templates=episerver\n\n### EPiServer ###\n######################\n## EPiServer Files\n*License.config\n\n# End of https://www.toptal.com/developers/gitignore/api/episerver\n","erlang":"# Created by https://www.toptal.com/developers/gitignore/api/erlang\n# Edit at https://www.toptal.com/developers/gitignore?templates=erlang\n\n### Erlang ###\n.eunit\n*.o\n*.beam\n*.plt\nerl_crash.dump\n.concrete/DEV_MODE\n\n# rebar 2.x\n.rebar\nrel/example_project\nebin/*.beam\ndeps\n\n# rebar 3\n.rebar3\n_build/\n_checkouts/\n\n### Erlang Patch ###\nrebar3.crashdump\n\n# End of https://www.toptal.com/developers/gitignore/api/erlang\n","espresso":"# Created by https://www.toptal.com/developers/gitignore/api/espresso\n# Edit at https://www.toptal.com/developers/gitignore?templates=espresso\n\n### Espresso ###\n*.esproj\n\n# End of https://www.toptal.com/developers/gitignore/api/espresso\n","executable":"# Created by https://www.toptal.com/developers/gitignore/api/executable\n# Edit at https://www.toptal.com/developers/gitignore?templates=executable\n\n### Executable ###\n*.app\n*.bat\n*.cgi\n*.com\n*.exe\n*.gadget\n*.jar\n*.pif\n*.vb\n*.wsf\n\n# End of https://www.toptal.com/developers/gitignore/api/executable\n","exercism":"# Created by https://www.toptal.com/developers/gitignore/api/exercism\n# Edit at https://www.toptal.com/developers/gitignore?templates=exercism\n\n### Exercism ###\n# gitignore template for Exercism project\n# website: https://exercism.io/\n\n# Ignore .exercism folder which contain sensitive data\n.exercism\n\n# End of https://www.toptal.com/developers/gitignore/api/exercism\n","expressionengine":"# Created by https://www.toptal.com/developers/gitignore/api/expressionengine\n# Edit at https://www.toptal.com/developers/gitignore?templates=expressionengine\n\n### ExpressionEngine ###\n.DS_Store\n\n# Images\nimages/avatars/\nimages/captchas/\nimages/smileys/\nimages/member_photos/\nimages/signature_attachments/\nimages/pm_attachments/\n\n# For security do not publish the following files\nsystem/expressionengine/config/database.php\nsystem/expressionengine/config/config.php\n\n# Caches\nsized/\nthumbs/\n_thumbs/\n*/expressionengine/cache/*\n\n# End of https://www.toptal.com/developers/gitignore/api/expressionengine\n","extjs":"# Created by https://www.toptal.com/developers/gitignore/api/extjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=extjs\n\n### ExtJs ###\n.architect\nbootstrap.css\nbootstrap.js\nbootstrap.json\nbootstrap.jsonp\nbuild/\nclassic.json\nclassic.jsonp\next/\nmodern.json\nmodern.jsonp\nresources/sass/.sass-cache/\nresources/.arch-internal-preview.css\n.arch-internal-preview.css\n\n# End of https://www.toptal.com/developers/gitignore/api/extjs\n","fancy":"# Created by https://www.toptal.com/developers/gitignore/api/fancy\n# Edit at https://www.toptal.com/developers/gitignore?templates=fancy\n\n### Fancy ###\n*.rbc\n*.fyc\n\n# End of https://www.toptal.com/developers/gitignore/api/fancy\n","fastlane":"# Created by https://www.toptal.com/developers/gitignore/api/fastlane\n# Edit at https://www.toptal.com/developers/gitignore?templates=fastlane\n\n### fastlane ###\n# fastlane - A streamlined workflow tool for Cocoa deployment\n#\n# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the\n# screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\n# fastlane specific\n**/fastlane/report.xml\n\n# deliver temporary files\n**/fastlane/Preview.html\n\n# snapshot generated screenshots\n**/fastlane/screenshots\n\n# scan temporary files\n**/fastlane/test_output\n\n# Fastlane.swift runner binary\n**/fastlane/FastlaneRunner\n\n# End of https://www.toptal.com/developers/gitignore/api/fastlane\n","finale":"# Created by https://www.toptal.com/developers/gitignore/api/finale\n# Edit at https://www.toptal.com/developers/gitignore?templates=finale\n\n### Finale ###\n*.bak\n*.db\n*.avi\n*.pdf\n*.ps\n*.mid\n*.midi\n*.mp3\n*.aif\n*.wav\n# Some versions of Finale have a bug and randomly save extra copies of\n# the music source as \" copy.mus\"\n*copy.mus\n\n# End of https://www.toptal.com/developers/gitignore/api/finale\n","firebase":"# Created by https://www.toptal.com/developers/gitignore/api/firebase\n# Edit at https://www.toptal.com/developers/gitignore?templates=firebase\n\n### Firebase ###\n.idea\n**/node_modules/*\n**/.firebaserc\n\n### Firebase Patch ###\n.runtimeconfig.json\n.firebase/\n\n# End of https://www.toptal.com/developers/gitignore/api/firebase\n","fish":"# Created by https://www.toptal.com/developers/gitignore/api/fish\n# Edit at https://www.toptal.com/developers/gitignore?templates=fish\n\n### Fish ###\nfishd.*\nfish_history\nfish_variables\nconfig.local.fish\n\n# End of https://www.toptal.com/developers/gitignore/api/fish\n","flashbuilder":"# Created by https://www.toptal.com/developers/gitignore/api/flashbuilder\n# Edit at https://www.toptal.com/developers/gitignore?templates=flashbuilder\n\n### FlashBuilder ###\n# Build and Release Folders\nbin/\nbin-debug/\nbin-release/\n.actionScriptProperties\n.flexConfig.xml\n\n# only for a non-library project\n.flexProperties\n\n# only for library project\n.flexLibProperties\n\n# Other files and folders\n.settings/\n.project/\n\n.FlexUnitSettings\n.externalToolBuilders\n.model\n\n\n# End of https://www.toptal.com/developers/gitignore/api/flashbuilder\n","flask":"# Created by https://www.toptal.com/developers/gitignore/api/flask\n# Edit at https://www.toptal.com/developers/gitignore?templates=flask\n\n### Flask ###\ninstance/*\n!instance/.gitignore\n.webassets-cache\n.env\n\n### Flask.Python Stack ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n# This is especially recommended for binary packages to ensure reproducibility, and is more\n# commonly ignored for libraries.\n# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n# in version control.\n# https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n# JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n# and can be added to the global gitignore or merged into this file. For a more nuclear\n# option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n# End of https://www.toptal.com/developers/gitignore/api/flask\n","flatpak":"# Created by https://www.toptal.com/developers/gitignore/api/flatpak\n# Edit at https://www.toptal.com/developers/gitignore?templates=flatpak\n\n### Flatpak ###\n.flatpak-builder\nbuild\nbuild-dir\nrepo\n\n# End of https://www.toptal.com/developers/gitignore/api/flatpak\n","flex":"# Created by https://www.toptal.com/developers/gitignore/api/flex\n# Edit at https://www.toptal.com/developers/gitignore?templates=flex\n\n### Flex ###\n\n# Build and Release Folders\nbin/\nbin-debug/\nbin-release/\n.actionScriptProperties\n\n# only for a non-library project\n.flexProperties\n\n# only for library project\n.flexLibProperties\n\n\n# Other files and folders\n.settings/\n.project/\n\n.FlexUnitSettings\n.externalToolBuilders\n.model\n\n# End of https://www.toptal.com/developers/gitignore/api/flex\n","flexbuilder":"# Created by https://www.toptal.com/developers/gitignore/api/flexbuilder\n# Edit at https://www.toptal.com/developers/gitignore?templates=flexbuilder\n\n### FlexBuilder ###\nbin/\nbin-debug/\nbin-release/\n\n# End of https://www.toptal.com/developers/gitignore/api/flexbuilder\n","floobits":"# Created by https://www.toptal.com/developers/gitignore/api/floobits\n# Edit at https://www.toptal.com/developers/gitignore?templates=floobits\n\n### floobits ###\n.floo\n\n# End of https://www.toptal.com/developers/gitignore/api/floobits\n","flutter":"# Created by https://www.toptal.com/developers/gitignore/api/flutter\n# Edit at https://www.toptal.com/developers/gitignore?templates=flutter\n\n### Flutter ###\n# Flutter/Dart/Pub related\n**/doc/api/\n.dart_tool/\n.flutter-plugins\n.flutter-plugins-dependencies\n.fvm/flutter_sdk\n.packages\n.pub-cache/\n.pub/\nbuild/\ncoverage/\nlib/generated_plugin_registrant.dart\n# For library packages, don’t commit the pubspec.lock file.\n# Regenerating the pubspec.lock file lets you test your package against the latest compatible versions of its dependencies.\n# See https://dart.dev/guides/libraries/private-files#pubspeclock\n#pubspec.lock\n\n# Android related\n**/android/**/gradle-wrapper.jar\n**/android/.gradle\n**/android/captures/\n**/android/gradlew\n**/android/gradlew.bat\n**/android/key.properties\n**/android/local.properties\n**/android/**/GeneratedPluginRegistrant.java\n\n# iOS/XCode related\n**/ios/**/*.mode1v3\n**/ios/**/*.mode2v3\n**/ios/**/*.moved-aside\n**/ios/**/*.pbxuser\n**/ios/**/*.perspectivev3\n**/ios/**/*sync/\n**/ios/**/.sconsign.dblite\n**/ios/**/.tags*\n**/ios/**/.vagrant/\n**/ios/**/DerivedData/\n**/ios/**/Icon?\n**/ios/**/Pods/\n**/ios/**/.symlinks/\n**/ios/**/profile\n**/ios/**/xcuserdata\n**/ios/.generated/\n**/ios/Flutter/.last_build_id\n**/ios/Flutter/App.framework\n**/ios/Flutter/Flutter.framework\n**/ios/Flutter/Flutter.podspec\n**/ios/Flutter/Generated.xcconfig\n**/ios/Flutter/app.flx\n**/ios/Flutter/app.zip\n**/ios/Flutter/flutter_assets/\n**/ios/Flutter/flutter_export_environment.sh\n**/ios/ServiceDefinitions.json\n**/ios/Runner/GeneratedPluginRegistrant.*\n\n# Exceptions to above rules.\n!**/ios/**/default.mode1v3\n!**/ios/**/default.mode2v3\n!**/ios/**/default.pbxuser\n!**/ios/**/default.perspectivev3\n!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages\n\n# End of https://www.toptal.com/developers/gitignore/api/flutter\n","font":"# Created by https://www.toptal.com/developers/gitignore/api/font\n# Edit at https://www.toptal.com/developers/gitignore?templates=font\n\n### Font ###\n*.fnt\n*.fon\n*.otf\n*.ttf\n\n# End of https://www.toptal.com/developers/gitignore/api/font\n","fontforge":"# Created by https://www.toptal.com/developers/gitignore/api/fontforge\n# Edit at https://www.toptal.com/developers/gitignore?templates=fontforge\n\n### FontForge ###\n*.sfd-*\n*.sfd~\n\n# No Binaries\n\n*.ttf\n*.ttc\n*.otf\n*.woff\n*.pfb\n*.pfa\n*.pt3\n*.suit\n\n# End of https://www.toptal.com/developers/gitignore/api/fontforge\n","forcedotcom":"# Created by https://www.toptal.com/developers/gitignore/api/forcedotcom\n# Edit at https://www.toptal.com/developers/gitignore?templates=forcedotcom\n\n### ForceDotCom ###\n.project\n.settings\nsalesforce.schema\nReferenced Packages\n\n# End of https://www.toptal.com/developers/gitignore/api/forcedotcom\n","forgegradle":"# Created by https://www.toptal.com/developers/gitignore/api/forgegradle\n# Edit at https://www.toptal.com/developers/gitignore?templates=forgegradle\n\n### ForgeGradle ###\n# Minecraft client/server files\nrun/\n\n# End of https://www.toptal.com/developers/gitignore/api/forgegradle\n","fortran":"# Created by https://www.toptal.com/developers/gitignore/api/fortran\n# Edit at https://www.toptal.com/developers/gitignore?templates=fortran\n\n### Fortran ###\n# Prerequisites\n*.d\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n*.smod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n\n# End of https://www.toptal.com/developers/gitignore/api/fortran\n","freecad":"# Created by https://www.toptal.com/developers/gitignore/api/freecad\n# Edit at https://www.toptal.com/developers/gitignore?templates=freecad\n\n### FreeCad ###\n*.pyc\n*.fcstd1\n*.FCStd1\n*.fcstd2\n*.FCStd2\n\n# End of https://www.toptal.com/developers/gitignore/api/freecad\n","freepascal":"# Created by https://www.toptal.com/developers/gitignore/api/freepascal\n# Edit at https://www.toptal.com/developers/gitignore?templates=freepascal\n\n### FreePascal ###\n*.lps\n*.compiled\n*.[oa]\n*.ppu\n*.rst\n*.cgi\n*.exe\n*.log\n*.bak*\nfp.ini\nfp.cfg\nfp.dsk\n\n# End of https://www.toptal.com/developers/gitignore/api/freepascal\n","fsharp":"# Created by https://www.toptal.com/developers/gitignore/api/fsharp\n# Edit at https://www.toptal.com/developers/gitignore?templates=fsharp\n\n### fsharp ###\nlib/debug\nlib/release\nDebug\n*.suo\n*.user\nobj\nbin\n/build/\n*.exe\n!.paket/paket.bootstrapper.exe\n.ionide/\n.fake\n\n# End of https://www.toptal.com/developers/gitignore/api/fsharp\n","fuelphp":"# Created by https://www.toptal.com/developers/gitignore/api/fuelphp\n# Edit at https://www.toptal.com/developers/gitignore?templates=fuelphp\n\n### FuelPHP ###\n# the composer package lock file and install directory\n# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file\n# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file\n# /composer.lock\n/fuel/vendor\n\n# the fuelphp document\n/docs/\n\n# you may install these packages with `oil package`.\n# http://fuelphp.com/docs/packages/oil/package.html\n# /fuel/packages/auth/\n# /fuel/packages/email/\n# /fuel/packages/oil/\n# /fuel/packages/orm/\n# /fuel/packages/parser/\n\n# dynamically generated files\n/fuel/app/logs/*/*/*\n/fuel/app/cache/*/*\n/fuel/app/config/crypt.php\n\n# End of https://www.toptal.com/developers/gitignore/api/fuelphp\n","fusetools":"# Created by https://www.toptal.com/developers/gitignore/api/fusetools\n# Edit at https://www.toptal.com/developers/gitignore?templates=fusetools\n\n### FuseTools ###\n# Built and preview application files\nbuild\n\n# Scripts conversion file\n.uno\n\n# End of https://www.toptal.com/developers/gitignore/api/fusetools\n","games":"# Created by https://www.toptal.com/developers/gitignore/api/games\n# Edit at https://www.toptal.com/developers/gitignore?templates=games\n\n### Games ###\n*.gam\n*.nes\n*.rom\n*.sav\n\n# End of https://www.toptal.com/developers/gitignore/api/games\n","gatsby":"# Created by https://www.toptal.com/developers/gitignore/api/gatsby\n# Edit at https://www.toptal.com/developers/gitignore?templates=gatsby\n\n### Gatsby ###\nnode_modules/\n\n.cache/\npublic\n\nsrc/gatsby-types.d.ts\n\n# End of https://www.toptal.com/developers/gitignore/api/gatsby\n","gcov":"# Created by https://www.toptal.com/developers/gitignore/api/gcov\n# Edit at https://www.toptal.com/developers/gitignore?templates=gcov\n\n### Gcov ###\n# gcc coverage testing tool files\n\n*.gcno\n*.gcda\n*.gcov\n\n# End of https://www.toptal.com/developers/gitignore/api/gcov\n","genero4gl":"# Created by https://www.toptal.com/developers/gitignore/api/genero4gl\n# Edit at https://www.toptal.com/developers/gitignore?templates=genero4gl\n\n### Genero4GL ###\n# fglcomp compiled outputs\n# http://www.4js.com/online_documentation/fjs-fgl-manual-html/?path=fjs-fgl-manual#c_fgl_FileExtensions_001.html\n*.42m\n*.42f\n*.42s\n*.42r\n*.42x\n*.iem\n\n# Workspace view state (undocumented)\n4pwvs\n\n# Workspace database (undocumented)\n4pwdb\n\n# fglpc and fglform compiled outputs\n# http://publib.boulder.ibm.com/epubs/pdf/ct1vbna.pdf\n*.4go\n*.4gi\n*.4ge\n*.frm\n*.err\n*.4bl\n*.4bo\n*.ec\n*.c\n*.erc\n*.4ec\n*.pbr\n*.fbm\n\n# End of https://www.toptal.com/developers/gitignore/api/genero4gl\n","geth":"# Created by https://www.toptal.com/developers/gitignore/api/geth\n# Edit at https://www.toptal.com/developers/gitignore?templates=geth\n\n### Geth ###\n# .gitignore for Geth 0.4.16\n\n# caches\n/geth\n/keystore\nhistory\n\n# logs\n*.out\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/geth\n","ggts":"# Created by https://www.toptal.com/developers/gitignore/api/ggts\n# Edit at https://www.toptal.com/developers/gitignore?templates=ggts\n\n### GGTS ###\n# .gitignore generated by: grails integrate-with --git\n# current using for Grails 2.4.4\n*.iws\n*Db.properties\n*Db.script\n.settings\nstacktrace.log\n/*.zip\n/plugin.xml\n/*.log\n/*DB.*\n/cobertura.ser\n/target/\n/target-eclipse/\n/out/\n/.link_to_grails_plugins/\n/web-app/plugins\n/web-app/WEB-INF/classes\n\n# End of https://www.toptal.com/developers/gitignore/api/ggts\n","gis":"# Created by https://www.toptal.com/developers/gitignore/api/gis\n# Edit at https://www.toptal.com/developers/gitignore?templates=gis\n\n### GIS ###\n*.gpx\n*.kml\n\n# End of https://www.toptal.com/developers/gitignore/api/gis\n","git":"# Created by https://www.toptal.com/developers/gitignore/api/git\n# Edit at https://www.toptal.com/developers/gitignore?templates=git\n\n### Git ###\n# Created by git for backups. To disable backups in Git:\n# $ git config --global mergetool.keepBackup false\n*.orig\n\n# Created by git when using merge tools for conflicts\n*.BACKUP.*\n*.BASE.*\n*.LOCAL.*\n*.REMOTE.*\n*_BACKUP_*.txt\n*_BASE_*.txt\n*_LOCAL_*.txt\n*_REMOTE_*.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/git\n","gitbook":"# Created by https://www.toptal.com/developers/gitignore/api/gitbook\n# Edit at https://www.toptal.com/developers/gitignore?templates=gitbook\n\n### GitBook ###\n# Node rules:\n## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n## Dependency directory\n## Commenting this out is preferred by some people, see\n## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git\nnode_modules\n\n# Book build output\n_book\n\n# eBook build output\n*.epub\n*.mobi\n*.pdf\n\n# End of https://www.toptal.com/developers/gitignore/api/gitbook\n","go":"# Created by https://www.toptal.com/developers/gitignore/api/go\n# Edit at https://www.toptal.com/developers/gitignore?templates=go\n\n### Go ###\n# If you prefer the allow list template instead of the deny list, see community template:\n# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore\n#\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n# Go workspace file\ngo.work\n\n# End of https://www.toptal.com/developers/gitignore/api/go\n","godot":"# Created by https://www.toptal.com/developers/gitignore/api/godot\n# Edit at https://www.toptal.com/developers/gitignore?templates=godot\n\n### Godot ###\n# Godot 4+ specific ignores\n.godot/\n\n# Godot-specific ignores\n.import/\nexport.cfg\nexport_presets.cfg\n\n# Imported translations (automatically generated from CSV files)\n*.translation\n\n# Mono-specific ignores\n.mono/\ndata_*/\nmono_crash.*.json\n\n# End of https://www.toptal.com/developers/gitignore/api/godot\n","goland":"# Created by https://www.toptal.com/developers/gitignore/api/goland\n# Edit at https://www.toptal.com/developers/gitignore?templates=goland\n\n### GoLand ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### GoLand Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/goland\n","goland+all":"# Created by https://www.toptal.com/developers/gitignore/api/goland+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=goland+all\n\n### GoLand+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### GoLand+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/goland+all\n","goland+iml":"# Created by https://www.toptal.com/developers/gitignore/api/goland+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=goland+iml\n\n### GoLand+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### GoLand+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/goland+iml\n","goodsync":"# Created by https://www.toptal.com/developers/gitignore/api/goodsync\n# Edit at https://www.toptal.com/developers/gitignore?templates=goodsync\n\n### GoodSync ###\n_gsdata_\n\n# End of https://www.toptal.com/developers/gitignore/api/goodsync\n","gpg":"# Created by https://www.toptal.com/developers/gitignore/api/gpg\n# Edit at https://www.toptal.com/developers/gitignore?templates=gpg\n\n### GPG ###\nsecring.*\n\n\n# End of https://www.toptal.com/developers/gitignore/api/gpg\n","gradle":"# Created by https://www.toptal.com/developers/gitignore/api/gradle\n# Edit at https://www.toptal.com/developers/gitignore?templates=gradle\n\n### Gradle ###\n.gradle\n**/build/\n!src/**/build/\n\n# Ignore Gradle GUI config\ngradle-app.setting\n\n# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)\n!gradle-wrapper.jar\n\n# Avoid ignore Gradle wrappper properties\n!gradle-wrapper.properties\n\n# Cache of project\n.gradletasknamecache\n\n# Eclipse Gradle plugin generated files\n# Eclipse Core\n.project\n# JDT-specific (Eclipse Java Development Tools)\n.classpath\n\n### Gradle Patch ###\n# Java heap dump\n*.hprof\n\n# End of https://www.toptal.com/developers/gitignore/api/gradle\n","grails":"# Created by https://www.toptal.com/developers/gitignore/api/grails\n# Edit at https://www.toptal.com/developers/gitignore?templates=grails\n\n### Grails ###\n# .gitignore for Grails 1.2 and 1.3\n# Although this should work for most versions of grails, it is\n# suggested that you use the \"grails integrate-with --git\" command\n# to generate your .gitignore file.\n\n# web application files\n/web-app/WEB-INF/classes\n\n# default HSQL database files for production mode\n/prodDb.*\n\n# general HSQL database files\n*Db.properties\n*Db.script\n\n# logs\n/stacktrace.log\n/test/reports\n/logs\n\n# project release file\n/*.war\n\n# plugin release files\n/*.zip\n/plugin.xml\n\n# older plugin install locations\n/plugins\n/web-app/plugins\n\n# \"temporary\" build files\n/target\n\n# End of https://www.toptal.com/developers/gitignore/api/grails\n","greenfoot":"# Created by https://www.toptal.com/developers/gitignore/api/greenfoot\n# Edit at https://www.toptal.com/developers/gitignore?templates=greenfoot\n\n### greenfoot ###\n# Greenfoot: A BlueJ-based game IDE for programming beginners. http://bluej.org\n\n# Editor preferences for a class etc, see http://lists.bluej.org/pipermail/bluej-discuss/2003-May/002351.html\n*.ctxt\n\n# Project file. Continously checking it in destroys the project ocasionally (while merging etc.) because it changes completely at every compile. Instead, make a copy of the file after the first runtime and name it e.g. `project.example.greenfoot`. See http://bugs.bluej.org/greenfoot/ticket/409\n\nproject.greenfoot\n\n# End of https://www.toptal.com/developers/gitignore/api/greenfoot\n","groovy":"# Created by https://www.toptal.com/developers/gitignore/api/groovy\n# Edit at https://www.toptal.com/developers/gitignore?templates=groovy\n\n### Groovy ###\n# .gitignore created from Groovy contributors in https://github.com/apache/groovy/blob/master/.gitignore\n\nuser.gradle\n.gradle/\ntarget/\nbuild/\nout/\n\n*.DS_Store\n*.class\n*.swp\n*~\n\n.idea\n*.iml\n*.ipr\n*.iws\n.shelf\n\n.settings/\n.classpath\n.project\nbin/\n\n\n# End of https://www.toptal.com/developers/gitignore/api/groovy\n","grunt":"# Created by https://www.toptal.com/developers/gitignore/api/grunt\n# Edit at https://www.toptal.com/developers/gitignore?templates=grunt\n\n### grunt ###\n# Grunt usually compiles files inside this directory\ndist/\n\n# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory\n.tmp/\n\n# End of https://www.toptal.com/developers/gitignore/api/grunt\n","gwt":"# Created by https://www.toptal.com/developers/gitignore/api/gwt\n# Edit at https://www.toptal.com/developers/gitignore?templates=gwt\n\n### GWT ###\n*.class\n\n# Package Files #\n*.jar\n*.war\n\n# gwt caches and compiled units #\nwar/gwt_bree/\ngwt-unitCache/\n\n# boilerplate generated classes #\n.apt_generated/\n\n# more caches and things from deploy #\nwar/WEB-INF/deploy/\nwar/WEB-INF/classes/\n\n#compilation logs\n.gwt/\n\n#gwt junit compilation files\nwww-test/\n\n#old GWT (1.5) created this dir\n.gwt-tmp/\n\n# End of https://www.toptal.com/developers/gitignore/api/gwt\n","haskell":"# Created by https://www.toptal.com/developers/gitignore/api/haskell\n# Edit at https://www.toptal.com/developers/gitignore?templates=haskell\n\n### Haskell ###\ndist\ndist-*\ncabal-dev\n*.o\n*.hi\n*.hie\n*.chi\n*.chs.h\n*.dyn_o\n*.dyn_hi\n.hpc\n.hsenv\n.cabal-sandbox/\ncabal.sandbox.config\n*.prof\n*.aux\n*.hp\n*.eventlog\n.stack-work/\ncabal.project.local\ncabal.project.local~\n.HTF/\n.ghc.environment.*\n\n# End of https://www.toptal.com/developers/gitignore/api/haskell\n","helm":"# Created by https://www.toptal.com/developers/gitignore/api/helm\n# Edit at https://www.toptal.com/developers/gitignore?templates=helm\n\n### Helm ###\n# Chart dependencies\n**/charts/*.tgz\n\n# End of https://www.toptal.com/developers/gitignore/api/helm\n","hexo":"# Created by https://www.toptal.com/developers/gitignore/api/hexo\n# Edit at https://www.toptal.com/developers/gitignore?templates=hexo\n\n### Hexo ###\ndb.json\npublic/\n.deploy_git/\n\n# End of https://www.toptal.com/developers/gitignore/api/hexo\n","hol":"# Created by https://www.toptal.com/developers/gitignore/api/hol\n# Edit at https://www.toptal.com/developers/gitignore?templates=hol\n\n### HOL ###\n*Script\n\n# Holmake generated files\n*Theory.sig\n*Theory.sml\n*.uo\n*.ui\n\n# Holmake auxiliary files\n.hollogs\n.HOLMK\n\n# End of https://www.toptal.com/developers/gitignore/api/hol\n","homeassistant":"# Created by https://www.toptal.com/developers/gitignore/api/homeassistant\n# Edit at https://www.toptal.com/developers/gitignore?templates=homeassistant\n\n### HomeAssistant ###\n# Files with personal details\n*.crt\n*.csr\n*.key\n.cloud\n.google.token\n.HA_VERSION\n.storage\n.uuid\nicloud/\ngoogle_calendars.yaml\nharmony_media_room.conf\nhome-assistant.db\nhome-assistant_v2.db\nhome-assistant_v2.db-*\nhtml5_push_registrations.conf\nimage\nip_bans.yaml\nknown_devices.yaml\nphue.conf\nplex.conf\npyozw.sqlite\nsecrets.yaml\ntradfri.conf\nwebostv.conf\n\n# Temporary files\n*.db-journal\n*.pid\ntts\n\n# automatically downloaded dependencies\ndeps\nlib\n\n# Log files\nhome-assistant.log\nhome-assistant.log.*\nozw_log.txt\n\n# Development files\ncustom_components\n\n# End of https://www.toptal.com/developers/gitignore/api/homeassistant\n","homebrew":"# Created by https://www.toptal.com/developers/gitignore/api/homebrew\n# Edit at https://www.toptal.com/developers/gitignore?templates=homebrew\n\n### Homebrew ###\nBrewfile.lock.json\n\n# End of https://www.toptal.com/developers/gitignore/api/homebrew\n","hsp":"# Created by https://www.toptal.com/developers/gitignore/api/hsp\n# Edit at https://www.toptal.com/developers/gitignore?templates=hsp\n\n### HSP ###\n# Object files\nhsptmp*\npackfile\nobj\n*.ax\n\n# Binaries\n*.exe\n*.dpm\n\n# End of https://www.toptal.com/developers/gitignore/api/hsp\n","hugo":"# Created by https://www.toptal.com/developers/gitignore/api/hugo\n# Edit at https://www.toptal.com/developers/gitignore?templates=hugo\n\n### Hugo ###\n# Generated files by hugo\n/public/\n/resources/_gen/\n/assets/jsconfig.json\nhugo_stats.json\n\n# Executable may be added to repository\nhugo.exe\nhugo.darwin\nhugo.linux\n\n# Temporary lock file while building\n/.hugo_build.lock\n\n# End of https://www.toptal.com/developers/gitignore/api/hugo\n","hyperledgercomposer":"# Created by https://www.toptal.com/developers/gitignore/api/hyperledgercomposer\n# Edit at https://www.toptal.com/developers/gitignore?templates=hyperledgercomposer\n\n### HyperledgerComposer ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n# Output of Hyperledger 'composer' tools\n*.bna\n*.card\n\n# End of https://www.toptal.com/developers/gitignore/api/hyperledgercomposer\n","iar":"# Created by https://www.toptal.com/developers/gitignore/api/iar\n# Edit at https://www.toptal.com/developers/gitignore?templates=iar\n\n### IAR ###\n# Compiled binaries\n*.o\n*.bin\n*.elf\n*.hex\n*.map\n*.out\n*.obj\n\n# Trash\n*.bak\nthumbs.db\n*.~*\n\n# IAR Settings\n**/settings/*.crun\n**/settings/*.dbgdt\n**/settings/*.cspy\n**/settings/*.cspy.*\n**/settings/*.xcl\n**/settings/*.dni\n**/settings/*.wsdt\n**/settings/*.wspos\n\n# IAR Debug Exe\n**/Exe/*.sim\n\n# IAR Debug Obj\n**/Obj/*.pbd\n**/Obj/*.pbd.*\n**/Obj/*.pbi\n**/Obj/*.pbi.*\n\n# IAR project \"Debug\" directory\nDebug/\n\n# IAR project \"Release\" directory\nRelease/\n\n# IAR project settings directory\nsettings/\n\n# IAR backup files\nBackup*\n\n# IAR .dep files\n*.dep\n\n# End of https://www.toptal.com/developers/gitignore/api/iar\n","iar_ewarm":"# Created by https://www.toptal.com/developers/gitignore/api/iar_ewarm\n# Edit at https://www.toptal.com/developers/gitignore?templates=iar_ewarm\n\n### IAR_EWARM ###\n# gitignore template for the IAR EWARM\n# website: https://www.iar.com/knowledge/support/technical-notes/ide/which-files-should-be-version-controlled/\n\n# Some tools will put the EWARM files\n# under a subdirectory with the same name\n# as the configuration.\n# Example\n# EWARM/Config1/Obj /List /Exe\n# EWARM/Config2/Obj /List /Exe\nEWARM/**/Obj\nEWARM/**/List\nEWARM/**/Exe\n\n# Autogenerated project files\n*.dep\n*.ewt\n\n# Autogenerated folder for debugger\nEWARM/settings\n\n# End of https://www.toptal.com/developers/gitignore/api/iar_ewarm\n","iarembeddedworkbench":"# Created by https://www.toptal.com/developers/gitignore/api/iarembeddedworkbench\n# Edit at https://www.toptal.com/developers/gitignore?templates=iarembeddedworkbench\n\n### IAREmbeddedWorkBench ###\n# IAR C-STAT and C-RUN\n# Comment this out if you use C-Stat or C-Run to compile/analyze your project\n*.ewt\n\n# IAR Debugger files\n*.ewd\n\n# IAR Workspace files\n*.eww\n\n# IAR Settings\n**/settings/*.crun\n**/settings/*.dbgdt\n**/settings/*.cspy\n**/settings/*.cspy.*\n**/settings/*.xcl\n**/settings/*.dni\n**/settings/*.wsdt\n**/settings/*.wspos\n\n# IAR Debug Exe\n**/Debug/Exe/\n\n# IAR Debug List\n**/Debug/List\n\n# IAR Debug Obj\n**/Obj/*.pbd\n**/Obj/*.pbd.*\n**/Obj/*.pbi\n**/Obj/*.pbi.*\n\n# Log files\n**.log\n\n# IAR backup files\nBackup*\n\n# IAR .dep files\n*.dep\n\n# Compiled Binaries\n*.bin\n*.elf\n*.hex\n*.map\n\n# Trash files\n*.bak\n\n# End of https://www.toptal.com/developers/gitignore/api/iarembeddedworkbench\n","idapro":"# Created by https://www.toptal.com/developers/gitignore/api/idapro\n# Edit at https://www.toptal.com/developers/gitignore?templates=idapro\n\n### IDAPro ###\n# gitginore template for IDA Pro\n# website: https://www.hex-rays.com/index.shtml\n\n# IDA Pro Runtime temporary file\n*.id0\n*.id1\n*.id2\n*.nam\n*.til\n\n# IDA Pro 64 packaged data\n# *.i64\n# IDA Pro 32 packaged data\n# *.idb\n\n# End of https://www.toptal.com/developers/gitignore/api/idapro\n","idris":"# Created by https://www.toptal.com/developers/gitignore/api/idris\n# Edit at https://www.toptal.com/developers/gitignore?templates=idris\n\n### Idris ###\n# Idris 2\n*.ttc\n*.ttm\n\n# Idris 1\n*.ibc\n*.o\n\n# End of https://www.toptal.com/developers/gitignore/api/idris\n","igorpro":"# Created by https://www.toptal.com/developers/gitignore/api/igorpro\n# Edit at https://www.toptal.com/developers/gitignore?templates=igorpro\n\n### IGORPro ###\n# Avoid including Experiment files: they can be created and edited locally to test the ipf files\n*.pxp\n*.pxt\n*.uxp\n*.uxt\n\n# End of https://www.toptal.com/developers/gitignore/api/igorpro\n","images":"# Created by https://www.toptal.com/developers/gitignore/api/images\n# Edit at https://www.toptal.com/developers/gitignore?templates=images\n\n### Images ###\n# JPEG\n*.jpg\n*.jpeg\n*.jpe\n*.jif\n*.jfif\n*.jfi\n\n# JPEG 2000\n*.jp2\n*.j2k\n*.jpf\n*.jpx\n*.jpm\n*.mj2\n\n# JPEG XR\n*.jxr\n*.hdp\n*.wdp\n\n# Graphics Interchange Format\n*.gif\n\n# RAW\n*.raw\n\n# Web P\n*.webp\n\n# Portable Network Graphics\n*.png\n\n# Animated Portable Network Graphics\n*.apng\n\n# Multiple-image Network Graphics\n*.mng\n\n# Tagged Image File Format\n*.tiff\n*.tif\n\n# Scalable Vector Graphics\n*.svg\n*.svgz\n\n# Portable Document Format\n*.pdf\n\n# X BitMap\n*.xbm\n\n# BMP\n*.bmp\n*.dib\n\n# ICO\n*.ico\n\n# 3D Images\n*.3dm\n*.max\n\n# End of https://www.toptal.com/developers/gitignore/api/images\n","infer":"# Created by https://www.toptal.com/developers/gitignore/api/infer\n# Edit at https://www.toptal.com/developers/gitignore?templates=infer\n\n### infer ###\n# infer- http://fbinfer.com/\ninfer-out\n\n# End of https://www.toptal.com/developers/gitignore/api/infer\n","inforcms":"# Created by https://www.toptal.com/developers/gitignore/api/inforcms\n# Edit at https://www.toptal.com/developers/gitignore?templates=inforcms\n\n### InforCMS ###\n# gitignore template for InforCRM (formerly SalesLogix)\n# website: https://www.infor.com/product-summary/cx/infor-crm/\n#\n# Recommended: VisualStudio.gitignore\n\n# Ignore model files that are auto-generated\nModelIndex.xml\nExportedFiles.xml\n\n# Ignore deployment files\n[Mm]odel/[Dd]eployment\n\n# Force include portal SupportFiles\n!Model/Portal/*/SupportFiles/[Bb]in/\n!Model/Portal/PortalTemplates/*/SupportFiles/[Bb]in\n\n# End of https://www.toptal.com/developers/gitignore/api/inforcms\n","inforcrm":"# Created by https://www.toptal.com/developers/gitignore/api/inforcrm\n# Edit at https://www.toptal.com/developers/gitignore?templates=inforcrm\n\n### InforCRM ###\n#########################################\n# .gitignore for Infor CRM (SalesLogix)\n\n# Ignore model files that are auto-generated\nModelIndex.xml\nExportedFiles.xml\n\n# Ignore deployment files\n[Mm]odel/[Dd]eployment\n\n# Ignore Visual Studio/IDE user/cache/build/tooling files generated by build process\n*.user\n*.aps\n*.pch\n*.vspscc\n*_i.c\n*_p.c\n*.ncb\n*.suo\n*.bak\n*.cache\n*.ilk\n*.log\n[Bb]in\n[Dd]ebug*/\n*.sbr\n.vs/\nobj/\n[Rr]elease*/\n_ReSharper*/\n\n# Force include portal SupportFiles\n!Model/Portal/*/SupportFiles/[Bb]in/\n!Model/Portal/PortalTemplates/*/SupportFiles/[Bb]in\n\n# End of https://www.toptal.com/developers/gitignore/api/inforcrm\n","intellij":"# Created by https://www.toptal.com/developers/gitignore/api/intellij\n# Edit at https://www.toptal.com/developers/gitignore?templates=intellij\n\n### Intellij ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/intellij\n","intellij+all":"# Created by https://www.toptal.com/developers/gitignore/api/intellij+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all\n\n### Intellij+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/intellij+all\n","intellij+iml":"# Created by https://www.toptal.com/developers/gitignore/api/intellij+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+iml\n\n### Intellij+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### Intellij+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/intellij+iml\n","ionic3":"# Created by https://www.toptal.com/developers/gitignore/api/ionic3\n# Edit at https://www.toptal.com/developers/gitignore?templates=ionic3\n\n### Ionic3 ###\n# Log an tmp data\n*~\n*.sw[mnpcod]\n*.log\n*.tmp\n*.tmp.*\nlog.txt\n\n# NPM (Node Package Manager) files\nnpm-debug.log*\nnode_modules/\ntmp/\ntemp/\n\n# Preprocessors cache files\n.sourcemaps/\n.sass-cache/\n.tmp/\nhooks/\n\n# Ionic 3 plugins files\nplatforms/\nplugins/\nplugins/android.json\nplugins/ios.json\nwww/\n\n# Trash data\n$RECYCLE.BIN/\n.DS_Store\nThumbs.db\nUserInterfaceState.xcuserstate\n\n# End of https://www.toptal.com/developers/gitignore/api/ionic3\n","jabref":"# Created by https://www.toptal.com/developers/gitignore/api/jabref\n# Edit at https://www.toptal.com/developers/gitignore?templates=jabref\n\n### JabRef ###\n# JabRef - https://www.jabref.org/\n*.bak\n*.sav\n\n# End of https://www.toptal.com/developers/gitignore/api/jabref\n","janet":"# Created by https://www.toptal.com/developers/gitignore/api/janet\n# Edit at https://www.toptal.com/developers/gitignore?templates=janet\n\n### Janet ###\n# Compiled binaries\nbuild/\n\n# Dependency directory\njpm_tree/\n\n# End of https://www.toptal.com/developers/gitignore/api/janet\n","java":"# Created by https://www.toptal.com/developers/gitignore/api/java\n# Edit at https://www.toptal.com/developers/gitignore?templates=java\n\n### Java ###\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\nreplay_pid*\n\n# End of https://www.toptal.com/developers/gitignore/api/java\n","jboss":"# Created by https://www.toptal.com/developers/gitignore/api/jboss\n# Edit at https://www.toptal.com/developers/gitignore?templates=jboss\n\n### JBoss ###\njboss/server/all/deploy/project.ext\njboss/server/default/deploy/project.ext\njboss/server/minimal/deploy/project.ext\njboss/server/all/log/*.log\njboss/server/all/tmp/**/*\njboss/server/all/data/**/*\njboss/server/all/work/**/*\njboss/server/default/log/*.log\njboss/server/default/tmp/**/*\njboss/server/default/data/**/*\njboss/server/default/work/**/*\njboss/server/minimal/log/*.log\njboss/server/minimal/tmp/**/*\njboss/server/minimal/data/**/*\njboss/server/minimal/work/**/*\n\n# deployed package files #\n\n*.DEPLOYED\n\n# End of https://www.toptal.com/developers/gitignore/api/jboss\n","jboss-4-2-3-ga":"# Created by https://www.toptal.com/developers/gitignore/api/jboss-4-2-3-ga\n# Edit at https://www.toptal.com/developers/gitignore?templates=jboss-4-2-3-ga\n\n### JBoss-4-2-3-GA ###\n/server/all/data\n/server/all/log\n/server/all/tmp\n/server/all/work\n/server/default/data\n/server/default/log\n/server/default/tmp\n/server/default/work\n/server/minimal/data\n/server/minimal/log\n/server/minimal/tmp\n/server/minimal/work\n/server/default/deploy/*.xml.failed\n/server/default/deploy/*.war.failed\n\n/**\n* For ignore * .war, * .war.failed and * .xml.failed if you use other directories of jboss.\n* Do it similar configuration used for\n* /server/default/deploy\n*\n*/\n\n# End of https://www.toptal.com/developers/gitignore/api/jboss-4-2-3-ga\n","jboss-6-x":"# Created by https://www.toptal.com/developers/gitignore/api/jboss-6-x\n# Edit at https://www.toptal.com/developers/gitignore?templates=jboss-6-x\n\n### JBoss-6-x ###\n/server/all/data\n/server/all/log\n/server/all/tmp\n/server/all/work\n/server/default/data\n/server/default/log\n/server/default/tmp\n/server/default/work\n/server/minimal/data\n/server/minimal/log\n/server/minimal/tmp\n/server/minimal/work\n/server/jbossweb-standalone/data\n/server/jbossweb-standalone/log\n/server/jbossweb-standalone/tmp\n/server/jbossweb-standalone/work\n/server/standard/data\n/server/standard/log\n/server/standard/tmp\n/server/standard/work\n/server/default/deploy/*.jar.failed\n/server/default/deploy/*.jar.dodeploy\n/server/default/deploy/*.xml.failed\n/server/default/deploy/*.xml.dodeploy\n/server/default/deploy/*.war.failed\n/server/default/deploy/*.war.dodeploy\n\n\n# Important\n# Case your directory for example\n# /server/minimal/lib be empty.\n# The git will not submit this directory for you repository.\n# In this case is necessary create one archive, for example .gitignore empty in this directory.\n# Case you run your server without this directory the server will faild when you start service.\n\n# End of https://www.toptal.com/developers/gitignore/api/jboss-6-x\n","jboss4":"# Created by https://www.toptal.com/developers/gitignore/api/jboss4\n# Edit at https://www.toptal.com/developers/gitignore?templates=jboss4\n\n### JBoss4 ###\n# gitignore for JBoss v4 projects\n\n/server/all/data\n/server/all/log\n/server/all/tmp\n/server/all/work\n/server/default/data\n/server/default/log\n/server/default/tmp\n/server/default/work\n/server/minimal/data\n/server/minimal/log\n/server/minimal/tmp\n/server/minimal/work\n\n# Note:\n# there may be other directories that contain *.xml.failed or *.war.failed files\n/server/default/deploy/*.xml.failed\n/server/default/deploy/*.war.failed\n\n# End of https://www.toptal.com/developers/gitignore/api/jboss4\n","jboss6":"# Created by https://www.toptal.com/developers/gitignore/api/jboss6\n# Edit at https://www.toptal.com/developers/gitignore?templates=jboss6\n\n### JBoss6 ###\n# gitignore for JBoss v6 projects\n#\n# Note: to ensure empty directories remain part of the repository, like\n# `/server/minimal/lib`, you should add an empty `.gitignore` or `.gitkeep` file\n# to the directory - otherwise you may have issues when starting the service.\n\n/server/all/data\n/server/all/log\n/server/all/tmp\n/server/all/work\n/server/default/data\n/server/default/log\n/server/default/tmp\n/server/default/work\n/server/minimal/data\n/server/minimal/log\n/server/minimal/tmp\n/server/minimal/work\n/server/jbossweb-standalone/data\n/server/jbossweb-standalone/log\n/server/jbossweb-standalone/tmp\n/server/jbossweb-standalone/work\n/server/standard/data\n/server/standard/log\n/server/standard/tmp\n/server/standard/work\n/server/default/deploy/*.jar.failed\n/server/default/deploy/*.jar.dodeploy\n/server/default/deploy/*.xml.failed\n/server/default/deploy/*.xml.dodeploy\n/server/default/deploy/*.war.failed\n/server/default/deploy/*.war.dodeploy\n\n\n# End of https://www.toptal.com/developers/gitignore/api/jboss6\n","jdeveloper":"# Created by https://www.toptal.com/developers/gitignore/api/jdeveloper\n# Edit at https://www.toptal.com/developers/gitignore?templates=jdeveloper\n\n### JDeveloper ###\n# default application storage directory used by the IDE Performance Cache feature\n.data/\n\n# used for ADF styles caching\ntemp/\n\n# default output directories\nclasses/\ndeploy/\njavadoc/\n\n# lock file, a part of Oracle Credential Store Framework\ncwallet.sso.lck\n\n# End of https://www.toptal.com/developers/gitignore/api/jdeveloper\n","jekyll":"# Created by https://www.toptal.com/developers/gitignore/api/jekyll\n# Edit at https://www.toptal.com/developers/gitignore?templates=jekyll\n\n### Jekyll ###\n_site/\n.sass-cache/\n.jekyll-cache/\n.jekyll-metadata\n# Ignore folders generated by Bundler\n.bundle/\nvendor/\n\n# End of https://www.toptal.com/developers/gitignore/api/jekyll\n","jenv":"# Created by https://www.toptal.com/developers/gitignore/api/jenv\n# Edit at https://www.toptal.com/developers/gitignore?templates=jenv\n\n### JEnv ###\n# JEnv local Java version configuration file\n.java-version\n\n# Used by previous versions of JEnv\n.jenv-version\n\n# End of https://www.toptal.com/developers/gitignore/api/jenv\n","jetbrains":"# Created by https://www.toptal.com/developers/gitignore/api/jetbrains\n# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains\n\n### JetBrains ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### JetBrains Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/jetbrains\n","jetbrains+all":"# Created by https://www.toptal.com/developers/gitignore/api/jetbrains+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+all\n\n### JetBrains+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### JetBrains+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all\n","jetbrains+iml":"# Created by https://www.toptal.com/developers/gitignore/api/jetbrains+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+iml\n\n### JetBrains+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### JetBrains+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/jetbrains+iml\n","jgiven":"# Created by https://www.toptal.com/developers/gitignore/api/jgiven\n# Edit at https://www.toptal.com/developers/gitignore?templates=jgiven\n\n### JGiven ###\njgiven-reports/\n\n# End of https://www.toptal.com/developers/gitignore/api/jgiven\n","jigsaw":"# Created by https://www.toptal.com/developers/gitignore/api/jigsaw\n# Edit at https://www.toptal.com/developers/gitignore?templates=jigsaw\n\n### Jigsaw ###\n# Jigsaw Static Site Generator\n\n# Ignore build folder\n/build_local/\n/build_staging/\n/build_production/\n\n# Ignore node dependencies resolved by npm/yarn\n/node_modules/\n\n# Ignore php dependecies resolved by composer\n/vendor/\n\n# Optionally ignore lock files\n# composer.lock\n# yarn.lock\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# End of https://www.toptal.com/developers/gitignore/api/jigsaw\n","jmeter":"# Created by https://www.toptal.com/developers/gitignore/api/jmeter\n# Edit at https://www.toptal.com/developers/gitignore?templates=jmeter\n\n### JMeter ###\n# JMeter common ignore files\n# http://jmeter.apache.org/\n\n# Ignore Summary/Aggregrate reports\n*.jtl\n\n# Ignore log files\n*.log\n\n# Ignore customized user.properties\nuser.properties\n\n# End of https://www.toptal.com/developers/gitignore/api/jmeter\n","joe":"# Created by https://www.toptal.com/developers/gitignore/api/joe\n# Edit at https://www.toptal.com/developers/gitignore?templates=joe\n\n### Joe ###\n# File backups\n*~\n# Gets created when joe crashes\nDEADJOE\n\n# End of https://www.toptal.com/developers/gitignore/api/joe\n","joomla":"# Created by https://www.toptal.com/developers/gitignore/api/joomla\n# Edit at https://www.toptal.com/developers/gitignore?templates=joomla\n\n### Joomla ###\n/.htaccess\n/administrator/cache/*\n/administrator/components/com_actionlogs/*\n/administrator/components/com_admin/*\n/administrator/components/com_ajax/*\n/administrator/components/com_associations/*\n/administrator/components/com_banners/*\n/administrator/components/com_cache/*\n/administrator/components/com_categories/*\n/administrator/components/com_checkin/*\n/administrator/components/com_config/*\n/administrator/components/com_contact/*\n/administrator/components/com_content/*\n/administrator/components/com_contenthistory/*\n/administrator/components/com_cpanel/*\n/administrator/components/com_fields/*\n/administrator/components/com_finder/*\n/administrator/components/com_installer/*\n/administrator/components/com_joomlaupdate/*\n/administrator/components/com_languages/*\n/administrator/components/com_login/*\n/administrator/components/com_media/*\n/administrator/components/com_menus/*\n/administrator/components/com_messages/*\n/administrator/components/com_modules/*\n/administrator/components/com_newsfeeds/*\n/administrator/components/com_plugins/*\n/administrator/components/com_postinstall/*\n/administrator/components/com_privacy/*\n/administrator/components/com_redirect/*\n/administrator/components/com_search/*\n/administrator/components/com_tags/*\n/administrator/components/com_templates/*\n/administrator/components/com_users/*\n/administrator/help/*\n/administrator/includes/*\n/administrator/index.php\n/administrator/language/en-GB/en-GB.com_actionlogs.ini\n/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini\n/administrator/language/en-GB/en-GB.com_admin.ini\n/administrator/language/en-GB/en-GB.com_admin.sys.ini\n/administrator/language/en-GB/en-GB.com_ajax.ini\n/administrator/language/en-GB/en-GB.com_ajax.sys.ini\n/administrator/language/en-GB/en-GB.com_associations.ini\n/administrator/language/en-GB/en-GB.com_associations.sys.ini\n/administrator/language/en-GB/en-GB.com_banners.ini\n/administrator/language/en-GB/en-GB.com_banners.sys.ini\n/administrator/language/en-GB/en-GB.com_cache.ini\n/administrator/language/en-GB/en-GB.com_cache.sys.ini\n/administrator/language/en-GB/en-GB.com_categories.ini\n/administrator/language/en-GB/en-GB.com_categories.sys.ini\n/administrator/language/en-GB/en-GB.com_checkin.ini\n/administrator/language/en-GB/en-GB.com_checkin.sys.ini\n/administrator/language/en-GB/en-GB.com_config.ini\n/administrator/language/en-GB/en-GB.com_config.sys.ini\n/administrator/language/en-GB/en-GB.com_contact.ini\n/administrator/language/en-GB/en-GB.com_contact.sys.ini\n/administrator/language/en-GB/en-GB.com_content.ini\n/administrator/language/en-GB/en-GB.com_content.sys.ini\n/administrator/language/en-GB/en-GB.com_contenthistory.ini\n/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini\n/administrator/language/en-GB/en-GB.com_cpanel.ini\n/administrator/language/en-GB/en-GB.com_cpanel.sys.ini\n/administrator/language/en-GB/en-GB.com_fields.ini\n/administrator/language/en-GB/en-GB.com_fields.sys.ini\n/administrator/language/en-GB/en-GB.com_finder.ini\n/administrator/language/en-GB/en-GB.com_finder.sys.ini\n/administrator/language/en-GB/en-GB.com_installer.ini\n/administrator/language/en-GB/en-GB.com_installer.sys.ini\n/administrator/language/en-GB/en-GB.com_joomlaupdate.ini\n/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini\n/administrator/language/en-GB/en-GB.com_languages.ini\n/administrator/language/en-GB/en-GB.com_languages.sys.ini\n/administrator/language/en-GB/en-GB.com_login.ini\n/administrator/language/en-GB/en-GB.com_login.sys.ini\n/administrator/language/en-GB/en-GB.com_mailto.sys.ini\n/administrator/language/en-GB/en-GB.com_media.ini\n/administrator/language/en-GB/en-GB.com_media.sys.ini\n/administrator/language/en-GB/en-GB.com_menus.ini\n/administrator/language/en-GB/en-GB.com_menus.sys.ini\n/administrator/language/en-GB/en-GB.com_messages.ini\n/administrator/language/en-GB/en-GB.com_messages.sys.ini\n/administrator/language/en-GB/en-GB.com_modules.ini\n/administrator/language/en-GB/en-GB.com_modules.sys.ini\n/administrator/language/en-GB/en-GB.com_newsfeeds.ini\n/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini\n/administrator/language/en-GB/en-GB.com_plugins.ini\n/administrator/language/en-GB/en-GB.com_plugins.sys.ini\n/administrator/language/en-GB/en-GB.com_postinstall.ini\n/administrator/language/en-GB/en-GB.com_postinstall.sys.ini\n/administrator/language/en-GB/en-GB.com_privacy.ini\n/administrator/language/en-GB/en-GB.com_privacy.sys.ini\n/administrator/language/en-GB/en-GB.com_redirect.ini\n/administrator/language/en-GB/en-GB.com_redirect.sys.ini\n/administrator/language/en-GB/en-GB.com_search.ini\n/administrator/language/en-GB/en-GB.com_search.sys.ini\n/administrator/language/en-GB/en-GB.com_tags.ini\n/administrator/language/en-GB/en-GB.com_tags.sys.ini\n/administrator/language/en-GB/en-GB.com_templates.ini\n/administrator/language/en-GB/en-GB.com_templates.sys.ini\n/administrator/language/en-GB/en-GB.com_users.ini\n/administrator/language/en-GB/en-GB.com_users.sys.ini\n/administrator/language/en-GB/en-GB.com_weblinks.ini\n/administrator/language/en-GB/en-GB.com_weblinks.sys.ini\n/administrator/language/en-GB/en-GB.com_wrapper.ini\n/administrator/language/en-GB/en-GB.com_wrapper.sys.ini\n/administrator/language/en-GB/en-GB.ini\n/administrator/language/en-GB/en-GB.lib_joomla.ini\n/administrator/language/en-GB/en-GB.localise.php\n/administrator/language/en-GB/en-GB.mod_custom.ini\n/administrator/language/en-GB/en-GB.mod_custom.sys.ini\n/administrator/language/en-GB/en-GB.mod_feed.ini\n/administrator/language/en-GB/en-GB.mod_feed.sys.ini\n/administrator/language/en-GB/en-GB.mod_latest.ini\n/administrator/language/en-GB/en-GB.mod_latest.sys.ini\n/administrator/language/en-GB/en-GB.mod_latestactions.ini\n/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini\n/administrator/language/en-GB/en-GB.mod_logged.ini\n/administrator/language/en-GB/en-GB.mod_logged.sys.ini\n/administrator/language/en-GB/en-GB.mod_login.ini\n/administrator/language/en-GB/en-GB.mod_login.sys.ini\n/administrator/language/en-GB/en-GB.mod_menu.ini\n/administrator/language/en-GB/en-GB.mod_menu.sys.ini\n/administrator/language/en-GB/en-GB.mod_multilangstatus.ini\n/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini\n/administrator/language/en-GB/en-GB.mod_online.ini\n/administrator/language/en-GB/en-GB.mod_online.sys.ini\n/administrator/language/en-GB/en-GB.mod_popular.ini\n/administrator/language/en-GB/en-GB.mod_popular.sys.ini\n/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini\n/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini\n/administrator/language/en-GB/en-GB.mod_quickicon.ini\n/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini\n/administrator/language/en-GB/en-GB.mod_sampledata.ini\n/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini\n/administrator/language/en-GB/en-GB.mod_stats_admin.ini\n/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini\n/administrator/language/en-GB/en-GB.mod_status.ini\n/administrator/language/en-GB/en-GB.mod_status.sys.ini\n/administrator/language/en-GB/en-GB.mod_submenu.ini\n/administrator/language/en-GB/en-GB.mod_submenu.sys.ini\n/administrator/language/en-GB/en-GB.mod_title.ini\n/administrator/language/en-GB/en-GB.mod_title.sys.ini\n/administrator/language/en-GB/en-GB.mod_toolbar.ini\n/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini\n/administrator/language/en-GB/en-GB.mod_unread.ini\n/administrator/language/en-GB/en-GB.mod_unread.sys.ini\n/administrator/language/en-GB/en-GB.mod_version.ini\n/administrator/language/en-GB/en-GB.mod_version.sys.ini\n/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini\n/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini\n/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini\n/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini\n/administrator/language/en-GB/en-GB.plg_authentication_example.ini\n/administrator/language/en-GB/en-GB.plg_authentication_example.sys.ini\n/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini\n/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini\n/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini\n/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini\n/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini\n/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini\n/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini\n/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini\n/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini\n/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini\n/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_contact.ini\n/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini\n/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_fields.ini\n/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_finder.ini\n/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_geshi.ini\n/administrator/language/en-GB/en-GB.plg_content_geshi.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_joomla.ini\n/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini\n/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini\n/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini\n/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini\n/administrator/language/en-GB/en-GB.plg_content_vote.ini\n/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini\n/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini\n/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors_none.ini\n/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini\n/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini\n/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini\n/administrator/language/en-GB/en-GB.plg_extension_joomla.ini\n/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_calendar.ini\n/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini\n/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_color.ini\n/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_editor.ini\n/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_image.ini\n/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini\n/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_integer.ini\n/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_list.ini\n/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_media.ini\n/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_radio.ini\n/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_repeatable.ini\n/administrator/language/en-GB/en-GB.plg_fields_repeatable.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_sql.ini\n/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_text.ini\n/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_textarea.ini\n/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_url.ini\n/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_user.ini\n/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini\n/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini\n/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_categories.ini\n/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_contacts.ini\n/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_content.ini\n/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini\n/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_tags.ini\n/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini\n/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini\n/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini\n/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini\n/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini\n/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini\n/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini\n/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini\n/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini\n/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini\n/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini\n/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_consents.ini\n/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_contact.ini\n/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_content.ini\n/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_message.ini\n/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini\n/administrator/language/en-GB/en-GB.plg_privacy_user.ini\n/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini\n/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini\n/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini\n/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_categories.ini\n/administrator/language/en-GB/en-GB.plg_search_categories.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_contacts.ini\n/administrator/language/en-GB/en-GB.plg_search_contacts.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_content.ini\n/administrator/language/en-GB/en-GB.plg_search_content.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_newsfeeds.ini\n/administrator/language/en-GB/en-GB.plg_search_newsfeeds.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_tags.ini\n/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini\n/administrator/language/en-GB/en-GB.plg_search_weblinks.ini\n/administrator/language/en-GB/en-GB.plg_search_weblinks.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini\n/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_cache.ini\n/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_debug.ini\n/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_fields.ini\n/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_highlight.ini\n/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_languagecode.ini\n/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini\n/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_log.ini\n/administrator/language/en-GB/en-GB.plg_system_log.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_logout.ini\n/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_logrotation.ini\n/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_p3p.ini\n/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini\n/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_redirect.ini\n/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_remember.ini\n/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_sef.ini\n/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini\n/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_stats.ini\n/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini\n/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini\n/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini\n/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini\n/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini\n/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini\n/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini\n/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini\n/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini\n/administrator/language/en-GB/en-GB.plg_user_joomla.ini\n/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini\n/administrator/language/en-GB/en-GB.plg_user_profile.ini\n/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini\n/administrator/language/en-GB/en-GB.plg_user_terms.ini\n/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini\n/administrator/language/en-GB/en-GB.tpl_hathor.ini\n/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini\n/administrator/language/en-GB/en-GB.tpl_isis.ini\n/administrator/language/en-GB/en-GB.tpl_isis.sys.ini\n/administrator/language/en-GB/en-GB.xml\n/administrator/language/en-GB/install.xml\n/administrator/language/overrides/*\n/administrator/language/index.html\n/administrator/logs/*\n/administrator/manifests/files/joomla.xml\n/administrator/manifests/libraries/fof.xml\n/administrator/manifests/libraries/idna_convert.xml\n/administrator/manifests/libraries/joomla.xml\n/administrator/manifests/libraries/phpass.xml\n/administrator/manifests/libraries/phputf8.xml\n/administrator/manifests/packages/pkg_en-GB.xml\n/administrator/manifests/packages/index.html\n/administrator/modules/mod_custom/*\n/administrator/modules/mod_feed/*\n/administrator/modules/mod_latest/*\n/administrator/modules/mod_latestactions/*\n/administrator/modules/mod_logged/*\n/administrator/modules/mod_login/*\n/administrator/modules/mod_menu/*\n/administrator/modules/mod_multilangstatus/*\n/administrator/modules/mod_online/*\n/administrator/modules/mod_popular/*\n/administrator/modules/mod_privacy_dashboard/*\n/administrator/modules/mod_quickicon/*\n/administrator/modules/mod_sampledata/*\n/administrator/modules/mod_stats_admin/*\n/administrator/modules/mod_status/*\n/administrator/modules/mod_submenu/*\n/administrator/modules/mod_title/*\n/administrator/modules/mod_toolbar/*\n/administrator/modules/mod_unread/*\n/administrator/modules/mod_version/*\n/administrator/templates/hathor/*\n/administrator/templates/isis/*\n/administrator/templates/system/*\n/bin/*\n/cache/*\n/cli/*\n/components/com_ajax/*\n/components/com_banners/*\n/components/com_config/*\n/components/com_contact/*\n/components/com_content/*\n/components/com_contenthistory/*\n/components/com_fields/*\n/components/com_finder/*\n/components/com_mailto/*\n/components/com_media/*\n/components/com_menus/*\n/components/com_modules/*\n/components/com_newsfeeds/*\n/components/com_privacy/*\n/components/com_search/*\n/components/com_tags/*\n/components/com_users/*\n/components/com_wrapper/*\n/components/index.html\n/images/banners/*\n/images/headers/*\n/images/sampledata/*\n/images/index.html\n/images/joomla*\n/images/powered_by.png\n/includes/*\n/installation/*\n/language/en-GB/en-GB.com_ajax.ini\n/language/en-GB/en-GB.com_config.ini\n/language/en-GB/en-GB.com_contact.ini\n/language/en-GB/en-GB.com_content.ini\n/language/en-GB/en-GB.com_finder.ini\n/language/en-GB/en-GB.com_mailto.ini\n/language/en-GB/en-GB.com_media.ini\n/language/en-GB/en-GB.com_messages.ini\n/language/en-GB/en-GB.com_newsfeeds.ini\n/language/en-GB/en-GB.com_privacy.ini\n/language/en-GB/en-GB.com_search.ini\n/language/en-GB/en-GB.com_tags.ini\n/language/en-GB/en-GB.com_users.ini\n/language/en-GB/en-GB.com_weblinks.ini\n/language/en-GB/en-GB.com_wrapper.ini\n/language/en-GB/en-GB.files_joomla.sys.ini\n/language/en-GB/en-GB.finder_cli.ini\n/language/en-GB/en-GB.ini\n/language/en-GB/en-GB.lib_fof.ini\n/language/en-GB/en-GB.lib_fof.sys.ini\n/language/en-GB/en-GB.lib_idna_convert.sys.ini\n/language/en-GB/en-GB.lib_joomla.ini\n/language/en-GB/en-GB.lib_joomla.sys.ini\n/language/en-GB/en-GB.lib_phpass.sys.ini\n/language/en-GB/en-GB.lib_phpmailer.sys.ini\n/language/en-GB/en-GB.lib_phputf8.sys.ini\n/language/en-GB/en-GB.lib_simplepie.sys.ini\n/language/en-GB/en-GB.localise.php\n/language/en-GB/en-GB.mod_articles_archive.ini\n/language/en-GB/en-GB.mod_articles_archive.sys.ini\n/language/en-GB/en-GB.mod_articles_categories.ini\n/language/en-GB/en-GB.mod_articles_categories.sys.ini\n/language/en-GB/en-GB.mod_articles_category.ini\n/language/en-GB/en-GB.mod_articles_category.sys.ini\n/language/en-GB/en-GB.mod_articles_latest.ini\n/language/en-GB/en-GB.mod_articles_latest.sys.ini\n/language/en-GB/en-GB.mod_articles_news.ini\n/language/en-GB/en-GB.mod_articles_news.sys.ini\n/language/en-GB/en-GB.mod_articles_popular.ini\n/language/en-GB/en-GB.mod_articles_popular.sys.ini\n/language/en-GB/en-GB.mod_banners.ini\n/language/en-GB/en-GB.mod_banners.sys.ini\n/language/en-GB/en-GB.mod_breadcrumbs.ini\n/language/en-GB/en-GB.mod_breadcrumbs.sys.ini\n/language/en-GB/en-GB.mod_custom.ini\n/language/en-GB/en-GB.mod_custom.sys.ini\n/language/en-GB/en-GB.mod_feed.ini\n/language/en-GB/en-GB.mod_feed.sys.ini\n/language/en-GB/en-GB.mod_finder.ini\n/language/en-GB/en-GB.mod_finder.sys.ini\n/language/en-GB/en-GB.mod_footer.ini\n/language/en-GB/en-GB.mod_footer.sys.ini\n/language/en-GB/en-GB.mod_languages.ini\n/language/en-GB/en-GB.mod_languages.sys.ini\n/language/en-GB/en-GB.mod_login.ini\n/language/en-GB/en-GB.mod_login.sys.ini\n/language/en-GB/en-GB.mod_menu.ini\n/language/en-GB/en-GB.mod_menu.sys.ini\n/language/en-GB/en-GB.mod_random_image.ini\n/language/en-GB/en-GB.mod_random_image.sys.ini\n/language/en-GB/en-GB.mod_related_items.ini\n/language/en-GB/en-GB.mod_related_items.sys.ini\n/language/en-GB/en-GB.mod_search.ini\n/language/en-GB/en-GB.mod_search.sys.ini\n/language/en-GB/en-GB.mod_stats.ini\n/language/en-GB/en-GB.mod_stats.sys.ini\n/language/en-GB/en-GB.mod_syndicate.ini\n/language/en-GB/en-GB.mod_syndicate.sys.ini\n/language/en-GB/en-GB.mod_tags_popular.ini\n/language/en-GB/en-GB.mod_tags_popular.sys.ini\n/language/en-GB/en-GB.mod_tags_similar.ini\n/language/en-GB/en-GB.mod_tags_similar.sys.ini\n/language/en-GB/en-GB.mod_users_latest.ini\n/language/en-GB/en-GB.mod_users_latest.sys.ini\n/language/en-GB/en-GB.mod_weblinks.ini\n/language/en-GB/en-GB.mod_weblinks.sys.ini\n/language/en-GB/en-GB.mod_whosonline.ini\n/language/en-GB/en-GB.mod_whosonline.sys.ini\n/language/en-GB/en-GB.mod_wrapper.ini\n/language/en-GB/en-GB.mod_wrapper.sys.ini\n/language/en-GB/en-GB.tpl_atomic.ini\n/language/en-GB/en-GB.tpl_atomic.sys.ini\n/language/en-GB/en-GB.tpl_beez3.ini\n/language/en-GB/en-GB.tpl_beez3.sys.ini\n/language/en-GB/en-GB.tpl_beez5.ini\n/language/en-GB/en-GB.tpl_beez5.sys.ini\n/language/en-GB/en-GB.tpl_beez_20.ini\n/language/en-GB/en-GB.tpl_beez_20.sys.ini\n/language/en-GB/en-GB.tpl_protostar.ini\n/language/en-GB/en-GB.tpl_protostar.sys.ini\n/language/en-GB/en-GB.xml\n/language/en-GB/install.xml\n/language/overrides/*\n/language/index.html\n/layouts/joomla/*\n/layouts/libraries/*\n/layouts/plugins/*\n/layouts/index.html\n/libraries/cms/*\n/libraries/fof/*\n/libraries/idna_convert/*\n/libraries/joomla/*\n/libraries/legacy/*\n/libraries/php-encryption/*\n/libraries/phpass/*\n/libraries/phpmailer/*\n/libraries/phputf8/*\n/libraries/simplepie/*\n/libraries/src/*\n/libraries/vendor/*\n/libraries/classmap.php\n/libraries/cms.php\n/libraries/import.legacy.php\n/libraries/import.php\n/libraries/index.html\n/libraries/loader.php\n/media/cms/*\n/media/com_associations/*\n/media/com_contact/*\n/media/com_content/*\n/media/com_contenthistory/*\n/media/com_fields/*\n/media/com_finder/*\n/media/com_joomlaupdate/*\n/media/com_menus/*\n/media/com_modules/*\n/media/com_wrapper/*\n/media/contacts/*\n/media/editors/*\n/media/jui/*\n/media/mailto/*\n/media/media/*\n/media/mod_languages/*\n/media/mod_sampledata/*\n/media/overrider/*\n/media/plg_captcha_recaptcha/*\n/media/plg_captcha_recaptcha_invisible/*\n/media/plg_quickicon_extensionupdate/*\n/media/plg_quickicon_joomlaupdate/*\n/media/plg_quickicon_privacycheck/*\n/media/plg_system_highlight/*\n/media/plg_system_stats/*\n/media/plg_twofactorauth_totp/*\n/media/system/*\n/media/index.html\n/modules/mod_articles_archive/*\n/modules/mod_articles_categories/*\n/modules/mod_articles_category/*\n/modules/mod_articles_latest/*\n/modules/mod_articles_news/*\n/modules/mod_articles_popular/*\n/modules/mod_banners/*\n/modules/mod_breadcrumbs/*\n/modules/mod_custom/*\n/modules/mod_feed/*\n/modules/mod_finder/*\n/modules/mod_footer/*\n/modules/mod_languages/*\n/modules/mod_login/*\n/modules/mod_menu/*\n/modules/mod_random_image/*\n/modules/mod_related_items/*\n/modules/mod_search/*\n/modules/mod_stats/*\n/modules/mod_syndicate/*\n/modules/mod_tags_popular/*\n/modules/mod_tags_similar/*\n/modules/mod_users_latest/*\n/modules/mod_whosonline/*\n/modules/mod_wrapper/*\n/modules/index.html\n/plugins/actionlog/joomla/*\n/plugins/authentication/cookie/*\n/plugins/authentication/example/*\n/plugins/authentication/gmail/*\n/plugins/authentication/joomla/*\n/plugins/authentication/ldap/*\n/plugins/captcha/recaptcha/*\n/plugins/captcha/recaptcha_invisible/*\n/plugins/content/confirmconsent/*\n/plugins/content/contact/*\n/plugins/content/emailcloak/*\n/plugins/content/example/*\n/plugins/content/fields/*\n/plugins/content/finder/*\n/plugins/content/geshi/*\n/plugins/content/joomla/*\n/plugins/content/loadmodule/*\n/plugins/content/pagebreak/*\n/plugins/content/pagenavigation/*\n/plugins/content/vote/*\n/plugins/editors/codemirror/*\n/plugins/editors/none/*\n/plugins/editors/tinymce/*\n/plugins/editors-xtd/article/*\n/plugins/editors-xtd/contact/*\n/plugins/editors-xtd/fields/*\n/plugins/editors-xtd/image/*\n/plugins/editors-xtd/menu/*\n/plugins/editors-xtd/module/*\n/plugins/editors-xtd/pagebreak/*\n/plugins/editors-xtd/readmore/*\n/plugins/extension/example/*\n/plugins/extension/joomla/*\n/plugins/fields/calendar/*\n/plugins/fields/checkboxes/*\n/plugins/fields/color/*\n/plugins/fields/editor/*\n/plugins/fields/imagelist/*\n/plugins/fields/integer/*\n/plugins/fields/list/*\n/plugins/fields/media/*\n/plugins/fields/radio/*\n/plugins/fields/repeatable/*\n/plugins/fields/sql/*\n/plugins/fields/text/*\n/plugins/fields/textarea/*\n/plugins/fields/url/*\n/plugins/fields/user/*\n/plugins/fields/usergrouplist/*\n/plugins/finder/categories/*\n/plugins/finder/contacts/*\n/plugins/finder/content/*\n/plugins/finder/newsfeeds/*\n/plugins/finder/tags/*\n/plugins/installer/folderinstaller/*\n/plugins/installer/packageinstaller/*\n/plugins/installer/urlinstaller/*\n/plugins/privacy/actionlogs/*\n/plugins/privacy/consents/*\n/plugins/privacy/contact/*\n/plugins/privacy/content/*\n/plugins/privacy/message/*\n/plugins/privacy/user/*\n/plugins/quickicon/extensionupdate/*\n/plugins/quickicon/joomlaupdate/*\n/plugins/quickicon/phpversioncheck/*\n/plugins/quickicon/privacycheck/*\n/plugins/quickicon/index.html\n/plugins/sampledata/blog/*\n/plugins/search/categories/*\n/plugins/search/contacts/*\n/plugins/search/content/*\n/plugins/search/newsfeeds/*\n/plugins/search/tags/*\n/plugins/search/weblinks/*\n/plugins/search/index.html\n/plugins/system/actionlogs/*\n/plugins/system/cache/*\n/plugins/system/debug/*\n/plugins/system/fields/*\n/plugins/system/highlight/*\n/plugins/system/languagecode/*\n/plugins/system/languagefilter/*\n/plugins/system/log/*\n/plugins/system/logout/*\n/plugins/system/logrotation/*\n/plugins/system/p3p/*\n/plugins/system/privacyconsent/*\n/plugins/system/redirect/*\n/plugins/system/remember/*\n/plugins/system/sef/*\n/plugins/system/sessiongc/*\n/plugins/system/stats/*\n/plugins/system/updatenotification/*\n/plugins/system/index.html\n/plugins/twofactorauth/totp/*\n/plugins/twofactorauth/yubikey/*\n/plugins/user/contactcreator/*\n/plugins/user/example/*\n/plugins/user/joomla/*\n/plugins/user/profile/*\n/plugins/user/terms/*\n/plugins/user/index.html\n/plugins/index.html\n/templates/beez3/*\n/templates/protostar/*\n/templates/system/*\n/templates/index.html\n/tmp/*\n/configuration.php\n/htaccess.txt\n/index.php\n/joomla.xml\n/LICENSE.txt\n/README.txt\n/robots.txt.dist\n/web.config.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/joomla\n","jsonnet":"# Created by https://www.toptal.com/developers/gitignore/api/jsonnet\n# Edit at https://www.toptal.com/developers/gitignore?templates=jsonnet\n\n### Jsonnet ###\n# Dependency directories\nvendor/\n\n\n# End of https://www.toptal.com/developers/gitignore/api/jsonnet\n","jspm":"# Created by https://www.toptal.com/developers/gitignore/api/jspm\n# Edit at https://www.toptal.com/developers/gitignore?templates=jspm\n\n### jspm ###\njspm_packages\n\n# End of https://www.toptal.com/developers/gitignore/api/jspm\n","julia":"# Created by https://www.toptal.com/developers/gitignore/api/julia\n# Edit at https://www.toptal.com/developers/gitignore?templates=julia\n\n### Julia ###\n# Files generated by invoking Julia with --code-coverage\n*.jl.cov\n*.jl.*.cov\n\n# Files generated by invoking Julia with --track-allocation\n*.jl.mem\n\n# System-specific files and directories generated by the BinaryProvider and BinDeps packages\n# They contain absolute paths specific to the host computer, and so should not be committed\ndeps/deps.jl\ndeps/build.log\ndeps/downloads/\ndeps/usr/\ndeps/src/\n\n# Build artifacts for creating documentation generated by the Documenter package\ndocs/build/\ndocs/site/\n\n# File generated by Pkg, the package manager, based on a corresponding Project.toml\n# It records a fixed state of all packages used by the project. As such, it should not be\n# committed for packages, but should be committed for applications that require a static\n# environment.\nManifest.toml\n\n# End of https://www.toptal.com/developers/gitignore/api/julia\n","jupyternotebooks":"# Created by https://www.toptal.com/developers/gitignore/api/jupyternotebooks\n# Edit at https://www.toptal.com/developers/gitignore?templates=jupyternotebooks\n\n### JupyterNotebooks ###\n# gitignore template for Jupyter Notebooks\n# website: http://jupyter.org/\n\n.ipynb_checkpoints\n*/.ipynb_checkpoints/*\n\n# IPython\nprofile_default/\nipython_config.py\n\n# Remove previous ipynb_checkpoints\n# git rm -r .ipynb_checkpoints/\n\n# End of https://www.toptal.com/developers/gitignore/api/jupyternotebooks\n","justcode":"# Created by https://www.toptal.com/developers/gitignore/api/justcode\n# Edit at https://www.toptal.com/developers/gitignore?templates=justcode\n\n### JustCode ###\n.JustCode\n\n# End of https://www.toptal.com/developers/gitignore/api/justcode\n","kaldi":"# Created by https://www.toptal.com/developers/gitignore/api/kaldi\n# Edit at https://www.toptal.com/developers/gitignore?templates=kaldi\n\n### kaldi ###\nexp/\ndata/\n\n# End of https://www.toptal.com/developers/gitignore/api/kaldi\n","kate":"# Created by https://www.toptal.com/developers/gitignore/api/kate\n# Edit at https://www.toptal.com/developers/gitignore?templates=kate\n\n### Kate ###\n# Swap Files #\n.*.kate-swp\n.swp.*\n\n# End of https://www.toptal.com/developers/gitignore/api/kate\n","kdevelop4":"# Created by https://www.toptal.com/developers/gitignore/api/kdevelop4\n# Edit at https://www.toptal.com/developers/gitignore?templates=kdevelop4\n\n### KDevelop4 ###\n*.kdev4\n.kdev4/\n\n# End of https://www.toptal.com/developers/gitignore/api/kdevelop4\n","kdiff3":"# Created by https://www.toptal.com/developers/gitignore/api/kdiff3\n# Edit at https://www.toptal.com/developers/gitignore?templates=kdiff3\n\n### KDiff3 ###\n*.BACKUP.*\n*_BACKUP_*\n*.BASE.*\n*_BASE_*\n*.LOCAL.*\n*_LOCAL_*\n*.REMOTE.*\n*_REMOTE_*\n*.orig\n\n\n# End of https://www.toptal.com/developers/gitignore/api/kdiff3\n","keil":"# Created by https://www.toptal.com/developers/gitignore/api/keil\n# Edit at https://www.toptal.com/developers/gitignore?templates=keil\n\n### Keil ###\n# something maybe need to add\n# *.bat # generate batch file\n# output/project_name # uv project generate without file ext\n# *.SRC # generate source file\n# *.src\n\n# Prerequisites\n*.d\n*.dep\n# Object files\n*.o\n*.obj\n*.OBJ\n\n# Listing Files\n*.COD\n*.cod\n*.[Ii]\n*.LST\n*.lst\n*.MAP\n*.map\n*.[Mm]51\n*.[Mm]66\n*.SCR\n*.scr\n\n# LINK output\n*.ABS\n\n# Libraries\n*.lib\n*.LIB\n\n\n# uv output\n*.lnp\n*.build_log.htm\n*.HEX\n*.hex\n\n\n# Build Files\n*._IA\n*.__[Ii]\n*._[Ii][Ii]\n\n# project\n*.uvgui.*\n*.uvguix.*\n\n\n# End of https://www.toptal.com/developers/gitignore/api/keil\n","kentico":"# Created by https://www.toptal.com/developers/gitignore/api/kentico\n# Edit at https://www.toptal.com/developers/gitignore?templates=kentico\n\n### Kentico ###\n# gitignore template for using Kentico CMS\n# website: http://www.kentico.com/\n#\n# Recommended template: VisualStudio.gitignore\n\n# Include some Kentico folders excluded by Visual Studio rules\n!CMS/CMSAdminControls/*/\n!CMS/CMSModules/System/*/\n!CMS/App_Data/CIRepository/**\n\n# Kentico temporary/environment files\nCMS/App_Data/AzureCache\nCMS/App_Data/AzureTemp\nCMS/App_Data/CMSModules/DeviceProfile/logFiftyOne.txt\nCMS/App_Data/CMSModules/DeviceProfiles/logFiftyOne.txt\nCMS/App_Data/CMSModules/WebFarm/webfarm.sync\nCMS/App_Data/CMSTemp\nCMS/App_Data/Persistent\nCMS/CMSSiteUtils/Export\nCMS/CMSSiteUtils/Import\n\n# Ignore all smart search indexes, but not the other system folder contents\nCMS/App_Data/CMSModules/SmartSearch/**\n!CMS/App_Data/CMSModules/SmartSearch/*/\n!CMS/App_Data/CMSModules/SmartSearch/_StopWords/**\n!CMS/App_Data/CMSModules/SmartSearch/_Synonyms/**\n\n## Kentico Starter Sites\n# Starter site resource Files\nCMS/App_Data/DancingGoat\n\n# Starter site web templates\nCMS/App_Data/Templates/CommunitySite\nCMS/App_Data/Templates/CorporateSite\nCMS/App_Data/Templates/DancingGoat\nCMS/App_Data/Templates/EcommerceSite\nCMS/App_Data/Templates/IntranetPortal\nCMS/App_Data/Templates/PersonalSite\n\n# Starter site app themes\nCMS/App_Themes/CommunitySite\nCMS/App_Themes/CorporateSite\nCMS/App_Themes/EcommerceSite\nCMS/App_Themes/IntranetPortal*\nCMS/App_Themes/PersonalSite\n\n# Starter site ASPX templates\nCMS/CMSTemplates/CorporateSite\n\n# Starter site media libraries\nCMS/CommunitySite\nCMS/CorporateSite\nCMS/DancingGoat\nCMS/EcommerceSite\nCMS/IntranetPortal\nCMS/PersonalSite\n\n## Project specific ignores\n# Sensitive settings\nAppSettings.config\nConnectionStrings.config\n\n# Project media libraries (recommend shared file storage)\n# e.g. CMS/{SiteCodeName}\n\n# End of https://www.toptal.com/developers/gitignore/api/kentico\n","keystonejs":"# Created by https://www.toptal.com/developers/gitignore/api/keystonejs\n# Edit at https://www.toptal.com/developers/gitignore?templates=keystonejs\n\n### KeystoneJS ###\n# Keystone build dir\n.keystone/\n\n# End of https://www.toptal.com/developers/gitignore/api/keystonejs\n","kicad":"# Created by https://www.toptal.com/developers/gitignore/api/kicad\n# Edit at https://www.toptal.com/developers/gitignore?templates=kicad\n\n### KiCad ###\n# For PCBs designed using KiCad: https://www.kicad.org/\n# Format documentation: https://kicad.org/help/file-formats/\n\n# Temporary files\n*.000\n*.bak\n*.bck\n*.kicad_pcb-bak\n*.kicad_sch-bak\n*-backups\n*.kicad_prl\n*.sch-bak\n*~\n_autosave-*\n*.tmp\n*-save.pro\n*-save.kicad_pcb\nfp-info-cache\n\n# Netlist files (exported from Eeschema)\n*.net\n\n# Autorouter files (exported from Pcbnew)\n*.dsn\n*.ses\n\n# Exported BOM files\n*.xml\n*.csv\n\n### KiCad Patch ###\nrescue-backup/\n\n*.tsv\nbom/\n\n# Gerber export output\nout/\n\n# End of https://www.toptal.com/developers/gitignore/api/kicad\n","kirby2":"# Created by https://www.toptal.com/developers/gitignore/api/kirby2\n# Edit at https://www.toptal.com/developers/gitignore?templates=kirby2\n\n### Kirby2 ###\n## Kirby 2 | http://getkirby.com\n\n# Exclude account information\nsite/accounts/*\n!site/accounts/index.html\nassets/avatars/*\n!assets/avatars/.gitkeep\n\n# Exclude thumbnails\nthumbs/*\n!thumbs/index.html\n\n# Exclude cache\nsite/cache/*\n!site/cache/index.html\n\n# End of https://www.toptal.com/developers/gitignore/api/kirby2\n","kirby3":"# Created by https://www.toptal.com/developers/gitignore/api/kirby3\n# Edit at https://www.toptal.com/developers/gitignore?templates=kirby3\n\n### Kirby3 ###\n# Uncomment if you install core files via composer\n# kirby\n# site/plugins/*\n\n# Temporary files\n/media/*\n!/media/index.html\n\n# Lock files\n.lock\n\n# -------------SECURITY-------------\n# NEVER publish these files via Git!\n# -------------SECURITY-------------\n\n# Cache Files\n/site/cache/*\n!/site/cache/index.html\n\n# Accounts\n/site/accounts/*\n!/site/accounts/index.html\n\n# Sessions\n/site/sessions/*\n!/site/sessions/index.html\n\n# License\n/site/config/.license\n\n# End of https://www.toptal.com/developers/gitignore/api/kirby3\n","kobalt":"# Created by https://www.toptal.com/developers/gitignore/api/kobalt\n# Edit at https://www.toptal.com/developers/gitignore?templates=kobalt\n\n### Kobalt ###\n.kobalt\nkobaltBuild/\n\n\n# End of https://www.toptal.com/developers/gitignore/api/kobalt\n","kohana":"# Created by https://www.toptal.com/developers/gitignore/api/kohana\n# Edit at https://www.toptal.com/developers/gitignore?templates=kohana\n\n### Kohana ###\napplication/cache/*\napplication/logs/*\n\n# End of https://www.toptal.com/developers/gitignore/api/kohana\n","komodoedit":"# Created by https://www.toptal.com/developers/gitignore/api/komodoedit\n# Edit at https://www.toptal.com/developers/gitignore?templates=komodoedit\n\n### KomodoEdit ###\n*.komodoproject\n.komodotools\n\n# End of https://www.toptal.com/developers/gitignore/api/komodoedit\n","konyvisualizer":"# Created by https://www.toptal.com/developers/gitignore/api/konyvisualizer\n# Edit at https://www.toptal.com/developers/gitignore?templates=konyvisualizer\n\n### KonyVisualizer ###\n# Below list few files not added to gitignore list to support Headless/CI build\n\n# Auto-generated javascript. It's important to note that jssrc at the project root\n# can be ignored, but resources/customlibs/jsSrc must NOT be ignored because it stores\n# FFI binding code which can only be generated during development.\n/jssrc/*\n!/resources/customlibs/jsSrc\n\n# Starting with Visualizer 8.0 the Ant Contrib Jar used for the build process will\n# be regenerated when the build is fired. If you're using Visualizer 7.x then comment out\n# this line. Vis 7.x will not regenerate this jar and ignoring it will break your project.\nant-contrib-0.6.jar\n\n# Binaries Ex:*.apk,*.KAR,\nbinaries/\n\n# Other Visualizer auto-generated files.\n/.webmeta\n/.meta\nmiddleware.properties\nmodules/kony_sdk.js\nmodules/KonySyncLib.js\n\n\n# Starting with Vis 7.3 the /sdkplugin.properties file has been moved to\n# `/resources/customlibs/sdkplugin.properties` and can be automatically re-created by Vis\n# during the build, provided that folder `resources/customlibs` exists -consider adding a `.gitkeep` #file to it in order to force it into source control.\nresources/customlibs/sdkplugin.properties\n\n# The jar's and zip's under these directories can be ignored so long as\n# sdkplugin.properties (See above) is also ignored. The missing sdkplugin.properties will\n# trigger the regeneration of these jar's and zip's during the build.\nresources/customlibs/lib/android/\nresources/customlibs/lib/tabrcandroid/\nresources/customlibs/lib/iphone/\nresources/customlibs/lib/ipad/\nresources/customlibs/lib/windows10/\nresources/customlibs/lib/winphone10/\n\n# The missing sdkplugin.properties will regenerate below files\nresources/customlibs/jsSrc/android/com/konylabs/ffi/N_KonyLogger.java\nresources/customlibs/jsSrc/tabrcandroid/com/konylabs/ffi/N_KonyLogger.java\nresources/customlibs/jsSrc/kiosk/KonyLogger.js\nresources/customlibs/jsSrc/kiosk/KonyLogger.xml\nresources/customlibs/jsSrc/windows8/KonyLogger.js\nresources/customlibs/jsSrc/windows8/KonyLogger.xml\nresources/customlibs/jsSrc/winmobile/KonyLogger.js\nresources/customlibs/jsSrc/winmobile/KonyLogger.xml\nresources/customlibs/jsSrc/winphone8/KonyLogger.js\nresources/customlibs/jsSrc/winphone8/KonyLogger.xml\nresources/customlibs/jsSrc/winphone81s/KonyLogger.js\nresources/customlibs/jsSrc/winphone81s/KonyLogger.xml\nresources/customlibs/jsXml/KonyLogger.js\nresources/customlibs/jsXml/KonyLogger.xml\nresources/customlibs/jsSrc/kiosk/SSOFFI.js\nresources/customlibs/jsSrc/kiosk/SSOFFI.xml\nresources/customlibs/jsSrc/windows8/SSOFFI.js\nresources/customlibs/jsSrc/windows8/SSOFFI.xml\nresources/customlibs/jsSrc/winmobile/SSOFFI.js\nresources/customlibs/jsSrc/winmobile/SSOFFI.xml\nresources/customlibs/jsSrc/winphone8/SSOFFI.js\nresources/customlibs/jsSrc/winphone8/SSOFFI.xml\nresources/customlibs/jsSrc/winphone81s/SSOFFI.js\nresources/customlibs/jsSrc/winphone81s/SSOFFI.xml\nresources/customlibs/jsXml/SSOFFI.js\nresources/customlibs/jsXml/SSOFFI.xml\nresources/customlibs/jsSrc/android/com/konylabs/ffi/ND_binary_util.java\nresources/customlibs/jsSrc/android/com/konylabs/ffi/N_binarydata.java\nresources/customlibs/jsSrc/tabrcandroid/com/konylabs/ffi/ND_binary_util.java\nresources/customlibs/jsSrc/tabrcandroid/com/konylabs/ffi/N_binarydata.java\nresources/customlibs/jsXml/binary.util.js\nresources/customlibs/jsXml/binary.util.xml\nresources/customlibs/jsXml/binarydata.js\nresources/customlibs/jsXml/binarydata.xml\n\n# Visualizer headless build configuration files\nHeadlessBuild.properties\nHeadlessBuild-Global.properties\n\n# Out-of-the-box NFI bindings for Sync\n/nativeapi.json\n/nativebindings/Android/*/Kony_SyncV2-Android-*.zip\n/nativebindings/Android/*/Kony_SyncV2-Android-*/tern.json\n/nativebindings/iOS/*/Kony_SyncV2-iOS-*.zip\n/nativebindings/iOS/*/Kony_SyncV2-iOS-*/tern.json\n\n# Auxiliary files generated during a build to expose the widget ID's for testing.\nresources/mobile/native/android/values/widgetids.xml\nresources/tablet/native/androidtab/values/widgetids.xml\n\n# NPM dependencies folder which gets created for apps using the \"Nitro\" Cordova integration\n# when you choose NOT to use a globally installed Cordova SDK by unchecking\n# `Project Settings>Application>Cordova Settings>Use globally installed Cordova version`.\nnode_modules\n\n# When using the \"Nitro\" Cordova integration, the Cordova `plugins` and `platforms`\n# directories can be ignored as with any Cordova project.\ncordovatemp\nweb/cordova/plugins\nweb/cordova/platforms\n\n# Auto-generated temporary internationalization files.\nresources/i18n/\n\n# Logs created by building from the command line.\nvelocity.log\n\n# A Logger FFI that gets bundled with each project and is auto-generated by Visualizer with each build.\n\n# A Single Sign-On FFI that gets bundled with each project and is auto-generated by Visualizer with each build.\n\n# A utility FFI that gets bundled with each project and is auto-generated by Visualizer with each build.\n\n# Auto-generated project setting files. Generated the first time you save project settings, even if no changes were made.\n/_clouddata/\n/appSettings.xml\n/context.properties\n/defaults/\n/projectsplash.xml\n/windowsViews.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/konyvisualizer\n","kotlin":"# Created by https://www.toptal.com/developers/gitignore/api/kotlin\n# Edit at https://www.toptal.com/developers/gitignore?templates=kotlin\n\n### Kotlin ###\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\nreplay_pid*\n\n# End of https://www.toptal.com/developers/gitignore/api/kotlin\n","labview":"# Created by https://www.toptal.com/developers/gitignore/api/labview\n# Edit at https://www.toptal.com/developers/gitignore?templates=labview\n\n### LabVIEW ###\n# Libraries\n*.lvlibp\n*.llb\n\n# Shared objects (inc. Windows DLLs)\n*.dll\n*.so\n*.so.*\n*.dylib\n\n# Executables\n*.exe\n\n# Metadata\n*.aliases\n*.lvlps\n.cache/\n\n# End of https://www.toptal.com/developers/gitignore/api/labview\n","labviewnxg":"# Created by https://www.toptal.com/developers/gitignore/api/labviewnxg\n# Edit at https://www.toptal.com/developers/gitignore?templates=labviewnxg\n\n### LabVIEWNXG ###\n# caches\n.cache/\n*.lvcompile\n*.lvindexcache\n\n# Default build output directory for packages, executables, etc.\nBuilds/\n\n# End of https://www.toptal.com/developers/gitignore/api/labviewnxg\n","lamp":"# Created by https://www.toptal.com/developers/gitignore/api/lamp\n# Edit at https://www.toptal.com/developers/gitignore?templates=lamp\n\n### LAMP ###\n# LAMP Stack Base\n\n### LAMP.PHP Stack ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### LAMP.Linux Stack ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n# End of https://www.toptal.com/developers/gitignore/api/lamp\n","laravel":"# Created by https://www.toptal.com/developers/gitignore/api/laravel\n# Edit at https://www.toptal.com/developers/gitignore?templates=laravel\n\n### Laravel ###\n/vendor/\nnode_modules/\nnpm-debug.log\nyarn-error.log\n\n# Laravel 4 specific\nbootstrap/compiled.php\napp/storage/\n\n# Laravel 5 & Lumen specific\npublic/storage\npublic/hot\n\n# Laravel 5 & Lumen specific with changed public path\npublic_html/storage\npublic_html/hot\n\nstorage/*.key\n.env\nHomestead.yaml\nHomestead.json\n/.vagrant\n.phpunit.result.cache\n\n# End of https://www.toptal.com/developers/gitignore/api/laravel\n","latex":"# Created by https://www.toptal.com/developers/gitignore/api/latex\n# Edit at https://www.toptal.com/developers/gitignore?templates=latex\n\n### LaTeX ###\n## Core latex/pdflatex auxiliary files:\n*.aux\n*.lof\n*.log\n*.lot\n*.fls\n*.out\n*.toc\n*.fmt\n*.fot\n*.cb\n*.cb2\n.*.lb\n\n## Intermediate documents:\n*.dvi\n*.xdv\n*-converted-to.*\n# these rules might exclude image files for figures etc.\n# *.ps\n# *.eps\n# *.pdf\n\n## Generated if empty string is given at \"Please type another file name for output:\"\n.pdf\n\n## Bibliography auxiliary files (bibtex/biblatex/biber):\n*.bbl\n*.bcf\n*.blg\n*-blx.aux\n*-blx.bib\n*.run.xml\n\n## Build tool auxiliary files:\n*.fdb_latexmk\n*.synctex\n*.synctex(busy)\n*.synctex.gz\n*.synctex.gz(busy)\n*.pdfsync\n\n## Build tool directories for auxiliary files\n# latexrun\nlatex.out/\n\n## Auxiliary and intermediate files from other packages:\n# algorithms\n*.alg\n*.loa\n\n# achemso\nacs-*.bib\n\n# amsthm\n*.thm\n\n# beamer\n*.nav\n*.pre\n*.snm\n*.vrb\n\n# changes\n*.soc\n\n# comment\n*.cut\n\n# cprotect\n*.cpt\n\n# elsarticle (documentclass of Elsevier journals)\n*.spl\n\n# endnotes\n*.ent\n\n# fixme\n*.lox\n\n# feynmf/feynmp\n*.mf\n*.mp\n*.t[1-9]\n*.t[1-9][0-9]\n*.tfm\n\n#(r)(e)ledmac/(r)(e)ledpar\n*.end\n*.?end\n*.[1-9]\n*.[1-9][0-9]\n*.[1-9][0-9][0-9]\n*.[1-9]R\n*.[1-9][0-9]R\n*.[1-9][0-9][0-9]R\n*.eledsec[1-9]\n*.eledsec[1-9]R\n*.eledsec[1-9][0-9]\n*.eledsec[1-9][0-9]R\n*.eledsec[1-9][0-9][0-9]\n*.eledsec[1-9][0-9][0-9]R\n\n# glossaries\n*.acn\n*.acr\n*.glg\n*.glo\n*.gls\n*.glsdefs\n*.lzo\n*.lzs\n*.slg\n*.slo\n*.sls\n\n# uncomment this for glossaries-extra (will ignore makeindex's style files!)\n# *.ist\n\n# gnuplot\n*.gnuplot\n*.table\n\n# gnuplottex\n*-gnuplottex-*\n\n# gregoriotex\n*.gaux\n*.glog\n*.gtex\n\n# htlatex\n*.4ct\n*.4tc\n*.idv\n*.lg\n*.trc\n*.xref\n\n# hyperref\n*.brf\n\n# knitr\n*-concordance.tex\n# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files\n# *.tikz\n*-tikzDictionary\n\n# listings\n*.lol\n\n# luatexja-ruby\n*.ltjruby\n\n# makeidx\n*.idx\n*.ilg\n*.ind\n\n# minitoc\n*.maf\n*.mlf\n*.mlt\n*.mtc[0-9]*\n*.slf[0-9]*\n*.slt[0-9]*\n*.stc[0-9]*\n\n# minted\n_minted*\n*.pyg\n\n# morewrites\n*.mw\n\n# newpax\n*.newpax\n\n# nomencl\n*.nlg\n*.nlo\n*.nls\n\n# pax\n*.pax\n\n# pdfpcnotes\n*.pdfpc\n\n# sagetex\n*.sagetex.sage\n*.sagetex.py\n*.sagetex.scmd\n\n# scrwfile\n*.wrt\n\n# svg\nsvg-inkscape/\n\n# sympy\n*.sout\n*.sympy\nsympy-plots-for-*.tex/\n\n# pdfcomment\n*.upa\n*.upb\n\n# pythontex\n*.pytxcode\npythontex-files-*/\n\n# tcolorbox\n*.listing\n\n# thmtools\n*.loe\n\n# TikZ & PGF\n*.dpth\n*.md5\n*.auxlock\n\n# titletoc\n*.ptc\n\n# todonotes\n*.tdo\n\n# vhistory\n*.hst\n*.ver\n\n# easy-todo\n*.lod\n\n# xcolor\n*.xcp\n\n# xmpincl\n*.xmpi\n\n# xindy\n*.xdy\n\n# xypic precompiled matrices and outlines\n*.xyc\n*.xyd\n\n# endfloat\n*.ttt\n*.fff\n\n# Latexian\nTSWLatexianTemp*\n\n## Editors:\n# WinEdt\n*.bak\n*.sav\n\n# Texpad\n.texpadtmp\n\n# LyX\n*.lyx~\n\n# Kile\n*.backup\n\n# gummi\n.*.swp\n\n# KBibTeX\n*~[0-9]*\n\n# TeXnicCenter\n*.tps\n\n# auto folder when using emacs and auctex\n./auto/*\n*.el\n\n# expex forward references with \\gathertags\n*-tags.tex\n\n# standalone packages\n*.sta\n\n# Makeindex log files\n*.lpz\n\n# xwatermark package\n*.xwm\n\n# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib\n# option is specified. Footnotes are the stored in a file with suffix Notes.bib.\n# Uncomment the next line to have this generated file ignored.\n#*Notes.bib\n\n### LaTeX Patch ###\n# LIPIcs / OASIcs\n*.vtc\n\n# glossaries\n*.glstex\n\n# End of https://www.toptal.com/developers/gitignore/api/latex\n","lazarus":"# Created by https://www.toptal.com/developers/gitignore/api/lazarus\n# Edit at https://www.toptal.com/developers/gitignore?templates=lazarus\n\n### Lazarus ###\n# Lazarus compiler-generated binaries (safe to delete)\n*.exe\n*.dll\n*.so\n*.dylib\n*.lrs\n*.res\n*.compiled\n*.dbg\n*.ppu\n*.o\n*.or\n*.a\n\n# Lazarus autogenerated files (duplicated info)\n*.rst\n*.rsj\n*.lrt\n\n# Lazarus local files (user-specific info)\n*.lps\n\n# Lazarus backups and unit output folders.\n# These can be changed by user in Lazarus/project options.\nbackup/\n*.bak\nlib/\n\n# Application bundle for Mac OS\n*.app/\n\n# End of https://www.toptal.com/developers/gitignore/api/lazarus\n","leiningen":"# Created by https://www.toptal.com/developers/gitignore/api/leiningen\n# Edit at https://www.toptal.com/developers/gitignore?templates=leiningen\n\n### Leiningen ###\npom.xml\npom.xml.asc\n*.jar\n*.class\n/lib/\n/classes/\n/target/\n/checkouts/\n.lein-deps-sum\n.lein-repl-history\n.lein-plugins/\n.lein-failures\n.nrepl-port\n.cpcache/\n\n# End of https://www.toptal.com/developers/gitignore/api/leiningen\n","lemonstand":"# Created by https://www.toptal.com/developers/gitignore/api/lemonstand\n# Edit at https://www.toptal.com/developers/gitignore?templates=lemonstand\n\n### LemonStand ###\nboot.php\nindex.php\ninstall.php\n/config/*\n!/config/config.php\n/controllers/*\n/init/*\n/logs/*\n/phproad/*\n/temp/*\n/uploaded/*\n/installer_files/*\n/modules/backend/*\n/modules/blog/*\n/modules/cms/*\n/modules/core/*\n/modules/session/*\n/modules/shop/*\n/modules/system/*\n/modules/users/*\n# add content_*.php if you don't want erase client changes to content\n\n# End of https://www.toptal.com/developers/gitignore/api/lemonstand\n","less":"# Created by https://www.toptal.com/developers/gitignore/api/less\n# Edit at https://www.toptal.com/developers/gitignore?templates=less\n\n### Less ###\n*.less\n\n# End of https://www.toptal.com/developers/gitignore/api/less\n","liberosoc":"# Created by https://www.toptal.com/developers/gitignore/api/liberosoc\n# Edit at https://www.toptal.com/developers/gitignore?templates=liberosoc\n\n### LiberoSOC ###\n#Microsemi Libero SOC v11.5\n\n#ignore miscellaneous files in the following directories\nsimulation/*\npackage/*\ncomponent/*\ntooldata/*\nviewdraw/*\n\n#auto-generated constraint log files\nconstraint/*\n!constraint/**.sdc\n!constraint/**.pdc\n\n#synthesis log files\nsynthesis/*\n\n#smartgen log files\nsmartgen/*/*\nsmartgen/smartgen.aws\n!smartgen/*/*.gen\n!smartgen/*/*.vhd\n!smartgen/*/*.v\n\n#designer files\ndesigner/*/*\n\n# End of https://www.toptal.com/developers/gitignore/api/liberosoc\n","librarian-chef":"# Created by https://www.toptal.com/developers/gitignore/api/librarian-chef\n# Edit at https://www.toptal.com/developers/gitignore?templates=librarian-chef\n\n### librarian-chef ###\n# librarian-chef - https://github.com/applicationsonline/librarian-chef\n.tmp/\ncookbooks/\n\n# End of https://www.toptal.com/developers/gitignore/api/librarian-chef\n","libreoffice":"# Created by https://www.toptal.com/developers/gitignore/api/libreoffice\n# Edit at https://www.toptal.com/developers/gitignore?templates=libreoffice\n\n### LibreOffice ###\n# LibreOffice locks\n.~lock.*#\n\n# End of https://www.toptal.com/developers/gitignore/api/libreoffice\n","lighthouseci":"# Created by https://www.toptal.com/developers/gitignore/api/lighthouseci\n# Edit at https://www.toptal.com/developers/gitignore?templates=lighthouseci\n\n### LighthouseCI ###\n# Lighthouse reports\n.lighthouseci/\n\n# End of https://www.toptal.com/developers/gitignore/api/lighthouseci\n","lilypond":"# Created by https://www.toptal.com/developers/gitignore/api/lilypond\n# Edit at https://www.toptal.com/developers/gitignore?templates=lilypond\n\n### Lilypond ###\n*.pdf\n*.ps\n*.midi\n*.mid\n*.log\n*~\n\n# End of https://www.toptal.com/developers/gitignore/api/lilypond\n","linux":"# Created by https://www.toptal.com/developers/gitignore/api/linux\n# Edit at https://www.toptal.com/developers/gitignore?templates=linux\n\n### Linux ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n# End of https://www.toptal.com/developers/gitignore/api/linux\n","lithium":"# Created by https://www.toptal.com/developers/gitignore/api/lithium\n# Edit at https://www.toptal.com/developers/gitignore?templates=lithium\n\n### Lithium ###\nlibraries/*\nresources/tmp/*\n\n# End of https://www.toptal.com/developers/gitignore/api/lithium\n","localstack":"# Created by https://www.toptal.com/developers/gitignore/api/localstack\n# Edit at https://www.toptal.com/developers/gitignore?templates=localstack\n\n### LocalStack ###\n# https://github.com/localstack/localstack\n**/.localstack\n\n# End of https://www.toptal.com/developers/gitignore/api/localstack\n","logtalk":"# Created by https://www.toptal.com/developers/gitignore/api/logtalk\n# Edit at https://www.toptal.com/developers/gitignore?templates=logtalk\n\n### Logtalk ###\n# gitignore template for LogTalk, a programming language that builds upon Prolog\n# website: https://logtalk.org/\n\n# Logtalk temporary file directories\n.lgt_tmp/\nlgt_tmp/\n\n# Logtalk default unit testing and doclet results and logs directories\nlogtalk_tester_logs/\nlogtalk_doclet_logs/\n\n# backend Prolog compiler temporary files\n.pl-history\n*.out\n*.xwam\n*.qo\n*.ql\n*.itf\n*.po\n\n# End of https://www.toptal.com/developers/gitignore/api/logtalk\n","lsspice":"# Created by https://www.toptal.com/developers/gitignore/api/lsspice\n# Edit at https://www.toptal.com/developers/gitignore?templates=lsspice\n\n### LSspice ###\n### LTspice ###\n*.raw\n\n# End of https://www.toptal.com/developers/gitignore/api/lsspice\n","ltspice":"# Created by https://www.toptal.com/developers/gitignore/api/ltspice\n# Edit at https://www.toptal.com/developers/gitignore?templates=ltspice\n\n### LTspice ###\n# gitignore template for LTspice\n# website: https://www.analog.com/en/design-center/design-tools-and-calculators/ltspice-simulator.html\n\n# Logfile\n*.log\n\n# Simulation Outputs\n*.raw\n*.fft\n\n# Netlist\n*.net\n\n# End of https://www.toptal.com/developers/gitignore/api/ltspice\n","lua":"# Created by https://www.toptal.com/developers/gitignore/api/lua\n# Edit at https://www.toptal.com/developers/gitignore?templates=lua\n\n### Lua ###\n# Compiled Lua sources\nluac.out\n\n# luarocks build files\n*.src.rock\n*.zip\n*.tar.gz\n\n# Object files\n*.o\n*.os\n*.ko\n*.obj\n*.elf\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Libraries\n*.lib\n*.a\n*.la\n*.lo\n*.def\n*.exp\n\n# Shared objects (inc. Windows DLLs)\n*.dll\n*.so\n*.so.*\n*.dylib\n\n# Executables\n*.exe\n*.out\n*.app\n*.i*86\n*.x86_64\n*.hex\n\n\n# End of https://www.toptal.com/developers/gitignore/api/lua\n","lyx":"# Created by https://www.toptal.com/developers/gitignore/api/lyx\n# Edit at https://www.toptal.com/developers/gitignore?templates=lyx\n\n### LyX ###\n# Ignore LyX backup and autosave files\n# http://www.lyx.org/\n*.lyx~\n*.lyx#\n\n# End of https://www.toptal.com/developers/gitignore/api/lyx\n","macos":"# Created by https://www.toptal.com/developers/gitignore/api/macos\n# Edit at https://www.toptal.com/developers/gitignore?templates=macos\n\n### macOS ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\r\r\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n### macOS Patch ###\n# iCloud generated files\n*.icloud\n\n# End of https://www.toptal.com/developers/gitignore/api/macos\n","magento":"# Created by https://www.toptal.com/developers/gitignore/api/magento\n# Edit at https://www.toptal.com/developers/gitignore?templates=magento\n\n### Magento ###\n#--------------------------#\n# Magento Default Files #\n\n/PATCH_*.sh\n\n/app/etc/local.xml\n\n/media/*\n!/media/.htaccess\n\n!/media/customer\n/media/customer/*\n!/media/customer/.htaccess\n\n!/media/dhl\n/media/dhl/*\n!/media/dhl/logo.jpg\n\n!/media/downloadable\n/media/downloadable/*\n!/media/downloadable/.htaccess\n\n!/media/xmlconnect\n/media/xmlconnect/*\n\n!/media/xmlconnect/custom\n/media/xmlconnect/custom/*\n!/media/xmlconnect/custom/ok.gif\n\n!/media/xmlconnect/original\n/media/xmlconnect/original/*\n!/media/xmlconnect/original/ok.gif\n\n!/media/xmlconnect/system\n/media/xmlconnect/system/*\n!/media/xmlconnect/system/ok.gif\n\n/var/*\n!/var/.htaccess\n\n!/var/package\n/var/package/*\n!/var/package/*.xml\n\n\n### Magento Patch ###\napp/bootstrap.php\nshell/.htaccess\ndev/tests/functional\n\n# End of https://www.toptal.com/developers/gitignore/api/magento\n","magento1":"# Created by https://www.toptal.com/developers/gitignore/api/magento1\n# Edit at https://www.toptal.com/developers/gitignore?templates=magento1\n\n### Magento1 ###\n# gitignore template for Magento v1 projects\n#\n# It is recommended that you use `Magento.gitignore` as this is the latest version\n\n/PATCH_*.sh\n\n/app/etc/local.xml\n\n/media/*\n!/media/.htaccess\n\n!/media/customer\n/media/customer/*\n!/media/customer/.htaccess\n\n!/media/dhl\n/media/dhl/*\n!/media/dhl/logo.jpg\n\n!/media/downloadable\n/media/downloadable/*\n!/media/downloadable/.htaccess\n\n!/media/xmlconnect\n/media/xmlconnect/*\n\n!/media/xmlconnect/custom\n/media/xmlconnect/custom/*\n!/media/xmlconnect/custom/ok.gif\n\n!/media/xmlconnect/original\n/media/xmlconnect/original/*\n!/media/xmlconnect/original/ok.gif\n\n!/media/xmlconnect/system\n/media/xmlconnect/system/*\n!/media/xmlconnect/system/ok.gif\n\n/var/*\n!/var/.htaccess\n\n!/var/package\n/var/package/*\n!/var/package/*.xml\n\n\n# End of https://www.toptal.com/developers/gitignore/api/magento1\n","magento2":"# Created by https://www.toptal.com/developers/gitignore/api/magento2\n# Edit at https://www.toptal.com/developers/gitignore?templates=magento2\n\n### Magento2 ###\n/.buildpath\n/.cache\n/.metadata\n/.project\n/.settings\n/.vscode\natlassian*\n/nbproject\n/robots.txt\n/pub/robots.txt\n/sitemap\n/sitemap.xml\n/pub/sitemap\n/pub/sitemap.xml\n/.idea\n/.gitattributes\n/app/config_sandbox\n/app/etc/config.php\n/app/etc/env.php\n/app/code/Magento/TestModule*\n/lib/internal/flex/uploader/.actionScriptProperties\n/lib/internal/flex/uploader/.flexProperties\n/lib/internal/flex/uploader/.project\n/lib/internal/flex/uploader/.settings\n/lib/internal/flex/varien/.actionScriptProperties\n/lib/internal/flex/varien/.flexLibProperties\n/lib/internal/flex/varien/.project\n/lib/internal/flex/varien/.settings\n/node_modules\n/.grunt\n/Gruntfile.js\n/package.json\n/.php_cs\n/.php_cs.cache\n/grunt-config.json\n/pub/media/*.*\n!/pub/media/.htaccess\n/pub/media/attribute/*\n!/pub/media/attribute/.htaccess\n/pub/media/analytics/*\n/pub/media/catalog/*\n!/pub/media/catalog/.htaccess\n/pub/media/customer/*\n!/pub/media/customer/.htaccess\n/pub/media/downloadable/*\n!/pub/media/downloadable/.htaccess\n/pub/media/favicon/*\n/pub/media/import/*\n!/pub/media/import/.htaccess\n/pub/media/logo/*\n/pub/media/custom_options/*\n!/pub/media/custom_options/.htaccess\n/pub/media/theme/*\n/pub/media/theme_customization/*\n!/pub/media/theme_customization/.htaccess\n/pub/media/wysiwyg/*\n!/pub/media/wysiwyg/.htaccess\n/pub/media/tmp/*\n!/pub/media/tmp/.htaccess\n/pub/media/captcha/*\n/pub/static/*\n!/pub/static/.htaccess\n\n/var/*\n!/var/.htaccess\n/vendor/*\n!/vendor/.htaccess\n/generated/*\n!/generated/.htaccess\n.DS_Store\n\n# End of https://www.toptal.com/developers/gitignore/api/magento2\n","magic-xpa":"# Created by https://www.toptal.com/developers/gitignore/api/magic-xpa\n# Edit at https://www.toptal.com/developers/gitignore?templates=magic-xpa\n\n### Magic-xpa ###\n# refer to http://www.magicsoftware.com/magic-xpa-application-platform\n# ProgramHeaders.xml serves as a cache file for program headers which is rebuilt if non existent\nProgramHeaders.xml\n# DataSourcesIndex.xml serves as a cache file for header information of DataSources.xml which is rebuilt if non existent\nDataSourcesIndex.xml\n# ignores cross reference results stored in project directory\n*.XRF\n# ignores studio options and their backups\n*.opt\n*.optback\n# sometimes Magic creates backups of different xml files, not deleted on crashes\n*_BCK.xml\n# if a project is opened, a .lock file is created in the project directory to prevent multiple opens, ignored.\n*.lock\n\n# End of https://www.toptal.com/developers/gitignore/api/magic-xpa\n","matlab":"# Created by https://www.toptal.com/developers/gitignore/api/matlab\n# Edit at https://www.toptal.com/developers/gitignore?templates=matlab\n\n### MATLAB ###\n# Windows default autosave extension\n*.asv\n\n# OSX / *nix default autosave extension\n*.m~\n\n# Compiled MEX binaries (all platforms)\n*.mex*\n\n# Packaged app and toolbox files\n*.mlappinstall\n*.mltbx\n\n# Generated helpsearch folders\nhelpsearch*/\n\n# Simulink code generation folders\nslprj/\nsccprj/\n\n# Matlab code generation folders\ncodegen/\n\n# Simulink autosave extension\n*.autosave\n\n# Simulink cache files\n*.slxc\n\n# Octave session info\noctave-workspace\n\n# End of https://www.toptal.com/developers/gitignore/api/matlab\n","maven":"# Created by https://www.toptal.com/developers/gitignore/api/maven\n# Edit at https://www.toptal.com/developers/gitignore?templates=maven\n\n### Maven ###\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n# https://github.com/takari/maven-wrapper#usage-without-binary-jar\n.mvn/wrapper/maven-wrapper.jar\n\n# Eclipse m2e generated files\n# Eclipse Core\n.project\n# JDT-specific (Eclipse Java Development Tools)\n.classpath\n\n# End of https://www.toptal.com/developers/gitignore/api/maven\n","mavensmate":"# Created by https://www.toptal.com/developers/gitignore/api/mavensmate\n# Edit at https://www.toptal.com/developers/gitignore?templates=mavensmate\n\n### MavensMate ###\n#Sublime Text exclusions\n*.sublime-workspace\n*.sublime-project\n*.sublime-settings\n\n#MavensMate exclusions\nconfig/*\nmm.log\n\n#OPTIONAL Apex scripts exclusion\n# - Rule 1 excludes the contents of the apex-scripts folder\n# - Rule 2 adds in NAMED apex scripts\n# Together: excludes scripts run though MavensMate’s\n# execute anonymous functionality, while keeping named scripts\napex-scripts/*\n!apex-scripts/*.cls\n\n# End of https://www.toptal.com/developers/gitignore/api/mavensmate\n","mdbook":"# Created by https://www.toptal.com/developers/gitignore/api/mdbook\n# Edit at https://www.toptal.com/developers/gitignore?templates=mdbook\n\n### MdBook ###\nbook\n\n\n# End of https://www.toptal.com/developers/gitignore/api/mdbook\n","mean":"# Created by https://www.toptal.com/developers/gitignore/api/mean\n# Edit at https://www.toptal.com/developers/gitignore?templates=mean\n\n### MEAN ###\n# MEAN Stack Base\n\n### MEAN.Anglar Stack ###\n## Angular ##\n# compiled output\ndist/\ntmp/\napp/**/*.js\napp/**/*.js.map\n\n# dependencies\nnode_modules/\nbower_components/\n\n# IDEs and editors\n.idea/\n\n# misc\n.sass-cache/\nconnect.lock/\ncoverage/\nlibpeerconnection.log/\nnpm-debug.log\ntestem.log\ntypings/\n.angular/\n\n# e2e\ne2e/*.js\ne2e/*.map\n\n# System Files\n.DS_Store/\n\n### MEAN.Node Stack ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# End of https://www.toptal.com/developers/gitignore/api/mean\n","mercurial":"# Created by https://www.toptal.com/developers/gitignore/api/mercurial\n# Edit at https://www.toptal.com/developers/gitignore?templates=mercurial\n\n### Mercurial ###\n.hg/\n.hgignore\n.hgsigs\n.hgsub\n.hgsubstate\n.hgtags\n\n# End of https://www.toptal.com/developers/gitignore/api/mercurial\n","mercury":"# Created by https://www.toptal.com/developers/gitignore/api/mercury\n# Edit at https://www.toptal.com/developers/gitignore?templates=mercury\n\n### Mercury ###\nMercury/\nMercury.modules\n*.mh\n*.err\n*.init\n*.dll\n*.exe\n*.a\n*.so\n*.dylib\n*.beams\n*.d\n*.c_date\n\n# End of https://www.toptal.com/developers/gitignore/api/mercury\n","meson":"# Created by https://www.toptal.com/developers/gitignore/api/meson\n# Edit at https://www.toptal.com/developers/gitignore?templates=meson\n\n### Meson ###\n# subproject directories\n/subprojects/*\n!/subprojects/*.wrap\n\n# Meson Directories\nmeson-logs\nmeson-private\n\n# Meson Files\nmeson_benchmark_setup.dat\nmeson_test_setup.dat\nsanitycheckcpp.cc # C++ specific\nsanitycheckcpp.exe # C++ specific\n\n# Ninja\nbuild.ninja\n.ninja_deps\n.ninja_logs\n\n# Misc\ncompile_commands.json\n\n# End of https://www.toptal.com/developers/gitignore/api/meson\n","metals":"# Created by https://www.toptal.com/developers/gitignore/api/metals\n# Edit at https://www.toptal.com/developers/gitignore?templates=metals\n\n### Metals ###\n.metals/\n.bloop/\nproject/**/metals.sbt\n\n# End of https://www.toptal.com/developers/gitignore/api/metals\n","metalsmith":"# Created by https://www.toptal.com/developers/gitignore/api/metalsmith\n# Edit at https://www.toptal.com/developers/gitignore?templates=metalsmith\n\n### Metalsmith ###\n# gitignore template for the Metalsmith, a simple, pluggable static site generator\n# website: https://metalsmith.io/\n\nbuild\n\n# End of https://www.toptal.com/developers/gitignore/api/metalsmith\n","metaprogrammingsystem":"# Created by https://www.toptal.com/developers/gitignore/api/metaprogrammingsystem\n# Edit at https://www.toptal.com/developers/gitignore?templates=metaprogrammingsystem\n\n### MetaProgrammingSystem ###\nworkspace.xml\njunitvmwatcher*.properties\nbuild.properties\n\n# generated java classes and java source files\n# manually add any custom artifacts that can't be generated from the models\n# http://confluence.jetbrains.com/display/MPSD25/HowTo+--+MPS+and+Git\nclasses_gen\nsource_gen\nsource_gen.caches\n\n# generated test code and test results\ntest_gen\ntest_gen.caches\nTEST-*.xml\njunit*.properties\n\n# End of https://www.toptal.com/developers/gitignore/api/metaprogrammingsystem\n","meteor":"# Created by https://www.toptal.com/developers/gitignore/api/meteor\n# Edit at https://www.toptal.com/developers/gitignore?templates=meteor\n\n### Meteor ###\n# gitignore template for the Meteor framework\n# website: https://www.meteor.com/\n#\n# Recommended template: Node.gitignore\n\n# protect api keys in setting json\nsettings-production.json\nsettings.json\n\n# protect your mup.json settings\nmup.json\nmup.js\n\n# End of https://www.toptal.com/developers/gitignore/api/meteor\n","meteorjs":"# Created by https://www.toptal.com/developers/gitignore/api/meteorjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=meteorjs\n\n### MeteorJS ###\n# default meteor build and local packages\n.meteor/local\n\n# meteor settings file\nsettings.json\n\n# meteor build output files\n*.tar.gz\n\n# general swp files from vim\n*.swp\n\n# End of https://www.toptal.com/developers/gitignore/api/meteorjs\n","microsoftoffice":"# Created by https://www.toptal.com/developers/gitignore/api/microsoftoffice\n# Edit at https://www.toptal.com/developers/gitignore?templates=microsoftoffice\n\n### MicrosoftOffice ###\n*.tmp\n\n# Word temporary\n~$*.doc*\n\n# Word Auto Backup File\nBackup of *.doc*\n\n# Excel temporary\n~$*.xls*\n\n# Excel Backup File\n*.xlk\n\n# PowerPoint temporary\n~$*.ppt*\n\n# Visio autosave temporary files\n*.~vsd*\n\n# End of https://www.toptal.com/developers/gitignore/api/microsoftoffice\n","mikroc":"# Created by https://www.toptal.com/developers/gitignore/api/mikroc\n# Edit at https://www.toptal.com/developers/gitignore?templates=mikroc\n\n### MikroC ###\n# Backup\n*.pdsbak\n\n# Debug\nDebug\n\n# Generated files\n*.workspace\n*.log\n*.bmk\n*.brk\n*.cfg\n*.dbg\n*.dct\n*.dlt\n*.hex\n*.lst\n*.dic\n*_callertable.txt\n*.user.dic\n*.asm\n*.c.ini\n*.h.ini\n*.cp\n*.mcl\n\n# End of https://www.toptal.com/developers/gitignore/api/mikroc\n","mill":"# Created by https://www.toptal.com/developers/gitignore/api/mill\n# Edit at https://www.toptal.com/developers/gitignore?templates=mill\n\n### Mill ###\nout\n\n# End of https://www.toptal.com/developers/gitignore/api/mill\n","moban":"# Created by https://www.toptal.com/developers/gitignore/api/moban\n# Edit at https://www.toptal.com/developers/gitignore?templates=moban\n\n### Moban ###\n.moban.hashes\n\n# End of https://www.toptal.com/developers/gitignore/api/moban\n","modelsim":"# Created by https://www.toptal.com/developers/gitignore/api/modelsim\n# Edit at https://www.toptal.com/developers/gitignore?templates=modelsim\n\n### ModelSim ###\n# ignore ModelSim generated files and directories (temp files and so on)\n[_@]*\n\n# ignore compilation output of ModelSim\n*.mti\n*.dat\n*.dbs\n*.psm\n*.bak\n*.cmp\n*.jpg\n*.html\n*.bsf\n\n# ignore simulation output of ModelSim\nwlf*\n*.wlf\n*.vstf\n*.ucdb\ncov*/\ntranscript*\nsc_dpiheader.h\nvsim.dbg\n\n# End of https://www.toptal.com/developers/gitignore/api/modelsim\n","modx":"# Created by https://www.toptal.com/developers/gitignore/api/modx\n# Edit at https://www.toptal.com/developers/gitignore?templates=modx\n\n### MODX ###\n/core/cache/*\n!/core/cache/.gitkeep\n\n# Depends on how you version your MODX site\n/core/packages/*/*\n\n/core/import/*\n/core/export/*\n\n# End of https://www.toptal.com/developers/gitignore/api/modx\n","momentics":"# Created by https://www.toptal.com/developers/gitignore/api/momentics\n# Edit at https://www.toptal.com/developers/gitignore?templates=momentics\n\n### Momentics ###\n# Built files\nx86/\narm/\narm-p/\ntranslations/*.qm\n\n# IDE settings\n.settings/\n\n# End of https://www.toptal.com/developers/gitignore/api/momentics\n","monodevelop":"# Created by https://www.toptal.com/developers/gitignore/api/monodevelop\n# Edit at https://www.toptal.com/developers/gitignore?templates=monodevelop\n\n### MonoDevelop ###\n#User Specific\n*.userprefs\n*.usertasks\n\n#Mono Project Files\n*.pidb\n*.resources\ntest-results/\n\n# End of https://www.toptal.com/developers/gitignore/api/monodevelop\n","mplabx":"# Created by https://www.toptal.com/developers/gitignore/api/mplabx\n# Edit at https://www.toptal.com/developers/gitignore?templates=mplabx\n\n### MPLabX ###\n#Ignore List for Microchip's MPLAB X IDE\n#It's a form of NetBeans with vendor specific changes\n#Taken from zeha on GIST https://gist.github.com/zeha/5999375\n#Updated by Cristian Cristea (https://github.com/cristiancristea00)\n\n*.d\n*.pre\n*.p1\n*.lst\n*.sym\n*.obj\n*.o\n*.sdb\n*.obj.dmp\n*.mk\n*.map\n*.properties\n*.production\n*.debug\nhtml/\nnbproject/private/\nnbproject/Package-*.bash\nbuild/\nnbbuild/\ndist/\nnbdist/\nnbactions.xml\nnb-configuration.xml\nfunclist\nnbproject/Makefile-*\ndisassembly/\n.generated_files/\n\n# End of https://www.toptal.com/developers/gitignore/api/mplabx\n","mule":"# Created by https://www.toptal.com/developers/gitignore/api/mule\n# Edit at https://www.toptal.com/developers/gitignore?templates=mule\n\n### mule ###\n# gitignore template for Mule projects\n# website: https://www.mulesoft.com/platform/enterprise-integration\n#\n# Recommended template: Mule.gitignore\n\n# Java defaults (https://github.com/github/gitignore/blob/master/Java.gitignore) #\n# --- #\n*.class\n# Package Files #\n*.jar\n*.war\n*.ear\n# --- #\n# Eclipse-specific (https://github.com/github/gitignore/blob/master/Global/Eclipse.gitignore) #\n# --- #\n*.pydevproject\n.metadata\nbin/**\ntmp/**\ntmp/**/*\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.settings/\n.loadpath\n# You may want to remove the sharp symbols here if you are using Maven\n.project\n.classpath\n# External tool builders\n.externalToolBuilders/\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n# CDT-specific\n.cproject\n# PDT-specific\n.buildpath\n# --------------- #\n# Studio-specific #\n# --------------- #\n.studio/\nflows/\ntarget/\n**/*.mflow\ncatalog/\nreports/\n.mule/\n\n# End of https://www.toptal.com/developers/gitignore/api/mule\n","nanoc":"# Created by https://www.toptal.com/developers/gitignore/api/nanoc\n# Edit at https://www.toptal.com/developers/gitignore?templates=nanoc\n\n### Nanoc ###\n# For projects using Nanoc (http://nanoc.ws/)\n\n# Default location for output (needs to match output_dir's value found in nanoc.yaml)\noutput/\n\n# Temporary file directory\ntmp/nanoc/\n\n# Crash Log\ncrash.log\n\n# End of https://www.toptal.com/developers/gitignore/api/nanoc\n","nativescript":"# Created by https://www.toptal.com/developers/gitignore/api/nativescript\n# Edit at https://www.toptal.com/developers/gitignore?templates=nativescript\n\n### NativeScript ###\n/platforms\n/hooks\n/node_modules\n/lib\n/debugger\n/dist\n\n/app/**/*.map\n# shouldn't ignore this for vanilla nativescript\n# /app/**/*.js\n\n.DS_Store\n**/*.log\ntags\n\ners.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/nativescript\n","ncrunch":"# Created by https://www.toptal.com/developers/gitignore/api/ncrunch\n# Edit at https://www.toptal.com/developers/gitignore?templates=ncrunch\n\n### NCrunch ###\n# NCrunch\n*.ncrunch*.user\n_NCrunch_*\n*.crunch.xml\nnCrunchTemp_*\n\n# End of https://www.toptal.com/developers/gitignore/api/ncrunch\n","nesc":"# Created by https://www.toptal.com/developers/gitignore/api/nesc\n# Edit at https://www.toptal.com/developers/gitignore?templates=nesc\n\n### NesC ###\n# telosB build folder\nbuild/\n\n# End of https://www.toptal.com/developers/gitignore/api/nesc\n","netbeans":"# Created by https://www.toptal.com/developers/gitignore/api/netbeans\n# Edit at https://www.toptal.com/developers/gitignore?templates=netbeans\n\n### NetBeans ###\n**/nbproject/private/\n**/nbproject/Makefile-*.mk\n**/nbproject/Package-*.bash\nbuild/\nnbbuild/\ndist/\nnbdist/\n.nb-gradle/\n\n# End of https://www.toptal.com/developers/gitignore/api/netbeans\n","nette":"# Created by https://www.toptal.com/developers/gitignore/api/nette\n# Edit at https://www.toptal.com/developers/gitignore?templates=nette\n\n### Nette ###\n# Nette - http://nette.org\n# Ignore /log and /temp directories content but commit .htaccess settings\n/log/*\n!/log/.htaccess\n/temp/*\n!/temp/.htaccess\n\n# End of https://www.toptal.com/developers/gitignore/api/nette\n","nextjs":"# Created by https://www.toptal.com/developers/gitignore/api/nextjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=nextjs\n\n### NextJS ###\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n\n# End of https://www.toptal.com/developers/gitignore/api/nextjs\n","nikola":"# Created by https://www.toptal.com/developers/gitignore/api/nikola\n# Edit at https://www.toptal.com/developers/gitignore?templates=nikola\n\n### Nikola ###\n# gitignore template for Nikola static site generator\n# website: https://getnikola.com/\n\n.doit.db\n*.py[cod]\ncache/\noutput/\n\n# End of https://www.toptal.com/developers/gitignore/api/nikola\n","nim":"# Created by https://www.toptal.com/developers/gitignore/api/nim\n# Edit at https://www.toptal.com/developers/gitignore?templates=nim\n\n### Nim ###\nnimcache/\nnimblecache/\nhtmldocs/\n\n# End of https://www.toptal.com/developers/gitignore/api/nim\n","ninja":"# Created by https://www.toptal.com/developers/gitignore/api/ninja\n# Edit at https://www.toptal.com/developers/gitignore?templates=ninja\n\n### Ninja ###\n.ninja_deps\n.ninja_log\n\n# End of https://www.toptal.com/developers/gitignore/api/ninja\n","node":"# Created by https://www.toptal.com/developers/gitignore/api/node\n# Edit at https://www.toptal.com/developers/gitignore?templates=node\n\n### Node ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n### Node Patch ###\n# Serverless Webpack directories\n.webpack/\n\n# Optional stylelint cache\n\n# SvelteKit build / generate output\n.svelte-kit\n\n# End of https://www.toptal.com/developers/gitignore/api/node\n","nodechakratimetraveldebug":"# Created by https://www.toptal.com/developers/gitignore/api/nodechakratimetraveldebug\n# Edit at https://www.toptal.com/developers/gitignore?templates=nodechakratimetraveldebug\n\n### NodeChakraTimeTravelDebug ###\n# gitignore template for NodeChakra Time Travel Debug\n# website: https://marketplace.visualstudio.com/items?itemName=ttd-trace-tools.node-chakracore-time-travel-debugger\n\n# Ignore time-travel trace files\n# https://github.com/mrkmarron/node-chakracore-time-travel-debugger#notes\n_ttd_log_\n\n# End of https://www.toptal.com/developers/gitignore/api/nodechakratimetraveldebug\n","nohup":"# Created by https://www.toptal.com/developers/gitignore/api/nohup\n# Edit at https://www.toptal.com/developers/gitignore?templates=nohup\n\n### Nohup ###\nnohup.out\n\n# End of https://www.toptal.com/developers/gitignore/api/nohup\n","notepadpp":"# Created by https://www.toptal.com/developers/gitignore/api/notepadpp\n# Edit at https://www.toptal.com/developers/gitignore?templates=notepadpp\n\n### NotepadPP ###\n# Notepad++ backups #\n*.bak\n\n# End of https://www.toptal.com/developers/gitignore/api/notepadpp\n","nova":"# Created by https://www.toptal.com/developers/gitignore/api/nova\n# Edit at https://www.toptal.com/developers/gitignore?templates=nova\n\n### nova ###\n.nova/*\n\n# End of https://www.toptal.com/developers/gitignore/api/nova\n","now":"# Created by https://www.toptal.com/developers/gitignore/api/now\n# Edit at https://www.toptal.com/developers/gitignore?templates=now\n\n### now ###\n.now\n.vercel\n\n# End of https://www.toptal.com/developers/gitignore/api/now\n","nuxtjs":"# Created by https://www.toptal.com/developers/gitignore/api/nuxtjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=nuxtjs\n\n### NuxtJS ###\n# Generated dirs\ndist\n.nuxt\n.nuxt-*\n.output\n.gen\n\n# Node dependencies\nnode_modules\n\n# System files\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/nuxtjs\n","nwjs":"# Created by https://www.toptal.com/developers/gitignore/api/nwjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=nwjs\n\n### NWjs ###\n# gitignore template for NW.js projects\n# website: https://nwjs.io/\n\n# Seen in standard and sdk versions\ncredits.html\nlocales/\nlibEGL.dll\nlibGLEv2.dll\nnode.dll\nnw.dll\nnw.exe\nnatives_blob.bin\nnw_100_percent.pak\nnw_200_percent.pak\nnw_elf.dll\nsnapshot_blob.bin\nresources.pak\n\n# Seen only in standard\nd3dcompiler_47.dll\nffmpeg.dll\nicudtl.dat\n\n# Seen only in sdk\npnacl/\nchromedriver.exe\nnacl_irt_x86_64.nexe\nnwjc.exe\npayload.exe\n\n# End of https://www.toptal.com/developers/gitignore/api/nwjs\n","objective-c":"# Created by https://www.toptal.com/developers/gitignore/api/objective-c\n# Edit at https://www.toptal.com/developers/gitignore?templates=objective-c\n\n### Objective-C ###\n# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n## User settings\nxcuserdata/\n\n## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)\n*.xcscmblueprint\n*.xccheckout\n\n## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)\nbuild/\nDerivedData/\n*.moved-aside\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\n\n## Obj-C/Swift specific\n*.hmap\n\n## App packaging\n*.ipa\n*.dSYM.zip\n*.dSYM\n\n# CocoaPods\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n# Pods/\n# Add this line if you want to avoid checking in source code from the Xcode workspace\n# *.xcworkspace\n\n# Carthage\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build/\n\n# fastlane\n# It is recommended to not store the screenshots in the git repo.\n# Instead, use fastlane to re-generate the screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots/**/*.png\nfastlane/test_output\n\n# Code Injection\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n\n### Objective-C Patch ###\n\n# End of https://www.toptal.com/developers/gitignore/api/objective-c\n","obsidian":"# Created by https://www.toptal.com/developers/gitignore/api/obsidian\n# Edit at https://www.toptal.com/developers/gitignore?templates=obsidian\n\n### Obsidian ###\n# config dir\n.obsidian/\n\n# End of https://www.toptal.com/developers/gitignore/api/obsidian\n","ocaml":"# Created by https://www.toptal.com/developers/gitignore/api/ocaml\n# Edit at https://www.toptal.com/developers/gitignore?templates=ocaml\n\n### OCaml ###\n*.annot\n*.cmo\n*.cma\n*.cmi\n*.a\n*.o\n*.cmx\n*.cmxs\n*.cmxa\n\n# ocamlbuild working directory\n_build/\n\n# ocamlbuild targets\n*.byte\n*.native\n\n# oasis generated files\nsetup.data\nsetup.log\n\n# Merlin configuring file for Vim and Emacs\n.merlin\n\n# Dune generated files\n*.install\n\n# Local OPAM switch\n_opam/\n\n# End of https://www.toptal.com/developers/gitignore/api/ocaml\n","octave":"# Created by https://www.toptal.com/developers/gitignore/api/octave\n# Edit at https://www.toptal.com/developers/gitignore?templates=octave\n\n### Octave ###\n# Windows default autosave extension\n*.asv\n\n# OSX / *nix default autosave extension\n*.m~\n\n# Compiled MEX binaries (all platforms)\n*.mex*\n\n# Packaged app and toolbox files\n*.mlappinstall\n*.mltbx\n\n# Generated helpsearch folders\nhelpsearch*/\n\n# Simulink code generation folders\nslprj/\nsccprj/\n\n# Matlab code generation folders\ncodegen/\n\n# Simulink autosave extension\n*.autosave\n\n# Simulink cache files\n*.slxc\n\n# Octave session info\noctave-workspace\n\n# End of https://www.toptal.com/developers/gitignore/api/octave\n","octobercms":"# Created by https://www.toptal.com/developers/gitignore/api/octobercms\n# Edit at https://www.toptal.com/developers/gitignore?templates=octobercms\n\n### OctoberCms ###\n/bootstrap/compiled.php\n/vendor\ncomposer.phar\n.DS_Store\n.idea\n.env\n.env.*.php\n.env.php\nphp_errors.log\nnginx-error.log\nnginx-access.log\nnginx-ssl.access.log\nnginx-ssl.error.log\nphp-errors.log\nsftp-config.json\nselenium.php\n\n# for netbeans compiler\nnbproject\n\n# End of https://www.toptal.com/developers/gitignore/api/octobercms\n","opa":"# Created by https://www.toptal.com/developers/gitignore/api/opa\n# Edit at https://www.toptal.com/developers/gitignore?templates=opa\n\n### Opa ###\n_build\n_tracks\n\nopa-debug-js\n\n*.opp\n*.opx\n*.opx.broken\n*.dump\n*.api\n*.api-txt\n*.exe\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/opa\n","opencart":"# Created by https://www.toptal.com/developers/gitignore/api/opencart\n# Edit at https://www.toptal.com/developers/gitignore?templates=opencart\n\n### OpenCart ###\n.htaccess\n/config.php\nadmin/config.php\n\n!index.html\n\ndownload/\nimage/data/\nimage/cache/\nsystem/cache/\nsystem/logs/\n\nsystem/storage/\n\n# vQmod log files\nvqmod/logs/*\n# vQmod cache files\nvqmod/vqcache/*\nvqmod/checked.cache\nvqmod/mods.cache\n\n# End of https://www.toptal.com/developers/gitignore/api/opencart\n","opencv":"# Created by https://www.toptal.com/developers/gitignore/api/opencv\n# Edit at https://www.toptal.com/developers/gitignore?templates=opencv\n\n### OpenCV ###\n#OpenCV for Mac and Linux\n#build and release folders\n*/CMakeFiles\n*/CMakeCache.txt\n*/Makefile\n*/cmake_install.cmake\n.DS_Store\n\n# End of https://www.toptal.com/developers/gitignore/api/opencv\n","openfoam":"# Created by https://www.toptal.com/developers/gitignore/api/openfoam\n# Edit at https://www.toptal.com/developers/gitignore?templates=openfoam\n\n### OpenFOAM ###\n# Timestep directories\n0.*\n[1-9]*\n\n# Fields that must get ignored\n*/phi*\n*/meshPhi*\n\n# Decomposed domain\nprocessor*\n\n# logfiles\nlog.*\n*log\n\n# paraview/ParaFoam\n*.foam\n*.OpenFOAM\n*.pvsm\n\n# mesh data that is no dictionary\nconstant/polyMesh/points*\nconstant/polyMesh/faces*\nconstant/polyMesh/owner*\nconstant/polyMesh/neighbour*\nconstant/polyMesh/boundary*\nconstant/polyMesh/sets\nconstant/polyMesh/*\n\n# Zones and levels\n*Level*\n*Zone*\n\n# snappyHexMesh files that are not snappyHexMeshDict\nconstant/polyMesh/refinementHistory*\nconstant/polyMesh/surfaceIndex*\nconstant/triSurface\n\n# function object and post-processing data\nforces\npostProcessing\n\n# Needed when C++ code is built using the OpenFOAM library, since it does not\n# use a \"normal\" build tool (make, cmake, ..).\nlnInclude\n*.dep\nlinux*\nDarwin*\n\n#exclude important folders and files:\n!0/\n!constant/thermophysicalProperties\n!constant/turbulenceProperties\n!system/\n!.gitignore\n\n# End of https://www.toptal.com/developers/gitignore/api/openfoam\n","openframeworks":"# Created by https://www.toptal.com/developers/gitignore/api/openframeworks\n# Edit at https://www.toptal.com/developers/gitignore?templates=openframeworks\n\n### OpenFrameworks ###\n# ignore generated binaries\n# but not the data folder\n\n/bin/*\n!/bin/data/\n\n# general\n\n[Bb]uild/\n[Oo]bj/\n*.o\n[Dd]ebug*/\n[Rr]elease*/\n*.mode*\n*.app/\n*.pyc\n.svn/\n*.log\n\n# IDE files which should\n# be ignored\n\n# XCode\n*.pbxuser\n*.perspective\n*.perspectivev3\n*.mode1v3\n*.mode2v3\n# XCode 4\nxcuserdata\n*.xcworkspace\n\n# Code::Blocks\n*.depend\n*.layout\n\n# Visual Studio\n*.sdf\n*.opensdf\n*.suo\n*.pdb\n*.ilk\n*.aps\nipch/\n\n# Eclipse\n.metadata\nlocal.properties\n.externalToolBuilders\n\n# operating system\n\n# Linux\n*~\n# KDE\n.directory\n.AppleDouble\n\n# OSX\n.DS_Store\n*.swp\n*~.nib\n# Thumbnails\n._*\n\n# Windows\n# Image file caches\nThumbs.db\n# Folder config file\nDesktop.ini\n\n# Android\n.csettings\n\n# End of https://www.toptal.com/developers/gitignore/api/openframeworks\n","openframeworks+visualstudio":"# Created by https://www.toptal.com/developers/gitignore/api/openframeworks+visualstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=openframeworks+visualstudio\n\n### OpenFrameworks+VisualStudio ###\n# ignore generated binaries\n# but not the data folder\n\n/bin/*\n!/bin/data/\n\n# general\n\n[Bb]uild/\n[Oo]bj/\n*.o\n[Dd]ebug*/\n[Rr]elease*/\n*.mode*\n*.app/\n*.pyc\n.svn/\n*.log\n\n# IDE files which should\n# be ignored\n\n# XCode\n*.pbxuser\n*.perspective\n*.perspectivev3\n*.mode1v3\n*.mode2v3\n# XCode 4\nxcuserdata\n*.xcworkspace\n\n# Code::Blocks\n*.depend\n*.layout\n\n# Visual Studio\n*.sdf\n*.opensdf\n*.suo\n*.pdb\n*.ilk\n*.aps\nipch/\n\n# Eclipse\n.metadata\nlocal.properties\n.externalToolBuilders\n\n# operating system\n\n# Linux\n*~\n# KDE\n.directory\n.AppleDouble\n\n# OSX\n.DS_Store\n*.swp\n*~.nib\n# Thumbnails\n._*\n\n# Windows\n# Image file caches\nThumbs.db\n# Folder config file\nDesktop.ini\n\n# Android\n.csettings\n\n### OpenFrameworks+VisualStudio Patch ###\n.vs/\n*.ncb\n*.opendb\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# End of https://www.toptal.com/developers/gitignore/api/openframeworks+visualstudio\n","oracleforms":"# Created by https://www.toptal.com/developers/gitignore/api/oracleforms\n# Edit at https://www.toptal.com/developers/gitignore?templates=oracleforms\n\n### OracleForms ###\n# Compiled Form Modules\n*.fmx\n\n# Compiled Menu Modules\n*.mmx\n\n# Compiled Pre-Linked Libraries\n*.plx\n\n# End of https://www.toptal.com/developers/gitignore/api/oracleforms\n","orcad":"# Created by https://www.toptal.com/developers/gitignore/api/orcad\n# Edit at https://www.toptal.com/developers/gitignore?templates=orcad\n\n### OrCAD ###\n### Cadence Design Systm's OrCAD ###\n*.dat\n\n# End of https://www.toptal.com/developers/gitignore/api/orcad\n","osx":"# Created by https://www.toptal.com/developers/gitignore/api/osx\n# Edit at https://www.toptal.com/developers/gitignore?templates=osx\n\n### OSX ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\r\r\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# End of https://www.toptal.com/developers/gitignore/api/osx\n","otto":"# Created by https://www.toptal.com/developers/gitignore/api/otto\n# Edit at https://www.toptal.com/developers/gitignore?templates=otto\n\n### Otto ###\n.otto/\n\n# End of https://www.toptal.com/developers/gitignore/api/otto\n","oxideshop":"# Created by https://www.toptal.com/developers/gitignore/api/oxideshop\n# Edit at https://www.toptal.com/developers/gitignore?templates=oxideshop\n\n### OxidEshop ###\n/vendor\n\n!/source\n/source/*\n/source/tmp\n\n!/source/Application\n/source/Application/translations\n/source/Application/views/admin\n/source/Application/views/azure\n/source/Application/views/flow\n\n!/source/modules\n/source/modules/*\n\n!/source/out\n/source/out/admin\n/source/out/azure\n/source/out/downloads\n/source/out/flow\n/source/out/media\n/source/out/pictures\n\n# End of https://www.toptal.com/developers/gitignore/api/oxideshop\n","oxygenxmleditor":"# Created by https://www.toptal.com/developers/gitignore/api/oxygenxmleditor\n# Edit at https://www.toptal.com/developers/gitignore?templates=oxygenxmleditor\n\n### oXygenXMLEditor ###\n# oXygen XML Editor by SyncRO Soft SRL\n# https://www.oxygenxml.com\n/**/out/\n\n# End of https://www.toptal.com/developers/gitignore/api/oxygenxmleditor\n","packer":"# Created by https://www.toptal.com/developers/gitignore/api/packer\n# Edit at https://www.toptal.com/developers/gitignore?templates=packer\n\n### Packer ###\n# Cache objects\npacker_cache/\n\n# Crash log\ncrash.log\n\n# https://www.packer.io/guides/hcl/variables\n# Exclude all .pkrvars.hcl files, which are likely to contain sensitive data,\n# such as password, private keys, and other secrets. These should not be part of\n# version control as they are data points which are potentially sensitive and\n# subject to change depending on the environment.\n#\n*.pkrvars.hcl\n\n# For built boxes\n*.box\n\n### Packer Patch ###\n# ignore temporary output files\noutput-*/\n\n# End of https://www.toptal.com/developers/gitignore/api/packer\n","pants":"# Created by https://www.toptal.com/developers/gitignore/api/pants\n# Edit at https://www.toptal.com/developers/gitignore?templates=pants\n\n### Pants ###\n# Pants workspace files\n/.pants.*\n/dist/\n/.pids\n\n# End of https://www.toptal.com/developers/gitignore/api/pants\n","particle":"# Created by https://www.toptal.com/developers/gitignore/api/particle\n# Edit at https://www.toptal.com/developers/gitignore?templates=particle\n\n### Particle ###\n# Compiled output #\n\n###################\n\n*.bin\n\n# End of https://www.toptal.com/developers/gitignore/api/particle\n","patch":"# Created by https://www.toptal.com/developers/gitignore/api/patch\n# Edit at https://www.toptal.com/developers/gitignore?templates=patch\n\n### Patch ###\n*.orig\n*.rej\n\n# End of https://www.toptal.com/developers/gitignore/api/patch\n","pawn":"# Created by https://www.toptal.com/developers/gitignore/api/pawn\n# Edit at https://www.toptal.com/developers/gitignore?templates=pawn\n\n### PAWN ###\n# Compiled Bytecode, precompiled output and assembly\n*.amx\n*.lst\n*.asm\n\n# Vendor directory for dependencies\ndependencies/\n\n# Dependency versions lockfile\npawn.lock\n\n# IDE setting files\nsettings.ini\n\n# End of https://www.toptal.com/developers/gitignore/api/pawn\n","perl":"# Created by https://www.toptal.com/developers/gitignore/api/perl\n# Edit at https://www.toptal.com/developers/gitignore?templates=perl\n\n### Perl ###\n!Build/\n.last_cover_stats\n/META.yml\n/META.json\n/MYMETA.*\n*.o\n*.pm.tdy\n*.bs\n\n# Devel::Cover\ncover_db/\n\n# Devel::NYTProf\nnytprof.out\n\n# Dist::Zilla\n/.build/\n\n# Module::Build\n_build/\nBuild\nBuild.bat\n\n# Module::Install\ninc/\n\n# ExtUtils::MakeMaker\n/blib/\n/_eumm/\n/*.gz\n/Makefile\n/Makefile.old\n/MANIFEST.bak\n/pm_to_blib\n/*.zip\n\n# Carton\nlocal/\n\n# End of https://www.toptal.com/developers/gitignore/api/perl\n","perl6":"# Created by https://www.toptal.com/developers/gitignore/api/perl6\n# Edit at https://www.toptal.com/developers/gitignore?templates=perl6\n\n### Perl6 ###\n# Gitignore for Perl 6 (http://www.perl6.org)\n# As part of https://github.com/github/gitignore\n\n# precompiled files\n.precomp\nlib/.precomp\n\n# End of https://www.toptal.com/developers/gitignore/api/perl6\n","ph7cms":"# Created by https://www.toptal.com/developers/gitignore/api/ph7cms\n# Edit at https://www.toptal.com/developers/gitignore?templates=ph7cms\n\n### pH7CMS ###\n_test/*\npublic/_repository/upgrade/*\npublic/_install/_license-key.txt\npublic/_install/data/caches/smarty_cache/*\npublic/_install/data/caches/smarty_compile/*\npublic/data/*\npublic/_constants.php\n_protected/app/configs/config.ini\n_protected/app/system/core/assets/cron/_delay/\n_protected/data/tmp/\n_protected/data/backup/*\n_protected/data/cache/pH7_cache/*\n_protected/data/cache/pH7_static/*\n_protected/data/cache/pH7tpl_cache/*\n_protected/data/cache/pH7tpl_compile/*\n*.log\n*.tmp\n\n## Gettext ##\n*.mo\n\n# End of https://www.toptal.com/developers/gitignore/api/ph7cms\n","phalcon":"# Created by https://www.toptal.com/developers/gitignore/api/phalcon\n# Edit at https://www.toptal.com/developers/gitignore?templates=phalcon\n\n### Phalcon ###\n/cache/\n/config/development/\n\n# End of https://www.toptal.com/developers/gitignore/api/phalcon\n","phoenix":"# Created by https://www.toptal.com/developers/gitignore/api/phoenix\n# Edit at https://www.toptal.com/developers/gitignore?templates=phoenix\n\n### Phoenix ###\n# gitignore template for Phoenix projects\n# website: http://www.phoenixframework.org/\n#\n# Recommended template: Elixir.gitignore\n\n# Temporary files\n/tmp\n\n# Static artifacts\n/node_modules\n/assets/node_modules\n\n# Since we are building assets from web/static,\n# we ignore priv/static. You may want to comment\n# this depending on your deployment strategy.\n/priv/static/\n\n# Installer-related files\n/installer/_build\n/installer/tmp\n/installer/doc\n/installer/deps\n\n# End of https://www.toptal.com/developers/gitignore/api/phoenix\n","php-cs-fixer":"# Created by https://www.toptal.com/developers/gitignore/api/php-cs-fixer\n# Edit at https://www.toptal.com/developers/gitignore?templates=php-cs-fixer\n\n### PHP-CS-Fixer ###\n# Covers PHP CS Fixer\n# Reference: https://cs.symfony.com/\n\n# Generated files\n.php-cs-fixer.cache\n\n# Local config See: https://cs.symfony.com/doc/config.html\n.php-cs-fixer.php\n\n# End of https://www.toptal.com/developers/gitignore/api/php-cs-fixer\n","phpcodesniffer":"# Created by https://www.toptal.com/developers/gitignore/api/phpcodesniffer\n# Edit at https://www.toptal.com/developers/gitignore?templates=phpcodesniffer\n\n### PHPCodeSniffer ###\n# CodeSniffer\nphpcs.xml\n\n/vendor/*\n/wpcs/*\n\n# End of https://www.toptal.com/developers/gitignore/api/phpcodesniffer\n","phpstorm":"# Created by https://www.toptal.com/developers/gitignore/api/phpstorm\n# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm\n\n### PhpStorm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PhpStorm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/phpstorm\n","phpstorm+all":"# Created by https://www.toptal.com/developers/gitignore/api/phpstorm+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+all\n\n### PhpStorm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PhpStorm+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/phpstorm+all\n","phpstorm+iml":"# Created by https://www.toptal.com/developers/gitignore/api/phpstorm+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=phpstorm+iml\n\n### PhpStorm+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PhpStorm+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/phpstorm+iml\n","phpunit":"# Created by https://www.toptal.com/developers/gitignore/api/phpunit\n# Edit at https://www.toptal.com/developers/gitignore?templates=phpunit\n\n### PHPUnit ###\n# Covers PHPUnit\n# Reference: https://phpunit.de/\n\n# Generated files\n.phpunit.result.cache\n.phpunit.cache\n\n# PHPUnit\n/app/phpunit.xml\n/phpunit.xml\n\n# Build data\n/build/\n\n# End of https://www.toptal.com/developers/gitignore/api/phpunit\n","pico8":"# Created by https://www.toptal.com/developers/gitignore/api/pico8\n# Edit at https://www.toptal.com/developers/gitignore?templates=pico8\n\n### pico8 ###\n# exported file types\n*.p8.png\n*.png\n*.wav\n*.js\n*.html\n*.bin\n\n# demos folder\ndemos/\n\n# End of https://www.toptal.com/developers/gitignore/api/pico8\n","pimcore":"# Created by https://www.toptal.com/developers/gitignore/api/pimcore\n# Edit at https://www.toptal.com/developers/gitignore?templates=pimcore\n\n### Pimcore ###\n# gitignore template for Pimcore CMS\n\n# pimcore source files\n/pimcore\n\n# asset files\n/website/var/assets/*\n\n# backups\n/website/var/backup/*\n\n# file cache\n/website/var/cache/*\n\n# generated PHP classes, keep definition files (.psf)\n/website/var/classes/Object*\n!/website/var/classes/objectbricks\n\n# various configuration files\n/website/var/config/system.xml\n/website/var/config/cache.xml\n/website/var/config/robots.txt\n/website/var/config/Geo*\n/website/var/config/object/*\n/website/var/config/portal/*\n/website/var/config/sqlreport/*\n\n# sent e-mail log files\n/website/var/email/*\n\n# log files\n/website/var/log/*.log\n\n# serialized recyclebin files\n/website/var/recyclebin/*\n\n# search plugin\n/website/var/search/*\n\n# various temp files\n/website/var/system/*\n/website/var/tmp/*\n\n# serialized version files\n/website/var/versions/asset/*\n/website/var/versions/document/*\n/website/var/versions/object/*\n\n# user profile images\n/website/var/user-image/*\n\n# keep .dummy files\n!.dummy\n\n# End of https://www.toptal.com/developers/gitignore/api/pimcore\n","pimcore4":"# Created by https://www.toptal.com/developers/gitignore/api/pimcore4\n# Edit at https://www.toptal.com/developers/gitignore?templates=pimcore4\n\n### Pimcore4 ###\n# pimcore source files\n/pimcore\n\n# asset files\n/website/var/assets/*\n\n# backups\n/website/var/backup/*\n\n# file cache\n/website/var/cache/*\n\n# generated PHP classes, keep definition files (.psf)\n/website/var/classes/Object*\n!/website/var/classes/objectbricks\n\n# various configuration files\n/website/var/config/system.xml\n/website/var/config/cache.xml\n/website/var/config/robots.txt\n/website/var/config/Geo*\n/website/var/config/object/*\n/website/var/config/portal/*\n/website/var/config/sqlreport/*\n\n# sent e-mail log files\n/website/var/email/*\n\n# log files\n/website/var/log/*.log\n\n# serialized recyclebin files\n/website/var/recyclebin/*\n\n# search plugin\n/website/var/search/*\n\n# various temp files\n/website/var/system/*\n/website/var/tmp/*\n\n# serialized version files\n/website/var/versions/asset/*\n/website/var/versions/document/*\n/website/var/versions/object/*\n\n# user profile images\n/website/var/user-image/*\n\n# keep .dummy files\n!.dummy\n\n# End of https://www.toptal.com/developers/gitignore/api/pimcore4\n","pimcore5":"# Created by https://www.toptal.com/developers/gitignore/api/pimcore5\n# Edit at https://www.toptal.com/developers/gitignore?templates=pimcore5\n\n### Pimcore5 ###\n/var/*\n\n# keep directories\n!/var/cache/.gitkeep\n!/var/logs/.gitkeep\n!/var/sessions/.gitkeep\n\n# keep generated classes in Git to have auto-completion\n!/var/classes\n\n# keep configuration\n!/var/config/\n\n# Auto-generated by bin/console assets:install\n/web/bundles/\n/web/var/\n\n!/var/SymfonyRequirements.php\n\n# End of https://www.toptal.com/developers/gitignore/api/pimcore5\n","pinegrow":"# Created by https://www.toptal.com/developers/gitignore/api/pinegrow\n# Edit at https://www.toptal.com/developers/gitignore?templates=pinegrow\n\n### PineGrow ###\n#Pinegrow backup files\n_pgbackup/*\n\n#Pinegrow setting file\npinegrow.json\n\n# End of https://www.toptal.com/developers/gitignore/api/pinegrow\n","platformio":"# Created by https://www.toptal.com/developers/gitignore/api/platformio\n# Edit at https://www.toptal.com/developers/gitignore?templates=platformio\n\n### PlatformIO ###\n.pioenvs\n.piolibdeps\n.clang_complete\n.gcc-flags.json\n.pio\n\n# End of https://www.toptal.com/developers/gitignore/api/platformio\n","playframework":"# Created by https://www.toptal.com/developers/gitignore/api/playframework\n# Edit at https://www.toptal.com/developers/gitignore?templates=playframework\n\n### PlayFramework ###\n# Ignore Play! working directory #\nbin/\n/db\n.eclipse\n/lib/\n/logs/\n/modules\n/project/project\n/project/target\n/target\ntmp/\ntest-result\nserver.pid\n*.eml\n/dist/\n.cache\n\n# End of https://www.toptal.com/developers/gitignore/api/playframework\n","plone":"# Created by https://www.toptal.com/developers/gitignore/api/plone\n# Edit at https://www.toptal.com/developers/gitignore?templates=plone\n\n### Plone ###\n*.pyc\n*.pyo\n*.tmp*\n*.mo\n*.egg\n*.EGG\n*.egg-info\n*.EGG-INFO\n.*.cfg\nbin/\nbuild/\ndevelop-eggs/\ndownloads/\neggs/\nfake-eggs/\nparts/\ndist/\nvar/\n\n# End of https://www.toptal.com/developers/gitignore/api/plone\n","polymer":"# Created by https://www.toptal.com/developers/gitignore/api/polymer\n# Edit at https://www.toptal.com/developers/gitignore?templates=polymer\n\n### Polymer ###\n#Installed components via bower\nbower_components/\n\n#Installed modules via npm\nnode_modules/\n\n#Polymer-cli builds\nbuild/\n\n# End of https://www.toptal.com/developers/gitignore/api/polymer\n","powershell":"# Created by https://www.toptal.com/developers/gitignore/api/powershell\n# Edit at https://www.toptal.com/developers/gitignore?templates=powershell\n\n### PowerShell ###\n# Exclude packaged modules\n*.zip\n\n# Exclude .NET assemblies from source\n*.dll\n\n# End of https://www.toptal.com/developers/gitignore/api/powershell\n","premake-gmake":"# Created by https://www.toptal.com/developers/gitignore/api/premake-gmake\n# Edit at https://www.toptal.com/developers/gitignore?templates=premake-gmake\n\n### premake-gmake ###\nMakefile\n*.make\nobj/\n\n# End of https://www.toptal.com/developers/gitignore/api/premake-gmake\n","prepros":"# Created by https://www.toptal.com/developers/gitignore/api/prepros\n# Edit at https://www.toptal.com/developers/gitignore?templates=prepros\n\n### Prepros ###\n# Config Prepros files to ignore\nprepros.cfg\nprepros-6.config\nprepros.config\n\n# End of https://www.toptal.com/developers/gitignore/api/prepros\n","prestashop":"# Created by https://www.toptal.com/developers/gitignore/api/prestashop\n# Edit at https://www.toptal.com/developers/gitignore?templates=prestashop\n\n### Prestashop ###\n# Cache, temp and personal files\n\n/.htaccess\n*.log\n\n# Cache\n/cache/*\n!/cache/.htaccess\n!/cache/cachefs/index.php\n!/cache/deprecated.txt\n!/cache/index.php\n!/cache/purifier/index.php\n!/cache/push/activity\n!/cache/push/index.php\n!/cache/push/trends\n!/cache/sandbox/index.php\n!/cache/smarty/cache/index.php\n!/cache/smarty/compile/index.php\n!/cache/smarty/index.php\n!/cache/tcpdf/index.php\n\n# Download\n/download/*\n!/download/.htaccess\n!/download/index.php\n\n# Images\n/img/*\n!/img/.htaccess\n!/img/index.php\n!/img/404.gif\n!/img/bg_500.png\n!/img/bg_loader.png\n!/img/favicon.ico\n!/img/loader.gif\n!/img/loadingAnimation.gif\n!/img/logo.jpg\n!/img/logo.png\n!/img/logo_invoice.jpg\n!/img/logo_stores.png\n!/img/macFFBgHack.png\n!/img/prestashop-avatar.png\n!/img/prestashop@2x.png\n!/img/preston-login-wink@2x.png\n!/img/preston-login@2x.png\n!/img/questionmark.png\n!/img/genders/index.php\n!/img/admin/index.php\n!/img/c/index.php\n!/img/cms/index.php\n!/img/co/index.php\n!/img/jquery-ui\n!/img/l/index.php\n!/img/m/index.php\n!/img/os/index.php\n!/img/p/index.php\n!/img/s/index.php\n!/img/scenes\n!/img/st/index.php\n!/img/su/index.php\n!/img/t/index.php\n!/img/tmp/index.php\n\n# Upload\n/upload/*\n!/upload/.htaccess\n\n/vendor/*\n/docs/phpdoc-sf/\n/composer.lock\n*.hot-update.js\n*.hot-update.json\n\n\n/admin-dev/autoupgrade/*\n!/admin-dev/autoupgrade/index.php\n!/admin-dev/autoupgrade/backup/index.php\n\n/admin-dev/backups/*\n!/admin-dev/backups/.htaccess\n\n/admin-dev/import/*\n!/admin-dev/import/.htaccess\n!/admin-dev/import/index.php\n\n/admin-dev/export/*\n!/admin-dev/export/.htaccess\n!/admin-dev/export/index.php\n\n# Downloaded RTL files\n/admin-dev/themes/default/css/bundle/default_rtl.css\n/admin-dev/themes/default/css/bundle/shared_rtl.css\n/admin-dev/themes/default/css/font_rtl.css\n/admin-dev/themes/default/css/overrides_rtl.css\n/admin-dev/themes/default/css/vendor/font-awesome/font-awesome_rtl.css\n/admin-dev/themes/default/css/vendor/nv.d3_rtl.css\n/admin-dev/themes/default/css/vendor/titatoggle-min_rtl.css\n/admin-dev/themes/default/public/theme_rtl.css\n/admin-dev/themes/new-theme/css/module/drop_rtl.css\n/admin-dev/themes/new-theme/css/right-sidebar_rtl.css\n\nthemes/*/cache/*\n\n# Config\n\nconfig/settings.inc.php\nconfig/settings.old.php\nconfig/xml/*\nconfig/themes/*\n!config/xml/themes/default.xml\nthemes/*/config/settings_*.json\napp/config/parameters.old.yml\napp/config/config.php\n\n# Themes, modules and overrides\n\nmodules/*\noverride/*\nthemes/*/\n!themes/classic\n!themes/_core\n!themes/_libraries\n\n# Vendors and dependencies\n\nbower_components/\nnode_modules/\ncomposer.phar\nphp-cs-fixer\n.grunt/*\n\n# Translations and emails templates\n\ntranslations/*\nmails/*\n!mails/themes/\n!mails/_partials/\nthemes/default-bootstrap/lang/*\nthemes/default-bootstrap/modules/*/translations/*.php\nthemes/default-bootstrap/mails/*\n!themes/default-bootstrap/mails/en/\nthemes/default-bootstrap/modules/*/mails/*\n!themes/default-bootstrap/modules/*/mails/en\n\n# MISC\n\n*sitemap.xml\n/robots.txt\n\n# Symfony\n\n/bin/\n/app/Resources/geoip/GeoLite2-City.mmdb\n/app/Resources/translations/*\n!/app/Resources/translations/default\n/app/config/parameters.yml\n/app/config/parameters.php\n/build/\n/phpunit.xml\n/var/*\n!/var/cache\n/var/cache/*\n!var/cache/.gitkeep\n!/var/logs\n/var/logs/*\n!var/logs/.gitkeep\n!/var/sessions\n/var/sessions/*\n!var/sessions/.gitkeep\n!var/SymfonyRequirements.php\n/vendor/\n/web/bundles/\n\n\n# End of https://www.toptal.com/developers/gitignore/api/prestashop\n","processing":"# Created by https://www.toptal.com/developers/gitignore/api/processing\n# Edit at https://www.toptal.com/developers/gitignore?templates=processing\n\n### Processing ###\n.DS_Store\napplet\napplication.linux-arm64\napplication.linux-armv6hf\napplication.linux32\napplication.linux64\napplication.windows32\napplication.windows64\napplication.macosx\nout\n\n# End of https://www.toptal.com/developers/gitignore/api/processing\n","progressabl":"# Created by https://www.toptal.com/developers/gitignore/api/progressabl\n# Edit at https://www.toptal.com/developers/gitignore?templates=progressabl\n\n### ProgressABL ###\n# Developer Studio\n/.dbconnection\n/.project\n/.propath\n/debugger.pref\n/protrace*\n*.r\n*.pl\n\n# DataDigger\nDataDigger/Cache\nDataDigger/backup\nDataDigger/DataDigger-*.ini\nDataDigger/DataDigger*.log\n\n# General folders\n/old/\n/debug/\n\n# General files\n.gitignore\n*.tmp\n*.bak\n*.bck\n*.old\n*- Copy.*\n*- Copy (*).*\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/progressabl\n","psoccreator":"# Created by https://www.toptal.com/developers/gitignore/api/psoccreator\n# Edit at https://www.toptal.com/developers/gitignore?templates=psoccreator\n\n### PSoCCreator ###\n# Project Settings\n*.cywrk.*\n*.cyprj.*\n\n# Generated Assets and Resources\nDebug/\nRelease/\nExport/\n*/codegentemp\n*/Generated_Source\n*_datasheet.pdf\n*_timing.html\n*.cycdx\n*.cyfit\n*.rpt\n*.svd\n*.log\n*.zip\n\n# End of https://www.toptal.com/developers/gitignore/api/psoccreator\n","pulumi":"# Created by https://www.toptal.com/developers/gitignore/api/pulumi\n# Edit at https://www.toptal.com/developers/gitignore?templates=pulumi\n\n### Pulumi ###\n# Ignore temp build directory for Pulumi\n# Info: https://www.pulumi.com/docs/\n\n.pulumi/\n\n# End of https://www.toptal.com/developers/gitignore/api/pulumi\n","pulumi+stacks":"# Created by https://www.toptal.com/developers/gitignore/api/pulumi+stacks\n# Edit at https://www.toptal.com/developers/gitignore?templates=pulumi+stacks\n\n### Pulumi+stacks ###\n# Ignore temp build directory for Pulumi\n# Info: https://www.pulumi.com/docs/\n\n.pulumi/\n\n### Pulumi+stacks Patch ###\n# Pulumi stacks configuration files\n# See https://www.pulumi.com/docs/intro/concepts/project/#stack-settings-file\n\nPulumi.*.yaml\n\n# End of https://www.toptal.com/developers/gitignore/api/pulumi+stacks\n","puppet":"# Created by https://www.toptal.com/developers/gitignore/api/puppet\n# Edit at https://www.toptal.com/developers/gitignore?templates=puppet\n\n### Puppet ###\n# gitignore template for Puppet modules\n# website: https://forge.puppet.com/\n\n# Built packages\npkg/*\n\n# Should run on multiple platforms so don't check in\nGemfile.lock\n\n# Tests\nspec/fixtures/*\ncoverage/*\n\n# Third-party\nvendor/*\n.bundle/*\n\n# End of https://www.toptal.com/developers/gitignore/api/puppet\n","puppet-librarian":"# Created by https://www.toptal.com/developers/gitignore/api/puppet-librarian\n# Edit at https://www.toptal.com/developers/gitignore?templates=puppet-librarian\n\n### puppet-librarian ###\n# puppet-librarian - https://github.com/rodjek/librarian-puppet\n.tmp/\nmodules/\n\n# End of https://www.toptal.com/developers/gitignore/api/puppet-librarian\n","purebasic":"# Created by https://www.toptal.com/developers/gitignore/api/purebasic\n# Edit at https://www.toptal.com/developers/gitignore?templates=purebasic\n\n### PureBasic ###\n# PureBasic\n\n# Settings files\n*.pb.cfg\n*.pbi.cfg\nproject.cfg\n\n# Shared libraries\n*.so\n*.dylib\n*.dll\n\n# Binary executables\n*.exe\n*.app\n*.out\n\n# End of https://www.toptal.com/developers/gitignore/api/purebasic\n","purescript":"# Created by https://www.toptal.com/developers/gitignore/api/purescript\n# Edit at https://www.toptal.com/developers/gitignore?templates=purescript\n\n### PureScript ###\n# Dependencies\n.psci_modules\n.spago\nbower_components\nnode_modules\n\n# Generated files\n.psci\noutput\n\n# End of https://www.toptal.com/developers/gitignore/api/purescript\n","putty":"# Created by https://www.toptal.com/developers/gitignore/api/putty\n# Edit at https://www.toptal.com/developers/gitignore?templates=putty\n\n### PuTTY ###\n# Private key\n*.ppk\n\n# End of https://www.toptal.com/developers/gitignore/api/putty\n","pvs":"# Created by https://www.toptal.com/developers/gitignore/api/pvs\n# Edit at https://www.toptal.com/developers/gitignore?templates=pvs\n\n### PVS ###\n# Ignore binary files\npvsbin/\nbin/\nlib/\n*.bin\n.pvscontext\n# Ignore orphaned and old proofs\norphaned-proofs.prf\n*.prf~\n# Ignore compiled Lisp files\n*fasl\n# Ignore PVS dump files\n*.dmp\n# Ignore PVS log files\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/pvs\n","pycharm":"# Created by https://www.toptal.com/developers/gitignore/api/pycharm\n# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm\n\n### PyCharm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PyCharm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/pycharm\n","pycharm+all":"# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all\n\n### PyCharm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PyCharm+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/pycharm+all\n","pycharm+iml":"# Created by https://www.toptal.com/developers/gitignore/api/pycharm+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+iml\n\n### PyCharm+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### PyCharm+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/pycharm+iml\n","pydev":"# Created by https://www.toptal.com/developers/gitignore/api/pydev\n# Edit at https://www.toptal.com/developers/gitignore?templates=pydev\n\n### pydev ###\n.pydevproject\n\n# End of https://www.toptal.com/developers/gitignore/api/pydev\n","python":"# Created by https://www.toptal.com/developers/gitignore/api/python\n# Edit at https://www.toptal.com/developers/gitignore?templates=python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n# Usually these files are written by a python script from a template\n# before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n# This is especially recommended for binary packages to ensure reproducibility, and is more\n# commonly ignored for libraries.\n# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n# in version control.\n# https://pdm.fming.dev/#use-with-ide\n.pdm.toml\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n# JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n# and can be added to the global gitignore or merged into this file. For a more nuclear\n# option (not recommended) you can uncomment the following to ignore the entire idea folder.\n#.idea/\n\n### Python Patch ###\n# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration\npoetry.toml\n\n# ruff\n.ruff_cache/\n\n# LSP config files\npyrightconfig.json\n\n# End of https://www.toptal.com/developers/gitignore/api/python\n","pythonvanilla":"# Created by https://www.toptal.com/developers/gitignore/api/pythonvanilla\n# Edit at https://www.toptal.com/developers/gitignore?templates=pythonvanilla\n\n### PythonVanilla ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# pyenv\n# For a library or package, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# .python-version\n\n# pipenv\n# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n# However, in case of collaboration, if having platform-specific dependencies or dependencies\n# having no cross-platform support, pipenv may install dependencies that don't work, or not\n# install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n\n# End of https://www.toptal.com/developers/gitignore/api/pythonvanilla\n","qml":"# Created by https://www.toptal.com/developers/gitignore/api/qml\n# Edit at https://www.toptal.com/developers/gitignore?templates=qml\n\n### QML ###\n# Cached binary representations of QML and JS files\n*.qmlc\n*.jsc\n\n# End of https://www.toptal.com/developers/gitignore/api/qml\n","qooxdoo":"# Created by https://www.toptal.com/developers/gitignore/api/qooxdoo\n# Edit at https://www.toptal.com/developers/gitignore?templates=qooxdoo\n\n### Qooxdoo ###\ncache\ncache-downloads\ninspector\napi\nsource/inspector.html\n\n# End of https://www.toptal.com/developers/gitignore/api/qooxdoo\n","qt":"# Created by https://www.toptal.com/developers/gitignore/api/qt\n# Edit at https://www.toptal.com/developers/gitignore?templates=qt\n\n### Qt ###\n# C++ objects and libs\n*.slo\n*.lo\n*.o\n*.a\n*.la\n*.lai\n*.so\n*.so.*\n*.dll\n*.dylib\n\n# Qt-es\nobject_script.*.Release\nobject_script.*.Debug\n*_plugin_import.cpp\n/.qmake.cache\n/.qmake.stash\n*.pro.user\n*.pro.user.*\n*.qbs.user\n*.qbs.user.*\n*.moc\nmoc_*.cpp\nmoc_*.h\nqrc_*.cpp\nui_*.h\n*.qmlc\n*.jsc\nMakefile*\n*build-*\n*.qm\n*.prl\n\n# Qt unit tests\ntarget_wrapper.*\n\n# QtCreator\n*.autosave\n\n# QtCreator Qml\n*.qmlproject.user\n*.qmlproject.user.*\n\n# QtCreator CMake\nCMakeLists.txt.user*\n\n# QtCreator 4.8< compilation database\ncompile_commands.json\n\n# QtCreator local machine specific files for imported projects\n*creator.user*\n\n*_qmlcache.qrc\n\n# End of https://www.toptal.com/developers/gitignore/api/qt\n","qtcreator":"# Created by https://www.toptal.com/developers/gitignore/api/qtcreator\n# Edit at https://www.toptal.com/developers/gitignore?templates=qtcreator\n\n### QtCreator ###\n# gitignore for Qt Creator like IDE for pure C/C++ project without Qt\n#\n# Reference: http://doc.qt.io/qtcreator/creator-project-generic.html\n\n\n\n# Qt Creator autogenerated files\n\n\n# A listing of all the files included in the project\n*.files\n\n# Include directories\n*.includes\n\n# Project configuration settings like predefined Macros\n*.config\n\n# Qt Creator settings\n*.creator\n\n# User project settings\n*.creator.user*\n\n# Qt Creator backups\n*.autosave\n\n# Flags for Clang Code Model\n*.cxxflags\n*.cflags\n\n\n# End of https://www.toptal.com/developers/gitignore/api/qtcreator\n","r":"# Created by https://www.toptal.com/developers/gitignore/api/r\n# Edit at https://www.toptal.com/developers/gitignore?templates=r\n\n### R ###\n# History files\n.Rhistory\n.Rapp.history\n\n# Session Data files\n.RData\n.RDataTmp\n\n# User-specific files\n.Ruserdata\n\n# Example code in package build process\n*-Ex.R\n\n# Output files from R CMD build\n/*.tar.gz\n\n# Output files from R CMD check\n/*.Rcheck/\n\n# RStudio files\n.Rproj.user/\n\n# produced vignettes\nvignettes/*.html\nvignettes/*.pdf\n\n# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3\n.httr-oauth\n\n# knitr and R markdown default cache directories\n*_cache/\n/cache/\n\n# Temporary files created by R markdown\n*.utf8.md\n*.knit.md\n\n# R Environment Variables\n.Renviron\n\n# pkgdown site\ndocs/\n\n# translation temp files\npo/*~\n\n# RStudio Connect folder\nrsconnect/\n\n### R.Bookdown Stack ###\n# R package: bookdown caching files\n/*_files/\n\n# End of https://www.toptal.com/developers/gitignore/api/r\n","racket":"# Created by https://www.toptal.com/developers/gitignore/api/racket\n# Edit at https://www.toptal.com/developers/gitignore?templates=racket\n\n### Racket ###\n# gitignore template for the Racket language\n# website: http://www.racket-lang.org/\n\n# DrRacket autosave files\n*.rkt~\n*.rkt.bak\n\\#*.rkt#\n\\#*.rkt#*#\n\n# Compiled racket bytecode\ncompiled/\n*.zo\n\n# Dependency tracking files\n*.dep\n\n# End of https://www.toptal.com/developers/gitignore/api/racket\n","rails":"# Created by https://www.toptal.com/developers/gitignore/api/rails\n# Edit at https://www.toptal.com/developers/gitignore?templates=rails\n\n### Rails ###\n*.rbc\ncapybara-*.html\n.rspec\n/db/*.sqlite3\n/db/*.sqlite3-journal\n/db/*.sqlite3-[0-9]*\n/public/system\n/coverage/\n/spec/tmp\n*.orig\nrerun.txt\npickle-email-*.html\n\n# Ignore all logfiles and tempfiles.\n/log/*\n/tmp/*\n!/log/.keep\n!/tmp/.keep\n\n# TODO Comment out this rule if you are OK with secrets being uploaded to the repo\nconfig/initializers/secret_token.rb\nconfig/master.key\n\n# Only include if you have production secrets in this file, which is no longer a Rails default\n# config/secrets.yml\n\n# dotenv, dotenv-rails\n# TODO Comment out these rules if environment variables can be committed\n.env\n.env*.local\n\n## Environment normalization:\n/.bundle\n/vendor/bundle\n\n# these should all be checked in to normalize the environment:\n# Gemfile.lock, .ruby-version, .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n\n# if using bower-rails ignore default bower_components path bower.json files\n/vendor/assets/bower_components\n*.bowerrc\nbower.json\n\n# Ignore pow environment settings\n.powenv\n\n# Ignore Byebug command history file.\n.byebug_history\n\n# Ignore node_modules\nnode_modules/\n\n# Ignore precompiled javascript packs\n/public/packs\n/public/packs-test\n/public/assets\n\n# Ignore yarn files\n/yarn-error.log\nyarn-debug.log*\n.yarn-integrity\n\n# Ignore uploaded files in development\n/storage/*\n!/storage/.keep\n/public/uploads\n\n# End of https://www.toptal.com/developers/gitignore/api/rails\n","react":"# Created by https://www.toptal.com/developers/gitignore/api/react\n# Edit at https://www.toptal.com/developers/gitignore?templates=react\n\n### react ###\n.DS_*\n*.log\nlogs\n**/*.backup.*\n**/*.back.*\n\nnode_modules\nbower_components\n\n*.sublime*\n\npsd\nthumb\nsketch\n\n# End of https://www.toptal.com/developers/gitignore/api/react\n","reactnative":"# Created by https://www.toptal.com/developers/gitignore/api/reactnative\n# Edit at https://www.toptal.com/developers/gitignore?templates=reactnative\n\n### ReactNative ###\n# React Native Stack Base\n\n.expo\n__generated__\n\n### ReactNative.Buck Stack ###\nbuck-out/\n.buckconfig.local\n.buckd/\n.buckversion\n.fakebuckversion\n\n### ReactNative.Node Stack ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n### ReactNative.Gradle Stack ###\n.gradle\n**/build/\n!src/**/build/\n\n# Ignore Gradle GUI config\ngradle-app.setting\n\n# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)\n!gradle-wrapper.jar\n\n# Avoid ignore Gradle wrappper properties\n!gradle-wrapper.properties\n\n# Cache of project\n.gradletasknamecache\n\n# Eclipse Gradle plugin generated files\n# Eclipse Core\n.project\n# JDT-specific (Eclipse Java Development Tools)\n.classpath\n\n### ReactNative.Android Stack ###\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Log/OS Files\n\n# Android Studio generated files and folders\ncaptures/\n.externalNativeBuild/\n.cxx/\n*.apk\noutput.json\n\n# IntelliJ\n*.iml\n.idea/\nmisc.xml\ndeploymentTargetDropDown.xml\nrender.experimental.xml\n\n# Keystore files\n*.jks\n*.keystore\n\n# Google Services (e.g. APIs or Firebase)\ngoogle-services.json\n\n# Android Profiling\n*.hprof\n\n### ReactNative.Linux Stack ###\n*~\n\n# temporary files which can be created if a process still has a handle open of a deleted file\n.fuse_hidden*\n\n# KDE directory preferences\n.directory\n\n# Linux trash folder which might appear on any partition or disk\n.Trash-*\n\n# .nfs files are created when an open file is removed but is still being accessed\n.nfs*\n\n### ReactNative.Xcode Stack ###\n## User settings\nxcuserdata/\n\n## Xcode 8 and earlier\n*.xcscmblueprint\n*.xccheckout\n\n### ReactNative.macOS Stack ###\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\r\r\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# End of https://www.toptal.com/developers/gitignore/api/reactnative\n","reasonml":"# Created by https://www.toptal.com/developers/gitignore/api/reasonml\n# Edit at https://www.toptal.com/developers/gitignore?templates=reasonml\n\n### Reasonml ###\n/lib\n\n# End of https://www.toptal.com/developers/gitignore/api/reasonml\n","red":"# Created by https://www.toptal.com/developers/gitignore/api/red\n# Edit at https://www.toptal.com/developers/gitignore?templates=red\n\n### Red ###\n# gitignore template for Red programming language\n# website: http://www.red-lang.org/\n\n# Red Compiled code\n*.red\n\n# Libraries\ncrush.dll\ncrush.dylib\ncrush.so\n\n# Files generated during test\nquick-test/quick-test.log\nquick-test/runnable/\nsystem/tests/source/units/auto-tests/\ntests/source/units/auto-tests/\n\n# End of https://www.toptal.com/developers/gitignore/api/red\n","redcar":"# Created by https://www.toptal.com/developers/gitignore/api/redcar\n# Edit at https://www.toptal.com/developers/gitignore?templates=redcar\n\n### Redcar ###\n.redcar\n\n# End of https://www.toptal.com/developers/gitignore/api/redcar\n","redis":"# Created by https://www.toptal.com/developers/gitignore/api/redis\n# Edit at https://www.toptal.com/developers/gitignore?templates=redis\n\n### Redis ###\n# Ignore redis binary dump (dump.rdb) files\n\n*.rdb\n\n# End of https://www.toptal.com/developers/gitignore/api/redis\n","remix":"# Created by https://www.toptal.com/developers/gitignore/api/remix\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix\n\n### Remix ###\nnode_modules\n/.cache\n/build\n/public/build\n.env\n\n# End of https://www.toptal.com/developers/gitignore/api/remix\n","remix+arc":"# Created by https://www.toptal.com/developers/gitignore/api/remix+arc\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix+arc\n\n### Remix+Arc ###\n/server/index.js\npreferences.arc\nsam.json\nsam.yaml\n\n# End of https://www.toptal.com/developers/gitignore/api/remix+arc\n","remix+cloudflarepages":"# Created by https://www.toptal.com/developers/gitignore/api/remix+cloudflarepages\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix+cloudflarepages\n\n### Remix+CloudflarePages ###\n/functions/\\[\\[path\\]\\].js\n/functions/\\[\\[path\\]\\].js.map\n\n# End of https://www.toptal.com/developers/gitignore/api/remix+cloudflarepages\n","remix+cloudflareworkers":"# Created by https://www.toptal.com/developers/gitignore/api/remix+cloudflareworkers\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix+cloudflareworkers\n\n### Remix+CloudflareWorkers ###\n/dist\n/.mf\n\n# End of https://www.toptal.com/developers/gitignore/api/remix+cloudflareworkers\n","remix+netlify":"# Created by https://www.toptal.com/developers/gitignore/api/remix+netlify\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix+netlify\n\n### Remix+Netlify ###\n/.netlify\n\n# End of https://www.toptal.com/developers/gitignore/api/remix+netlify\n","remix+vercel":"# Created by https://www.toptal.com/developers/gitignore/api/remix+vercel\n# Edit at https://www.toptal.com/developers/gitignore?templates=remix+vercel\n\n### Remix+Vercel ###\n.vercel\n.output\n/api/index.js\n/api/index.js.map\n\n# End of https://www.toptal.com/developers/gitignore/api/remix+vercel\n","renpy":"# Created by https://www.toptal.com/developers/gitignore/api/renpy\n# Edit at https://www.toptal.com/developers/gitignore?templates=renpy\n\n### Renpy ###\n# Don't track content of these folders #\n########################################\ncache/\nsaves/\n\n\n# Compiled source #\n###################\n*.com\n*.class\n*.dll\n*.exe\n*.o\n*.so\n\n*.rpyc\n*.rpymc\n\n\n# The log file and other misc things #\n######################################\nlog.txt\nerrors.txt\ntraceback.txt\nfiles.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/renpy\n","replit":"# Created by https://www.toptal.com/developers/gitignore/api/replit\n# Edit at https://www.toptal.com/developers/gitignore?templates=replit\n\n### replit ###\n# Replit config file\n.replit\n\n# Replit Nix config file\nreplit.nix\n\n# End of https://www.toptal.com/developers/gitignore/api/replit\n","retool":"# Created by https://www.toptal.com/developers/gitignore/api/retool\n# Edit at https://www.toptal.com/developers/gitignore?templates=retool\n\n### retool ###\n_tools\n\n# End of https://www.toptal.com/developers/gitignore/api/retool\n","rhodesrhomobile":"# Created by https://www.toptal.com/developers/gitignore/api/rhodesrhomobile\n# Edit at https://www.toptal.com/developers/gitignore?templates=rhodesrhomobile\n\n### RhodesRhomobile ###\nrholog-*\nsim-*\nbin/libs\nbin/RhoBundle\nbin/tmp\nbin/target\nbin/*.ap_\n*.o\n*.jar\n\n# End of https://www.toptal.com/developers/gitignore/api/rhodesrhomobile\n","rider":"# Created by https://www.toptal.com/developers/gitignore/api/rider\n# Edit at https://www.toptal.com/developers/gitignore?templates=rider\n\n### Rider ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n# End of https://www.toptal.com/developers/gitignore/api/rider\n","robotframework":"# Created by https://www.toptal.com/developers/gitignore/api/robotframework\n# Edit at https://www.toptal.com/developers/gitignore?templates=robotframework\n\n### robotframework ###\nlog.html\noutput.xml\nreport.html\nselenium-screenshot-*.png\n\n# End of https://www.toptal.com/developers/gitignore/api/robotframework\n","root":"# Created by https://www.toptal.com/developers/gitignore/api/root\n# Edit at https://www.toptal.com/developers/gitignore?templates=root\n\n### ROOT ###\n# ROOT Home Page : https://root.cern.ch/\n# ROOT Used by Experimental Physicists, not necessarily HEP\n# ROOT based on C++\n\n# Files generated by ROOT, observed with v6.xy\n\n*.pcm\n*.so\n*.d\n\n\n\n# End of https://www.toptal.com/developers/gitignore/api/root\n","ros":"# Created by https://www.toptal.com/developers/gitignore/api/ros\n# Edit at https://www.toptal.com/developers/gitignore?templates=ros\n\n### ROS ###\ndevel/\nlogs/\nbuild/\nbin/\nlib/\nmsg_gen/\nsrv_gen/\nmsg/*Action.msg\nmsg/*ActionFeedback.msg\nmsg/*ActionGoal.msg\nmsg/*ActionResult.msg\nmsg/*Feedback.msg\nmsg/*Goal.msg\nmsg/*Result.msg\nmsg/_*.py\nbuild_isolated/\ndevel_isolated/\n\n# Generated by dynamic reconfigure\n*.cfgc\n/cfg/cpp/\n/cfg/*.py\n\n# Ignore generated docs\n*.dox\n*.wikidoc\n\n# eclipse stuff\n.project\n.cproject\n\n# qcreator stuff\nCMakeLists.txt.user\n\nsrv/_*.py\n*.pcd\n*.pyc\nqtcreator-*\n*.user\n\n/planning/cfg\n/planning/docs\n/planning/src\n\n*~\n\n# Emacs\n.#*\n\n# Catkin custom files\nCATKIN_IGNORE\n\n# End of https://www.toptal.com/developers/gitignore/api/ros\n","ros2":"# Created by https://www.toptal.com/developers/gitignore/api/ros2\n# Edit at https://www.toptal.com/developers/gitignore?templates=ros2\n\n### ROS2 ###\ninstall/\nlog/\nbuild/\n\n# Ignore generated docs\n*.dox\n*.wikidoc\n\n# eclipse stuff\n.project\n.cproject\n\n# qcreator stuff\nCMakeLists.txt.user\n\nsrv/_*.py\n*.pcd\n*.pyc\nqtcreator-*\n*.user\n\n*~\n\n# Emacs\n.#*\n\n# Colcon custom files\nCOLCON_IGNORE\nAMENT_IGNORE\n\n# End of https://www.toptal.com/developers/gitignore/api/ros2\n","ruby":"# Created by https://www.toptal.com/developers/gitignore/api/ruby\n# Edit at https://www.toptal.com/developers/gitignore?templates=ruby\n\n### Ruby ###\n*.gem\n*.rbc\n/.config\n/coverage/\n/InstalledFiles\n/pkg/\n/spec/reports/\n/spec/examples.txt\n/test/tmp/\n/test/version_tmp/\n/tmp/\n\n# Used by dotenv library to load environment variables.\n# .env\n\n# Ignore Byebug command history file.\n.byebug_history\n\n## Specific to RubyMotion:\n.dat*\n.repl_history\nbuild/\n*.bridgesupport\nbuild-iPhoneOS/\nbuild-iPhoneSimulator/\n\n## Specific to RubyMotion (use of CocoaPods):\n#\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n# vendor/Pods/\n\n## Documentation cache and generated files:\n/.yardoc/\n/_yardoc/\n/doc/\n/rdoc/\n\n## Environment normalization:\n/.bundle/\n/vendor/bundle\n/lib/bundler/man/\n\n# for a library or gem, you might want to ignore these files since the code is\n# intended to run in multiple environments; otherwise, check them in:\n# Gemfile.lock\n# .ruby-version\n# .ruby-gemset\n\n# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:\n.rvmrc\n\n# Used by RuboCop. Remote config files pulled in from inherit_from directive.\n# .rubocop-https?--*\n\n# End of https://www.toptal.com/developers/gitignore/api/ruby\n","rubymine":"# Created by https://www.toptal.com/developers/gitignore/api/rubymine\n# Edit at https://www.toptal.com/developers/gitignore?templates=rubymine\n\n### RubyMine ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### RubyMine Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/rubymine\n","rubymine+all":"# Created by https://www.toptal.com/developers/gitignore/api/rubymine+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=rubymine+all\n\n### RubyMine+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### RubyMine+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/rubymine+all\n","rubymine+iml":"# Created by https://www.toptal.com/developers/gitignore/api/rubymine+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=rubymine+iml\n\n### RubyMine+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### RubyMine+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/rubymine+iml\n","rust":"# Created by https://www.toptal.com/developers/gitignore/api/rust\n# Edit at https://www.toptal.com/developers/gitignore?templates=rust\n\n### Rust ###\n# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\nCargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# MSVC Windows builds of rustc generate these, which store debugging information\n*.pdb\n\n# End of https://www.toptal.com/developers/gitignore/api/rust\n","rust-analyzer":"# Created by https://www.toptal.com/developers/gitignore/api/rust-analyzer\n# Edit at https://www.toptal.com/developers/gitignore?templates=rust-analyzer\n\n### rust-analyzer ###\n# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) \nrust-project.json\n\n\n# End of https://www.toptal.com/developers/gitignore/api/rust-analyzer\n","salesforce":"# Created by https://www.toptal.com/developers/gitignore/api/salesforce\n# Edit at https://www.toptal.com/developers/gitignore?templates=salesforce\n\n### Salesforce ###\n# GitIgnore for Salesforce Projects\n# Project Settings and MetaData\n.project\n.settings/\n.metadata\nbuild.properties\nconfig\n\n# Apex Log as optional\napex-scripts/log\n\n# Eclipse specific\nsalesforce.schema\nReferenced Packages\nbin/\ntmp/\nconfig/\n*.tmp\n*.bak\nlocal.properties\n.settings\n.loadpath\n.classpath\n*.cache\n\n# Illuminated Cloud (IntelliJ IDEA)\nIlluminatedCloud\nout\n.idea\n*.iml\n\n# Mavensmate\n*.sublime-project\n*.sublime-settings\n*.sublime-workspace\nmm.log\n\n# Haoide SublimeText\n.config\n.deploy\n.history\n\n# OSX-specific exclusions\n.[dD][sS]_[sS]tore\n\n# The Welkin Suite specific\n**/.localHistory\n\n*.sfuo\n\nTestCache.xml\n\nTestsResultsCache.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/salesforce\n","salesforcedx":"# Created by https://www.toptal.com/developers/gitignore/api/salesforcedx\n# Edit at https://www.toptal.com/developers/gitignore?templates=salesforcedx\n\n### SalesforceDX ###\n# GitIgnore for Salesforce Projects\n# Project Settings and MetaData\n.project\n.settings/\n.metadata\nbuild.properties\nconfig\n\n# Apex Log as optional\napex-scripts/log\n\n# Eclipse specific\nsalesforce.schema\nReferenced Packages\nbin/\ntmp/\nconfig/\n*.tmp\n*.bak\nlocal.properties\n.settings\n.loadpath\n.classpath\n*.cache\n\n# Illuminated Cloud (IntelliJ IDEA)\nIlluminatedCloud\nout\n.idea\n*.iml\n\n# Mavensmate\n*.sublime-project\n*.sublime-settings\n*.sublime-workspace\nmm.log\n\n# Haoide SublimeText\n.config\n.deploy\n.history\n\n# OSX-specific exclusions\n.[dD][sS]_[sS]tore\n\n# The Welkin Suite specific\n**/.localHistory\n\n*.sfuo\n\nTestCache.xml\n\nTestsResultsCache.xml\n\n### SalesforceDX Patch ###\n.sfdx\n\n# End of https://www.toptal.com/developers/gitignore/api/salesforcedx\n","sam":"# Created by https://www.toptal.com/developers/gitignore/api/sam\n# Edit at https://www.toptal.com/developers/gitignore?templates=sam\n\n### SAM ###\n# Ignore build directories for the AWS Serverless Application Model (SAM)\n# Info: https://aws.amazon.com/serverless/sam/\n# Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html\n\n**/.aws-sam\n\n# End of https://www.toptal.com/developers/gitignore/api/sam\n","sam+config":"# Created by https://www.toptal.com/developers/gitignore/api/sam+config\n# Edit at https://www.toptal.com/developers/gitignore?templates=sam+config\n\n### SAM+config ###\n# Ignore build directories for the AWS Serverless Application Model (SAM)\n# Info: https://aws.amazon.com/serverless/sam/\n# Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html\n\n**/.aws-sam\n\n### SAM+config Patch ###\n# SAM config - exclude this file if sharing publicly\nsamconfig.toml\n\n# End of https://www.toptal.com/developers/gitignore/api/sam+config\n","sas":"# Created by https://www.toptal.com/developers/gitignore/api/sas\n# Edit at https://www.toptal.com/developers/gitignore?templates=sas\n\n### SAS ###\n# Binary data formats\n*.sas7baud\n*.sas7bdat\n*.sas7bvew\n*.sas7bndx\n*.sas7bcat\n*.sas7bacs\n*.sas7bfdb\n*.sas7bmdb\n*.sas7bdmd\n*.sas7bitm\n*.sas7butl\n*.sas7bput\n*.sas7bbak\n\n# End of https://www.toptal.com/developers/gitignore/api/sas\n","sass":"# Created by https://www.toptal.com/developers/gitignore/api/sass\n# Edit at https://www.toptal.com/developers/gitignore?templates=sass\n\n### Sass ###\n.sass-cache/\n*.css.map\n*.sass.map\n*.scss.map\n\n# End of https://www.toptal.com/developers/gitignore/api/sass\n","sbt":"# Created by https://www.toptal.com/developers/gitignore/api/sbt\n# Edit at https://www.toptal.com/developers/gitignore?templates=sbt\n\n### SBT ###\n# Simple Build Tool\n# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control\n\ndist/*\ntarget/\nlib_managed/\nsrc_managed/\nproject/boot/\nproject/plugins/project/\n.history\n.cache\n.lib/\n\n### SBT Patch ###\n.bsp/\n\n# End of https://www.toptal.com/developers/gitignore/api/sbt\n","scala":"# Created by https://www.toptal.com/developers/gitignore/api/scala\n# Edit at https://www.toptal.com/developers/gitignore?templates=scala\n\n### Scala ###\n*.class\n*.log\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n# End of https://www.toptal.com/developers/gitignore/api/scala\n","scheme":"# Created by https://www.toptal.com/developers/gitignore/api/scheme\n# Edit at https://www.toptal.com/developers/gitignore?templates=scheme\n\n### Scheme ###\n*.ss~\n*.ss#*\n.#*.ss\n\n*.scm~\n*.scm#*\n.#*.scm\n\n# End of https://www.toptal.com/developers/gitignore/api/scheme\n","scons":"# Created by https://www.toptal.com/developers/gitignore/api/scons\n# Edit at https://www.toptal.com/developers/gitignore?templates=scons\n\n### SCons ###\n# for projects that use SCons for building: http://http://www.scons.org/\n.sconsign.dblite\n\n# When configure fails, SCons outputs these\nconfig.log\n.sconf_temp\n\n# End of https://www.toptal.com/developers/gitignore/api/scons\n","scrivener":"# Created by https://www.toptal.com/developers/gitignore/api/scrivener\n# Edit at https://www.toptal.com/developers/gitignore?templates=scrivener\n\n### Scrivener ###\n*/Files/binder.autosave\n*/Files/binder.backup\n*/Files/search.indexes\n*/Files/user.lock\n*/Files/Docs/docs.checksum\n*/Files/Data/docs.checksum\n*/QuickLook/\n*/Settings/ui.plist\n\n# End of https://www.toptal.com/developers/gitignore/api/scrivener\n","sdcc":"# Created by https://www.toptal.com/developers/gitignore/api/sdcc\n# Edit at https://www.toptal.com/developers/gitignore?templates=sdcc\n\n### Sdcc ###\n# SDCC stuff\n*.lnk\n*.lst\n*.map\n*.mem\n*.rel\n*.rst\n*.sym\n\n# End of https://www.toptal.com/developers/gitignore/api/sdcc\n","seamgen":"# Created by https://www.toptal.com/developers/gitignore/api/seamgen\n# Edit at https://www.toptal.com/developers/gitignore?templates=seamgen\n\n### SeamGen ###\n/bootstrap/data\n/bootstrap/tmp\n/classes/\n/dist/\n/exploded-archives/\n/test-build/\n/test-output/\n/test-report/\n/target/\ntemp-testng-customsuite.xml\n\n# based on http://stackoverflow.com/a/8865858/422476 I am removing inline comments\n\n#/classes/ \t\t all class files\n#/dist/ contains generated war files for deployment\n#/exploded-archives/\t\t war content generation during deploy (or explode)\n#/test-build/ test compilation (ant target for Seam)\n#/test-output/ test results\n#/test-report/ test report generation for, e.g., Hudson\n#/target/ maven output folder\n#temp-testng-customsuite.xml\tgenerated when running test cases under Eclipse\n\n# Thanks to @VonC and @kraftan for their helpful answers on a related question\n# on StackOverflow.com:\n# http://stackoverflow.com/questions/4176687\n# /what-is-the-recommended-source-control-ignore-pattern-for-seam-projects\n\n# End of https://www.toptal.com/developers/gitignore/api/seamgen\n","senchatouch":"# Created by https://www.toptal.com/developers/gitignore/api/senchatouch\n# Edit at https://www.toptal.com/developers/gitignore?templates=senchatouch\n\n### SenchaTouch ###\n# Build and Release Folders\narchive/\nbuild/\n\n# sass cache folder\nresources/sass/.sass-cache/\n\n# End of https://www.toptal.com/developers/gitignore/api/senchatouch\n","serverless":"# Created by https://www.toptal.com/developers/gitignore/api/serverless\n# Edit at https://www.toptal.com/developers/gitignore?templates=serverless\n\n### Serverless ###\n# Ignore build directory\n.serverless\n\n# End of https://www.toptal.com/developers/gitignore/api/serverless\n","shopware":"# Created by https://www.toptal.com/developers/gitignore/api/shopware\n# Edit at https://www.toptal.com/developers/gitignore?templates=shopware\n\n### Shopware ###\n# PhpStorm-Project\n.idea/\n\n# Cache and logs\n/var/cache/*\n/var/log/*\n!var/cache/clear_cache.sh\n!var/log/clear_cache.sh\n\n/web/cache\n!web/cache/.gitkeep\n\n/bin/*\n!bin/console\n/vendor/\n\n# User provided content\n/media/archive/\n/media/image/\n/media/music/\n/media/pdf/\n/media/temp/\n/media/unknown/\n/media/video/\n\n/files/documents/\n/files/downloads/\n\n# Caches/Proxies\n/tests/Shopware/TempFiles/\n/cache/\n\n# OS generated files\n.DS_Store\n.DS_Store?\n._*\n.Spotlight-V100\n.Trashes\nIcon?\nehthumbs.db\nThumbs.db\n\n# SASS/SCSS cache\n.sass-cache/\n\n# End of https://www.toptal.com/developers/gitignore/api/shopware\n","silverstripe":"# Created by https://www.toptal.com/developers/gitignore/api/silverstripe\n# Edit at https://www.toptal.com/developers/gitignore?templates=silverstripe\n\n### Silverstripe ###\nassets/_combinedfiles/\nassets/_resampled/\nassets/Uploads/\nsilverstripe-cache/\n.env\nvendor/\nresources/\nthemes/simple/\npublic/resources/\n\n# End of https://www.toptal.com/developers/gitignore/api/silverstripe\n","sketchup":"# Created by https://www.toptal.com/developers/gitignore/api/sketchup\n# Edit at https://www.toptal.com/developers/gitignore?templates=sketchup\n\n### SketchUp ###\n*.skb\n\n# End of https://www.toptal.com/developers/gitignore/api/sketchup\n","slickedit":"# Created by https://www.toptal.com/developers/gitignore/api/slickedit\n# Edit at https://www.toptal.com/developers/gitignore?templates=slickedit\n\n### SlickEdit ###\n# SlickEdit workspace and project files are ignored by default because\n# typically they are considered to be developer-specific and not part of a\n# project.\n*.vpw\n*.vpj\n\n# SlickEdit workspace history and tag files always contain user-specific\n# data so they should not be stored in a repository.\n*.vpwhistu\n*.vpwhist\n*.vtg\n\n# End of https://www.toptal.com/developers/gitignore/api/slickedit\n","smalltalk":"# Created by https://www.toptal.com/developers/gitignore/api/smalltalk\n# Edit at https://www.toptal.com/developers/gitignore?templates=smalltalk\n\n### Smalltalk ###\n# changes file\n*.changes\n*.chg\n\n# system image\n*.image\n*.img7\n*.img\n\n# Pharo Smalltalk Debug log file\nPharoDebug.log\n\n# Squeak Smalltalk Debug log file\nSqueakDebug.log\n\n# Dolphin Smalltalk source file\n*.sml\n\n# Dolphin Smalltalk error file\n*.errors\n\n# Monticello package cache\n/package-cache\n\n# playground cache\n/play-cache\n/play-stash\n\n# Metacello-github cache\n/github-cache\ngithub-*.zip\n\n# End of https://www.toptal.com/developers/gitignore/api/smalltalk\n","snap":"# Created by https://www.toptal.com/developers/gitignore/api/snap\n# Edit at https://www.toptal.com/developers/gitignore?templates=snap\n\n### Snap ###\n# gitginore template for creating Snap packages\n# website: https://snapcraft.io/\n\nparts/\nprime/\nstage/\n*.snap\n\n# Snapcraft global state tracking data(automatically generated)\n# https://forum.snapcraft.io/t/location-to-save-global-state/768\n/snap/.snapcraft/\n\n# Source archive packed by `snapcraft cleanbuild` before pushing to the LXD container\n/*_source.tar.bz2\n\n# End of https://www.toptal.com/developers/gitignore/api/snap\n","snapcraft":"# Created by https://www.toptal.com/developers/gitignore/api/snapcraft\n# Edit at https://www.toptal.com/developers/gitignore?templates=snapcraft\n\n### Snapcraft ###\n/parts/\n/stage/\n/prime/\n/*.snap\n\n# Snapcraft global state tracking data(automatically generated)\n# https://forum.snapcraft.io/t/location-to-save-global-state/768\n/snap/.snapcraft/\n\n# Source archive packed by `snapcraft cleanbuild` before pushing to the LXD container\n/*_source.tar.bz2\n\n# End of https://www.toptal.com/developers/gitignore/api/snapcraft\n","snyk":"# Created by https://www.toptal.com/developers/gitignore/api/snyk\n# Edit at https://www.toptal.com/developers/gitignore?templates=snyk\n\n### Snyk ###\n# DeepCode\n.dccache\n\n# End of https://www.toptal.com/developers/gitignore/api/snyk\n","solidity":"# Created by https://www.toptal.com/developers/gitignore/api/solidity\n# Edit at https://www.toptal.com/developers/gitignore?templates=solidity\n\n### Solidity ###\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# End of https://www.toptal.com/developers/gitignore/api/solidity\n","soliditytruffle":"# Created by https://www.toptal.com/developers/gitignore/api/soliditytruffle\n# Edit at https://www.toptal.com/developers/gitignore?templates=soliditytruffle\n\n### SolidityTruffle ###\n# depedencies\nnode_modules\n\n# testing\ncoverage\n\n# production\nbuild\nbuild_webpack\n\n# misc\n.DS_Store\n.env\nnpm-debug.log\n.truffle-solidity-loader\n.vagrant/**\nblockchain/geth/**\nblockchain/keystore/**\nblockchain/history\n\n#truffle\n.tern-port\nyarn.lock\npackage-lock.json\n\n# End of https://www.toptal.com/developers/gitignore/api/soliditytruffle\n","sonar":"# Created by https://www.toptal.com/developers/gitignore/api/sonar\n# Edit at https://www.toptal.com/developers/gitignore?templates=sonar\n\n### Sonar ###\n#Sonar generated dir\n/.sonar/\n\n# End of https://www.toptal.com/developers/gitignore/api/sonar\n","sonarqube":"# Created by https://www.toptal.com/developers/gitignore/api/sonarqube\n# Edit at https://www.toptal.com/developers/gitignore?templates=sonarqube\n\n### SonarQube ###\n# SonarQube ignore files.\n#\n# https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner\n# Sonar Scanner working directories\n.sonar/\n.sonarqube/\n.scannerwork/\n\n# http://www.sonarlint.org/commandline/\n# SonarLint working directories, configuration files (including credentials)\n.sonarlint/\n\n# End of https://www.toptal.com/developers/gitignore/api/sonarqube\n","sourcepawn":"# Created by https://www.toptal.com/developers/gitignore/api/sourcepawn\n# Edit at https://www.toptal.com/developers/gitignore?templates=sourcepawn\n\n### SourcePawn ###\n# SourcePawn\n*.dll # extensions\n*.so # extensions\n*.smx # plugins\n\n# End of https://www.toptal.com/developers/gitignore/api/sourcepawn\n","spark":"# Created by https://www.toptal.com/developers/gitignore/api/spark\n# Edit at https://www.toptal.com/developers/gitignore?templates=spark\n\n### Spark ###\n*#*#\n*.#*\n*.iml\n*.ipr\n*.iws\n*.pyc\n*.pyo\n*.swp\n*~\n.DS_Store\n.cache\n.classpath\n.ensime\n.ensime_cache/\n.ensime_lucene\n.generated-mima*\n.idea/\n.idea_modules/\n.project\n.pydevproject\n.scala_dependencies\n.settings\n/lib/\nR-unit-tests.log\nR/unit-tests.out\nR/cran-check.out\nR/pkg/vignettes/sparkr-vignettes.html\nR/pkg/tests/fulltests/Rplots.pdf\nbuild/*.jar\nbuild/apache-maven*\nbuild/scala*\nbuild/zinc*\ncache\ncheckpoint\nconf/*.cmd\nconf/*.conf\nconf/*.properties\nconf/*.sh\nconf/*.xml\nconf/java-opts\nconf/slaves\ndependency-reduced-pom.xml\nderby.log\ndev/create-release/*final\ndev/create-release/*txt\ndev/pr-deps/\ndist/\ndocs/_site\ndocs/api\nsql/docs\nsql/site\nlib_managed/\nlint-r-report.log\nlog/\nlogs/\nout/\nproject/boot/\nproject/build/target/\nproject/plugins/lib_managed/\nproject/plugins/project/build.properties\nproject/plugins/src_managed/\nproject/plugins/target/\npython/lib/pyspark.zip\npython/deps\npython/test_coverage/coverage_data\npython/test_coverage/htmlcov\npython/pyspark/python\nreports/\nscalastyle-on-compile.generated.xml\nscalastyle-output.xml\nscalastyle.txt\nspark-*-bin-*.tgz\nspark-tests.log\nsrc_managed/\nstreaming-tests.log\ntarget/\nunit-tests.log\nwork/\ndocs/.jekyll-metadata\n\n# For Hive\nTempStatsStore/\nmetastore/\nmetastore_db/\nsql/hive-thriftserver/test_warehouses\nwarehouse/\nspark-warehouse/\n\n# For R session data\n.RData\n.RHistory\n.Rhistory\n*.Rproj\n*.Rproj.*\n\n.Rproj.user\n\n# For SBT\n.jvmopts\n\n\n# End of https://www.toptal.com/developers/gitignore/api/spark\n","specflow":"# Created by https://www.toptal.com/developers/gitignore/api/specflow\n# Edit at https://www.toptal.com/developers/gitignore?templates=specflow\n\n### SpecFlow ###\n# Feature Files\n*.feature.cs\n*.feature.xlsx.*\n\n# End of https://www.toptal.com/developers/gitignore/api/specflow\n","splunk":"# Created by https://www.toptal.com/developers/gitignore/api/splunk\n# Edit at https://www.toptal.com/developers/gitignore?templates=splunk\n\n### Splunk ###\n# gitignore template for Splunk apps\n# documentation: http://docs.splunk.com/Documentation/Splunk/6.2.3/admin/Defaultmetaconf\n\n# Splunk local meta file\nlocal.meta\n\n# Splunk local folder\nlocal\n\n# End of https://www.toptal.com/developers/gitignore/api/splunk\n","spreadsheet":"# Created by https://www.toptal.com/developers/gitignore/api/spreadsheet\n# Edit at https://www.toptal.com/developers/gitignore?templates=spreadsheet\n\n### Spreadsheet ###\n*.xlr\n*.xls\n*.xlsx\n\n# End of https://www.toptal.com/developers/gitignore/api/spreadsheet\n","ssh":"# Created by https://www.toptal.com/developers/gitignore/api/ssh\n# Edit at https://www.toptal.com/developers/gitignore?templates=ssh\n\n### SSH ###\n**/.ssh/id_*\n**/.ssh/*_id_*\n**/.ssh/known_hosts\n\n# End of https://www.toptal.com/developers/gitignore/api/ssh\n","standardml":"# Created by https://www.toptal.com/developers/gitignore/api/standardml\n# Edit at https://www.toptal.com/developers/gitignore?templates=standardml\n\n### StandardML ###\n## Ignore all SML/NJ Compiler Manager build directories\n.cm/\n\n# End of https://www.toptal.com/developers/gitignore/api/standardml\n","stata":"# Created by https://www.toptal.com/developers/gitignore/api/stata\n# Edit at https://www.toptal.com/developers/gitignore?templates=stata\n\n### Stata ###\n# .gitignore file for git projects containing Stata files\n# Commercial statistical software: http://www.stata.com\n\n# Stata dataset and output files\n*.dta\n*.gph\n*.log\n*.smcl\n*.stpr\n*.stsem\n\n# Graphic export files from Stata\n# Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf\n#\n# You may add graphic export files to your .gitignore. However you should be\n# aware that this will exclude all image files from this main directory\n# and subdirectories.\n# *.ps\n# *.eps\n# *.wmf\n# *.emf\n# *.pdf\n# *.png\n# *.tif\n\n# End of https://www.toptal.com/developers/gitignore/api/stata\n","stdlib":"# Created by https://www.toptal.com/developers/gitignore/api/stdlib\n# Edit at https://www.toptal.com/developers/gitignore?templates=stdlib\n\n### StdLib ###\n/node_modules\nenv.json\n\n# End of https://www.toptal.com/developers/gitignore/api/stdlib\n","stella":"# Created by https://www.toptal.com/developers/gitignore/api/stella\n# Edit at https://www.toptal.com/developers/gitignore?templates=stella\n\n### Stella ###\n# Atari 2600 (Stella) support for multiple assemblers\n# - DASM\n# - CC65\n\n# Assembled binaries and object directories\nobj/\na.out\n*.bin\n*.a26\n\n# Add in special Atari 7800-based binaries for good measure\n*.a78\n\n# End of https://www.toptal.com/developers/gitignore/api/stella\n","stellar":"# Created by https://www.toptal.com/developers/gitignore/api/stellar\n# Edit at https://www.toptal.com/developers/gitignore?templates=stellar\n\n### Stellar ###\nstellar.yaml\n\n# End of https://www.toptal.com/developers/gitignore/api/stellar\n","storybookjs":"# Created by https://www.toptal.com/developers/gitignore/api/storybookjs\n# Edit at https://www.toptal.com/developers/gitignore?templates=storybookjs\n\n### StorybookJs ###\n# gitignore template for the Storybook, UI guide for front apps\n# website: https://storybook.js.org/\n\nstorybook-static/\n\n# End of https://www.toptal.com/developers/gitignore/api/storybookjs\n","strapi":"# Created by https://www.toptal.com/developers/gitignore/api/strapi\n# Edit at https://www.toptal.com/developers/gitignore?templates=strapi\n\n### Strapi ###\n\n.env\nlicense.txt\nexports\n*.cache\nbuild\n.strapi-updater.json\n\n############################\n# OS X\n\n.DS_Store\n.AppleDouble\n.LSOverride\nIcon\n.Spotlight-V100\n.Trashes\n._*\n\n# Linux\n\n*~\n\n\n# Windows\n\nThumbs.db\nehthumbs.db\nDesktop.ini\n$RECYCLE.BIN/\n*.cab\n*.msi\n*.msm\n*.msp\n\n\n# Packages\n\n*.7z\n*.csv\n*.dat\n*.dmg\n*.gz\n*.iso\n*.jar\n*.rar\n*.tar\n*.zip\n*.com\n*.class\n*.dll\n*.exe\n*.o\n*.seed\n*.so\n*.swo\n*.swp\n*.swn\n*.swm\n*.out\n*.pid\n\n# Logs and databases\n\n.tmp\n*.log\n*.sql\n*.sqlite\n*.sqlite3\n\n\n# Misc.\n\n*#\nssl\n.idea\nnbproject\npublic/uploads/*\n!public/uploads/.gitkeep\n\n# Node.js\n\nlib-cov\nlcov.info\npids\nlogs\nresults\nnode_modules\n.node_history\n\n# Tests\n\ntestApp\ncoverage\n\n# End of https://www.toptal.com/developers/gitignore/api/strapi\n","stylus":"# Created by https://www.toptal.com/developers/gitignore/api/stylus\n# Edit at https://www.toptal.com/developers/gitignore?templates=stylus\n\n### Stylus ###\n*.css\n\n# End of https://www.toptal.com/developers/gitignore/api/stylus\n","sublimetext":"# Created by https://www.toptal.com/developers/gitignore/api/sublimetext\n# Edit at https://www.toptal.com/developers/gitignore?templates=sublimetext\n\n### SublimeText ###\n# Cache files for Sublime Text\n*.tmlanguage.cache\n*.tmPreferences.cache\n*.stTheme.cache\n\n# Workspace files are user-specific\n*.sublime-workspace\n\n# Project files should be checked into the repository, unless a significant\n# proportion of contributors will probably not be using Sublime Text\n# *.sublime-project\n\n# SFTP configuration file\nsftp-config.json\nsftp-config-alt*.json\n\n# Package control specific files\nPackage Control.last-run\nPackage Control.ca-list\nPackage Control.ca-bundle\nPackage Control.system-ca-bundle\nPackage Control.cache/\nPackage Control.ca-certs/\nPackage Control.merged-ca-bundle\nPackage Control.user-ca-bundle\noscrypto-ca-bundle.crt\nbh_unicode_properties.cache\n\n# Sublime-github package stores a github token in this file\n# https://packagecontrol.io/packages/sublime-github\nGitHub.sublime-settings\n\n# End of https://www.toptal.com/developers/gitignore/api/sublimetext\n","sugarcrm":"# Created by https://www.toptal.com/developers/gitignore/api/sugarcrm\n# Edit at https://www.toptal.com/developers/gitignore?templates=sugarcrm\n\n### SugarCRM ###\n## SugarCRM\n# Ignore custom .htaccess stuff.\n/.htaccess\n# Ignore the cache directory completely.\n# This will break the current behaviour. Which was often leading to\n# the misuse of the repository as backup replacement.\n# For development the cache directory can be safely ignored and\n# therefore it is ignored.\n/cache/*\n!/cache/index.html\n# Ignore some files and directories from the custom directory.\n/custom/history/\n/custom/modulebuilder/\n/custom/working/\n/custom/modules/*/Ext/\n/custom/application/Ext/\n# Custom configuration should also be ignored.\n/config.php\n/config_override.php\n# The silent upgrade scripts aren't needed.\n/silentUpgrade*.php\n# Logs files can safely be ignored.\n*.log\n# Ignore the new upload directories.\n/upload/*\n!/upload/index.html\n/upload_backup/\n\n# End of https://www.toptal.com/developers/gitignore/api/sugarcrm\n","svelte":"# Created by https://www.toptal.com/developers/gitignore/api/svelte\n# Edit at https://www.toptal.com/developers/gitignore?templates=svelte\n\n### Svelte ###\n# gitignore template for the SvelteKit, frontend web component framework\n# website: https://kit.svelte.dev/\n\n.svelte-kit/\npackage\n\n# End of https://www.toptal.com/developers/gitignore/api/svelte\n","svn":"# Created by https://www.toptal.com/developers/gitignore/api/svn\n# Edit at https://www.toptal.com/developers/gitignore?templates=svn\n\n### SVN ###\n.svn/\n\n# End of https://www.toptal.com/developers/gitignore/api/svn\n","swift":"# Created by https://www.toptal.com/developers/gitignore/api/swift\n# Edit at https://www.toptal.com/developers/gitignore?templates=swift\n\n### Swift ###\n# Xcode\n#\n# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore\n\n## User settings\nxcuserdata/\n\n## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)\n*.xcscmblueprint\n*.xccheckout\n\n## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)\nbuild/\nDerivedData/\n*.moved-aside\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\n\n## Obj-C/Swift specific\n*.hmap\n\n## App packaging\n*.ipa\n*.dSYM.zip\n*.dSYM\n\n## Playgrounds\ntimeline.xctimeline\nplayground.xcworkspace\n\n# Swift Package Manager\n# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.\n# Packages/\n# Package.pins\n# Package.resolved\n# *.xcodeproj\n# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata\n# hence it is not needed unless you have added a package configuration file to your project\n# .swiftpm\n\n.build/\n\n# CocoaPods\n# We recommend against adding the Pods directory to your .gitignore. However\n# you should judge for yourself, the pros and cons are mentioned at:\n# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control\n# Pods/\n# Add this line if you want to avoid checking in source code from the Xcode workspace\n# *.xcworkspace\n\n# Carthage\n# Add this line if you want to avoid checking in source code from Carthage dependencies.\n# Carthage/Checkouts\n\nCarthage/Build/\n\n# Accio dependency management\nDependencies/\n.accio/\n\n# fastlane\n# It is recommended to not store the screenshots in the git repo.\n# Instead, use fastlane to re-generate the screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/#source-control\n\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots/**/*.png\nfastlane/test_output\n\n# Code Injection\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n\n# End of https://www.toptal.com/developers/gitignore/api/swift\n","swiftpackagemanager":"# Created by https://www.toptal.com/developers/gitignore/api/swiftpackagemanager\n# Edit at https://www.toptal.com/developers/gitignore?templates=swiftpackagemanager\n\n### SwiftPackageManager ###\nPackages\n.build/\nxcuserdata\nDerivedData/\n*.xcodeproj\n\n\n# End of https://www.toptal.com/developers/gitignore/api/swiftpackagemanager\n","swiftpm":"# Created by https://www.toptal.com/developers/gitignore/api/swiftpm\n# Edit at https://www.toptal.com/developers/gitignore?templates=swiftpm\n\n### SwiftPM ###\nPackages\n.build/\nxcuserdata\nDerivedData/\n*.xcodeproj\n\n\n# End of https://www.toptal.com/developers/gitignore/api/swiftpm\n","symfony":"# Created by https://www.toptal.com/developers/gitignore/api/symfony\n# Edit at https://www.toptal.com/developers/gitignore?templates=symfony\n\n### Symfony ###\n# Cache and logs (Symfony2)\n/app/cache/*\n/app/logs/*\n!app/cache/.gitkeep\n!app/logs/.gitkeep\n\n# Email spool folder\n/app/spool/*\n\n# Cache, session files and logs (Symfony3)\n/var/cache/*\n/var/logs/*\n/var/sessions/*\n!var/cache/.gitkeep\n!var/logs/.gitkeep\n!var/sessions/.gitkeep\n\n# Logs (Symfony4)\n/var/log/*\n!var/log/.gitkeep\n\n# Parameters\n/app/config/parameters.yml\n/app/config/parameters.ini\n\n# Managed by Composer\n/app/bootstrap.php.cache\n/var/bootstrap.php.cache\n/bin/*\n!bin/console\n!bin/symfony_requirements\n/vendor/\n\n# Assets and user uploads\n/web/bundles/\n/web/uploads/\n\n# PHPUnit\n/app/phpunit.xml\n/phpunit.xml\n\n# Build data\n/build/\n\n# Composer PHAR\n/composer.phar\n\n# Backup entities generated with doctrine:generate:entities command\n**/Entity/*~\n\n# Embedded web-server pid file\n/.web-server-pid\n\n### Symfony Patch ###\n/web/css/\n/web/js/\n\n# End of https://www.toptal.com/developers/gitignore/api/symfony\n","symphonycms":"# Created by https://www.toptal.com/developers/gitignore/api/symphonycms\n# Edit at https://www.toptal.com/developers/gitignore?templates=symphonycms\n\n### SymphonyCMS ###\nmanifest/cache/\nmanifest/logs/\nmanifest/tmp/\nsymphony/\nworkspace/uploads/\ninstall-log.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/symphonycms\n","synology":"# Created by https://www.toptal.com/developers/gitignore/api/synology\n# Edit at https://www.toptal.com/developers/gitignore?templates=synology\n\n### Synology ###\n# Thumbnails\n@eaDir\n# Recycle bin\n\\#recycle\n\n# End of https://www.toptal.com/developers/gitignore/api/synology\n","synopsysvcs":"# Created by https://www.toptal.com/developers/gitignore/api/synopsysvcs\n# Edit at https://www.toptal.com/developers/gitignore?templates=synopsysvcs\n\n### SynopsysVCS ###\n# Waveform formats\n*.vcd\n*.vpd\n*.evcd\n*.fsdb\n\n# Default name of the simulation executable. A different name can be\n# specified with this switch (the associated daidir database name is\n# also taken from here): -o /\nsimv\n\n# Generated for Verilog and VHDL top configs\nsimv.daidir/\nsimv.db.dir/\n\n# Infrastructure necessary to co-simulate SystemC models with\n# Verilog/VHDL models. An alternate directory may be specified with this\n# switch: -Mdir=\ncsrc/\n\n# Log file - the following switch allows to specify the file that will be\n# used to write all messages from simulation: -l \n*.log\n\n# Coverage results (generated with urg) and database location. The\n# following switch can also be used: urg -dir .vdb\nsimv.vdb/\nurgReport/\n\n# DVE and UCLI related files.\nDVEfiles/\nucli.key\n\n# When the design is elaborated for DirectC, the following file is created\n# with declarations for C/C++ functions.\nvc_hdrs.h\n\n# End of https://www.toptal.com/developers/gitignore/api/synopsysvcs\n","tags":"# Created by https://www.toptal.com/developers/gitignore/api/tags\n# Edit at https://www.toptal.com/developers/gitignore?templates=tags\n\n### Tags ###\n# Ignore tags created by etags, ctags, gtags (GNU global) and cscope\nTAGS\n.TAGS\n!TAGS/\ntags\n.tags\n!tags/\ngtags.files\nGTAGS\nGRTAGS\nGPATH\nGSYMS\ncscope.files\ncscope.out\ncscope.in.out\ncscope.po.out\n\n\n# End of https://www.toptal.com/developers/gitignore/api/tags\n","tarmainstallmate":"# Created by https://www.toptal.com/developers/gitignore/api/tarmainstallmate\n# Edit at https://www.toptal.com/developers/gitignore?templates=tarmainstallmate\n\n### TarmaInstallMate ###\n# Workspace files\n*.iw7\n*.iw9\n\n# Build log files\n*.log\n\n# End of https://www.toptal.com/developers/gitignore/api/tarmainstallmate\n","terraform":"# Created by https://www.toptal.com/developers/gitignore/api/terraform\n# Edit at https://www.toptal.com/developers/gitignore?templates=terraform\n\n### Terraform ###\n# Local .terraform directories\n**/.terraform/*\n\n# .tfstate files\n*.tfstate\n*.tfstate.*\n\n# Crash log files\ncrash.log\ncrash.*.log\n\n# Exclude all .tfvars files, which are likely to contain sensitive data, such as\n# password, private keys, and other secrets. These should not be part of version\n# control as they are data points which are potentially sensitive and subject\n# to change depending on the environment.\n*.tfvars\n*.tfvars.json\n\n# Ignore override files as they are usually used to override resources locally and so\n# are not checked in\noverride.tf\noverride.tf.json\n*_override.tf\n*_override.tf.json\n\n# Include override files you do wish to add to version control using negated pattern\n# !example_override.tf\n\n# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan\n# example: *tfplan*\n\n# Ignore CLI configuration files\n.terraformrc\nterraform.rc\n\n# End of https://www.toptal.com/developers/gitignore/api/terraform\n","terragrunt":"# Created by https://www.toptal.com/developers/gitignore/api/terragrunt\n# Edit at https://www.toptal.com/developers/gitignore?templates=terragrunt\n\n### Terragrunt ###\n# terragrunt cache directories\n**/.terragrunt-cache/*\n\n# Terragrunt debug output file (when using `--terragrunt-debug` option)\n# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug\nterragrunt-debug.tfvars.json\n\n# End of https://www.toptal.com/developers/gitignore/api/terragrunt\n","test":"# Created by https://www.toptal.com/developers/gitignore/api/test\n# Edit at https://www.toptal.com/developers/gitignore?templates=test\n\n### Test ###\n### Ignore all files that could be used to test your code and\n### you wouldn't want to push\n\n# Reference https://en.wikipedia.org/wiki/Metasyntactic_variable\n\n# Most common\n*foo\n*bar\n*fubar\n*foobar\n*baz\n\n# Less common\n*qux\n*quux\n*bongo\n*bazola\n*ztesch\n\n# UK, Australia\n*wibble\n*wobble\n*wubble\n*flob\n*blep\n*blah\n*boop\n*beep\n\n# Japanese\n*hoge\n*piyo\n*fuga\n*hogera\n*hogehoge\n\n# Portugal, Spain\n*fulano\n*sicrano\n*beltrano\n*mengano\n*perengano\n*zutano\n\n# France, Italy, the Netherlands\n*toto\n*titi\n*tata\n*tutu\n*pipppo\n*pluto\n*paperino\n*aap\n*noot\n*mies\n\n# Other names that would make sense\n*tests\n*testsdir\n*testsfile\n*testsfiles\n*test\n*testdir\n*testfile\n*testfiles\n*testing\n*testingdir\n*testingfile\n*testingfiles\n*temp\n*tempdir\n*tempfile\n*tempfiles\n*tmp\n*tmpdir\n*tmpfile\n*tmpfiles\n*lol\n\n# End of https://www.toptal.com/developers/gitignore/api/test\n","testcomplete":"# Created by https://www.toptal.com/developers/gitignore/api/testcomplete\n# Edit at https://www.toptal.com/developers/gitignore?templates=testcomplete\n\n### TestComplete ###\n# Test Complete ignore files: https://support.smartbear.com/viewarticle/68002/\n\n# Tester-specific Settings\n*.tcCFGExtender\n*.tcLS\n\n# Type library declarations\n*.tlb\n\n# Log files\n*.tcLogs\n\n# Backup files\n*.bak\n\n# End of https://www.toptal.com/developers/gitignore/api/testcomplete\n","testinfra":"# Created by https://www.toptal.com/developers/gitignore/api/testinfra\n# Edit at https://www.toptal.com/developers/gitignore?templates=testinfra\n\n### Testinfra ###\n__pycache__/\n.pytest_cache/\n\n# End of https://www.toptal.com/developers/gitignore/api/testinfra\n","tex":"# Created by https://www.toptal.com/developers/gitignore/api/tex\n# Edit at https://www.toptal.com/developers/gitignore?templates=tex\n\n### TeX ###\n## Core latex/pdflatex auxiliary files:\n*.aux\n*.lof\n*.log\n*.lot\n*.fls\n*.out\n*.toc\n*.fmt\n*.fot\n*.cb\n*.cb2\n.*.lb\n\n## Intermediate documents:\n*.dvi\n*.xdv\n*-converted-to.*\n# these rules might exclude image files for figures etc.\n# *.ps\n# *.eps\n# *.pdf\n\n## Generated if empty string is given at \"Please type another file name for output:\"\n.pdf\n\n## Bibliography auxiliary files (bibtex/biblatex/biber):\n*.bbl\n*.bcf\n*.blg\n*-blx.aux\n*-blx.bib\n*.run.xml\n\n## Build tool auxiliary files:\n*.fdb_latexmk\n*.synctex\n*.synctex(busy)\n*.synctex.gz\n*.synctex.gz(busy)\n*.pdfsync\n\n## Build tool directories for auxiliary files\n# latexrun\nlatex.out/\n\n## Auxiliary and intermediate files from other packages:\n# algorithms\n*.alg\n*.loa\n\n# achemso\nacs-*.bib\n\n# amsthm\n*.thm\n\n# beamer\n*.nav\n*.pre\n*.snm\n*.vrb\n\n# changes\n*.soc\n\n# comment\n*.cut\n\n# cprotect\n*.cpt\n\n# elsarticle (documentclass of Elsevier journals)\n*.spl\n\n# endnotes\n*.ent\n\n# fixme\n*.lox\n\n# feynmf/feynmp\n*.mf\n*.mp\n*.t[1-9]\n*.t[1-9][0-9]\n*.tfm\n\n#(r)(e)ledmac/(r)(e)ledpar\n*.end\n*.?end\n*.[1-9]\n*.[1-9][0-9]\n*.[1-9][0-9][0-9]\n*.[1-9]R\n*.[1-9][0-9]R\n*.[1-9][0-9][0-9]R\n*.eledsec[1-9]\n*.eledsec[1-9]R\n*.eledsec[1-9][0-9]\n*.eledsec[1-9][0-9]R\n*.eledsec[1-9][0-9][0-9]\n*.eledsec[1-9][0-9][0-9]R\n\n# glossaries\n*.acn\n*.acr\n*.glg\n*.glo\n*.gls\n*.glsdefs\n*.lzo\n*.lzs\n*.slg\n*.slo\n*.sls\n\n# uncomment this for glossaries-extra (will ignore makeindex's style files!)\n# *.ist\n\n# gnuplot\n*.gnuplot\n*.table\n\n# gnuplottex\n*-gnuplottex-*\n\n# gregoriotex\n*.gaux\n*.glog\n*.gtex\n\n# htlatex\n*.4ct\n*.4tc\n*.idv\n*.lg\n*.trc\n*.xref\n\n# hyperref\n*.brf\n\n# knitr\n*-concordance.tex\n# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files\n# *.tikz\n*-tikzDictionary\n\n# listings\n*.lol\n\n# luatexja-ruby\n*.ltjruby\n\n# makeidx\n*.idx\n*.ilg\n*.ind\n\n# minitoc\n*.maf\n*.mlf\n*.mlt\n*.mtc[0-9]*\n*.slf[0-9]*\n*.slt[0-9]*\n*.stc[0-9]*\n\n# minted\n_minted*\n*.pyg\n\n# morewrites\n*.mw\n\n# newpax\n*.newpax\n\n# nomencl\n*.nlg\n*.nlo\n*.nls\n\n# pax\n*.pax\n\n# pdfpcnotes\n*.pdfpc\n\n# sagetex\n*.sagetex.sage\n*.sagetex.py\n*.sagetex.scmd\n\n# scrwfile\n*.wrt\n\n# svg\nsvg-inkscape/\n\n# sympy\n*.sout\n*.sympy\nsympy-plots-for-*.tex/\n\n# pdfcomment\n*.upa\n*.upb\n\n# pythontex\n*.pytxcode\npythontex-files-*/\n\n# tcolorbox\n*.listing\n\n# thmtools\n*.loe\n\n# TikZ & PGF\n*.dpth\n*.md5\n*.auxlock\n\n# titletoc\n*.ptc\n\n# todonotes\n*.tdo\n\n# vhistory\n*.hst\n*.ver\n\n# easy-todo\n*.lod\n\n# xcolor\n*.xcp\n\n# xmpincl\n*.xmpi\n\n# xindy\n*.xdy\n\n# xypic precompiled matrices and outlines\n*.xyc\n*.xyd\n\n# endfloat\n*.ttt\n*.fff\n\n# Latexian\nTSWLatexianTemp*\n\n## Editors:\n# WinEdt\n*.bak\n*.sav\n\n# Texpad\n.texpadtmp\n\n# LyX\n*.lyx~\n\n# Kile\n*.backup\n\n# gummi\n.*.swp\n\n# KBibTeX\n*~[0-9]*\n\n# TeXnicCenter\n*.tps\n\n# auto folder when using emacs and auctex\n./auto/*\n*.el\n\n# expex forward references with \\gathertags\n*-tags.tex\n\n# standalone packages\n*.sta\n\n# Makeindex log files\n*.lpz\n\n# xwatermark package\n*.xwm\n\n# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib\n# option is specified. Footnotes are the stored in a file with suffix Notes.bib.\n# Uncomment the next line to have this generated file ignored.\n#*Notes.bib\n\n### TeX Patch ###\n# LIPIcs / OASIcs\n*.vtc\n\n# glossaries\n*.glstex\n\n# End of https://www.toptal.com/developers/gitignore/api/tex\n","text":"# Created by https://www.toptal.com/developers/gitignore/api/text\n# Edit at https://www.toptal.com/developers/gitignore?templates=text\n\n### Text ###\n*.doc\n*.docx\n*.log\n*.msg\n*.pages\n*.rtf\n*.txt\n*.wpd\n*.wps\n\n# End of https://www.toptal.com/developers/gitignore/api/text\n","textmate":"# Created by https://www.toptal.com/developers/gitignore/api/textmate\n# Edit at https://www.toptal.com/developers/gitignore?templates=textmate\n\n### TextMate ###\n*.tmproj\n*.tmproject\ntmtags\n\n# End of https://www.toptal.com/developers/gitignore/api/textmate\n","textpattern":"# Created by https://www.toptal.com/developers/gitignore/api/textpattern\n# Edit at https://www.toptal.com/developers/gitignore?templates=textpattern\n\n### Textpattern ###\n.htaccess\ncss.php\nrpc/\nsites/site*/admin/\nsites/site*/private/\nsites/site*/public/admin/\nsites/site*/public/setup/\nsites/site*/public/theme/\ntextpattern/\nHISTORY.txt\nREADME.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/textpattern\n","theos-tweak":"# Created by https://www.toptal.com/developers/gitignore/api/theos-tweak\n# Edit at https://www.toptal.com/developers/gitignore?templates=theos-tweak\n\n### THEOS-Tweak ###\n._*\n*.deb\n.debmake\n_\nobj\n.theos\n.DS_Store\n\n# End of https://www.toptal.com/developers/gitignore/api/theos-tweak\n","thinkphp":"# Created by https://www.toptal.com/developers/gitignore/api/thinkphp\n# Edit at https://www.toptal.com/developers/gitignore?templates=thinkphp\n\n### ThinkPHP ###\n# gitignore template for ThinkPHP v3.2.3\n# website: http://www.thinkphp.cn/\n\n# Logs and Cache files\n/Application/Runtime/\n\n# Common configure file\n/Application/Common/Conf/config.php\n\n# End of https://www.toptal.com/developers/gitignore/api/thinkphp\n","tla+":"# Created by https://www.toptal.com/developers/gitignore/api/tla+\n# Edit at https://www.toptal.com/developers/gitignore?templates=tla+\n\n### TLA+ ###\n# Reference: https://github.com/jameshfisher/tlaplus/blob/master/.gitignore\n\n# TLA+ Toolbox\n# Exclude all files related to TLA+ Toolbox\n# *.toolbox/**\n\n*.toolbox/*.launch\n*.toolbox/*.pmap\n\n# TLA+ Toolbox models\n*.toolbox/**/*.tla\n*.toolbox/**/*.out\n*.toolbox/**/*.cfg\n\n# PlusCal\n*.old\n\n# LaTex output\n*.toolbox/*aux\n*.toolbox/*.log\n*.toolbox/*.pdf\n*.toolbox/*.tex\n*aux\n*.log\n*.pdf\n*.tex\n\n# Vim backup file\n*~\n\n# End of https://www.toptal.com/developers/gitignore/api/tla+\n","toit":"# Created by https://www.toptal.com/developers/gitignore/api/toit\n# Edit at https://www.toptal.com/developers/gitignore?templates=toit\n\n### Toit ###\n# Package downloads\n.packages/\n\n# End of https://www.toptal.com/developers/gitignore/api/toit\n","tortoisegit":"# Created by https://www.toptal.com/developers/gitignore/api/tortoisegit\n# Edit at https://www.toptal.com/developers/gitignore?templates=tortoisegit\n\n### TortoiseGit ###\n# Project-level settings\n/.tgitconfig\n\n# End of https://www.toptal.com/developers/gitignore/api/tortoisegit\n","tower":"# Created by https://www.toptal.com/developers/gitignore/api/tower\n# Edit at https://www.toptal.com/developers/gitignore?templates=tower\n\n### Tower ###\n# Tower.app - http://www.git-tower.com/\nIcon.png\n\n# End of https://www.toptal.com/developers/gitignore/api/tower\n","turbo":"# Created by https://www.toptal.com/developers/gitignore/api/turbo\n# Edit at https://www.toptal.com/developers/gitignore?templates=turbo\n\n### Turbo ###\n# Turborepo task cache\n.turbo\n\n# End of https://www.toptal.com/developers/gitignore/api/turbo\n","turbogears2":"# Created by https://www.toptal.com/developers/gitignore/api/turbogears2\n# Edit at https://www.toptal.com/developers/gitignore?templates=turbogears2\n\n### TurboGears2 ###\n*.py[co]\n\n# Default development database\ndevdata.db\n\n# Default data directory\ndata/*\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\n\n# End of https://www.toptal.com/developers/gitignore/api/turbogears2\n","twincat3":"# Created by https://www.toptal.com/developers/gitignore/api/twincat3\n# Edit at https://www.toptal.com/developers/gitignore?templates=twincat3\n\n### TwinCAT3 ###\n# TwinCAT3 Stack Base\n\n# TwinCAT 3.1\n\n# include suggestions from GitHub: https://github.com/github/gitignore/pull/3092\n#\n# website: https://www.beckhoff.com/twincat3/\n\n# TwinCAT3 PLC\n*.plcproj.bak\n*.plcproj.orig\n*.tpy\n*.tclrs\n*.compiled-library\n*.library\n*.compileinfo\nLineIDs.dbg\nLineIDs.dbg.bak\n\n# Don't include the tmc-file rule if either of the following is true:\n# 1. You've got TwinCAT C++ projects, as the information in the TMC-file is created manually for the C++ projects (in that case, only (manually) ignore the tmc-files for the PLC projects)\n# 2. You've created a standalone PLC-project and added events to it, as these are stored in the TMC-file.\n# 3. shared tmc files are used to exchange data types that should be persisted in the project\n*.tmc\n*.tmcRefac\n\n# TwinCAT3 project files\n*.tsproj.bak\n*.tsproj.orig\n*.xti.bak\n*.xti.orig\n*.project.~u\n\n# folders that should be excluded\n_Boot/\n_CompileInfo/\n_Libraries/\n_ModuleInstall/\n_Deployment/\n_Repository/\n\n### TwinCAT3.VisualStudio Stack ###\nVisualStudio.gitignore\n\n\n### TwinCAT3.OpenFrameworks Stack ###\nOpenFrameworks.gitignore\n\n\n# End of https://www.toptal.com/developers/gitignore/api/twincat3\n","tye":"# Created by https://www.toptal.com/developers/gitignore/api/tye\n# Edit at https://www.toptal.com/developers/gitignore?templates=tye\n\n### Tye ###\n# Tye | https://github.com/dotnet/tye/\n.tye/*\n\n# End of https://www.toptal.com/developers/gitignore/api/tye\n","typings":"# Created by https://www.toptal.com/developers/gitignore/api/typings\n# Edit at https://www.toptal.com/developers/gitignore?templates=typings\n\n### Typings ###\n## Ignore downloaded typings\ntypings\n\n# End of https://www.toptal.com/developers/gitignore/api/typings\n","typo3":"# Created by https://www.toptal.com/developers/gitignore/api/typo3\n# Edit at https://www.toptal.com/developers/gitignore?templates=typo3\n\n### Typo3 ###\n## TYPO3 v6.2\n# Ignore several upload and file directories.\n/fileadmin/user_upload/\n/fileadmin/_temp_/\n/fileadmin/_processed_/\n/uploads/\n# Ignore cache\n/typo3conf/temp_CACHED*\n/typo3conf/temp_fieldInfo.php\n/typo3conf/deprecation_*.log\n/typo3conf/ENABLE_INSTALL_TOOL\n/typo3conf/realurl_autoconf.php\n/FIRST_INSTALL\n# Ignore system folders, you should have them symlinked.\n# If not comment out the following entries.\n/typo3\n/typo3_src\n/typo3_src-*\n/Packages\n/.htaccess\n/index.php\n# Ignore temp directory.\n/typo3temp/\n\n# End of https://www.toptal.com/developers/gitignore/api/typo3\n","typo3-composer":"# Created by https://www.toptal.com/developers/gitignore/api/typo3-composer\n# Edit at https://www.toptal.com/developers/gitignore?templates=typo3-composer\n\n### TYPO3-composer ###\n/vendor\n/web/index.php\n/web/fileadmin/\n/web/FIRST_INSTALL\n/web/typo3\n/web/typo3conf/ENABLE_INSTALL_TOOL\n/web/typo3conf/deprecation*\n/web/typo3conf/PackageStates.php\n/web/typo3conf/ext/\n/web/typo3conf/l10n/\n/web/typo3conf/realurl_autoconf.php\n/web/typo3temp\n/web/uploads/\n\n# End of https://www.toptal.com/developers/gitignore/api/typo3-composer\n","umbraco":"# Created by https://www.toptal.com/developers/gitignore/api/umbraco\n# Edit at https://www.toptal.com/developers/gitignore?templates=umbraco\n\n### Umbraco ###\n## Ignore Umbraco files/folders generated for each instance\n##\n## Get latest from https://github.com/github/gitignore/blob/main/Umbraco.gitignore\n\n# Note: VisualStudio gitignore rules may also be relevant\n\n# Umbraco\n# Ignore unimportant folders generated by Umbraco\n**/App_Data/Logs/\n**/App_Data/[Pp]review/\n**/App_Data/TEMP/\n**/App_Data/NuGetBackup/\n\n# Ignore Umbraco content cache file\n**/App_Data/umbraco.config\n\n## this [Uu]mbraco/ folder should be created by cmd like `Install-Package UmbracoCms -Version 8.5.3`\n## you can find your Umbraco version in your Web.config. (i.e. )\n## Uncomment this line if you think it fits the way you work on your project.\n## **/[Uu]mbraco/\n\n## The [Mm]edia/ folder contains content. Content may vary by environment and should therefore not be added to source control.\n## **/[Mm]edia/\n\n# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder)\n# Make sure to include details from VisualStudio.gitignore BEFORE this\n!**/App_Data/[Pp]ackages/*\n!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages/*\n!**/[Uu]mbraco/[Vv]iews/[Pp]ackages/*\n\n# ImageProcessor DiskCache\n**/App_Data/cache/\n\n# Ignore the Models Builder models out of date flag\n**/ood.flag\n\n# NEW for version 9 .Net 5 (Core)\n#ignore umbraco backoffice assest from wwwroot\n**/wwwroot/umbraco/\n\n# SQLite files\n*.sqlite.db*\n\n#ignore umbraco data/views/settings\n**/umbraco/\n\n#include default location for modelsbuilder output\n!**/umbraco/models\n\n#include default location for packages\n!**/umbraco/Data/packages\n\n# End of https://www.toptal.com/developers/gitignore/api/umbraco\n","unity":"# Created by https://www.toptal.com/developers/gitignore/api/unity\n# Edit at https://www.toptal.com/developers/gitignore?templates=unity\n\n### Unity ###\n# This .gitignore file should be placed at the root of your Unity project directory\n#\n# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore\n/[Ll]ibrary/\n/[Tt]emp/\n/[Oo]bj/\n/[Bb]uild/\n/[Bb]uilds/\n/[Ll]ogs/\n/[Uu]ser[Ss]ettings/\n\n# MemoryCaptures can get excessive in size.\n# They also could contain extremely sensitive data\n/[Mm]emoryCaptures/\n\n# Recordings can get excessive in size\n/[Rr]ecordings/\n\n# Uncomment this line if you wish to ignore the asset store tools plugin\n# /[Aa]ssets/AssetStoreTools*\n\n# Autogenerated Jetbrains Rider plugin\n/[Aa]ssets/Plugins/Editor/JetBrains*\n\n# Visual Studio cache directory\n.vs/\n\n# Gradle cache directory\n.gradle/\n\n# Autogenerated VS/MD/Consulo solution and project files\nExportedObj/\n.consulo/\n*.csproj\n*.unityproj\n*.sln\n*.suo\n*.tmp\n*.user\n*.userprefs\n*.pidb\n*.booproj\n*.svd\n*.pdb\n*.mdb\n*.opendb\n*.VC.db\n\n# Unity3D generated meta files\n*.pidb.meta\n*.pdb.meta\n*.mdb.meta\n\n# Unity3D generated file on crash reports\nsysinfo.txt\n\n# Builds\n*.apk\n*.aab\n*.unitypackage\n*.app\n\n# Crashlytics generated file\ncrashlytics-build.properties\n\n# Packed Addressables\n/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*\n\n# Temporary auto-generated Android Assets\n/[Aa]ssets/[Ss]treamingAssets/aa.meta\n/[Aa]ssets/[Ss]treamingAssets/aa/*\n\n# End of https://www.toptal.com/developers/gitignore/api/unity\n","unrealengine":"# Created by https://www.toptal.com/developers/gitignore/api/unrealengine\n# Edit at https://www.toptal.com/developers/gitignore?templates=unrealengine\n\n### UnrealEngine ###\n# Visual Studio 2015 user specific files\n.vs/\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Fortran module files\n*.mod\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n*.ipa\n\n# These project files can be generated by the engine\n*.xcodeproj\n*.xcworkspace\n*.sln\n*.suo\n*.opensdf\n*.sdf\n*.VC.db\n*.VC.opendb\n\n# Precompiled Assets\nSourceArt/**/*.png\nSourceArt/**/*.tga\n\n# Binary Files\nBinaries/*\nPlugins/*/Binaries/*\n\n# Builds\nBuild/*\n\n# Whitelist PakBlacklist-.txt files\n!Build/*/\nBuild/*/**\n!Build/*/PakBlacklist*.txt\n\n# Don't ignore icon files in Build\n!Build/**/*.ico\n\n# Built data for maps\n*_BuiltData.uasset\n\n# Configuration files generated by the Editor\nSaved/*\n\n# Compiled source files for the engine to use\nIntermediate/*\nPlugins/*/Intermediate/*\n\n# Cache files for the editor to use\nDerivedDataCache/*\n\n### UnrealEngine Patch ###\n# Don't ignore icon and splash images for mobile app\n!Build/IOS/Resources/\nBuild/IOS/Resources/*\n!Build/IOS/Resources/Graphics/\nBuild/IOS/Resources/Graphics/*\n!Build/IOS/Resources/Graphics/*.png\n!Build/Android/res/\nBuild/Android/res/*\n!Build/Android/res/*/\nBuild/Android/res/*/*\n!Build/Android/res/*/*.png\n# Ignore plugins binaries on deep subfolders\nPlugins/**/Binaries/*\nPlugins/**/Intermediate/*\n\n# End of https://www.toptal.com/developers/gitignore/api/unrealengine\n","vaadin":"# Created by https://www.toptal.com/developers/gitignore/api/vaadin\n# Edit at https://www.toptal.com/developers/gitignore?templates=vaadin\n\n### Vaadin ###\n.vaadin-designer\n.designer\n\n# End of https://www.toptal.com/developers/gitignore/api/vaadin\n","vagrant":"# Created by https://www.toptal.com/developers/gitignore/api/vagrant\n# Edit at https://www.toptal.com/developers/gitignore?templates=vagrant\n\n### Vagrant ###\n# General\n.vagrant/\n\n# Log files (if you are creating logs in debug mode, uncomment this)\n# *.log\n\n### Vagrant Patch ###\n*.box\n\n# End of https://www.toptal.com/developers/gitignore/api/vagrant\n","valgrind":"# Created by https://www.toptal.com/developers/gitignore/api/valgrind\n# Edit at https://www.toptal.com/developers/gitignore?templates=valgrind\n\n### Valgrind ###\n# Callgrind output files\ncallgrind.out\ncallgrind.out.*\n\n# Cachegrind output files\ncachegrind.out\ncachegrind.out.*\n\n# Massif output files\nmassif.out\nmassif.out.*\n\n# BBV output files\nbb.out\nbb.out.*\n\n\n\n# End of https://www.toptal.com/developers/gitignore/api/valgrind\n","vapor":"# Created by https://www.toptal.com/developers/gitignore/api/vapor\n# Edit at https://www.toptal.com/developers/gitignore?templates=vapor\n\n### Vapor ###\nConfig/secrets\n\n### Vapor Patch ###\nPackages\n.build/\nxcuserdata\nDerivedData/\n*.xcodeproj\n\n\n# End of https://www.toptal.com/developers/gitignore/api/vapor\n","vcpkg":"# Created by https://www.toptal.com/developers/gitignore/api/vcpkg\n# Edit at https://www.toptal.com/developers/gitignore?templates=vcpkg\n\n### vcpkg ###\n# Vcpkg\n\nvcpkg-manifest-install.log\n\n## Manifest Mode\nvcpkg_installed/\n\n# End of https://www.toptal.com/developers/gitignore/api/vcpkg\n","venv":"# Created by https://www.toptal.com/developers/gitignore/api/venv\n# Edit at https://www.toptal.com/developers/gitignore?templates=venv\n\n### venv ###\n# Virtualenv\n# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/\n.Python\n[Bb]in\n[Ii]nclude\n[Ll]ib\n[Ll]ib64\n[Ll]ocal\n[Ss]cripts\npyvenv.cfg\n.venv\npip-selfcheck.json\n\n# End of https://www.toptal.com/developers/gitignore/api/venv\n","vercel":"# Created by https://www.toptal.com/developers/gitignore/api/vercel\n# Edit at https://www.toptal.com/developers/gitignore?templates=vercel\n\n### Vercel ###\n.vercel\n\n# End of https://www.toptal.com/developers/gitignore/api/vercel\n","vertx":"# Created by https://www.toptal.com/developers/gitignore/api/vertx\n# Edit at https://www.toptal.com/developers/gitignore?templates=vertx\n\n### Vertx ###\n# Vertx cache directory\n.vertx\n\n# End of https://www.toptal.com/developers/gitignore/api/vertx\n","video":"# Created by https://www.toptal.com/developers/gitignore/api/video\n# Edit at https://www.toptal.com/developers/gitignore?templates=video\n\n### Video ###\n*.3g2\n*.3gp\n*.asf\n*.asx\n*.avi\n*.flv\n*.mkv\n*.mov\n*.mp4\n*.mpg\n*.ogv\n*.rm\n*.swf\n*.vob\n*.wmv\n*.webm\n\n# End of https://www.toptal.com/developers/gitignore/api/video\n","vim":"# Created by https://www.toptal.com/developers/gitignore/api/vim\n# Edit at https://www.toptal.com/developers/gitignore?templates=vim\n\n### Vim ###\n# Swap\n[._]*.s[a-v][a-z]\n!*.svg # comment out if you don't need vector files\n[._]*.sw[a-p]\n[._]s[a-rt-v][a-z]\n[._]ss[a-gi-z]\n[._]sw[a-p]\n\n# Session\nSession.vim\nSessionx.vim\n\n# Temporary\n.netrwhist\n*~\n# Auto-generated tag files\ntags\n# Persistent undo\n[._]*.un~\n\n# End of https://www.toptal.com/developers/gitignore/api/vim\n","virtualenv":"# Created by https://www.toptal.com/developers/gitignore/api/virtualenv\n# Edit at https://www.toptal.com/developers/gitignore?templates=virtualenv\n\n### VirtualEnv ###\n# Virtualenv\n# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/\n.Python\n[Bb]in\n[Ii]nclude\n[Ll]ib\n[Ll]ib64\n[Ll]ocal\n[Ss]cripts\npyvenv.cfg\n.venv\npip-selfcheck.json\n\n# End of https://www.toptal.com/developers/gitignore/api/virtualenv\n","virtuoso":"# Created by https://www.toptal.com/developers/gitignore/api/virtuoso\n# Edit at https://www.toptal.com/developers/gitignore?templates=virtuoso\n\n### Virtuoso ###\n# Gitignore for Cadence Virtuoso\n################################################################\n\n# Log files\n*.log\npanic*.log.*\n\n# OpenAccess database lock files\n*.cdslck*\n\n# Run directories for layout vs. schematic and design rule check\nlvsRunDir/*\ndrcRunDir/*\n\n# Abstract generation tool\nabstract.log*\nabstract.record*\n\n\n# End of https://www.toptal.com/developers/gitignore/api/virtuoso\n","visualbasic":"# Created by https://www.toptal.com/developers/gitignore/api/visualbasic\n# Edit at https://www.toptal.com/developers/gitignore?templates=visualbasic\n\n### VisualBasic ###\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n.DS_Store\n.Trashes\n*.vbw\n*.csi\n*.exp\n*.lib\n*.lvw\n*.dca\n*.scc\n*.tmp\n*.exe\n*.bat\n*.bak\n*.zip\n*.old\n*.enc\n*.key\n# Windows image file caches\nThumbs.db\nehthumbs.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# =========================\n# Operating System Files\n# =========================\n\n# OSX\n# =========================\n\n.AppleDouble\n.LSOverride\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.VolumeIcon.icns\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# End of https://www.toptal.com/developers/gitignore/api/visualbasic\n","visualstudio":"# Created by https://www.toptal.com/developers/gitignore/api/visualstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio\n\n### VisualStudio ###\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.tlog\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio 6 auto-generated project file (contains which files were open etc.)\n*.vbp\n\n# Visual Studio 6 workspace and project file (working project files containing files to include in project)\n*.dsw\n*.dsp\n\n# Visual Studio 6 technical files\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# Visual Studio History (VSHistory) files\n.vshistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n\n# VS Code files for those working on multiple tools\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n*.code-workspace\n\n# Local History for Visual Studio Code\n.history/\n\n# Windows Installer files from build outputs\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# JetBrains Rider\n*.sln.iml\n\n### VisualStudio Patch ###\n# Additional files built by Visual Studio\n\n# End of https://www.toptal.com/developers/gitignore/api/visualstudio\n","visualstudiocode":"# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode\n# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode\n\n### VisualStudioCode ###\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n!.vscode/*.code-snippets\n\n# Local History for Visual Studio Code\n.history/\n\n# Built Visual Studio Code Extensions\n*.vsix\n\n### VisualStudioCode Patch ###\n# Ignore all local history of files\n.history\n.ionide\n\n# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode\n","vivado":"# Created by https://www.toptal.com/developers/gitignore/api/vivado\n# Edit at https://www.toptal.com/developers/gitignore?templates=vivado\n\n### Vivado ###\n#########################################################################################################\n##\tThis is an example .gitignore file for Vivado, please treat it as an example as\n##\tit might not be complete. In addition, XAPP 1165 should be followed.\n#########\n#Exclude all\n*\n!*/\n!.gitignore\n###########################################################################\n##\tVIVADO\n#Source files:\n#Do NOT ignore VHDL, Verilog, block diagrams or EDIF files.\n!*.vhd\n!*.v\n!*.sv\n!*.bd\n!*.edif\n#IP files\n#.xci: synthesis and implemented not possible - you need to return back to the previous version to generate output products\n#.xci + .dcp: implementation possible but not re-synthesis\n#*.xci(www.spiritconsortium.org)\n!*.xci\n#.xcix: Core container file\n#.xcix: https://www.xilinx.com/support/documentation/sw_manuals/xilinx2016_2/ug896-vivado-ip.pdf (Page 41)\n!*.xcix\n#*.dcp(checkpoint files)\n!*.dcp\n!*.vds\n!*.pb\n#All bd comments and layout coordinates are stored within .ui\n!*.ui\n!*.ooc\n#System Generator\n!*.mdl\n!*.slx\n!*.bxml\n#Simulation logic analyzer\n!*.wcfg\n!*.coe\n#MIG\n!*.prj\n!*.mem\n#Project files\n#XPR + *.XML ? XPR (Files are merged into a single XPR file for 2014.1 version)\n#Do NOT ignore *.xpr files\n!*.xpr\n#Include *.xml files for 2013.4 or earlier version\n!*.xml\n#Constraint files\n#Do NOT ignore *.xdc files\n!*.xdc\n#TCL - files\n!*.tcl\n#Journal - files\n!*.jou\n#Reports\n!*.rpt\n!*.txt\n!*.vdi\n#C-files\n!*.c\n!*.h\n!*.elf\n!*.bmm\n!*.xmp\n\n# End of https://www.toptal.com/developers/gitignore/api/vivado\n","vlab":"# Created by https://www.toptal.com/developers/gitignore/api/vlab\n# Edit at https://www.toptal.com/developers/gitignore?templates=vlab\n\n### VLab ###\n# VLab/Lstudio files\ninput.i\nlsys.dll\nlsys.so\nlsys.exp\nlsys.i\nlsys.ii\nlsys.lib\nlsys.obj\nlsys.o\nlpfg.log\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Compiled Dynamic libraries\n*.so\n*.dylib\n*.dll\n\n# Compiled Static libraries\n*.lai\n*.la\n*.a\n*.lib\n\n# Executables\n*.exe\n*.out\n*.app\n\n# End of https://www.toptal.com/developers/gitignore/api/vlab\n","vrealizeorchestrator":"# Created by https://www.toptal.com/developers/gitignore/api/vrealizeorchestrator\n# Edit at https://www.toptal.com/developers/gitignore?templates=vrealizeorchestrator\n\n### vRealizeOrchestrator ###\n# Default Resource Elements\nResources/Library/*\n\n# Library Workflows\nWorkflows/Library\n\n# Environment Specific variables\nConfigurations/*\n\n# Multi-Node Workflows\nWorkflows/VCO@*\n\n# All Packages\n**/Packages/\n\n# Test actions\nActions/test/*\n\n# All packages, resources, configurations, policies and Workflows\n**/Resources/\n**/Configurations/\n**/Workflows/\n**/Policy Templates/\n\n# End of https://www.toptal.com/developers/gitignore/api/vrealizeorchestrator\n","vs":"# Created by https://www.toptal.com/developers/gitignore/api/vs\n# Edit at https://www.toptal.com/developers/gitignore?templates=vs\n\n### vs ###\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*[.json, .xml, .info]\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# End of https://www.toptal.com/developers/gitignore/api/vs\n","vue":"# Created by https://www.toptal.com/developers/gitignore/api/vue\n# Edit at https://www.toptal.com/developers/gitignore?templates=vue\n\n### Vue ###\n# gitignore template for Vue.js projects\n#\n# Recommended template: Node.gitignore\n\n# TODO: where does this rule come from?\ndocs/_book\n\n# TODO: where does this rule come from?\ntest/\n\n# End of https://www.toptal.com/developers/gitignore/api/vue\n","vuejs":"# Created by https://www.toptal.com/developers/gitignore/api/vuejs\n# Edit at https://www.toptal.com/developers/gitignore?templates=vuejs\n\n### Vuejs ###\n# Recommended template: Node.gitignore\n\nnode_modules/\ndist/\nnpm-debug.log\nyarn-error.log\n\n# End of https://www.toptal.com/developers/gitignore/api/vuejs\n","vvvv":"# Created by https://www.toptal.com/developers/gitignore/api/vvvv\n# Edit at https://www.toptal.com/developers/gitignore?templates=vvvv\n\n### VVVV ###\n\n# .v4p backup files\n*~.xml\n\n# Dynamic plugins .dll\nbin/\n\n# End of https://www.toptal.com/developers/gitignore/api/vvvv\n","waf":"# Created by https://www.toptal.com/developers/gitignore/api/waf\n# Edit at https://www.toptal.com/developers/gitignore?templates=waf\n\n### Waf ###\n# For projects that use the Waf build system: https://waf.io/\n# Dot-hidden on Unix-like systems\n.waf-*-*/\n.waf3-*-*/\n# Hidden directory on Windows (no dot)\nwaf-*-*/\nwaf3-*-*/\n# Lockfile\n.lock-waf_*_build\n\n# End of https://www.toptal.com/developers/gitignore/api/waf\n","wakanda":"# Created by https://www.toptal.com/developers/gitignore/api/wakanda\n# Edit at https://www.toptal.com/developers/gitignore?templates=wakanda\n\n### Wakanda ###\n# Wakanda studio preferences\n*.waPreferences\n\n# Wakanda log\n*.waLog\n*/Logs\nstudio_log*\n\n# Wakanda code symbols\n*.waSym\n*.waSymData\n\n# Wakanda index\n*.waIndx\n*.Match\n\n# Wakanda data\n*.waData\n*(compacting)*\n\n# Wakanda User and Group\n*.cacheUAG\n*.waDirectory\n\n# Wakanda backup\n*.waBackup\n*.breakpoints\n\n# End of https://www.toptal.com/developers/gitignore/api/wakanda\n","web":"# Created by https://www.toptal.com/developers/gitignore/api/web\n# Edit at https://www.toptal.com/developers/gitignore?templates=web\n\n### Web ###\n*.asp\n*.cer\n*.csr\n*.css\n*.htm\n*.html\n*.js\n*.jsp\n*.php\n*.rss\n*.wasm\n*.wat\n*.xhtml\n\n# End of https://www.toptal.com/developers/gitignore/api/web\n","webmethods":"# Created by https://www.toptal.com/developers/gitignore/api/webmethods\n# Edit at https://www.toptal.com/developers/gitignore?templates=webmethods\n\n### WebMethods ###\n**/IntegrationServer/datastore/\n**/IntegrationServer/db/\n**/IntegrationServer/DocumentStore/\n**/IntegrationServer/lib/\n**/IntegrationServer/logs/\n**/IntegrationServer/replicate/\n**/IntegrationServer/sdk/\n**/IntegrationServer/support/\n**/IntegrationServer/update/\n**/IntegrationServer/userFtpRoot/\n**/IntegrationServer/web/\n**/IntegrationServer/WmRepository4/\n**/IntegrationServer/XAStore/\n**/IntegrationServer/packages/Wm*/\n\n# End of https://www.toptal.com/developers/gitignore/api/webmethods\n","webstorm":"# Created by https://www.toptal.com/developers/gitignore/api/webstorm\n# Edit at https://www.toptal.com/developers/gitignore?templates=webstorm\n\n### WebStorm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### WebStorm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# Azure Toolkit for IntelliJ plugin\n# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij\n.idea/**/azureSettings.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/webstorm\n","webstorm+all":"# Created by https://www.toptal.com/developers/gitignore/api/webstorm+all\n# Edit at https://www.toptal.com/developers/gitignore?templates=webstorm+all\n\n### WebStorm+all ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### WebStorm+all Patch ###\n# Ignore everything but code style settings and run configurations\n# that are supposed to be shared within teams.\n\n.idea/*\n\n!.idea/codeStyles\n!.idea/runConfigurations\n\n# End of https://www.toptal.com/developers/gitignore/api/webstorm+all\n","webstorm+iml":"# Created by https://www.toptal.com/developers/gitignore/api/webstorm+iml\n# Edit at https://www.toptal.com/developers/gitignore?templates=webstorm+iml\n\n### WebStorm+iml ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# AWS User-specific\n.idea/**/aws.xml\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn. Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# SonarLint plugin\n.idea/sonarlint/\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### WebStorm+iml Patch ###\n# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023\n\n*.iml\nmodules.xml\n.idea/misc.xml\n*.ipr\n\n# End of https://www.toptal.com/developers/gitignore/api/webstorm+iml\n","werckercli":"# Created by https://www.toptal.com/developers/gitignore/api/werckercli\n# Edit at https://www.toptal.com/developers/gitignore?templates=werckercli\n\n### WerckerCLI ###\n_steps\n_builds\n_cache\n_projects\n\n# End of https://www.toptal.com/developers/gitignore/api/werckercli\n","windows":"# Created by https://www.toptal.com/developers/gitignore/api/windows\n# Edit at https://www.toptal.com/developers/gitignore?templates=windows\n\n### Windows ###\n# Windows thumbnail cache files\nThumbs.db\nThumbs.db:encryptable\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msix\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# End of https://www.toptal.com/developers/gitignore/api/windows\n","wintersmith":"# Created by https://www.toptal.com/developers/gitignore/api/wintersmith\n# Edit at https://www.toptal.com/developers/gitignore?templates=wintersmith\n\n### Wintersmith ###\ncontents/\nnode_modules/\nplugins/\ntemplates/\nconfig.json\npackage.json\n\n\n# End of https://www.toptal.com/developers/gitignore/api/wintersmith\n","wordpress":"# Created by https://www.toptal.com/developers/gitignore/api/wordpress\n# Edit at https://www.toptal.com/developers/gitignore?templates=wordpress\n\n### WordPress ###\n# Core\n#\n# Note: if you want to stage/commit WP core files\n# you can delete this whole section/until Configuration.\n/wp-admin/\n/wp-content/index.php\n/wp-content/languages\n/wp-content/plugins/index.php\n/wp-content/themes/index.php\n/wp-includes/\n/index.php\n/license.txt\n/readme.html\n/wp-*.php\n/xmlrpc.php\n\n# Configuration\nwp-config.php\n\n# Example themes\n/wp-content/themes/twenty*/\n\n# Example plugin\n/wp-content/plugins/hello.php\n\n# Uploads\n/wp-content/uploads/\n\n# Log files\n*.log\n\n# htaccess\n/.htaccess\n\n# All plugins\n# Note: If you wish to whitelist plugins,\n# uncomment the next line\n#/wp-content/plugins\n\n# All themes\n# Note: If you wish to whitelist themes,\n# uncomment the next line\n#/wp-content/themes\n\n\n# End of https://www.toptal.com/developers/gitignore/api/wordpress\n","wyam":"# Created by https://www.toptal.com/developers/gitignore/api/wyam\n# Edit at https://www.toptal.com/developers/gitignore?templates=wyam\n\n### Wyam ###\n# Wyam Ignore - Based on https://wyam.io/docs/advanced/what_to_exclude_from_source_control\noutput/*\nconfig.wyam.dll\nconfig.wyam.hash\n*packages.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/wyam\n","xamarinstudio":"# Created by https://www.toptal.com/developers/gitignore/api/xamarinstudio\n# Edit at https://www.toptal.com/developers/gitignore?templates=xamarinstudio\n\n### XamarinStudio ###\nbin/\nobj/\n*.userprefs\n.packages\n\n# End of https://www.toptal.com/developers/gitignore/api/xamarinstudio\n","xcode":"# Created by https://www.toptal.com/developers/gitignore/api/xcode\n# Edit at https://www.toptal.com/developers/gitignore?templates=xcode\n\n### Xcode ###\n## User settings\nxcuserdata/\n\n## Xcode 8 and earlier\n*.xcscmblueprint\n*.xccheckout\n\n### Xcode Patch ###\n*.xcodeproj/*\n!*.xcodeproj/project.pbxproj\n!*.xcodeproj/xcshareddata/\n!*.xcodeproj/project.xcworkspace/\n!*.xcworkspace/contents.xcworkspacedata\n/*.gcno\n**/xcshareddata/WorkspaceSettings.xcsettings\n\n# End of https://www.toptal.com/developers/gitignore/api/xcode\n","xcodeinjection":"# Created by https://www.toptal.com/developers/gitignore/api/xcodeinjection\n# Edit at https://www.toptal.com/developers/gitignore?templates=xcodeinjection\n\n### XcodeInjection ###\n# Code Injection\n#\n# After new code Injection tools there's a generated folder /iOSInjectionProject\n# https://github.com/johnno1962/injectionforxcode\n\niOSInjectionProject/\n\n# End of https://www.toptal.com/developers/gitignore/api/xcodeinjection\n","xilinx":"# Created by https://www.toptal.com/developers/gitignore/api/xilinx\n# Edit at https://www.toptal.com/developers/gitignore?templates=xilinx\n\n### Xilinx ###\n# gitignore template for Xilinx Vivado Design Suite\n# website: https://www.xilinx.com/support/download.html\n\n# [home]\n*.jou\n*.log\n*.debug\n*.str\n*.zip\n*.tmp\n*.rst\n*.os\n*.js\n*.pb\n*.dcp\n*.hwdef\n*.vds\n*.veo\n*.wdf\n*.vdi\n*.dmp\n*.rpx\n*.rpt\n*_stub.v\n*_stub.vhdl\n*_funcsim.v\n*_funcsim.vhdl\n.project\n\n# [dir]\n*.cache\n.metadata\n*.data\n*.ipdefs\n.Xil\n*.sdk\n*.hw\n*.ip_user_files\n\n### IP synth\n*_synth_*\n\n.jobs\n\n### project synth\n*/*.runs/synth*/*.xml\n*/*.runs/synth*/*.txt\n*/*.runs/synth*/*.sh\n*/*.runs/synth*/*.tcl\n*/*.runs/synth*/*.bat\n*/*.runs/synth*/*.xdc\n!*/*.runs/synth*/*utilization*.rpt\n\n*.runs/synth*/*.xml\n*.runs/synth*/*.txt\n*.runs/synth*/*.sh\n*.runs/synth*/*.tcl\n*.runs/synth*/*.bat\n*.runs/synth*/*.xdc\n!*.runs/synth*/*utilization*.rpt\n\n### project impl\n*/*.runs/impl*/*.xml\n*/*.runs/impl*/*.html\n*/*.runs/impl*/*.txt\n*/*.runs/impl*/*.sh\n*/*.runs/impl*/*.tcl\n*/*.runs/impl*/*.bat\n!*/*.runs/impl*/*utilization*.rpt\n\n*.runs/impl*/*.xml\n*.runs/impl*/*.html\n*.runs/impl*/*.txt\n*.runs/impl*/*.sh\n*.runs/impl*/*.tcl\n*.runs/impl*/*.bat\n!*.runs/impl*/*utilization*.rpt\n\n### block design\n*/*/bd/*/hdl\n*/*/*/bd/*/hdl\n\n*/*/bd/*/*.xdc\n*/*/*/bd/*/*.xdc\n\n*/*/bd/*/ip/*/*.xdc\n*/*/*/bd/*/ip/*/*.xdc\n\n*/*/bd/*/ip/*/*/\n*/*/*/bd/*/ip/*/*/\n\n*/*/bd/*/ip/*/*.vhd\n*/*/*/bd/*/ip/*/*.vhd\n\n*/*/bd/*/ip/*/*.xml\n*/*/*/bd/*/ip/*/*.xml\n\n*.c\n*.h\n*.vho\n*.html\n*/*/bd/*/ip/*/*.tcl\n*/*/*/bd/*/ip/*/*.tcl\nhw_handoff\nipshared\n\n# End of https://www.toptal.com/developers/gitignore/api/xilinx\n","xilinxise":"# Created by https://www.toptal.com/developers/gitignore/api/xilinxise\n# Edit at https://www.toptal.com/developers/gitignore?templates=xilinxise\n\n### XilinxISE ###\n# intermediate build files\n*.bgn\n*.bit\n*.bld\n*.cmd_log\n*.drc\n*.ll\n*.lso\n*.msd\n*.msk\n*.ncd\n*.ngc\n*.ngd\n*.ngr\n*.pad\n*.par\n*.pcf\n*.prj\n*.ptwx\n*.rbb\n*.rbd\n*.stx\n*.syr\n*.twr\n*.twx\n*.unroutes\n*.ut\n*.xpi\n*.xst\n*_bitgen.xwbt\n*_envsettings.html\n*_map.map\n*_map.mrp\n*_map.ngm\n*_map.xrpt\n*_ngdbuild.xrpt\n*_pad.csv\n*_pad.txt\n*_par.xrpt\n*_summary.html\n*_summary.xml\n*_usage.xml\n*_xst.xrpt\n\n# iMPACT generated files\n_impactbatch.log\nimpact.xsl\nimpact_impact.xwbt\nise_impact.cmd\nwebtalk_impact.xml\n\n# Core Generator generated files\nxaw2verilog.log\n\n# project-wide generated files\n*.gise\npar_usage_statistics.html\nusage_statistics_webtalk.html\nwebtalk.log\nwebtalk_pn.xml\n\n# generated folders\niseconfig/\nxlnx_auto_0_xdb/\nxst/\n_ngo/\n_xmsgs/\n\n# End of https://www.toptal.com/developers/gitignore/api/xilinxise\n","xilinxvivado":"# Created by https://www.toptal.com/developers/gitignore/api/xilinxvivado\n# Edit at https://www.toptal.com/developers/gitignore?templates=xilinxvivado\n\n### XilinxVivado ###\n#########################################################################################################\n##\tThis is an example .gitignore file for Vivado, please treat it as an example as\n##\tit might not be complete. In addition, XAPP 1165 should be followed.\n#########\n#Exclude all\n*\n!*/\n!.gitignore\n###########################################################################\n##\tVIVADO\n#Source files:\n#Do NOT ignore VHDL, Verilog, block diagrams or EDIF files.\n!*.vhd\n!*.v\n!*.bd\n!*.edif\n#IP files\n#.xci: IP-core property file with core container disabled\n!*.xci\n#*.dcp (checkpoint files: better be ignored!)\n#!*.dcp\n!*.vds\n!*.pb\n#All bd comments and layout coordinates are stored within .ui\n!*.ui\n!*.ooc\n#System Generator\n!*.mdl\n!*.slx\n!*.bxml\n#Simulation logic analyzer\n!*.wcfg\n!*.coe\n#MIG\n!*.prj\n!*.mem\n#Project files\n#XPR + *.XML ? XPR (Files are merged into a single XPR file for 2014.1 version)\n#Do NOT ignore *.xpr files\n!*.xpr\n#Include *.xml files for 2013.4 or earlier version\n!*.xml\n#Constraint files\n#Do NOT ignore *.xdc files\n!*.xdc\n#TCL - files\n!*.tcl\n#Journal - files\n!*.jou\n#Reports\n!*.rpt\n!*.txt\n!*.vdi\n#C-files\n!*.c\n!*.h\n!*.elf\n!*.bmm\n!*.xmp\n\n# End of https://www.toptal.com/developers/gitignore/api/xilinxvivado\n","xill":"# Created by https://www.toptal.com/developers/gitignore/api/xill\n# Edit at https://www.toptal.com/developers/gitignore?templates=xill\n\n### Xill ###\n.project\n.xlib\ntestit-reports/\n.generated-tests/\ngenerated-tests/\nxill.properties\n\n# End of https://www.toptal.com/developers/gitignore/api/xill\n","xmake":"# Created by https://www.toptal.com/developers/gitignore/api/xmake\n# Edit at https://www.toptal.com/developers/gitignore?templates=xmake\n\n### xmake ###\n# Xmake cache\n.xmake/\nbuild/\n\n# End of https://www.toptal.com/developers/gitignore/api/xmake\n","xojo":"# Created by https://www.toptal.com/developers/gitignore/api/xojo\n# Edit at https://www.toptal.com/developers/gitignore?templates=xojo\n\n### Xojo ###\n# Xojo (formerly REALbasic and Real Studio)\n\nBuilds*\n*.debug\n*.debug.app\nDebug*.exe\nDebug*/Debug*.exe\nDebug*/Debug*\\ Libs\n*.rbuistate\n*.xojo_uistate\n*.obsolete*\n\n# End of https://www.toptal.com/developers/gitignore/api/xojo\n","xtext":"# Created by https://www.toptal.com/developers/gitignore/api/xtext\n# Edit at https://www.toptal.com/developers/gitignore?templates=xtext\n\n### XText ###\n# Those source folders can be generated by maven and Eclipse\ngenerated/\nsrc-gen/\nxtend-gen/\n\n# Ignore plugin.xml_gen and feature.xml_gen files, when build Eclipse Plugins.\n*.xml_gen\n\n# End of https://www.toptal.com/developers/gitignore/api/xtext\n","y86":"# Created by https://www.toptal.com/developers/gitignore/api/y86\n# Edit at https://www.toptal.com/developers/gitignore?templates=y86\n\n### Y86 ###\n# Object File\n*.yo\n\n# End of https://www.toptal.com/developers/gitignore/api/y86\n","yalc":"# Created by https://www.toptal.com/developers/gitignore/api/yalc\n# Edit at https://www.toptal.com/developers/gitignore?templates=yalc\n\n### yalc ###\n# yalc\n# https://www.npmjs.com/package/yalc/v/1.0.0-pre.17#keep-it-out-of-git\n\n.yalc\nyalc.lock\n\n# End of https://www.toptal.com/developers/gitignore/api/yalc\n","yarn":"# Created by https://www.toptal.com/developers/gitignore/api/yarn\n# Edit at https://www.toptal.com/developers/gitignore?templates=yarn\n\n### yarn ###\n# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored\n\n.yarn/*\n!.yarn/releases\n!.yarn/patches\n!.yarn/plugins\n!.yarn/sdks\n!.yarn/versions\n\n# if you are NOT using Zero-installs, then:\n# comment the following lines\n!.yarn/cache\n\n# and uncomment the following lines\n# .pnp.*\n\n# End of https://www.toptal.com/developers/gitignore/api/yarn\n","yeoman":"# Created by https://www.toptal.com/developers/gitignore/api/yeoman\n# Edit at https://www.toptal.com/developers/gitignore?templates=yeoman\n\n### Yeoman ###\nnode_modules/\nbower_components/\n*.log\n\nbuild/\ndist/\n\n# End of https://www.toptal.com/developers/gitignore/api/yeoman\n","yii":"# Created by https://www.toptal.com/developers/gitignore/api/yii\n# Edit at https://www.toptal.com/developers/gitignore?templates=yii\n\n### Yii ###\nassets/*\n!assets/.gitignore\nprotected/runtime/*\n!protected/runtime/.gitignore\nprotected/data/*.db\nthemes/classic/views/\n\n# End of https://www.toptal.com/developers/gitignore/api/yii\n","yii2":"# Created by https://www.toptal.com/developers/gitignore/api/yii2\n# Edit at https://www.toptal.com/developers/gitignore?templates=yii2\n\n### Yii2 ###\n# yii console commands\n/yii\n/yii_test\n/yii_test.bat\n\n# phpstorm project files\n.idea\n\n# netbeans project files\nnbproject\n\n# zend studio for eclipse project files\n.buildpath\n.project\n.settings\n\n# windows thumbnail cache\nThumbs.db\n\n# composer vendor dir\n/vendor\n\n# composer itself is not needed\ncomposer.phar\n\n# Mac DS_Store Files\n.DS_Store\n\n# phpunit itself is not needed\nphpunit.phar\n# local phpunit config\n/phpunit.xml\n\n# vagrant runtime\n/.vagrant\n\n# ignore generated files\n/frontend/web/index.php\n/frontend/web/index-test.php\n/frontend/web/robots.txt\n/backend/web/index.php\n/backend/web/index-test.php\n/backend/web/robots.txt\n\n# End of https://www.toptal.com/developers/gitignore/api/yii2\n","zendframework":"# Created by https://www.toptal.com/developers/gitignore/api/zendframework\n# Edit at https://www.toptal.com/developers/gitignore?templates=zendframework\n\n### ZendFramework ###\n# Composer files\ncomposer.phar\nvendor/\n\n# Local configs\nconfig/autoload/*.local.php\n\n# Binary gettext files\n*.mo\n\n# Data\ndata/logs/\ndata/cache/\ndata/sessions/\ndata/tmp/\ntemp/\n\n#Doctrine 2\ndata/DoctrineORMModule/Proxy/\ndata/DoctrineORMModule/cache/\n\n# Legacy ZF1\ndemos/\nextras/documentation\n\n# End of https://www.toptal.com/developers/gitignore/api/zendframework\n","zephir":"# Created by https://www.toptal.com/developers/gitignore/api/zephir\n# Edit at https://www.toptal.com/developers/gitignore?templates=zephir\n\n### Zephir ###\n# Cache files, generates by Zephir\n.temp/\n.libs/\n\n# Object files, generates by linker\n*.lo\n*.la\n*.o\n*.loT\n\n# Files generated by configure and Zephir,\n# not required for extension compilation.\next/build/\next/modules/\next/Makefile*\next/config*\next/acinclude.m4\next/aclocal.m4\next/autom4te*\next/install-sh\next/ltmain.sh\next/missing\next/mkinstalldirs\next/run-tests.php\next/.deps\next/libtool\n\n# End of https://www.toptal.com/developers/gitignore/api/zephir\n","zig":"# Created by https://www.toptal.com/developers/gitignore/api/zig\n# Edit at https://www.toptal.com/developers/gitignore?templates=zig\n\n### zig ###\n# Zig programming language\n\nzig-cache/\nzig-out/\nbuild/\nbuild-*/\ndocgen_tmp/\n\n# End of https://www.toptal.com/developers/gitignore/api/zig\n","zsh":"# Created by https://www.toptal.com/developers/gitignore/api/zsh\n# Edit at https://www.toptal.com/developers/gitignore?templates=zsh\n\n### Zsh ###\n# Zsh compiled script + zrecompile backup\n*.zwc\n*.zwc.old\n\n# Zsh completion-optimization dumpfile\n*zcompdump*\n\n# Zsh history\n.zsh_history\n\n# Zsh sessions\n.zsh_sessions\n\n# Zsh zcalc history\n.zcalc_history\n\n# A popular plugin manager's files\n._zinit\n.zinit_lstupd\n\n# zdharma/zshelldoc tool's files\nzsdoc/data\n\n# robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files\n# (when set-up to store the history in the local directory)\n.directory_history\n\n# MichaelAquilina/zsh-autoswitch-virtualenv plugin's files\n# (for Zsh plugins using Python)\n.venv\n\n# Zunit tests' output\n/tests/_output/*\n!/tests/_output/.gitkeep\n\n# End of https://www.toptal.com/developers/gitignore/api/zsh\n","zukencr8000":"# Created by https://www.toptal.com/developers/gitignore/api/zukencr8000\n# Edit at https://www.toptal.com/developers/gitignore?templates=zukencr8000\n\n### ZukenCR8000 ###\n## Zuken Global\n\n# log files\n*.log\n\n\n## Schematic\n\n# Schematic lock / edit in progress files\n*.s_t\n\n# Schematic Sheet backups\n*.shtb\n*.shtq\n\n# Schematic Design backups\n*undo.dsgn_\n\n# Schematic ERC/DRC results\n**/*template.cir/drc*\n\n# Schematic Frame\n**/*template.cir/frame*\n\n# Schematic log\n**/*template.cir/log*\n\n# Schematic exported netlist readme\n**/*template.cir/ext/readme.txt\n\n\n## Schematic <-> Layout communication\n\n# forward annotation files:\n*.coh\n*.loh\n*.coe\n\n\n## Layout\n\n# Layout Backups\n*.dsgn.bk\n\n# Layout Lockfiles\n*.dsgn.lk\n\n# Layout .crdb folder\n**/.crdb/\n\n# Layout .df_cache folder\n**/.df_cache/\n\n# Layout Automatic backup folders\n**/*dsgn.autobk/\n\n# Layout DRC result folder\n**/*.chk/\n\n# Layout Parameter file folders\n**/*.dsgn.prm/\n\n# Layout Signal Integrity result folders\n**/*.si/\n\n# Layout Power Integrity / EMI result folders\n**/*.emc/\n\n\n## DFM\n\n# DFM backups\n*.mdgn.bk\n\n# DFM lockfiles\n*.mdgn.lk\n\n# End of https://www.toptal.com/developers/gitignore/api/zukencr8000\n"}} diff --git a/apps/web/scripts/build-tauri.mjs b/apps/web/scripts/build-tauri.mjs new file mode 100644 index 00000000..e32b0ef6 --- /dev/null +++ b/apps/web/scripts/build-tauri.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +/** + * Tauri desktop static-export build. + * + * Next's `output: 'export'` cannot build API route handlers, middleware, or + * dynamic segments without generateStaticParams. Those are all server/marketing + * surface the desktop app doesn't need, so this script moves them aside, + * builds with TAURI_BUILD=1, and restores them afterwards (failure-safe). + * + * Requires Node >= 20.19 (older 20.x fails the export with a misleading + * `util.markAsUncloneable` error). Run: `nvm use 20.19.5` (or 22+) first. + * + * Usage: node scripts/build-tauri.mjs (from apps/web or repo root) + * Output: apps/web/out/ + */ +import { execSync, spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const excludedRoot = path.join(webRoot, ".tauri-excluded"); + +// --- Node version gate ----------------------------------------------------- +const [major, minor] = process.versions.node.split(".").map(Number); +if (major < 20 || (major === 20 && minor < 19)) { + console.error( + `build-tauri: Node ${process.versions.node} is too old — the static export fails on it ` + + `(red-herring 'util.markAsUncloneable' error). Use Node >= 20.19, e.g. 'nvm use 20.19.5'.` + ); + process.exit(1); +} + +// Paths (relative to apps/web) that cannot exist in a static export. +const EXCLUDES = [ + "src/app/api", // 50 route handlers + "src/middleware.ts", // middleware unsupported in export + "src/app/[username]", // dynamic, no generateStaticParams + "src/app/s", // short-link redirect segment + "src/app/blog", // marketing, server data + "src/app/compare", // marketing, server data + "src/app/tools", // marketing, server data + "src/app/llms.txt", // route handler + "src/app/robots.ts", // useless in app bundle + "src/app/sitemap.ts", // useless in app bundle +]; + +function moveAside() { + fs.mkdirSync(excludedRoot, { recursive: true }); + const moved = []; + for (const rel of EXCLUDES) { + const src = path.join(webRoot, rel); + if (!fs.existsSync(src)) { + console.warn(`build-tauri: skip missing exclude ${rel}`); + continue; + } + const dest = path.join(excludedRoot, rel); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.renameSync(src, dest); + moved.push(rel); + } + fs.writeFileSync(path.join(excludedRoot, "manifest.json"), JSON.stringify(moved, null, 2)); + return moved; +} + +function restore() { + if (!fs.existsSync(excludedRoot)) return; + const manifestPath = path.join(excludedRoot, "manifest.json"); + const moved = fs.existsSync(manifestPath) + ? JSON.parse(fs.readFileSync(manifestPath, "utf8")) + : EXCLUDES; + for (const rel of moved) { + const src = path.join(excludedRoot, rel); + const dest = path.join(webRoot, rel); + if (!fs.existsSync(src)) continue; + if (fs.existsSync(dest)) { + console.error(`build-tauri: restore conflict — both ${rel} and its excluded copy exist; leaving copy in .tauri-excluded`); + continue; + } + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.renameSync(src, dest); + } + // Remove the holding dir if fully drained. + try { + fs.rmSync(path.join(excludedRoot, "manifest.json"), { force: true }); + fs.rmSync(excludedRoot, { recursive: true }); // throws if non-empty leftovers + } catch { + /* leftovers preserved intentionally */ + } +} + +// Recover from a previous crashed run BEFORE doing anything else. +if (fs.existsSync(excludedRoot)) { + console.warn("build-tauri: found leftover .tauri-excluded from a previous run — restoring first."); + restore(); +} + +let restored = false; +const restoreOnce = () => { + if (restored) return; + restored = true; + restore(); +}; +process.on("SIGINT", () => { restoreOnce(); process.exit(130); }); +process.on("SIGTERM", () => { restoreOnce(); process.exit(143); }); +process.on("exit", restoreOnce); + +const moved = moveAside(); +console.log(`build-tauri: excluded ${moved.length} paths, building static export…`); + +const env = { + ...process.env, + TAURI_BUILD: "1", + NEXT_PUBLIC_TAURI: "1", + // Firebase init runs at import time; keep placeholders only when unset so a + // desktop build never hard-requires cloud credentials. + NEXT_PUBLIC_FIREBASE_API_KEY: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || "desktop-placeholder", + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || "desktop.invalid", + NEXT_PUBLIC_FIREBASE_PROJECT_ID: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || "desktop-placeholder", + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || "desktop.invalid", + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || "0", + NEXT_PUBLIC_FIREBASE_APP_ID: process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "desktop-placeholder", +}; + +const result = spawnSync("npx", ["next", "build"], { + cwd: webRoot, + env, + stdio: "inherit", +}); + +restoreOnce(); + +if (result.status !== 0) { + console.error("build-tauri: next build failed (excluded paths have been restored)."); + process.exit(result.status ?? 1); +} + +const outDir = path.join(webRoot, "out"); +const pageCount = execSync(`find ${JSON.stringify(outDir)} -name '*.html' | wc -l`).toString().trim(); +console.log(`build-tauri: OK — ${pageCount} HTML pages in apps/web/out/`); diff --git a/apps/web/src/app/app/database-explorer/page.tsx b/apps/web/src/app/app/database-explorer/page.tsx index 058838fa..c1146747 100644 --- a/apps/web/src/app/app/database-explorer/page.tsx +++ b/apps/web/src/app/app/database-explorer/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useEffect, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; import { ConnectionForm } from "@/components/nosql-explorer/connection-form"; @@ -211,7 +212,7 @@ export default function NoSQLExplorerPage() { updateTab(tab.id, { loading: true, error: null }); try { const skip = (tab.page - 1) * tab.limit; - const res = await fetch("/api/nosql/documents/query", { + const res = await apiFetch("/api/nosql/documents/query", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -302,7 +303,7 @@ export default function NoSQLExplorerPage() { try { const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/documents", { + const res = await apiFetch("/api/nosql/documents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -325,7 +326,7 @@ export default function NoSQLExplorerPage() { try { const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/documents", { + const res = await apiFetch("/api/nosql/documents", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -355,7 +356,7 @@ export default function NoSQLExplorerPage() { try { const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/documents", { + const res = await apiFetch("/api/nosql/documents", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -417,7 +418,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) return; const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/bulk-delete", { + const res = await apiFetch("/api/nosql/bulk-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -436,7 +437,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) return; const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/import", { + const res = await apiFetch("/api/nosql/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -455,7 +456,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) throw new Error("No active tab"); const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/schema", { + const res = await apiFetch("/api/nosql/schema", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -473,7 +474,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) throw new Error("No active tab"); const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/indexes/list", { + const res = await apiFetch("/api/nosql/indexes/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -491,7 +492,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) return; const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/indexes", { + const res = await apiFetch("/api/nosql/indexes", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -509,7 +510,7 @@ export default function NoSQLExplorerPage() { if (!activeTab) return; const conn = await getConnectionForTab(activeTab); - const res = await fetch("/api/nosql/indexes", { + const res = await apiFetch("/api/nosql/indexes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/apps/web/src/app/app/dns-lookup/page.tsx b/apps/web/src/app/app/dns-lookup/page.tsx index 9925a277..53251d82 100644 --- a/apps/web/src/app/app/dns-lookup/page.tsx +++ b/apps/web/src/app/app/dns-lookup/page.tsx @@ -1,8 +1,11 @@ import { DnsLookupLayout } from '@/components/dns-lookup/dns-lookup-layout'; +import { DesktopOnlineGate } from '@/components/desktop/desktop-online-gate'; export default function DnsLookupPage() { return ( -
- -
+ +
+ +
+
); } diff --git a/apps/web/src/app/app/email-validator/page.tsx b/apps/web/src/app/app/email-validator/page.tsx index 2cad6bd5..e61e8121 100644 --- a/apps/web/src/app/app/email-validator/page.tsx +++ b/apps/web/src/app/app/email-validator/page.tsx @@ -1,4 +1,9 @@ import { EmailValidator } from "@/components/email-validator/email-validator"; +import { DesktopOnlineGate } from "@/components/desktop/desktop-online-gate"; export default function EmailValidatorPage() { - return ; + return ( + + + + ); } diff --git a/apps/web/src/app/app/layout.tsx b/apps/web/src/app/app/layout.tsx index a36a960c..899a4963 100644 --- a/apps/web/src/app/app/layout.tsx +++ b/apps/web/src/app/app/layout.tsx @@ -4,11 +4,18 @@ import { AppContent } from "./app-content"; import { ToolJsonLd } from "@/components/seo/tool-json-ld"; import { toolSlugFromPathname } from "@/lib/seo/structured-data"; +// Static export (Tauri desktop) cannot use request APIs; fall back to defaults there. +const isTauriBuild = process.env.TAURI_BUILD === "1"; + export default async function Layout({ children }: { children: React.ReactNode }) { - const cookieStore = await cookies(); - const defaultOpen = cookieStore.get("sidebar:state")?.value !== "false"; - const headerList = await headers(); - const pathname = headerList.get("x-mdt-pathname") ?? ""; + let defaultOpen = true; + let pathname = ""; + if (!isTauriBuild) { + const cookieStore = await cookies(); + defaultOpen = cookieStore.get("sidebar:state")?.value !== "false"; + const headerList = await headers(); + pathname = headerList.get("x-mdt-pathname") ?? ""; + } const toolSlug = toolSlugFromPathname(pathname); return ( diff --git a/apps/web/src/app/app/redis-commander/page.tsx b/apps/web/src/app/app/redis-commander/page.tsx index 160e4c31..fec53ed9 100644 --- a/apps/web/src/app/app/redis-commander/page.tsx +++ b/apps/web/src/app/app/redis-commander/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -145,7 +146,7 @@ export default function RedisCommanderPage() { if (!activeTab) return; setFlushing(true); try { - const res = await fetch("/api/redis-commander/flush", { + const res = await apiFetch("/api/redis-commander/flush", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/app/app/s3-drive/page.tsx b/apps/web/src/app/app/s3-drive/page.tsx index 5085f263..f41dca8f 100644 --- a/apps/web/src/app/app/s3-drive/page.tsx +++ b/apps/web/src/app/app/s3-drive/page.tsx @@ -15,8 +15,17 @@ import { FileBrowser } from "@/components/s3-drive/file-browser" import { IconBucket, IconCloud, IconArrowRight } from "@tabler/icons-react" import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" +import { DesktopOnlineGate } from "@/components/desktop/desktop-online-gate" export default function S3DrivePage() { + return ( + + + + ) +} + +function S3DriveInner() { const { user, loading: authLoading } = useAuth(true) const { encryptionKey } = useMasterKeyStore() const { isUnlocked, isRestoring } = useVaultGuard() diff --git a/apps/web/src/app/app/url-shortener/page.tsx b/apps/web/src/app/app/url-shortener/page.tsx index e7d13b00..38fa5494 100644 --- a/apps/web/src/app/app/url-shortener/page.tsx +++ b/apps/web/src/app/app/url-shortener/page.tsx @@ -1,8 +1,11 @@ import { UrlShortenerTool } from '@/components/url-shortener/url-shortener-tool' +import { DesktopOnlineGate } from '@/components/desktop/desktop-online-gate' export default function UrlShortenerPage() { return ( -
- -
+ +
+ +
+
) } diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 6fa80af1..390f2741 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -7,6 +7,7 @@ import { GeistMono } from 'geist/font/mono' import { NextIntlClientProvider } from 'next-intl'; import { getLocale, getMessages } from 'next-intl/server'; import { ClientShell } from "@/components/client-shell"; +import { DesktopInit } from "@/components/desktop/desktop-init"; import "./globals.css"; @@ -122,6 +123,7 @@ export default async function RootLayout({ disableTransitionOnChange > + {children} diff --git a/apps/web/src/app/login/page.tsx b/apps/web/src/app/login/page.tsx index 63ccfe4c..f3bd122c 100644 --- a/apps/web/src/app/login/page.tsx +++ b/apps/web/src/app/login/page.tsx @@ -4,6 +4,7 @@ import { ArrowLeft, Cloud, ShieldCheck, Zap } from "lucide-react"; import { LegalAgreementFooter } from "@/components/legal-agreement-footer"; import { LoginForm } from "@/components/login-form"; import { LoginRedirectIfAuthed } from "@/components/login-redirect-if-authed"; +import { HideOnDesktop } from "@/components/desktop/hide-on-desktop"; import { Logo } from "@/components/logo"; import { Magnetic } from "@/components/mdt-magnetic"; import MdtAurora from "@/components/mdt-aurora"; @@ -69,15 +70,17 @@ export default function LoginPage() {
- - - - Back to home - - + + + + + Back to home + + +
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 6e82c835..ad2257a1 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -34,6 +34,7 @@ import { Building2, Check, } from "lucide-react"; +import { isDesktop } from "@/lib/desktop/is-desktop"; import { sidebarData } from "@/components/sidebar/data/sidebar-data"; import { homepageFaqItems } from "@/lib/seo/structured-data"; import MdtAurora from "@/components/mdt-aurora"; @@ -205,6 +206,21 @@ export default function Page() { const router = useRouter(); const goToLogin = () => router.push("/login"); + // Desktop launch: offline-first. The local master password is the real gate + // (MasterPasswordGate mounts inside /app), so always go straight to the tools + // — never block on the network. A background session probe just populates + // hasRemoteSession() for the sync engine and the "Sign in to sync" CTA; it + // must NOT influence routing, or an offline / signed-in-then-offline user + // would be bounced to /login. + const [desktopRedirecting] = useState(() => isDesktop()); + useEffect(() => { + if (!isDesktop()) return; + router.replace("/dashboard"); + void import("@/lib/desktop/remote").then(({ checkRemoteSession }) => + checkRemoteSession().catch(() => false) + ); + }, [router]); + const reduceMotion = useReducedMotion(); const [githubStars, setGithubStars] = useState(null); const [openFaqIdx, setOpenFaqIdx] = useState(null); @@ -240,6 +256,12 @@ export default function Page() { .catch(() => {}); }, []); + // On desktop we never render the marketing landing — hold a blank screen + // while the launch redirect resolves. + if (desktopRedirecting) { + return
; + } + return (
{/* deck atmosphere: fixed grid + grain behind everything + gradient def for icons */} diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 73c516bb..e04200a7 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -17,6 +17,9 @@ import { useRouter } from 'next/navigation' import { COLOR_THEME_OPTIONS, type ColorTheme, useColorTheme } from '@/hooks/use-color-theme' import { getToolMessageKey } from '@/lib/tool-i18n' import { PasskeySection } from '@/components/settings/passkey-section' +import { DesktopSyncSettings } from '@/components/desktop/desktop-sync-settings' +import { DesktopSyncConflicts } from '@/components/desktop/desktop-sync-conflicts' +import { DesktopBackupSettings } from '@/components/desktop/desktop-backup-settings' import { useActiveOrg, useActiveWorkspace } from '@/store/workspace-store' import { Briefcase, ChevronRight } from 'lucide-react' const colorDisplay: Record = { @@ -77,6 +80,12 @@ export default function SettingsPage() {
+ + + + + + diff --git a/apps/web/src/components/api-client/api-client.tsx b/apps/web/src/components/api-client/api-client.tsx index 726484b9..c0c074c5 100644 --- a/apps/web/src/components/api-client/api-client.tsx +++ b/apps/web/src/components/api-client/api-client.tsx @@ -1,5 +1,6 @@ "use client" +import { apiFetch } from "@/lib/desktop/api-fetch"; import * as React from "react" import { Card } from "@/components/ui/card" import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable" @@ -677,7 +678,7 @@ function ApiClientInner() { // ── SPNEGO / Kerberos branch ───────────────────────────────── if (activeTab.auth.type === "spnego" && activeTab.auth.spnego) { - const spnegoRes = await fetch("/api/proxy-spnego", { + const spnegoRes = await apiFetch("/api/proxy-spnego", { method: "POST", credentials: "include", signal: controller.signal, @@ -711,7 +712,7 @@ function ApiClientInner() { // route the request through the NTLM proxy and skip the streaming // branch (NTLM responses are bounded). if (activeTab.auth.type === "ntlm" && activeTab.auth.ntlm) { - const ntlmRes = await fetch("/api/proxy-ntlm", { + const ntlmRes = await apiFetch("/api/proxy-ntlm", { method: "POST", credentials: "include", signal: controller.signal, @@ -821,7 +822,7 @@ function ApiClientInner() { const finalUrlFromPlugins = beforeApplied.req.url // Send via Proxy - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", credentials: "include", signal: controller.signal, diff --git a/apps/web/src/components/api-client/fuzz-run-dialog.tsx b/apps/web/src/components/api-client/fuzz-run-dialog.tsx index 6ae9cfa9..bb5f2159 100644 --- a/apps/web/src/components/api-client/fuzz-run-dialog.tsx +++ b/apps/web/src/components/api-client/fuzz-run-dialog.tsx @@ -1,5 +1,6 @@ "use client" +import { apiFetch } from "@/lib/desktop/api-fetch"; import * as React from "react" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, @@ -60,7 +61,7 @@ export function FuzzRunDialog({ open, onOpenChange, tab }: FuzzRunDialogProps) { const sendOne = async (body: string) => { const start = nowMs() - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", credentials: "include", signal: abortRef.current!.signal, diff --git a/apps/web/src/components/api-client/recorder-dialog.tsx b/apps/web/src/components/api-client/recorder-dialog.tsx index 3f736ad9..a4611ed5 100644 --- a/apps/web/src/components/api-client/recorder-dialog.tsx +++ b/apps/web/src/components/api-client/recorder-dialog.tsx @@ -1,5 +1,6 @@ "use client" +import { apiFetch } from "@/lib/desktop/api-fetch"; import * as React from "react" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, @@ -62,7 +63,7 @@ export function RecorderDialog({ open, onOpenChange }: RecorderDialogProps) { session, async (req) => { const t0 = performance.now() - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/apps/web/src/components/bookmarks/bookmark-card.tsx b/apps/web/src/components/bookmarks/bookmark-card.tsx index 56f8085d..c79d8d59 100644 --- a/apps/web/src/components/bookmarks/bookmark-card.tsx +++ b/apps/web/src/components/bookmarks/bookmark-card.tsx @@ -14,6 +14,7 @@ import { } from "@tabler/icons-react" import { Bookmark, useBookmarkStore } from "@/store/bookmark-store" import { getFaviconUrl, getDomainFromUrl } from "@/lib/favicon-utils" +import { useFaviconSrc } from "@/lib/desktop/use-favicon" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" @@ -70,7 +71,9 @@ export default function BookmarkCard({ const { copyToClipboard } = useCopyToClipboard() const folder = folders.find(f => f.id === bookmark.folderId) - const faviconUrl = getFaviconUrl(bookmark.url) + // Desktop routes the favicon through the Rust proxy (data URL) and returns + // null offline; web uses the service URL directly. + const faviconUrl = useFaviconSrc(getFaviconUrl(bookmark.url)) const domain = getDomainFromUrl(bookmark.url) const handleOpenLink = useCallback(() => { @@ -144,7 +147,7 @@ export default function BookmarkCard({ )} {/* Favicon */}
- {!imageError ? ( + {faviconUrl && !imageError ? ( - {!imageError ? ( + {faviconUrl && !imageError ? ( String(n).padStart(2, "0"); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} + +function saveBlob(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); +} + +/** + * Desktop-only Backup & recovery card. Your data lives only on this Mac, so an + * encrypted backup file is the safety net against losing the app, the keychain, + * or the disk. Renders nothing on web. + */ +export function DesktopBackupSettings() { + const [backupOpen, setBackupOpen] = useState(false); + const [restoreOpen, setRestoreOpen] = useState(false); + const [busy, setBusy] = useState(false); + + // Backup dialog state + const [pass1, setPass1] = useState(""); + const [pass2, setPass2] = useState(""); + + // Restore dialog state + const [restorePass, setRestorePass] = useState(""); + const [file, setFile] = useState(null); + const fileInput = useRef(null); + + if (!isDesktop()) return null; + + const resetBackup = () => { + setPass1(""); + setPass2(""); + }; + const resetRestore = () => { + setRestorePass(""); + setFile(null); + if (fileInput.current) fileInput.current.value = ""; + }; + + const doBackup = async () => { + if (pass1 !== pass2) { + toast.error("The two passphrases don't match"); + return; + } + setBusy(true); + try { + const { exportBackup } = await import("@/lib/desktop/backup"); + const blob = await exportBackup(pass1); + saveBlob(blob, `mydevtools-backup-${todayStamp()}.json`); + toast.success("Backup downloaded. Store it somewhere safe."); + setBackupOpen(false); + resetBackup(); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Backup failed"); + } finally { + setBusy(false); + } + }; + + const doRestore = async () => { + if (!file) { + toast.error("Choose a backup file first"); + return; + } + setBusy(true); + try { + const text = await file.text(); + const { importBackup, hasConfiguredVault } = await import("@/lib/desktop/backup"); + if (await hasConfiguredVault()) { + const ok = window.confirm( + "This Mac already has a master password set up. Restoring keeps your " + + "current master password — entries from the backup will only open if " + + "they were encrypted with that same password. Continue?" + ); + if (!ok) { + setBusy(false); + return; + } + } + const { imported } = await importBackup(restorePass, text); + toast.success(`Restored ${imported} item${imported === 1 ? "" : "s"}.`); + setRestoreOpen(false); + resetRestore(); + } catch (e) { + toast.error(e instanceof Error ? e.message : "Restore failed"); + } finally { + setBusy(false); + } + }; + + return ( + + + + + Backup & recovery + + + Your data lives only on this Mac. Download an encrypted backup so you + can recover it on a new machine — or if this one is lost, reset, or its + keychain is wiped. The backup is protected by its own passphrase and + works even without Cloud Sync. + + + + + + + + {/* Backup dialog */} + { + setBackupOpen(o); + if (!o) resetBackup(); + }} + > + + + Download a backup + + Choose a passphrase to encrypt this backup. You'll need it to + restore — it can't be recovered if you forget it. + + +
+
+ + setPass1(e.target.value)} + /> +
+
+ + setPass2(e.target.value)} + /> +
+

+ + Anyone with this file and its passphrase can read your data. Store + both safely. +

+
+ + + +
+
+ + {/* Restore dialog */} + { + setRestoreOpen(o); + if (!o) resetRestore(); + }} + > + + + Restore from a backup + + Pick a backup file and enter the passphrase it was created with. + Restored items merge into what's already here. + + +
+
+ + setFile(e.target.files?.[0] ?? null)} + /> +
+
+ + setRestorePass(e.target.value)} + /> +
+
+ + + +
+
+
+ ); +} diff --git a/apps/web/src/components/desktop/desktop-init.tsx b/apps/web/src/components/desktop/desktop-init.tsx new file mode 100644 index 00000000..6a386dcc --- /dev/null +++ b/apps/web/src/components/desktop/desktop-init.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; + +import { isDesktop } from "@/lib/desktop/is-desktop"; + +/** + * Desktop-only bootstrap: deep-link sign-in listener + startup session probe. + * Renders nothing; a no-op on web (the isDesktop guard compiles to false). + */ +export function DesktopInit() { + const router = useRouter(); + useEffect(() => { + if (!isDesktop()) return; + // After browser sign-in completes, enter the workspace via Next routing. + const onAuthed = () => router.replace("/dashboard"); + window.addEventListener("mydevtools:desktop-authed", onAuthed); + void (async () => { + const [{ initDeepLinkListener }, { checkRemoteSession }, { startSyncEngine }] = + await Promise.all([ + import("@/lib/desktop/cloud-signin"), + import("@/lib/desktop/remote"), + import("@/lib/desktop/sync-engine"), + ]); + await initDeepLinkListener().catch(() => {}); + await checkRemoteSession().catch(() => {}); + startSyncEngine(); + })(); + return () => window.removeEventListener("mydevtools:desktop-authed", onAuthed); + }, [router]); + return null; +} diff --git a/apps/web/src/components/desktop/desktop-login.tsx b/apps/web/src/components/desktop/desktop-login.tsx new file mode 100644 index 00000000..d074e63e --- /dev/null +++ b/apps/web/src/components/desktop/desktop-login.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, ExternalLink, WifiOff } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +/** + * Desktop sign-in panel. OAuth popups don't work in WKWebView, so cloud + * sign-in opens the system browser (`/login?desktop=1`), which hands a + * Firebase custom token back through the loopback callback. + */ +export function DesktopLogin() { + const [busy, setBusy] = useState(false); + const [online, setOnline] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setOnline(navigator.onLine); + const on = () => setOnline(true); + const off = () => setOnline(false); + window.addEventListener("online", on); + window.addEventListener("offline", off); + // The deep-link handler navigates to /dashboard on success; nothing to do here. + return () => { + window.removeEventListener("online", on); + window.removeEventListener("offline", off); + }; + }, []); + + const signIn = async () => { + setBusy(true); + setError(null); + try { + const { startCloudSignIn } = await import("@/lib/desktop/cloud-signin"); + // Resolves once the browser hands the token back and the session is set; + // DesktopInit then routes to /dashboard. + await startCloudSignIn(); + } catch (e) { + setError(e instanceof Error ? e.message : "Sign-in failed. Please try again."); + } finally { + setBusy(false); + } + }; + + return ( +
+ {/* Stays clickable while waiting so a stalled attempt can be retried + (each click opens a fresh browser handoff; old waits time out). */} + + + {busy && ( +

+ Finish signing in in your browser, then return here. Click again to restart. +

+ )} + + {!online && ( +

+ + You're offline — sign-in needs a connection. +

+ )} + + {error &&

{error}

} + +

+ Your data is encrypted and stays on this Mac. +

+
+ ); +} diff --git a/apps/web/src/components/desktop/desktop-online-gate.tsx b/apps/web/src/components/desktop/desktop-online-gate.tsx new file mode 100644 index 00000000..e0d4c6fa --- /dev/null +++ b/apps/web/src/components/desktop/desktop-online-gate.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useOnlineSession } from "@/hooks/use-online-session"; +import { isDesktop } from "@/lib/desktop/is-desktop"; +import { CloudOff, Wifi } from "lucide-react"; + +/** + * Wrap tool pages that require the cloud even on desktop. On web (or when the + * desktop app is online with a cloud session) it renders children untouched. + */ +export function DesktopOnlineGate({ + toolName, + children, +}: { + toolName: string; + children: React.ReactNode; +}) { + const { online, hasCloudSession, ready } = useOnlineSession(); + + if (!isDesktop() || ready) return <>{children}; + + const signIn = async () => { + const { startCloudSignIn } = await import("@/lib/desktop/cloud-signin"); + await startCloudSignIn(); + }; + + return ( +
+ {online ? ( + + ) : ( + + )} +

{toolName} needs the cloud

+

+ {online + ? `${toolName} works with your MyDevTools cloud account. Sign in to continue — your other tools keep working offline.` + : `You're offline. ${toolName} needs a network connection; everything else keeps working locally.`} +

+ {online && !hasCloudSession && ( + + )} +
+ ); +} diff --git a/apps/web/src/components/desktop/desktop-sync-conflicts.tsx b/apps/web/src/components/desktop/desktop-sync-conflicts.tsx new file mode 100644 index 00000000..1c790ca1 --- /dev/null +++ b/apps/web/src/components/desktop/desktop-sync-conflicts.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AlertTriangle, RotateCcw, X } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { isDesktop } from "@/lib/desktop/is-desktop"; +import type { SyncConflict } from "@/lib/desktop/sync-engine"; + +/** Human labels for the tool_kinds that can conflict during sync. */ +const KIND_LABELS: Record = { + password_entries: "Password", + api_key_entries: "API key", + environment_entries: "Environment variable", + sql_connections: "SQL connection", + redis_connections: "Redis connection", + nosql_connections: "NoSQL connection", + code_snippets: "Code snippet", + bookmarks: "Bookmark", + bookmark_folders: "Bookmark folder", + notes: "Note", + projects: "Project", + tasks: "Task", + api_client_environments: "API client environment", + api_client_collections: "API client collection", + api_client_history: "API client history", +}; + +/** Best-effort human label for a discarded doc — plaintext only, never secrets. */ +function docLabel(doc: Record): string | null { + for (const key of ["name", "title", "text"] as const) { + const v = doc[key]; + if (typeof v === "string" && v.trim()) return v.trim(); + } + return null; +} + +function when(ms: number): string { + if (!ms) return "unknown time"; + try { + return new Date(ms).toLocaleString(); + } catch { + return "unknown time"; + } +} + +/** + * Desktop-only panel listing sync conflicts — the versions that last-writer-wins + * discarded when two devices edited the same row. Nothing is lost: each loser is + * kept and can be restored. Renders nothing on web or when there are none. + */ +export function DesktopSyncConflicts() { + const [conflicts, setConflicts] = useState([]); + const [busyId, setBusyId] = useState(null); + + useEffect(() => { + if (!isDesktop()) return; + // Promise-chain (not await) so the load stays off the effect's sync path. + const load = () => + void import("@/lib/desktop/sync-engine") + .then((m) => m.listConflicts()) + .then(setConflicts) + .catch(() => {}); + load(); + window.addEventListener("mydevtools:desktop-conflict", load); + window.addEventListener("mydevtools:desktop-synced", load); + return () => { + window.removeEventListener("mydevtools:desktop-conflict", load); + window.removeEventListener("mydevtools:desktop-synced", load); + }; + }, []); + + if (!isDesktop() || conflicts.length === 0) return null; + + const restore = async (c: SyncConflict) => { + setBusyId(c.id); + try { + const { restoreConflict } = await import("@/lib/desktop/sync-engine"); + await restoreConflict(c.id); + setConflicts((prev) => prev.filter((x) => x.id !== c.id)); + toast.success("Restored the other version — syncing it now."); + } catch { + toast.error("Couldn't restore that version."); + } finally { + setBusyId(null); + } + }; + + const dismiss = async (c: SyncConflict) => { + setBusyId(c.id); + try { + const { dismissConflict } = await import("@/lib/desktop/sync-engine"); + await dismissConflict(c.id); + setConflicts((prev) => prev.filter((x) => x.id !== c.id)); + } catch { + toast.error("Couldn't dismiss that conflict."); + } finally { + setBusyId(null); + } + }; + + return ( + + + + + Sync conflicts + + {conflicts.length} + + + + Another device edited the same item while you were offline. The most + recent edit is what you see now — the older version was kept here so + nothing is lost. Restore it to make it current, or dismiss once + reviewed. + + + + {conflicts.map((c) => { + const label = docLabel(c.loser_doc); + const kept = c.loser_side === "local" ? "this Mac's" : "the cloud"; + return ( +
+
+

+ {KIND_LABELS[c.tool_kind] ?? c.tool_kind} + {label ? · {label} : null} +

+

+ Discarded {kept} version · edited {when(c.loser_updated_at)} +

+
+
+ + +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/desktop/desktop-sync-settings.tsx b/apps/web/src/components/desktop/desktop-sync-settings.tsx new file mode 100644 index 00000000..6dd8de00 --- /dev/null +++ b/apps/web/src/components/desktop/desktop-sync-settings.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Cloud, RefreshCw } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { useOnlineSession } from "@/hooks/use-online-session"; +import { isDesktop } from "@/lib/desktop/is-desktop"; + +/** + * Desktop-only settings card: cloud sign-in + the personal-workspace sync + * toggle. Renders nothing on web. + */ +export function DesktopSyncSettings() { + const { online, hasCloudSession } = useOnlineSession(); + const [enabled, setEnabled] = useState(false); + const [busy, setBusy] = useState(false); + const [syncing, setSyncing] = useState(false); + + useEffect(() => { + if (!isDesktop()) return; + void import("@/lib/desktop/sync-engine").then(({ isSyncEnabled }) => + isSyncEnabled().then(setEnabled).catch(() => {}) + ); + }, []); + + if (!isDesktop()) return null; + + const toggle = async (next: boolean) => { + setBusy(true); + try { + const { setSyncEnabled } = await import("@/lib/desktop/sync-engine"); + await setSyncEnabled(next); + setEnabled(next); + } finally { + setBusy(false); + } + }; + + const signIn = async () => { + const { startCloudSignIn } = await import("@/lib/desktop/cloud-signin"); + await startCloudSignIn(); + }; + + const syncNow = async () => { + setSyncing(true); + try { + const { syncNow } = await import("@/lib/desktop/sync-engine"); + await syncNow(); + } finally { + setSyncing(false); + } + }; + + return ( + + + + + Cloud Sync + + + Your data lives on this Mac. Turn on sync to back up your personal + workspace to MyDevTools Cloud and share it with the web app. Shared + workspaces always live in the cloud. + + + + {!hasCloudSession ? ( +
+

+ {online + ? "Sign in to enable sync and shared workspaces." + : "You're offline — sign-in and sync will be available when you're back online."} +

+ +
+ ) : ( + <> +
+
+ +

+ Two-way sync; the most recent edit wins on conflicts. +

+
+ void toggle(v)} + /> +
+ {enabled && ( + + )} + + )} +
+
+ ); +} diff --git a/apps/web/src/components/desktop/hide-on-desktop.tsx b/apps/web/src/components/desktop/hide-on-desktop.tsx new file mode 100644 index 00000000..1639274f --- /dev/null +++ b/apps/web/src/components/desktop/hide-on-desktop.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { isDesktop } from "@/lib/desktop/is-desktop"; + +/** + * Renders children only on the web. Used to drop web-only chrome (e.g. the + * login page's "Back to home" link) from the desktop app, where there's no + * marketing site to go back to. + */ +export function HideOnDesktop({ children }: { children: React.ReactNode }) { + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + if (mounted && isDesktop()) return null; + return <>{children}; +} diff --git a/apps/web/src/components/favicon-img.tsx b/apps/web/src/components/favicon-img.tsx new file mode 100644 index 00000000..8e9f9f58 --- /dev/null +++ b/apps/web/src/components/favicon-img.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { useState } from "react"; + +import { useFaviconSrc } from "@/lib/desktop/use-favicon"; + +/** + * Renders a favicon `` or a fallback. On web the service URL is used + * directly; on desktop it's fetched through the Rust proxy (data URL) and falls + * back offline — so no third-party favicon request leaves the webview. + */ +export function FaviconImg({ + serviceUrl, + fallback, + className, +}: { + serviceUrl: string | null; + fallback: React.ReactNode; + className?: string; +}) { + const src = useFaviconSrc(serviceUrl); + const [error, setError] = useState(false); + if (!src || error) return <>{fallback}; + return setError(true)} />; +} diff --git a/apps/web/src/components/gitignore-generator/gitignore-layout.tsx b/apps/web/src/components/gitignore-generator/gitignore-layout.tsx index 5c5fc189..ba76fa1b 100644 --- a/apps/web/src/components/gitignore-generator/gitignore-layout.tsx +++ b/apps/web/src/components/gitignore-generator/gitignore-layout.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { useTranslations } from 'next-intl'; import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'; +import { isDesktop } from '@/lib/desktop/is-desktop'; import { Card } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; @@ -33,12 +34,15 @@ export function GitignoreLayout() { const [loadingOutput, setLoadingOutput] = React.useState(false); const [errorMsg, setErrorMsg] = React.useState(null); - // Fetch complete tag list on mount + // Fetch complete tag list on mount. Desktop reads the bundled template + // pack (public/gitignore-templates.json) so the tool works fully offline. React.useEffect(() => { - fetch('/api/gitignore?list=true') + const url = isDesktop() ? '/gitignore-templates.json' : '/api/gitignore?list=true'; + fetch(url) .then(r => r.json()) .then(data => { - if (Array.isArray(data)) setList(data); + const arr = isDesktop() ? (data as { list?: string[] }).list : data; + if (Array.isArray(arr)) setList(arr); else setErrorMsg(t('errorFetching')); }) .catch(() => setErrorMsg(t('errorFetching'))) @@ -56,11 +60,22 @@ export function GitignoreLayout() { setLoadingOutput(true); setErrorMsg(null); - fetch(`/api/gitignore?tags=${encodeURIComponent(selected.join(','))}`, { signal: abort.signal }) - .then(async r => { - if (!r.ok) throw new Error('Fetch failed'); - return r.text(); - }) + const load = isDesktop() + ? fetch('/gitignore-templates.json', { signal: abort.signal }) + .then(r => r.json()) + .then((data: { templates?: Record }) => + selected + .map(tag => (data.templates?.[tag] ?? '').trim()) + .filter(Boolean) + .join('\n\n') + ) + : fetch(`/api/gitignore?tags=${encodeURIComponent(selected.join(','))}`, { signal: abort.signal }) + .then(async r => { + if (!r.ok) throw new Error('Fetch failed'); + return r.text(); + }); + + load .then(text => setOutput(text)) .catch(err => { if (err.name !== 'AbortError') { diff --git a/apps/web/src/components/login-form.tsx b/apps/web/src/components/login-form.tsx index 31c22e04..50c651fe 100644 --- a/apps/web/src/components/login-form.tsx +++ b/apps/web/src/components/login-form.tsx @@ -14,14 +14,32 @@ import { import { auth } from "../database/firebase"; import { useEffect, useRef, useState } from "react"; import { establishBackendSession } from "@/lib/backend-auth"; +import { handoffDesktopToken } from "@/lib/desktop-handoff"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, AlertCircle, Github, Fingerprint } from "lucide-react"; import { signInWithPasskey, startConditionalPasskeyAuth } from "@/lib/passkey" import { acceptInvitation } from "@/lib/invitations-api" import { useWorkspaceStore } from "@/store/workspace-store" import { toast } from "sonner"; +import { isDesktop } from "@/lib/desktop/is-desktop"; +import { DesktopLogin } from "@/components/desktop/desktop-login"; export function LoginForm() { + // Gate on mount so the statically-exported HTML (window undefined → web form) + // doesn't mismatch the desktop client render. + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + if (!mounted) return null; + + // Desktop (Tauri) can't use OAuth popups in WKWebView — it signs in through + // the system browser instead. Web keeps the full provider/passkey flow. + if (isDesktop()) { + return ; + } + return ; +} + +function WebLoginForm() { const router = useRouter(); const [loadingProvider, setLoadingProvider] = useState<"google" | "github" | "passkey" | "">(""); const [error, setError] = useState(""); @@ -52,6 +70,16 @@ export function LoginForm() { const params = new URLSearchParams( typeof window !== "undefined" ? window.location.search : "" ) + // Desktop-app sign-in handoff: mint a token and hand it back to the app + // (loopback callback when ?cb= present, else the mydevtools:// deep link). + if (params.get("desktop") === "1") { + const ok = await handoffDesktopToken(window.location.search) + toast[ok ? "success" : "error"]( + ok ? "Signed in — returning to the MyDevTools app…" : "Could not hand off sign-in to the desktop app" + ) + return "/dashboard" + } + const token = params.get("invite") if (!token) return "/dashboard" diff --git a/apps/web/src/components/login-redirect-if-authed.tsx b/apps/web/src/components/login-redirect-if-authed.tsx index 3b1729fb..eecd9ce7 100644 --- a/apps/web/src/components/login-redirect-if-authed.tsx +++ b/apps/web/src/components/login-redirect-if-authed.tsx @@ -4,19 +4,38 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import useAuth from "@/utils/useAuth"; import { Loader2 } from "lucide-react"; +import { ensureBackendSession } from "@/lib/backend-auth"; +import { handoffDesktopToken, isDesktopHandoff } from "@/lib/desktop-handoff"; export function LoginRedirectIfAuthed() { const { user, loading } = useAuth(false); const router = useRouter(); + // On desktop the synthetic "desktop-local" user is always present; it must + // NOT bounce us off the sign-in screen. Only a real cloud user redirects. + const isRealUser = !!user && user.uid !== "desktop-local"; + useEffect(() => { - if (loading) return; - if (user) { - router.replace("/dashboard"); + if (loading || !isRealUser || !user) return; + // Desktop sign-in handoff (?desktop=1): an already-signed-in browser must + // mint the token and return to the app, NOT bounce to /dashboard. The + // backend session cookie may be stale (Firebase client outlives it), so + // refresh it before minting or the desktop-token endpoint 401s. + if (isDesktopHandoff(window.location.search)) { + void (async () => { + try { + await ensureBackendSession(user); + } catch { + /* fall through — handoff will surface failure to the app */ + } + await handoffDesktopToken(window.location.search); + })(); + return; } - }, [loading, user, router]); + router.replace("/dashboard"); + }, [loading, isRealUser, user, router]); - if (loading || user) { + if (loading || isRealUser) { return (
=> { if (!user) throw new Error(tCtx("authRequiredError")); + // Desktop: note images live in Firebase Storage (cloud). Only allow the + // upload when the user has turned on Cloud Sync AND is signed in — + // otherwise images would leave the machine while data is meant to stay + // local. Text notes always work offline. + if (isDesktop()) { + const { hasRemoteSession } = await import("@/lib/desktop/remote"); + const { isSyncEnabled } = await import("@/lib/desktop/sync-engine"); + const allowed = hasRemoteSession() && (await isSyncEnabled()); + if (user.uid === "desktop-local" || !allowed) { + throw new Error("Turn on Cloud Sync to store note images. Text notes stay local."); + } + } const timestamp = Date.now(); const safeName = sanitizeFileName(file.name); const storageRef = ref(storage, `notes/${user.uid}/${activeNoteId}/${timestamp}_${safeName}`); diff --git a/apps/web/src/components/password-manager/breach-check-dialog.tsx b/apps/web/src/components/password-manager/breach-check-dialog.tsx index 556abf8c..41a54847 100644 --- a/apps/web/src/components/password-manager/breach-check-dialog.tsx +++ b/apps/web/src/components/password-manager/breach-check-dialog.tsx @@ -3,6 +3,7 @@ import { useEffect, useCallback, useState } from "react" import { usePasswordStore } from "@/store/password-store" import { checkPasswordsBreached } from "@/lib/hibp" +import { useOnlineSession } from "@/hooks/use-online-session" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" @@ -26,8 +27,16 @@ export function BreachCheckDialog({ open, onOpenChange }: BreachCheckDialogProps clearBreachResults, } = usePasswordStore() + const { online } = useOnlineSession() + const runBreachCheck = useCallback(async () => { if (passwords.length === 0) return + // Breach check reaches HaveIBeenPwned (via the Rust proxy on desktop) — + // needs a connection. Don't attempt it offline. + if (typeof navigator !== "undefined" && !navigator.onLine) { + setBreachStatus("error") + return + } clearBreachResults() setBreachStatus("checking") try { @@ -42,12 +51,12 @@ export function BreachCheckDialog({ open, onOpenChange }: BreachCheckDialogProps } }, [passwords, clearBreachResults, setBreachStatus, setBreachProgress, setBreachResults]) - // Auto-start check when dialog opens and no results yet + // Auto-start check when dialog opens and no results yet (online only) useEffect(() => { - if (open && breachStatus === "idle") { + if (open && breachStatus === "idle" && online) { runBreachCheck() } - }, [open, breachStatus, runBreachCheck]) + }, [open, breachStatus, online, runBreachCheck]) const breachedEntries = passwords .filter((p) => (breachCounts.get(p.id) ?? 0) > 0) @@ -73,10 +82,15 @@ export function BreachCheckDialog({ open, onOpenChange }: BreachCheckDialogProps Checks all {passwords.length} passwords against Have I Been Pwned. Only a 5-char hash prefix is sent — your passwords never leave your browser.

- + {!online && ( +

+ Needs an internet connection. +

+ )}
)} diff --git a/apps/web/src/components/password-manager/password-card.tsx b/apps/web/src/components/password-manager/password-card.tsx index dec836fb..64b4debd 100644 --- a/apps/web/src/components/password-manager/password-card.tsx +++ b/apps/web/src/components/password-manager/password-card.tsx @@ -8,6 +8,7 @@ import { Copy, Eye, EyeOff, Trash2, ExternalLink, Pencil, MoreVertical, Clock, C import { computeTotp, decodeBase32Secret, getTotpSecondsRemaining } from "@/lib/totp-compute" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { calculatePasswordStrength, getStrengthColor, getFaviconUrl, getPasswordAgeStatus, getPasswordAgeBadge, getPasswordAgeDateColor } from "@/lib/password-utils" +import { FaviconImg } from "@/components/favicon-img" import { Badge } from "@/components/ui/badge" import { formatDistanceToNow } from "date-fns" import { cn } from "@/lib/utils" @@ -117,20 +118,11 @@ export function PasswordCard({
- {entry.url && getFaviconUrl(entry.url) ? ( - {entry.service} { - (e.target as HTMLImageElement).style.display = 'none'; - (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); - }} - /> - ) : null} - - {entry.service.charAt(0).toUpperCase()} - + {entry.service.charAt(0).toUpperCase()}} + />
{entry.service} diff --git a/apps/web/src/components/password-manager/password-item-swipeable.tsx b/apps/web/src/components/password-manager/password-item-swipeable.tsx index f0de330b..752ec9cd 100644 --- a/apps/web/src/components/password-manager/password-item-swipeable.tsx +++ b/apps/web/src/components/password-manager/password-item-swipeable.tsx @@ -6,6 +6,7 @@ import { PasswordEntry } from "@/store/password-store" import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" import { getFaviconUrl } from "@/lib/password-utils" +import { FaviconImg } from "@/components/favicon-img" import { useState } from "react" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { useTranslations } from "next-intl" @@ -96,20 +97,11 @@ export function PasswordItemSwipeable({ > {/* Icon */}
- {entry.url && getFaviconUrl(entry.url) ? ( - {entry.service} { - (e.target as HTMLImageElement).style.display = 'none'; - (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); - }} - /> - ) : null} - - {entry.service.charAt(0).toUpperCase()} - + {entry.service.charAt(0).toUpperCase()}} + />
{/* Content */} diff --git a/apps/web/src/components/password-manager/password-list.tsx b/apps/web/src/components/password-manager/password-list.tsx index fe3c5edb..90bba2e4 100644 --- a/apps/web/src/components/password-manager/password-list.tsx +++ b/apps/web/src/components/password-manager/password-list.tsx @@ -22,6 +22,7 @@ import { AddPasswordDialog } from "./add-password-dialog" import { PasswordEntry } from "@/store/password-store" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { calculatePasswordStrength, getStrengthColor, getFaviconUrl, getPasswordAgeStatus, getPasswordAgeBadge, getPasswordAgeDateColor } from "@/lib/password-utils" +import { FaviconImg } from "@/components/favicon-img" import { Badge } from "@/components/ui/badge" import { formatDistanceToNow } from "date-fns" import { ImportExportDialog } from "./import-export-dialog" @@ -645,20 +646,11 @@ export function PasswordList() {
- {entry.url && getFaviconUrl(entry.url) ? ( - {entry.service} { - (e.target as HTMLImageElement).style.display = 'none'; - (e.target as HTMLImageElement).nextElementSibling?.classList.remove('hidden'); - }} - /> - ) : null} - - {entry.service.charAt(0).toUpperCase()} - + {entry.service.charAt(0).toUpperCase()}} + />
{entry.service} diff --git a/apps/web/src/components/redis-commander/bulk-delete-dialog.tsx b/apps/web/src/components/redis-commander/bulk-delete-dialog.tsx index 259957aa..30d87183 100644 --- a/apps/web/src/components/redis-commander/bulk-delete-dialog.tsx +++ b/apps/web/src/components/redis-commander/bulk-delete-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState } from "react"; import { AlertDialog, @@ -37,7 +38,7 @@ export function BulkDeleteDialog({ if (!pattern.trim()) return; setBusy(true); try { - const res = await fetch("/api/redis-commander/bulk-delete", { + const res = await apiFetch("/api/redis-commander/bulk-delete", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -57,7 +58,7 @@ export function BulkDeleteDialog({ if (!pattern.trim() || !dryRun) return; setBusy(true); try { - const res = await fetch("/api/redis-commander/bulk-delete", { + const res = await apiFetch("/api/redis-commander/bulk-delete", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/bulk-export-dialog.tsx b/apps/web/src/components/redis-commander/bulk-export-dialog.tsx index d84bbfb2..8a66b6ec 100644 --- a/apps/web/src/components/redis-commander/bulk-export-dialog.tsx +++ b/apps/web/src/components/redis-commander/bulk-export-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState } from "react"; import { Dialog, @@ -31,7 +32,7 @@ export function BulkExportDialog({ async function run() { setBusy(true); try { - const res = await fetch("/api/redis-commander/export", { + const res = await apiFetch("/api/redis-commander/export", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/bulk-import-dialog.tsx b/apps/web/src/components/redis-commander/bulk-import-dialog.tsx index 973b4c6d..8b126057 100644 --- a/apps/web/src/components/redis-commander/bulk-import-dialog.tsx +++ b/apps/web/src/components/redis-commander/bulk-import-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import React, { useRef, useState } from "react"; import { Dialog, @@ -57,7 +58,7 @@ export function BulkImportDialog({ if (!keysToImport) return; setBusy(true); try { - const res = await fetch("/api/redis-commander/import", { + const res = await apiFetch("/api/redis-commander/import", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/command-panel.tsx b/apps/web/src/components/redis-commander/command-panel.tsx index 1972249e..61d72e7c 100644 --- a/apps/web/src/components/redis-commander/command-panel.tsx +++ b/apps/web/src/components/redis-commander/command-panel.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -76,7 +77,7 @@ export function CommandPanel({ redisUrl, db, connectionId }: CommandPanelProps) setLoading(true); const start = Date.now(); try { - const res = await fetch("/api/redis-commander/execute", { + const res = await apiFetch("/api/redis-commander/execute", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/connection-form.tsx b/apps/web/src/components/redis-commander/connection-form.tsx index 1bb2c5c5..5516d785 100644 --- a/apps/web/src/components/redis-commander/connection-form.tsx +++ b/apps/web/src/components/redis-commander/connection-form.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -98,7 +99,7 @@ export function ConnectionForm({ setTesting(true); try { - const res = await fetch("/api/redis-commander/connect", { + const res = await apiFetch("/api/redis-commander/connect", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/json-editor.tsx b/apps/web/src/components/redis-commander/json-editor.tsx index b5010bd4..644a02b1 100644 --- a/apps/web/src/components/redis-commander/json-editor.tsx +++ b/apps/web/src/components/redis-commander/json-editor.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -24,7 +25,7 @@ export function JsonEditor({ redisUrl, db, keyName, onChanged }: Props) { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/json", { + const res = await apiFetch("/api/redis-commander/json", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -60,7 +61,7 @@ export function JsonEditor({ redisUrl, db, keyName, onChanged }: Props) { } setSaving(true); try { - const res = await fetch("/api/redis-commander/json", { + const res = await apiFetch("/api/redis-commander/json", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -79,7 +80,7 @@ export function JsonEditor({ redisUrl, db, keyName, onChanged }: Props) { async function del() { try { - const res = await fetch("/api/redis-commander/json", { + const res = await apiFetch("/api/redis-commander/json", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/key-browser.tsx b/apps/web/src/components/redis-commander/key-browser.tsx index 64a1c6d7..270d58bf 100644 --- a/apps/web/src/components/redis-commander/key-browser.tsx +++ b/apps/web/src/components/redis-commander/key-browser.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useCallback, useEffect, useRef } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -210,7 +211,7 @@ export function KeyBrowser({ loadingRef.current = true; setLoading(true); try { - const res = await fetch("/api/redis-commander/keys", { + const res = await apiFetch("/api/redis-commander/keys", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/metrics-pane.tsx b/apps/web/src/components/redis-commander/metrics-pane.tsx index 94274cb9..3d9ff5e4 100644 --- a/apps/web/src/components/redis-commander/metrics-pane.tsx +++ b/apps/web/src/components/redis-commander/metrics-pane.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useEffect, useRef, useState } from "react"; import { Sparkline } from "./sparkline"; @@ -40,7 +41,7 @@ export function MetricsPane({ redisUrl, db }: { redisUrl: string; db: number }) let abort = false; async function tick() { try { - const res = await fetch("/api/redis-commander/info", { + const res = await apiFetch("/api/redis-commander/info", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/pubsub-pane.tsx b/apps/web/src/components/redis-commander/pubsub-pane.tsx index efcb617e..f143eca3 100644 --- a/apps/web/src/components/redis-commander/pubsub-pane.tsx +++ b/apps/web/src/components/redis-commander/pubsub-pane.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -95,7 +96,7 @@ export function PubSubPane({ redisUrl, db }: PubSubPaneProps) { if (!pubChannel.trim()) return; setPublishing(true); try { - const res = await fetch("/api/redis-commander/pubsub", { + const res = await apiFetch("/api/redis-commander/pubsub", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/scanner-pane.tsx b/apps/web/src/components/redis-commander/scanner-pane.tsx index 5d770c2b..38235ff2 100644 --- a/apps/web/src/components/redis-commander/scanner-pane.tsx +++ b/apps/web/src/components/redis-commander/scanner-pane.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -43,7 +44,7 @@ export function ScannerPane({ redisUrl, db, onSelectKey }: { redisUrl: string; d setBusy(true); setResult(null); try { - const res = await fetch("/api/redis-commander/scanner", { + const res = await apiFetch("/api/redis-commander/scanner", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/search-workbench.tsx b/apps/web/src/components/redis-commander/search-workbench.tsx index 818f5d02..d8e02348 100644 --- a/apps/web/src/components/redis-commander/search-workbench.tsx +++ b/apps/web/src/components/redis-commander/search-workbench.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -29,7 +30,7 @@ export function SearchWorkbench({ redisUrl, db, onSelectKey }: { redisUrl: strin const loadIndexes = useCallback(async () => { setBusy(true); try { - const res = await fetch("/api/redis-commander/search", { + const res = await apiFetch("/api/redis-commander/search", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -58,7 +59,7 @@ export function SearchWorkbench({ redisUrl, db, onSelectKey }: { redisUrl: strin async function loadInfo() { if (!selectedIndex) return; try { - const res = await fetch("/api/redis-commander/search", { + const res = await apiFetch("/api/redis-commander/search", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -76,7 +77,7 @@ export function SearchWorkbench({ redisUrl, db, onSelectKey }: { redisUrl: strin if (!selectedIndex || !query.trim()) return; setBusy(true); try { - const res = await fetch("/api/redis-commander/search", { + const res = await apiFetch("/api/redis-commander/search", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -97,7 +98,7 @@ export function SearchWorkbench({ redisUrl, db, onSelectKey }: { redisUrl: strin async function runExplain() { if (!selectedIndex || !query.trim()) return; try { - const res = await fetch("/api/redis-commander/search", { + const res = await apiFetch("/api/redis-commander/search", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/server-dashboard-panes.tsx b/apps/web/src/components/redis-commander/server-dashboard-panes.tsx index e812cd82..cfdfa054 100644 --- a/apps/web/src/components/redis-commander/server-dashboard-panes.tsx +++ b/apps/web/src/components/redis-commander/server-dashboard-panes.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -65,7 +66,7 @@ export function InfoPane({ redisUrl, db }: Props) { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/info", { + const res = await apiFetch("/api/redis-commander/info", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -165,7 +166,7 @@ export function SlowLogPane({ redisUrl, db }: Props) { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/slowlog", { + const res = await apiFetch("/api/redis-commander/slowlog", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -185,7 +186,7 @@ export function SlowLogPane({ redisUrl, db }: Props) { async function reset() { try { - const res = await fetch("/api/redis-commander/slowlog", { + const res = await apiFetch("/api/redis-commander/slowlog", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -272,7 +273,7 @@ export function ClientsPane({ redisUrl, db }: Props) { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/clients", { + const res = await apiFetch("/api/redis-commander/clients", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -292,7 +293,7 @@ export function ClientsPane({ redisUrl, db }: Props) { async function kill(clientId: string) { try { - const res = await fetch("/api/redis-commander/clients", { + const res = await apiFetch("/api/redis-commander/clients", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/stream-editor.tsx b/apps/web/src/components/redis-commander/stream-editor.tsx index 8c3e4113..c060e88b 100644 --- a/apps/web/src/components/redis-commander/stream-editor.tsx +++ b/apps/web/src/components/redis-commander/stream-editor.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -68,7 +69,7 @@ function EntriesPane({ redisUrl, db, streamKey, onRefresh }: Props) { const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/streams", { + const res = await apiFetch("/api/redis-commander/streams", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -93,7 +94,7 @@ function EntriesPane({ redisUrl, db, streamKey, onRefresh }: Props) { return; } try { - const res = await fetch("/api/redis-commander/streams", { + const res = await apiFetch("/api/redis-commander/streams", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -113,7 +114,7 @@ function EntriesPane({ redisUrl, db, streamKey, onRefresh }: Props) { async function deleteEntry(id: string) { try { - const res = await fetch("/api/redis-commander/streams", { + const res = await apiFetch("/api/redis-commander/streams", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -249,7 +250,7 @@ function GroupsPane({ redisUrl, db, streamKey }: Pick { setLoading(true); try { - const res = await fetch("/api/redis-commander/streams", { + const res = await apiFetch("/api/redis-commander/streams", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/timeseries-viewer.tsx b/apps/web/src/components/redis-commander/timeseries-viewer.tsx index 6a460a27..567cecb6 100644 --- a/apps/web/src/components/redis-commander/timeseries-viewer.tsx +++ b/apps/web/src/components/redis-commander/timeseries-viewer.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -21,7 +22,7 @@ export function TimeSeriesViewer({ redisUrl, db, keyName }: { redisUrl: string; const load = useCallback(async () => { setLoading(true); try { - const res = await fetch("/api/redis-commander/timeseries", { + const res = await apiFetch("/api/redis-commander/timeseries", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/redis-commander/types.ts b/apps/web/src/components/redis-commander/types.ts index c065cee5..f4dd9a2d 100644 --- a/apps/web/src/components/redis-commander/types.ts +++ b/apps/web/src/components/redis-commander/types.ts @@ -49,6 +49,8 @@ export interface SavedRedisConnection { folder?: string; createdAt: number; lastUsedAt: number; + /** Content-edit clock (sync LWW); server-managed. */ + updatedAt?: number; /** Populated client-side after decryption — never sent to the server. */ config?: RedisConnectionConfig; } diff --git a/apps/web/src/components/redis-commander/value-editor.tsx b/apps/web/src/components/redis-commander/value-editor.tsx index 8b7e4848..0e8ae7aa 100644 --- a/apps/web/src/components/redis-commander/value-editor.tsx +++ b/apps/web/src/components/redis-commander/value-editor.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -73,7 +74,7 @@ export function ValueEditor({ redisUrl, db, selectedKey, onKeyDeleted, onKeyRena async function loadKey(key: string) { setLoading(true); try { - const res = await fetch("/api/redis-commander/key", { + const res = await apiFetch("/api/redis-commander/key", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -139,7 +140,7 @@ export function ValueEditor({ redisUrl, db, selectedKey, onKeyDeleted, onKeyRena return; } const ttl = ttlInput ? parseInt(ttlInput) : -1; - const res = await fetch("/api/redis-commander/key", { + const res = await apiFetch("/api/redis-commander/key", { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -159,7 +160,7 @@ export function ValueEditor({ redisUrl, db, selectedKey, onKeyDeleted, onKeyRena async function handleDelete() { if (!detail) return; try { - const res = await fetch("/api/redis-commander/key", { + const res = await apiFetch("/api/redis-commander/key", { method: "DELETE", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -181,7 +182,7 @@ export function ValueEditor({ redisUrl, db, selectedKey, onKeyDeleted, onKeyRena if (!newKey || newKey === detail.key) return; setActionBusy(true); try { - const res = await fetch("/api/redis-commander/key/rename", { + const res = await apiFetch("/api/redis-commander/key/rename", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, @@ -209,7 +210,7 @@ export function ValueEditor({ redisUrl, db, selectedKey, onKeyDeleted, onKeyRena if (!destination || destination === detail.key) return; setActionBusy(true); try { - const res = await fetch("/api/redis-commander/key/copy", { + const res = await apiFetch("/api/redis-commander/key/copy", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/sql-client/connection-form.tsx b/apps/web/src/components/sql-client/connection-form.tsx index 27faa36d..4baca9da 100644 --- a/apps/web/src/components/sql-client/connection-form.tsx +++ b/apps/web/src/components/sql-client/connection-form.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -84,7 +85,7 @@ export function ConnectionForm({ } setIsTesting(true); try { - const res = await fetch("/api/sql-client/connect", { + const res = await apiFetch("/api/sql-client/connect", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/sql-client/query-editor.tsx b/apps/web/src/components/sql-client/query-editor.tsx index c5561ef3..868c3a2d 100644 --- a/apps/web/src/components/sql-client/query-editor.tsx +++ b/apps/web/src/components/sql-client/query-editor.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { @@ -43,7 +44,7 @@ export function QueryEditor({ tab, connection, onQueryChange, onResult, onClose setIsRunning(true); onResult(null, null); try { - const res = await fetch("/api/sql-client/query", { + const res = await apiFetch("/api/sql-client/query", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/sql-client/schema-sidebar.tsx b/apps/web/src/components/sql-client/schema-sidebar.tsx index 92ab4eee..2c28be95 100644 --- a/apps/web/src/components/sql-client/schema-sidebar.tsx +++ b/apps/web/src/components/sql-client/schema-sidebar.tsx @@ -1,5 +1,6 @@ "use client"; +import { apiFetch } from "@/lib/desktop/api-fetch"; import { useState, useEffect, useRef } from "react"; import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; import { Button } from "@/components/ui/button"; @@ -67,7 +68,7 @@ export function SchemaSidebar({ if (!conn.config) return; setState(conn.id, { loading: true, error: null }); try { - const res = await fetch("/api/sql-client/tables", { + const res = await apiFetch("/api/sql-client/tables", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/components/sql-client/types.ts b/apps/web/src/components/sql-client/types.ts index 711277ca..7e2094b9 100644 --- a/apps/web/src/components/sql-client/types.ts +++ b/apps/web/src/components/sql-client/types.ts @@ -22,6 +22,8 @@ export interface SavedSqlConnection { type: DbType; createdAt: number; lastUsedAt: number; + /** Content-edit clock (sync LWW); server-managed. */ + updatedAt?: number; /** Populated client-side after decryption — never sent to the server. */ config?: SqlConnectionConfig; } diff --git a/apps/web/src/hooks/use-online-session.ts b/apps/web/src/hooks/use-online-session.ts new file mode 100644 index 00000000..f9b46ae6 --- /dev/null +++ b/apps/web/src/hooks/use-online-session.ts @@ -0,0 +1,50 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { isDesktop } from "@/lib/desktop/is-desktop"; + +export type OnlineSessionState = { + /** navigator.onLine (desktop only; always true on web). */ + online: boolean; + /** Desktop: backend session present in the Rust jar. Web: always true. */ + hasCloudSession: boolean; + /** Convenience: online && hasCloudSession. */ + ready: boolean; +}; + +/** + * Gate for tools that require the cloud even on desktop (s3-drive, + * url-shortener, dns-lookup, email-validator, break-room, notes images). + * On web this is a constant `ready: true` — no behavior change. + */ +export function useOnlineSession(): OnlineSessionState { + const [online, setOnline] = useState(true); + const [hasCloudSession, setHasCloudSession] = useState(!isDesktop()); + + useEffect(() => { + if (!isDesktop()) return; + setOnline(navigator.onLine); + const onOnline = () => setOnline(true); + const onOffline = () => setOnline(false); + window.addEventListener("online", onOnline); + window.addEventListener("offline", onOffline); + + const onSession = (e: Event) => + setHasCloudSession(Boolean((e as CustomEvent<{ ok: boolean }>).detail?.ok)); + window.addEventListener("mydevtools:desktop-session", onSession); + + void import("@/lib/desktop/remote").then(({ hasRemoteSession, checkRemoteSession }) => { + setHasCloudSession(hasRemoteSession()); + void checkRemoteSession().then(setHasCloudSession); + }); + + return () => { + window.removeEventListener("online", onOnline); + window.removeEventListener("offline", onOffline); + window.removeEventListener("mydevtools:desktop-session", onSession); + }; + }, []); + + return { online, hasCloudSession, ready: online && hasCloudSession }; +} diff --git a/apps/web/src/i18n/request.ts b/apps/web/src/i18n/request.ts index cbfd5cea..5679dfd0 100644 --- a/apps/web/src/i18n/request.ts +++ b/apps/web/src/i18n/request.ts @@ -4,9 +4,15 @@ import { cookies } from 'next/headers'; const locales = ['en', 'fr', 'es', 'ar', 'ca', 'zh', 'cs', 'el', 'de', 'da', 'af', 'id', 'fa', 'ru', 'it', 'ja', 'ko', 'ms', 'nb', 'nl', 'sv', 'pl', 'tr', 'pt', 'pt-BR', 'vi', 'uk'] as const; type AppLocale = (typeof locales)[number]; +// Static export (Tauri desktop) has no request cookies; locale is fixed at build time. +const isTauriBuild = process.env.TAURI_BUILD === '1'; + export default getRequestConfig(async () => { - const cookieStore = await cookies(); - const raw = cookieStore.get('NEXT_LOCALE')?.value || 'en'; + let raw = 'en'; + if (!isTauriBuild) { + const cookieStore = await cookies(); + raw = cookieStore.get('NEXT_LOCALE')?.value || 'en'; + } const locale: AppLocale = locales.includes(raw as AppLocale) ? (raw as AppLocale) : 'en'; diff --git a/apps/web/src/lib/__tests__/backup-code-recovery.test.ts b/apps/web/src/lib/__tests__/backup-code-recovery.test.ts new file mode 100644 index 00000000..725e2ef4 --- /dev/null +++ b/apps/web/src/lib/__tests__/backup-code-recovery.test.ts @@ -0,0 +1,70 @@ +/** + * backup-code-recovery.test.ts + * + * Regression guard for master-password recovery via backup codes. + * + * The store side (encryptWithBackupCode) and the unlock side once derived the + * lookup codeId independently and drifted — storage used the first 10 chars, + * unlock used the first 6 — so every backup-code recovery silently failed to + * find its stored blob. Both now go through backupCodeId(); these tests lock + * that they agree, end to end. + */ + +import { webcrypto } from "node:crypto" + +// encryption.ts reaches for window.crypto / window.btoa / window.atob. +if (typeof globalThis.crypto === "undefined") { + Object.defineProperty(globalThis, "crypto", { + value: webcrypto, + writable: true, + configurable: true, + }) +} else if (!globalThis.crypto.subtle) { + // @ts-expect-error — polyfill subtle on a partial crypto + globalThis.crypto.subtle = webcrypto.subtle +} +if (typeof (globalThis as { window?: unknown }).window === "undefined") { + ;(globalThis as { window?: unknown }).window = globalThis +} + +import { + generateBackupCodes, + encryptWithBackupCode, + decryptWithBackupCode, + backupCodeId, +} from "@/lib/encryption" + +describe("backup-code recovery", () => { + it("store and lookup resolve the same codeId, and the master password round-trips", async () => { + const [code] = generateBackupCodes(1) + const master = "correct horse battery staple" + const stored = await encryptWithBackupCode(code, master) + + // Storage id is the H-7 10-char id, not the old 6-char one. + expect(stored.codeId).toHaveLength(10) + + // Simulate the unlock path: user pastes the code lower-cased and padded. + const typed = ` ${code.toLowerCase()} ` + const normalized = typed.trim().toUpperCase().replace(/\s/g, "") + const stripped = normalized.replace(/-/g, "") + const lookupId = backupCodeId(stripped) + + expect(lookupId).toBe(stored.codeId) + + const recovered = await decryptWithBackupCode( + stripped, + stored.codeSalt, + stored.encrypted, + stored.iv + ) + expect(recovered).toBe(master) + }) + + it("backupCodeId is stable across dashes, case and whitespace", () => { + const [code] = generateBackupCodes(1) + const stripped = code.replace(/-/g, "") + expect(backupCodeId(code)).toBe(backupCodeId(stripped)) + expect(backupCodeId(code.toLowerCase())).toBe(backupCodeId(code)) + expect(backupCodeId(` ${code} `)).toBe(backupCodeId(code)) + }) +}) diff --git a/apps/web/src/lib/backend-auth.ts b/apps/web/src/lib/backend-auth.ts index 5c863eb7..75d84ade 100644 --- a/apps/web/src/lib/backend-auth.ts +++ b/apps/web/src/lib/backend-auth.ts @@ -1,6 +1,14 @@ import type { User } from "firebase/auth" import { auth } from "@/database/firebase" import { dedupe } from "@/lib/auth-inflight" +import { isDesktop } from "@/lib/desktop/is-desktop" +import { normalizeBackendPath, toResponse } from "@/lib/desktop/bridge" +import { desktopDataFetch } from "@/lib/desktop/router" +import { + checkRemoteSession, + desktopEstablishSession, + desktopSignOut, +} from "@/lib/desktop/remote" /** Same-origin refresh endpoint (used by fetch helpers). */ export const BACKEND_AUTH_REFRESH_PATH = "/api/backend/auth/refresh" @@ -35,6 +43,12 @@ export async function establishBackendSession( checkRevoked?: boolean } = {} ): Promise { + // Desktop: exchange goes through the Rust remote bridge (cookies land in + // the persistent Rust jar, not the webview). + if (isDesktop()) { + await desktopEstablishSession(idToken) + return + } const maxAttempts = Math.max(1, opts.maxAttempts ?? 3) const checkRevoked = opts.checkRevoked ?? false return dedupe(`session:${checkRevoked ? "revoked" : "fast"}`, async () => { @@ -80,6 +94,14 @@ export async function establishBackendSession( * If JWT cookies are missing or expired but Firebase session exists, re-run the Firebase exchange. */ export async function ensureBackendSession(user: User): Promise { + if (isDesktop()) { + if (await checkRemoteSession()) return + // Synthetic local users can't mint Firebase ID tokens — stay offline. + if (user.uid === "desktop-local") return + const idToken = await user.getIdToken() + await desktopEstablishSession(idToken) + return + } const ok = await dedupe("session-check", async () => { let check = await fetch("/api/backend/auth/session/check", { method: "GET", @@ -109,6 +131,10 @@ export async function ensureBackendSession(user: User): Promise { } export async function logoutBackendSession(): Promise { + if (isDesktop()) { + await desktopSignOut() + return + } await fetch("/api/backend/auth/logout", { method: "POST", credentials: "include", @@ -203,6 +229,20 @@ export async function proxyJsonAuthed( path: string, body?: unknown ): Promise<{ status: number; data: T | null }> { + if (isDesktop()) { + // Desktop: local store or remote bridge, decided per workspace/path. + const res = await desktopDataFetch( + method, + path, + body !== undefined ? JSON.stringify(body) : undefined + ) + if (!res.body) return { status: res.status, data: null } + try { + return { status: res.status, data: JSON.parse(res.body) as T } + } catch { + return { status: res.status, data: res.body as unknown as T } + } + } let result = await rawProxyJson(backendBaseUrl, method, path, body) if (result.status === 401) { @@ -244,6 +284,12 @@ export async function proxyJsonAuthed( * Triggers a force-logout if the session cannot be recovered (persistent 401 or 403). */ export async function backendFetch(path: string, init?: RequestInit): Promise { + if (isDesktop()) { + const method = (init?.method || "GET").toUpperCase() + const body = typeof init?.body === "string" ? init.body : undefined + const res = await desktopDataFetch(method, normalizeBackendPath(path), body) + return toResponse(res) + } const run = () => fetch(path, { ...init, diff --git a/apps/web/src/lib/desktop-handoff.ts b/apps/web/src/lib/desktop-handoff.ts new file mode 100644 index 00000000..6a2a9238 --- /dev/null +++ b/apps/web/src/lib/desktop-handoff.ts @@ -0,0 +1,37 @@ +/** + * Web-side desktop sign-in handoff. Runs in the SYSTEM BROWSER (not the Tauri + * webview) on the `/login?desktop=1&cb=` page: mints a short-lived + * Firebase custom token for the current session and hands it back to the + * desktop app via the loopback callback (or the mydevtools:// deep link). + * + * Called both after a fresh OAuth login and when the browser is already signed + * in (so an existing session doesn't just bounce to /dashboard). + */ +import { backendFetch } from "@/lib/backend-auth"; + +/** True if the current URL is a desktop sign-in handoff. */ +export function isDesktopHandoff(search: string): boolean { + return new URLSearchParams(search).get("desktop") === "1"; +} + +/** + * Mint the desktop token and redirect to the app. Returns false if it couldn't + * (caller decides what to do). On success it navigates away, so the promise + * effectively never resolves in the happy path. + */ +export async function handoffDesktopToken(search: string): Promise { + const params = new URLSearchParams(search); + if (params.get("desktop") !== "1") return false; + try { + const res = await backendFetch("/api/backend/auth/desktop-token", { method: "POST" }); + if (!res.ok) return false; + const { token } = (await res.json()) as { token: string }; + const cb = params.get("cb"); + window.location.href = cb + ? `http://127.0.0.1:${cb}/callback?token=${encodeURIComponent(token)}` + : `mydevtools://auth?token=${encodeURIComponent(token)}`; + return true; + } catch { + return false; + } +} diff --git a/apps/web/src/lib/desktop/api-fetch.ts b/apps/web/src/lib/desktop/api-fetch.ts new file mode 100644 index 00000000..3e12bff7 --- /dev/null +++ b/apps/web/src/lib/desktop/api-fetch.ts @@ -0,0 +1,43 @@ +import { isDesktop } from "./is-desktop"; +import { localApi, normalizeBackendPath, toResponse } from "./bridge"; + +/** + * Desktop-aware fetch for same-origin `/api/...` paths. + * + * On web this is a plain `fetch` pass-through. On desktop it routes + * `/api/backend/*` (and `/api/v1/*`) paths to the Rust local router. + * Remote routing (shared workspaces / sync) is layered on in Phase 4. + */ +export async function apiFetch(path: string, init?: RequestInit): Promise { + if (!isDesktop()) { + return fetch(path, init); + } + const method = (init?.method || "GET").toUpperCase(); + const body = typeof init?.body === "string" ? init.body : undefined; + + // api-client HTTP proxy → Rust http_request (returns the /api/proxy envelope). + if (path === "/api/proxy" || path.startsWith("/api/proxy?")) { + const { invoke } = await import("@tauri-apps/api/core"); + const input = body ? JSON.parse(body) : {}; + const envelope = await invoke("http_request", { input }); + return new Response(JSON.stringify(envelope), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + // Advanced proxies not available on desktop yet (v1.1). + if ( + path.startsWith("/api/proxy-grpc") || + path.startsWith("/api/proxy-ntlm") || + path.startsWith("/api/proxy-spnego") || + path.startsWith("/api/mock") + ) { + return new Response( + JSON.stringify({ error: "This feature is not available in the desktop app yet" }), + { status: 501, headers: { "Content-Type": "application/json" } } + ); + } + + const res = await localApi(method, normalizeBackendPath(path), body); + return toResponse(res); +} diff --git a/apps/web/src/lib/desktop/backup.ts b/apps/web/src/lib/desktop/backup.ts new file mode 100644 index 00000000..edcea232 --- /dev/null +++ b/apps/web/src/lib/desktop/backup.ts @@ -0,0 +1,134 @@ +/** + * Desktop encrypted backup — a portable, device-independent vault export. + * + * The local Rust store hands us the raw logical dump (all entries + portable kv + * singletons). We encrypt it here in the webview under a user backup passphrase + * (PBKDF2-600k → AES-GCM-256) so the file written to disk is opaque, and hand + * the same JSON back on restore. This is the only recovery path that survives + * losing this Mac's keychain device key: the raw SQLCipher DB can't be moved + * because its key never leaves the keychain. + * + * The backup passphrase is independent of the master password (the user may + * reuse it) and is NOT recoverable — losing it means losing the backup. + */ +import { localApi } from "./bridge"; + +const MAGIC = "MDTBACKUP"; +const FORMAT_VERSION = 1; +const PBKDF2_ITERS = 600_000; + +type BackupEnvelope = { + magic: string; + format: number; + salt: string; // base64 + iv: string; // base64 + cipher: string; // base64 + createdAt: number; +}; + +function toBase64(bytes: Uint8Array): string { + let bin = ""; + for (let i = 0; i < bytes.length; i += 0x8000) { + bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); + } + return btoa(bin); +} + +function fromBase64(s: string): Uint8Array { + const bin = atob(s); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +async function deriveKey(passphrase: string, salt: Uint8Array): Promise { + const base = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(passphrase), + "PBKDF2", + false, + ["deriveKey"] + ); + return crypto.subtle.deriveKey( + { name: "PBKDF2", salt: salt as BufferSource, iterations: PBKDF2_ITERS, hash: "SHA-256" }, + base, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"] + ); +} + +/** Encrypt the full local store into a downloadable backup blob. */ +export async function exportBackup(passphrase: string): Promise { + if (!passphrase || passphrase.length < 8) { + throw new Error("Choose a backup passphrase of at least 8 characters"); + } + const res = await localApi("GET", "/desktop/backup/export"); + if (res.status !== 200) throw new Error(`Backup export failed (${res.status})`); + + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(passphrase, salt); + const cipher = new Uint8Array( + await crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv as BufferSource }, + key, + new TextEncoder().encode(res.body) + ) + ); + + const envelope: BackupEnvelope = { + magic: MAGIC, + format: FORMAT_VERSION, + salt: toBase64(salt), + iv: toBase64(iv), + cipher: toBase64(cipher), + createdAt: Date.now(), + }; + return new Blob([JSON.stringify(envelope)], { type: "application/json" }); +} + +/** True if the local vault already has a configured master password. */ +export async function hasConfiguredVault(): Promise { + const res = await localApi("GET", "/api/v1/auth/master-vault"); + return res.status === 200; +} + +export type ImportResult = { imported: number; kv_imported: number }; + +/** + * Decrypt a backup file and merge it into the local store. Entries land as + * dirty rows; the master-vault salt only lands if none is configured yet. + */ +export async function importBackup( + passphrase: string, + fileText: string +): Promise { + let env: Partial; + try { + env = JSON.parse(fileText) as Partial; + } catch { + throw new Error("That file isn't a valid MyDevTools backup"); + } + if (env.magic !== MAGIC || !env.salt || !env.iv || !env.cipher) { + throw new Error("That file isn't a valid MyDevTools backup"); + } + + const key = await deriveKey(passphrase, fromBase64(env.salt)); + let dumpText: string; + try { + const plain = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: fromBase64(env.iv) as BufferSource }, + key, + fromBase64(env.cipher) as BufferSource + ); + dumpText = new TextDecoder().decode(plain); + } catch { + throw new Error("Wrong passphrase, or the backup file is corrupted"); + } + + const res = await localApi("POST", "/desktop/backup/import", dumpText); + if (res.status !== 200) throw new Error(`Restore failed (${res.status})`); + window.dispatchEvent(new CustomEvent("mydevtools:desktop-data-mutated")); + return JSON.parse(res.body || "{}") as ImportResult; +} diff --git a/apps/web/src/lib/desktop/bridge.ts b/apps/web/src/lib/desktop/bridge.ts new file mode 100644 index 00000000..6b125ab1 --- /dev/null +++ b/apps/web/src/lib/desktop/bridge.ts @@ -0,0 +1,42 @@ +/** + * Thin wrappers over Tauri `invoke()`. Only ever called behind `isDesktop()`, + * and `@tauri-apps/api` is imported dynamically so web bundles never pull it in. + */ + +export type LocalApiResponse = { + status: number; + body: string; +}; + +async function invoke(cmd: string, args: Record): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + return invoke(cmd, args); +} + +/** + * Call the Rust local router (SQLCipher-backed, mirrors FastAPI `/api/v1/*`). + * Path must be a normalized `/api/v1/...` path. + */ +export function localApi( + method: string, + path: string, + body?: string +): Promise { + return invoke("local_api", { method, path, body: body ?? null }); +} + +/** Normalize a same-origin `/api/backend/` path to FastAPI `/api/v1/` (query preserved). */ +export function normalizeBackendPath(path: string): string { + if (path.startsWith("/api/backend/")) { + return "/api/v1/" + path.slice("/api/backend/".length); + } + return path; +} + +/** Build a synthetic fetch Response from a local_api result. */ +export function toResponse(r: LocalApiResponse): Response { + return new Response(r.body || null, { + status: r.status, + headers: { "Content-Type": "application/json" }, + }); +} diff --git a/apps/web/src/lib/desktop/cloud-signin.ts b/apps/web/src/lib/desktop/cloud-signin.ts new file mode 100644 index 00000000..48c466e4 --- /dev/null +++ b/apps/web/src/lib/desktop/cloud-signin.ts @@ -0,0 +1,81 @@ +/** + * Desktop cloud sign-in. OAuth popups don't work in WKWebView, so sign-in + * happens in the system browser (web login page with ?desktop=1). + * + * Return leg = loopback server: the app binds an ephemeral localhost port and + * the browser redirects the minted Firebase custom token to + * http://127.0.0.1:/callback?token=... This works identically in + * `tauri dev` and the packaged app (no mydevtools:// URL-scheme registration, + * which macOS only routes to a bundled .app). The mydevtools:// deep link is + * kept as a secondary path. + */ +import { signInWithCustomToken } from "firebase/auth"; + +import { auth } from "@/database/firebase"; +import { establishBackendSession } from "@/lib/backend-auth"; +import { checkRemoteSession, desktopWebBase } from "./remote"; + +/** Exchange a Firebase custom token for a session and enter the workspace. */ +async function completeSignIn(token: string): Promise { + const cred = await signInWithCustomToken(auth, token); + const idToken = await cred.user.getIdToken(); + await establishBackendSession(idToken); + await checkRemoteSession(); + // Refresh workspaces so remote orgs/workspaces appear in the switcher. + const { useWorkspaceStore } = await import("@/store/workspace-store"); + await useWorkspaceStore.getState().loadFromBackend().catch(() => {}); + // Signal the app to enter the workspace (DesktopInit routes via Next so + // static-export paths resolve correctly). + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("mydevtools:desktop-authed")); + } +} + +/** + * Start the browser sign-in: bind the loopback callback server, open the system + * browser at the login page with the callback port, and finish when the token + * arrives. Resolves after the session is established. + */ +export async function startCloudSignIn(): Promise { + const { invoke, Channel } = await import("@tauri-apps/api/core"); + const { openUrl } = await import("@tauri-apps/plugin-opener"); + + const portChannel = new Channel<{ port: number }>(); + portChannel.onmessage = (msg) => { + if (msg.port) { + void openUrl(`${desktopWebBase()}/login?desktop=1&cb=${msg.port}`); + } + }; + + const token = await invoke("await_browser_auth", { portChannel }); + await completeSignIn(token); +} + +// ── Secondary path: mydevtools:// deep link (packaged app fallback) ────────── + +async function handleDeepLink(urls: string[]): Promise { + for (const raw of urls) { + let url: URL; + try { + url = new URL(raw); + } catch { + continue; + } + if (url.protocol !== "mydevtools:" || url.hostname !== "auth") continue; + const token = url.searchParams.get("token"); + if (!token) continue; + await completeSignIn(token); + } +} + +/** Listen for deep-link sign-in callbacks (and process a cold-start URL). */ +export async function initDeepLinkListener(): Promise { + const { onOpenUrl, getCurrent } = await import("@tauri-apps/plugin-deep-link"); + await onOpenUrl((urls) => { + void handleDeepLink(urls).catch((e) => console.error("Desktop sign-in failed:", e)); + }); + const initial = await getCurrent().catch(() => null); + if (initial?.length) { + void handleDeepLink(initial).catch((e) => console.error("Desktop sign-in failed:", e)); + } +} diff --git a/apps/web/src/lib/desktop/is-desktop.ts b/apps/web/src/lib/desktop/is-desktop.ts new file mode 100644 index 00000000..f4de75cf --- /dev/null +++ b/apps/web/src/lib/desktop/is-desktop.ts @@ -0,0 +1,14 @@ +/** + * Desktop (Tauri) detection. + * + * `process.env.NEXT_PUBLIC_TAURI` is inlined at build time, so in web builds + * this whole function is `false` and every desktop branch behind it is + * dead-code-eliminated — the web bundle stays byte-identical. + */ +export function isDesktop(): boolean { + return ( + process.env.NEXT_PUBLIC_TAURI === "1" && + typeof window !== "undefined" && + "__TAURI_INTERNALS__" in window + ); +} diff --git a/apps/web/src/lib/desktop/proxy-get.ts b/apps/web/src/lib/desktop/proxy-get.ts new file mode 100644 index 00000000..5d15fc94 --- /dev/null +++ b/apps/web/src/lib/desktop/proxy-get.ts @@ -0,0 +1,65 @@ +import { isDesktop } from "./is-desktop"; + +export type ProxyGetResult = { + ok: boolean; + status: number; + body: string; + isBase64: boolean; + contentType: string; +}; + +type ProxyEnvelope = { + status: number; + headers?: Record; + body?: string; + isBase64?: boolean; + error?: string; +}; + +/** + * Desktop-only outbound GET routed through the Rust `http_request` command + * (no CORS, no data upload — just fetches a public resource). Used for the few + * tools that reach third-party endpoints (favicons, HIBP, OpenAPI specs) so + * they go through the app's controlled proxy instead of the webview directly. + * + * Callers must only invoke this on desktop AND when online — it has no offline + * fallback of its own (returns ok:false on any failure). + */ +export async function proxyGet( + url: string, + opts?: { headers?: Record; timeoutMs?: number } +): Promise { + if (!isDesktop()) { + // Web callers should use plain fetch; this is a safety net. + try { + const res = await fetch(url, { headers: opts?.headers }); + const body = await res.text(); + return { + ok: res.ok, + status: res.status, + body, + isBase64: false, + contentType: res.headers.get("content-type") || "", + }; + } catch { + return { ok: false, status: 0, body: "", isBase64: false, contentType: "" }; + } + } + try { + const { invoke } = await import("@tauri-apps/api/core"); + const env = (await invoke("http_request", { + input: { url, method: "GET", headers: opts?.headers ?? {}, timeoutMs: opts?.timeoutMs }, + })) as ProxyEnvelope; + const contentType = + env.headers?.["content-type"] || env.headers?.["Content-Type"] || ""; + return { + ok: env.status >= 200 && env.status < 300, + status: env.status, + body: env.body ?? "", + isBase64: Boolean(env.isBase64), + contentType, + }; + } catch { + return { ok: false, status: 0, body: "", isBase64: false, contentType: "" }; + } +} diff --git a/apps/web/src/lib/desktop/remote.ts b/apps/web/src/lib/desktop/remote.ts new file mode 100644 index 00000000..6f63f640 --- /dev/null +++ b/apps/web/src/lib/desktop/remote.ts @@ -0,0 +1,110 @@ +/** + * Desktop remote bridge: calls FastAPI through the Rust `remote_api` command. + * HttpOnly JWT cookies live in a Rust-side persistent jar (never the webview), + * so the existing backend cookie flow works unchanged and without CORS. + */ +import type { LocalApiResponse } from "./bridge"; + +export const DESKTOP_BACKEND_BASE: string = + process.env.NEXT_PUBLIC_FASTAPI_BASE_URL || + process.env.NEXT_PUBLIC_BACKEND_BASE_URL || + "http://localhost:8000"; + +/** + * Web origin the system browser opens for sign-in. In dev the webview is served + * over http(s) (localhost:3000) and shares the same backend — reuse that origin + * so the minted token validates. The packaged app runs on tauri://localhost, so + * it falls back to the configured public site. + */ +export function desktopWebBase(): string { + if (typeof window !== "undefined") { + const origin = window.location.origin; + if (origin.startsWith("http://") || origin.startsWith("https://")) { + return origin; + } + } + return process.env.NEXT_PUBLIC_SITE_URL || "https://mydevtools.tech"; +} + +export async function remoteApi( + method: string, + path: string, + body?: string +): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + const url = new URL(path, DESKTOP_BACKEND_BASE).toString(); + return invoke("remote_api", { method, url, body: body ?? null }); +} + +// ── Session state (module-level; survives route changes, not restarts — +// restarts recover via the persisted Rust cookie jar + checkRemoteSession) ─ + +let sessionOk = false; + +export function hasRemoteSession(): boolean { + return sessionOk; +} + +export function setRemoteSession(ok: boolean): void { + if (sessionOk === ok) return; + sessionOk = ok; + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("mydevtools:desktop-session", { detail: { ok } })); + } +} + +/** Authenticated remote call with the web's 401→refresh→retry ladder. */ +export async function remoteApiAuthed( + method: string, + path: string, + body?: string +): Promise { + let res = await remoteApi(method, path, body); + if (res.status === 401) { + const refr = await remoteApi("POST", "/api/v1/auth/refresh"); + if (refr.status >= 200 && refr.status < 300) { + res = await remoteApi(method, path, body); + } + } + if (res.status === 401 || res.status === 403) { + setRemoteSession(false); + } + return res; +} + +/** Probe the backend session (e.g. on startup, after network regain). */ +export async function checkRemoteSession(): Promise { + try { + const res = await remoteApiAuthed("GET", "/api/v1/auth/session/check"); + setRemoteSession(res.status >= 200 && res.status < 300); + } catch { + setRemoteSession(false); + } + return sessionOk; +} + +/** Exchange a Firebase ID token for backend cookies (into the Rust jar). */ +export async function desktopEstablishSession(idToken: string): Promise { + const res = await remoteApi( + "POST", + "/api/v1/auth/session", + // long_lived → 60-day refresh cookie for the desktop app. + JSON.stringify({ id_token: idToken, check_revoked: false, long_lived: true }) + ); + if (res.status < 200 || res.status >= 300) { + setRemoteSession(false); + throw new Error(`Desktop session exchange failed (${res.status})`); + } + setRemoteSession(true); +} + +export async function desktopSignOut(): Promise { + try { + await remoteApi("POST", "/api/v1/auth/logout"); + } catch { + // best-effort; jar is cleared regardless + } + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("clear_remote_session"); + setRemoteSession(false); +} diff --git a/apps/web/src/lib/desktop/router.ts b/apps/web/src/lib/desktop/router.ts new file mode 100644 index 00000000..22d2eda9 --- /dev/null +++ b/apps/web/src/lib/desktop/router.ts @@ -0,0 +1,131 @@ +/** + * Desktop data routing: decides per request whether a normalized + * `/api/v1/...` path is served by the local SQLCipher store or the remote + * FastAPI backend. + * + * Rules: + * - remote-only tools (url-shortener, s3-drive, game-scores, dns-lookup) + * always go remote; + * - the workspaces API merges the always-present local personal workspace + * with remote orgs/workspaces when signed in; + * - a non-local active workspace routes everything remote (shared/team + * workspaces are cloud-only); + * - everything else is local. + */ +import { localApi, type LocalApiResponse } from "./bridge"; +import { hasRemoteSession, remoteApiAuthed } from "./remote"; + +export const LOCAL_WORKSPACE_ID = "local-personal"; + +const REMOTE_ONLY_PREFIXES = [ + "/api/v1/url-shortener", + "/api/v1/s3-drive", + "/api/v1/game-scores", + "/api/v1/dns-lookup", +]; + +/** Live-DB routes run on native Rust drivers regardless of workspace. */ +const ALWAYS_LOCAL_PREFIXES = ["/api/sql-client/", "/api/nosql/", "/api/redis-commander/"]; + +export function activeWorkspaceId(): string { + if (typeof window === "undefined") return LOCAL_WORKSPACE_ID; + try { + const raw = window.localStorage.getItem("mdt-workspace-store"); + if (!raw) return LOCAL_WORKSPACE_ID; + const parsed = JSON.parse(raw) as { state?: { activeWorkspaceId?: string | null } }; + return parsed.state?.activeWorkspaceId || LOCAL_WORKSPACE_ID; + } catch { + return LOCAL_WORKSPACE_ID; + } +} + +export function isRemoteWorkspaceActive(): boolean { + return activeWorkspaceId() !== LOCAL_WORKSPACE_ID; +} + +function parseArray(res: LocalApiResponse): unknown[] { + try { + const v = JSON.parse(res.body); + return Array.isArray(v) ? v : []; + } catch { + return []; + } +} + +async function workspacesFetch( + method: string, + path: string, + body?: string +): Promise { + const bare = path.split("?")[0]; + if (method === "GET" && (bare === "/api/v1/workspaces-api/orgs" || bare === "/api/v1/workspaces-api/workspaces")) { + const local = await localApi(method, path, body); + if (!hasRemoteSession()) return local; + try { + const remote = await remoteApiAuthed(method, path, body); + if (remote.status >= 200 && remote.status < 300) { + const merged = [...parseArray(local), ...parseArray(remote)]; + return { status: 200, body: JSON.stringify(merged) }; + } + } catch { + // offline / backend unreachable — local list is still valid + } + return local; + } + if (method === "POST" && bare === "/api/v1/workspaces-api/workspaces/active") { + // Always record locally; propagate to the backend cookie when the target + // is a remote workspace and we have a session. + const local = await localApi(method, path, body); + let workspaceId = ""; + try { + workspaceId = (JSON.parse(body || "{}") as { workspace_id?: string }).workspace_id || ""; + } catch { + /* ignore */ + } + if (workspaceId && workspaceId !== LOCAL_WORKSPACE_ID && hasRemoteSession()) { + try { + return await remoteApiAuthed(method, path, body); + } catch { + return { status: 503, body: JSON.stringify({ detail: "Backend unreachable" }) }; + } + } + return local; + } + // Workspace detail / membership / crypto endpoints: local id → local, else remote. + if (bare.includes(`/${LOCAL_WORKSPACE_ID}`) || !hasRemoteSession()) { + return localApi(method, path, body); + } + return remoteApiAuthed(method, path, body); +} + +/** Route a normalized `/api/v1/...` request (desktop only). */ +export async function desktopDataFetch( + method: string, + path: string, + body?: string +): Promise { + if (ALWAYS_LOCAL_PREFIXES.some((p) => path.startsWith(p))) { + return localApi(method, path, body); + } + if (REMOTE_ONLY_PREFIXES.some((p) => path.startsWith(p))) { + if (!hasRemoteSession()) { + return { + status: 503, + body: JSON.stringify({ detail: "This tool requires a network connection and cloud sign-in" }), + }; + } + return remoteApiAuthed(method, path, body); + } + if (path.startsWith("/api/v1/workspaces-api/")) { + return workspacesFetch(method, path, body); + } + if (isRemoteWorkspaceActive() && hasRemoteSession()) { + return remoteApiAuthed(method, path, body); + } + const res = await localApi(method, path, body); + if (method !== "GET" && res.status < 300 && path.startsWith("/api/v1/")) { + // Nudge the sync engine (debounced) after successful local writes. + window.dispatchEvent(new CustomEvent("mydevtools:desktop-data-mutated")); + } + return res; +} diff --git a/apps/web/src/lib/desktop/sync-adapters.ts b/apps/web/src/lib/desktop/sync-adapters.ts new file mode 100644 index 00000000..4c485042 --- /dev/null +++ b/apps/web/src/lib/desktop/sync-adapters.ts @@ -0,0 +1,220 @@ +/** + * Sync adapters: map each local tool_kind to its remote REST contract. + * All remote calls go through remoteApiAuthed with the backend's personal + * workspace active (the engine sets the workspace cookie first). + */ + +export type SyncRow = { + id: string; + doc: Record; + updated_at: number; // local LWW clock (epoch ms) + deleted_at: number | null; + dirty: boolean; + last_synced_at: number | null; +}; + +export type SyncAdapter = { + /** Local entries.tool_kind */ + kind: string; + /** GET path returning the full remote list (bare array after unwrap). */ + listPath: string; + /** Unwrap a list response body into an array of docs. */ + unwrapList?: (parsed: unknown) => Record[]; + /** POST path for creates. */ + createPath: string; + /** Build the create body from a local doc. */ + createBody?: (doc: Record) => unknown; + /** PATCH path for updates (null = tool has no update, e.g. history). */ + updatePath: ((id: string) => string) | null; + updateBody?: (doc: Record) => unknown; + /** DELETE path. */ + deletePath: (id: string) => string; + /** Server generates ids on create (re-key local row afterwards). */ + serverAssignsId: boolean; + /** Extract a comparable epoch-ms updatedAt from a remote doc (0 = none). */ + remoteUpdatedAt: (doc: Record) => number; +}; + +function msOrIso(v: unknown): number { + if (typeof v === "number") return v; + if (typeof v === "string") { + const t = Date.parse(v); + return Number.isNaN(t) ? 0 : t; + } + return 0; +} + +const stdUpdatedAt = (d: Record) => msOrIso(d.updatedAt); + +function envelopeAdapter(kind: string, prefix: string): SyncAdapter { + return { + kind, + listPath: `/api/v1/${prefix}/entries`, + createPath: `/api/v1/${prefix}/entries`, + createBody: (d) => ({ + encryptedData: d.encryptedData, + iv: d.iv, + createdAt: d.createdAt, + updatedAt: d.updatedAt, + }), + updatePath: (id) => `/api/v1/${prefix}/entries/${id}`, + updateBody: (d) => ({ encryptedData: d.encryptedData, iv: d.iv, updatedAt: d.updatedAt }), + deletePath: (id) => `/api/v1/${prefix}/entries/${id}`, + serverAssignsId: true, + remoteUpdatedAt: stdUpdatedAt, + }; +} + +function connectionsAdapter(kind: string, prefix: string): SyncAdapter { + return { + kind, + listPath: `/api/v1/${prefix}/connections`, + createPath: `/api/v1/${prefix}/connections`, + createBody: (d) => { + const { id: _id, userId: _u, createdAt: _c, lastUsedAt: _l, updatedAt: _up, ...rest } = d; + return rest; + }, + updatePath: (id) => `/api/v1/${prefix}/connections/${id}`, + updateBody: (d) => { + const { id: _id, userId: _u, createdAt: _c, lastUsedAt: _l, updatedAt: _up, ...rest } = d; + return rest; + }, + deletePath: (id) => `/api/v1/${prefix}/connections/${id}`, + serverAssignsId: true, + // updatedAt (content-edit clock) is the reliable LWW signal; fall back to + // lastUsedAt/createdAt for legacy docs written before it existed. + remoteUpdatedAt: (d) => msOrIso(d.updatedAt) || msOrIso(d.lastUsedAt) || msOrIso(d.createdAt), + }; +} + +export const SYNC_ADAPTERS: SyncAdapter[] = [ + envelopeAdapter("password_entries", "password-manager"), + envelopeAdapter("api_key_entries", "api-keys"), + envelopeAdapter("environment_entries", "environment-manager"), + connectionsAdapter("sql_connections", "sql-client"), + connectionsAdapter("redis_connections", "redis-commander"), + connectionsAdapter("nosql_connections", "nosql"), + { + kind: "code_snippets", + listPath: "/api/v1/code-snippets", + createPath: "/api/v1/code-snippets", + updatePath: (id) => `/api/v1/code-snippets/${id}`, + updateBody: (d) => ({ + title: d.title, + language: d.language, + code: d.code, + tags: d.tags, + pinned: d.pinned, + }), + deletePath: (id) => `/api/v1/code-snippets/${id}`, + serverAssignsId: false, // POST accepts a client id + remoteUpdatedAt: stdUpdatedAt, + }, + { + kind: "bookmark_folders", + listPath: "/api/v1/bookmarks/snapshot", + unwrapList: (p) => ((p as { folders?: Record[] })?.folders ?? []), + createPath: "/api/v1/bookmark-folders", + updatePath: (id) => `/api/v1/bookmark-folders/${id}`, + deletePath: (id) => `/api/v1/bookmark-folders/${id}`, + serverAssignsId: false, + remoteUpdatedAt: (d) => msOrIso(d.createdAt), + }, + { + kind: "bookmarks", + listPath: "/api/v1/bookmarks/snapshot", + unwrapList: (p) => ((p as { bookmarks?: Record[] })?.bookmarks ?? []), + createPath: "/api/v1/bookmarks", + updatePath: (id) => `/api/v1/bookmarks/${id}`, + deletePath: (id) => `/api/v1/bookmarks/${id}`, + serverAssignsId: false, + remoteUpdatedAt: stdUpdatedAt, + }, + { + kind: "notes", + listPath: "/api/v1/notes?skip=0&limit=500", + createPath: "/api/v1/notes", + createBody: (d) => ({ + title: d.title, + content: d.content, + parentId: d.parentId, + icon: d.icon, + tags: d.tags, + }), + updatePath: (id) => `/api/v1/notes/${id}`, + updateBody: (d) => ({ + title: d.title, + content: d.content, + parentId: d.parentId, + icon: d.icon, + pinned: d.pinned, + tags: d.tags, + }), + deletePath: (id) => `/api/v1/notes/${id}`, + serverAssignsId: true, + remoteUpdatedAt: stdUpdatedAt, + }, + { + kind: "projects", + listPath: "/api/v1/projects", + createPath: "/api/v1/projects", + createBody: (d) => ({ name: d.name, color: d.color }), + updatePath: (id) => `/api/v1/projects/${id}`, + updateBody: (d) => ({ name: d.name, color: d.color }), + deletePath: (id) => `/api/v1/projects/${id}`, + serverAssignsId: true, + remoteUpdatedAt: (d) => msOrIso(d.createdAt), + }, + { + kind: "tasks", + listPath: "/api/v1/tasks/export?status=all&projectId=all&assignee=all&skip=0&limit=10000", + createPath: "/api/v1/tasks", + createBody: (d) => { + const { id: _id, created_by: _cb, createdAt: _ca, ...rest } = d; + return rest; + }, + updatePath: (id) => `/api/v1/tasks/${id}`, + updateBody: (d) => { + const { id: _id, created_by: _cb, createdAt: _ca, ...rest } = d; + return rest; + }, + deletePath: (id) => `/api/v1/tasks/${id}`, + serverAssignsId: true, + remoteUpdatedAt: (d) => msOrIso(d.completedAt) || msOrIso(d.createdAt), + }, + { + kind: "api_client_environments", + listPath: "/api/v1/api-client/environments", + createPath: "/api/v1/api-client/environments", + createBody: (d) => ({ name: d.name }), + updatePath: (id) => `/api/v1/api-client/environments/${id}`, + updateBody: (d) => ({ name: d.name, variables: d.variables }), + deletePath: (id) => `/api/v1/api-client/environments/${id}`, + serverAssignsId: true, + remoteUpdatedAt: () => 0, + }, + { + kind: "api_client_collections", + listPath: "/api/v1/api-client/collections", + createPath: "/api/v1/api-client/collections", + createBody: (d) => ({ name: d.name }), + updatePath: (id) => `/api/v1/api-client/collections/${id}`, + updateBody: (d) => ({ name: d.name, items: d.items, workspace: d.workspace }), + deletePath: (id) => `/api/v1/api-client/collections/${id}`, + serverAssignsId: true, + remoteUpdatedAt: () => 0, + }, + { + kind: "api_client_history", + listPath: "/api/v1/api-client/history?limit=100", + createPath: "/api/v1/api-client/history", + createBody: (d) => { + const { id: _id, ...rest } = d; + return rest; + }, + updatePath: null, // history entries are immutable + deletePath: (id) => `/api/v1/api-client/history/${id}`, + serverAssignsId: true, + remoteUpdatedAt: (d) => msOrIso(d.timestamp), + }, +]; diff --git a/apps/web/src/lib/desktop/sync-engine.ts b/apps/web/src/lib/desktop/sync-engine.ts new file mode 100644 index 00000000..bd5cc493 --- /dev/null +++ b/apps/web/src/lib/desktop/sync-engine.ts @@ -0,0 +1,394 @@ +/** + * Personal-workspace sync engine (desktop only). + * + * Reconciles the local SQLCipher store ("local-personal" workspace) with the + * user's REMOTE personal workspace when the per-workspace sync toggle is on. + * Envelope tools ship opaque ciphertext both ways — the same account's master + * key decrypts both sides, so sync never touches crypto. + * + * Push (dirty rows first): tombstoned → DELETE (404 ok); never-synced → POST + * create then re-key the local row to the server id; else PATCH. Pull: full + * list per tool; remote newer (LWW on updatedAt) → overwrite local; local + * clean+synced+absent-remote → delete local; local dirty rows are never + * overwritten by pull (they win until pushed). + * + * ponytail: rounds only run while the LOCAL personal workspace is active — + * pushing requires the backend active_workspace cookie pointed at the remote + * personal workspace, and flipping it mid-use of a shared workspace would + * misroute the user's own requests. Revisit with per-request workspace + * scoping if backend support lands. + */ +import { localApi } from "./bridge"; +import { hasRemoteSession, remoteApiAuthed } from "./remote"; +import { activeWorkspaceId, LOCAL_WORKSPACE_ID } from "./router"; +import { SYNC_ADAPTERS, type SyncAdapter, type SyncRow } from "./sync-adapters"; + +const LOCAL_WS = LOCAL_WORKSPACE_ID; + +let running = false; +let timer: ReturnType | null = null; +let debounceTimer: ReturnType | null = null; + +async function localJson(method: string, path: string, body?: unknown): Promise { + const res = await localApi(method, path, body !== undefined ? JSON.stringify(body) : undefined); + if (res.status < 200 || res.status >= 300) { + throw new Error(`local sync call failed: ${method} ${path} → ${res.status}`); + } + return JSON.parse(res.body || "null") as T; +} + +export async function isSyncEnabled(): Promise { + const r = await localJson<{ enabled: boolean }>( + "GET", + `/desktop/sync/settings?workspace_id=${LOCAL_WS}` + ); + return r.enabled; +} + +export async function setSyncEnabled(enabled: boolean): Promise { + await localJson("POST", "/desktop/sync/settings", { workspace_id: LOCAL_WS, enabled }); + if (enabled) void syncNow(); +} + +/** A discarded version preserved when two devices edited the same row. */ +export type SyncConflict = { + id: string; + tool_kind: string; + entry_id: string; + loser_side: "local" | "remote"; + loser_doc: Record; + loser_updated_at: number; + winner_updated_at: number; + created_at: number; +}; + +/** Open (unreviewed) sync conflicts, newest first. */ +export async function listConflicts(): Promise { + return localJson( + "GET", + `/desktop/sync/conflicts?workspace_id=${LOCAL_WS}` + ); +} + +/** Mark a conflict reviewed so it drops off the open list (kept for audit). */ +export async function dismissConflict(id: string): Promise { + await localJson("POST", "/desktop/sync/conflicts/dismiss", { id }); +} + +/** + * Restore the discarded version: re-applies it locally as a fresh edit (so it + * wins the next round), resolves the conflict, and kicks a sync + refresh. + */ +export async function restoreConflict(id: string): Promise { + await localJson("POST", "/desktop/sync/conflicts/restore", { id }); + window.dispatchEvent(new CustomEvent("mydevtools:desktop-data-mutated")); + void syncNow(); +} + +/** Find the user's remote personal workspace id (requires cloud session). */ +async function remotePersonalWorkspaceId(): Promise { + const res = await remoteApiAuthed("GET", "/api/v1/workspaces-api/workspaces"); + if (res.status < 200 || res.status >= 300) return null; + try { + const list = JSON.parse(res.body) as { id: string; is_personal: boolean }[]; + return list.find((w) => w.is_personal)?.id ?? null; + } catch { + return null; + } +} + +async function setRemoteActiveWorkspace(id: string): Promise { + const res = await remoteApiAuthed( + "POST", + "/api/v1/workspaces-api/workspaces/active", + JSON.stringify({ workspace_id: id }) + ); + return res.status >= 200 && res.status < 300; +} + +async function fetchRemoteList(adapter: SyncAdapter): Promise[]> { + const res = await remoteApiAuthed("GET", adapter.listPath); + if (res.status < 200 || res.status >= 300) { + throw new Error(`remote list failed: ${adapter.listPath} → ${res.status}`); + } + const parsed = JSON.parse(res.body || "[]"); + const list = adapter.unwrapList ? adapter.unwrapList(parsed) : parsed; + return Array.isArray(list) ? (list as Record[]) : []; +} + +async function resolve( + kind: string, + id: string, + action: "mark-synced" | "apply-remote" | "remove-local", + extra?: Record +): Promise { + await localJson("POST", "/desktop/sync/resolve", { + workspace_id: LOCAL_WS, + tool_kind: kind, + id, + action, + ...extra, + }); +} + +/** + * Persist the losing side of a concurrent edit so LWW never destroys data. + * Fire-and-forget: a failure to archive must not abort the sync round (the + * winning value still syncs), but we surface it so it's not silent. + */ +async function archiveConflict( + kind: string, + entryId: string, + loserDoc: Record, + loserSide: "local" | "remote", + loserUpdatedAt: number, + winnerUpdatedAt: number +): Promise { + try { + await localJson("POST", "/desktop/sync/conflict", { + workspace_id: LOCAL_WS, + tool_kind: kind, + entry_id: entryId, + loser_side: loserSide, + loser_doc: loserDoc, + loser_updated_at: loserUpdatedAt, + winner_updated_at: winnerUpdatedAt, + }); + window.dispatchEvent(new CustomEvent("mydevtools:desktop-conflict")); + } catch (e) { + console.warn(`[sync] failed to archive ${kind} conflict for ${entryId}:`, e); + } +} + +async function pushRow( + adapter: SyncAdapter, + row: SyncRow, + remoteBefore: Map> | null +): Promise { + const id = row.id; + const syncedAt = row.last_synced_at ?? 0; + // A remote copy whose updatedAt advanced past our last sync means another + // device edited this row while we were away — a true concurrent conflict. + const remoteDoc = remoteBefore?.get(id); + const remoteTs = remoteDoc ? adapter.remoteUpdatedAt(remoteDoc) : 0; + const remoteChangedSinceSync = remoteTs > 0 && remoteTs > syncedAt; + + if (row.deleted_at !== null) { + if (row.last_synced_at !== null) { + // Local delete would wipe a remote edit we never saw — archive it first. + if (remoteChangedSinceSync && remoteDoc) { + await archiveConflict(adapter.kind, id, remoteDoc, "remote", remoteTs, row.updated_at); + } + const res = await remoteApiAuthed("DELETE", adapter.deletePath(id)); + if (res.status >= 500) throw new Error(`remote delete failed (${res.status})`); + // 2xx or 404 both mean the entry is gone remotely. + } + await resolve(adapter.kind, id, "mark-synced"); + return; + } + if (row.last_synced_at === null) { + const body = adapter.createBody ? adapter.createBody(row.doc) : row.doc; + const res = await remoteApiAuthed("POST", adapter.createPath, JSON.stringify(body)); + if (res.status < 200 || res.status >= 300) { + // 409 (e.g. snippet id already exists remotely) → fall through to update. + if (res.status === 409 && adapter.updatePath) { + const upd = adapter.updateBody ? adapter.updateBody(row.doc) : row.doc; + const r2 = await remoteApiAuthed("PATCH", adapter.updatePath(id), JSON.stringify(upd)); + if (r2.status < 200 || r2.status >= 300) throw new Error(`push update failed (${r2.status})`); + await resolve(adapter.kind, id, "mark-synced"); + return; + } + throw new Error(`push create failed: ${adapter.createPath} → ${res.status}`); + } + const created = JSON.parse(res.body || "{}") as Record; + if (adapter.serverAssignsId && typeof created.id === "string" && created.id !== id) { + await resolve(adapter.kind, id, "mark-synced", { new_id: created.id, new_doc: { ...row.doc, ...created } }); + } else { + await resolve(adapter.kind, id, "mark-synced"); + } + return; + } + if (!adapter.updatePath) { + await resolve(adapter.kind, id, "mark-synced"); // immutable kind (history) + return; + } + // Edit-vs-edit: both sides changed this row since our last sync. Keep + // last-writer-wins on updatedAt, but never destroy the loser — archive it. + if (remoteChangedSinceSync && remoteDoc) { + if (row.updated_at >= remoteTs) { + // Local is newer → local wins; the remote version is the loser. + await archiveConflict(adapter.kind, id, remoteDoc, "remote", remoteTs, row.updated_at); + // fall through and PATCH the local version over remote + } else { + // Remote is newer → remote wins; keep our local edit as the loser and + // adopt the remote version locally instead of pushing. + await archiveConflict(adapter.kind, id, row.doc, "local", row.updated_at, remoteTs); + await resolve(adapter.kind, id, "apply-remote", { + doc: remoteDoc, + updated_at: remoteTs || undefined, + }); + return; + } + } + const body = adapter.updateBody ? adapter.updateBody(row.doc) : row.doc; + const res = await remoteApiAuthed("PATCH", adapter.updatePath(id), JSON.stringify(body)); + if (res.status === 404) { + // Deleted remotely but edited locally — local edit wins: recreate. + await resolve(adapter.kind, id, "mark-synced"); + const createBody = adapter.createBody ? adapter.createBody(row.doc) : row.doc; + const r2 = await remoteApiAuthed("POST", adapter.createPath, JSON.stringify(createBody)); + if (r2.status >= 200 && r2.status < 300) { + const created = JSON.parse(r2.body || "{}") as Record; + if (adapter.serverAssignsId && typeof created.id === "string" && created.id !== id) { + await resolve(adapter.kind, id, "mark-synced", { new_id: created.id }); + } + } + return; + } + if (res.status < 200 || res.status >= 300) throw new Error(`push update failed (${res.status})`); + await resolve(adapter.kind, id, "mark-synced"); +} + +async function syncTool(adapter: SyncAdapter): Promise { + const rows = await localJson( + "GET", + `/desktop/sync/rows?workspace_id=${LOCAL_WS}&tool_kind=${adapter.kind}` + ); + + // Snapshot remote BEFORE pushing so we can detect rows another device changed + // since our last sync. Best-effort: if it fails we push without conflict + // detection (pull below still fetches the live list). + let remoteBefore: Map> | null = null; + try { + const list = await fetchRemoteList(adapter); + remoteBefore = new Map( + list.filter((d) => typeof d.id === "string").map((d) => [d.id as string, d]) + ); + } catch { + remoteBefore = null; + } + + // 1. Push all dirty rows (conflict-aware). + for (const row of rows.filter((r) => r.dirty)) { + await pushRow(adapter, row, remoteBefore); + } + + // 2. Pull: reconcile against the full remote list. + const fresh = await localJson( + "GET", + `/desktop/sync/rows?workspace_id=${LOCAL_WS}&tool_kind=${adapter.kind}` + ); + const local = new Map(fresh.map((r) => [r.id, r])); + const remote = await fetchRemoteList(adapter); + const remoteIds = new Set(); + + for (const doc of remote) { + const id = typeof doc.id === "string" ? doc.id : null; + if (!id) continue; + remoteIds.add(id); + const l = local.get(id); + if (!l) { + await resolve(adapter.kind, id, "apply-remote", { + doc, + updated_at: adapter.remoteUpdatedAt(doc) || undefined, + }); + continue; + } + if (l.dirty || l.deleted_at !== null) continue; // local changes win until pushed + const remoteTs = adapter.remoteUpdatedAt(doc); + if (remoteTs === 0 || remoteTs > l.updated_at) { + await resolve(adapter.kind, id, "apply-remote", { doc, updated_at: remoteTs || undefined }); + } + } + + // 3. Remote deletions: local clean + previously synced + absent remotely. + for (const r of fresh) { + if (!r.dirty && r.deleted_at === null && r.last_synced_at !== null && !remoteIds.has(r.id)) { + await resolve(adapter.kind, r.id, "remove-local"); + } + } +} + +/** + * Sync the user-preferences singleton (kv-backed, not in the entries table). + * Container-level last-writer-wins on `updatedAt`: whichever side is newer + * PATCHes the whole editable preferences object onto the other. A fresh local + * baseline has updatedAt=0 so the cloud copy wins until the user changes a pref. + */ +const PREF_FIELDS = [ + "theme", + "accentColor", + "locale", + "enabledTools", + "toolFavorites", + "pinnedToolsByWorkspace", + "toolStats", +] as const; + +async function syncPreferences(): Promise { + const PATH = "/api/v1/user-preferences"; + const local = await localJson>("GET", PATH); + const remoteRes = await remoteApiAuthed("GET", PATH); + if (remoteRes.status < 200 || remoteRes.status >= 300) return; + const remote = JSON.parse(remoteRes.body || "{}") as Record; + + const localTs = Number(local?.updatedAt ?? 0); + const remoteTs = Number(remote?.updatedAt ?? 0); + if (localTs === remoteTs) return; + + const pick = (o: Record) => + Object.fromEntries(PREF_FIELDS.filter((k) => k in o).map((k) => [k, o[k]])); + + if (localTs > remoteTs) { + await remoteApiAuthed("PATCH", PATH, JSON.stringify(pick(local))); + } else { + await localJson("PATCH", PATH, pick(remote)); + } +} + +/** Run one full sync round. Safe to call repeatedly; rounds never overlap. */ +export async function syncNow(): Promise { + if (running) return; + if (!hasRemoteSession()) return; + if (activeWorkspaceId() !== LOCAL_WS) return; // see ponytail note above + if (!(await isSyncEnabled())) return; + + running = true; + try { + const personalId = await remotePersonalWorkspaceId(); + if (!personalId) return; + if (!(await setRemoteActiveWorkspace(personalId))) return; + + for (const adapter of SYNC_ADAPTERS) { + try { + await syncTool(adapter); + } catch (e) { + console.warn(`[sync] ${adapter.kind} round failed:`, e); + } + } + try { + await syncPreferences(); + } catch (e) { + console.warn("[sync] preferences round failed:", e); + } + window.dispatchEvent(new CustomEvent("mydevtools:desktop-synced")); + } finally { + running = false; + } +} + +/** Debounced trigger — call after local mutations. */ +export function scheduleSyncSoon(): void { + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => void syncNow(), 5_000); +} + +/** Start periodic + event-driven syncing (idempotent). */ +export function startSyncEngine(): void { + if (timer) return; + timer = setInterval(() => void syncNow(), 5 * 60_000); + window.addEventListener("online", () => void syncNow()); + window.addEventListener("mydevtools:desktop-session", () => void syncNow()); + window.addEventListener("mydevtools:desktop-data-mutated", scheduleSyncSoon); + void syncNow(); +} diff --git a/apps/web/src/lib/desktop/use-favicon.ts b/apps/web/src/lib/desktop/use-favicon.ts new file mode 100644 index 00000000..f8aa8e88 --- /dev/null +++ b/apps/web/src/lib/desktop/use-favicon.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; + +import { isDesktop } from "./is-desktop"; +import { proxyGet } from "./proxy-get"; + +// Resolved favicon data-URLs (desktop) keyed by the service URL. In-memory +// only — re-fetched on reload when online. `null` = tried and failed (offline +// or non-image response) so the caller shows its letter-avatar fallback. +const cache = new Map(); + +/** + * Given a favicon service URL (e.g. Google/DuckDuckGo), returns a usable `src`: + * - Web: the URL itself (browser fetches it directly — byte-identical to before). + * - Desktop: a `data:` URL fetched through the Rust proxy (no third-party call + * from the webview), online-only. Returns `null` offline / on failure so the + * component renders its letter-avatar fallback. + */ +export function useFaviconSrc(serviceUrl: string | null): string | null { + const [src, setSrc] = useState(() => { + if (!serviceUrl) return null; + if (!isDesktop()) return serviceUrl; + return cache.get(serviceUrl) ?? null; + }); + + useEffect(() => { + if (!serviceUrl) { + setSrc(null); + return; + } + if (!isDesktop()) { + setSrc(serviceUrl); + return; + } + if (cache.has(serviceUrl)) { + setSrc(cache.get(serviceUrl) ?? null); + return; + } + if (typeof navigator !== "undefined" && !navigator.onLine) { + setSrc(null); + return; + } + let cancelled = false; + void proxyGet(serviceUrl).then((r) => { + const dataUrl = + r.ok && r.isBase64 && r.body + ? `data:${r.contentType || "image/png"};base64,${r.body}` + : null; + cache.set(serviceUrl, dataUrl); + if (!cancelled) setSrc(dataUrl); + }); + return () => { + cancelled = true; + }; + }, [serviceUrl]); + + return src; +} diff --git a/apps/web/src/lib/encryption.ts b/apps/web/src/lib/encryption.ts index d70bc3b9..6ffbec49 100644 --- a/apps/web/src/lib/encryption.ts +++ b/apps/web/src/lib/encryption.ts @@ -140,14 +140,24 @@ export function generateBackupCodes(count = 8): string[] { }) } +/** + * Lookup id for a backup code: the upper-cased, whitespace-and-dash-stripped + * first 10 chars (H-7 — 30^10 ≈ 590T possibilities, vs 30^6 ≈ 729M for 6). + * + * Storage AND lookup MUST derive the id identically, so both call this. They + * previously duplicated the logic and drifted (store used 10 chars, unlock used + * 6), which silently broke every backup-code recovery. + */ +export function backupCodeId(code: string): string { + return code.toUpperCase().replace(/\s/g, "").replace(/-/g, "").slice(0, 10) +} + export async function encryptWithBackupCode( code: string, masterPassword: string, ): Promise<{ codeId: string; codeSalt: string; encrypted: string; iv: string }> { const normalized = code.replace(/-/g, "") - // H-7 fix: use first 10 chars for codeId (30^10 ≈ 590T possibilities) - // instead of 6 chars (30^6 ≈ 729M) to prevent brute-force enumeration. - const codeId = normalized.slice(0, 10) + const codeId = backupCodeId(code) const codeSalt = await generateSalt() const key = await deriveKey(normalized, codeSalt) const { encrypted, iv } = await encryptData(key, masterPassword) diff --git a/apps/web/src/lib/graphql-introspect.ts b/apps/web/src/lib/graphql-introspect.ts index bd14db4e..cd83181e 100644 --- a/apps/web/src/lib/graphql-introspect.ts +++ b/apps/web/src/lib/graphql-introspect.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * GraphQL schema introspection helper. * Sends the standard introspection query through the existing proxy so it @@ -60,7 +61,7 @@ export async function fetchGraphQLSchema(args: { }): Promise { if (!args.url.trim()) throw new Error("GraphQL URL is required") - const proxyRes = await fetch("/api/proxy", { + const proxyRes = await apiFetch("/api/proxy", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/lib/grpc-web.ts b/apps/web/src/lib/grpc-web.ts index 24366225..37c9d7c3 100644 --- a/apps/web/src/lib/grpc-web.ts +++ b/apps/web/src/lib/grpc-web.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * gRPC-Web protocol helpers. * @@ -184,7 +185,7 @@ export async function sendNativeGrpc(args: { const singleFrame = !bodyFrames && args.message ? bytesToBase64(encodeMessageFrame(args.message)) : undefined - const res = await fetch("/api/proxy-grpc", { + const res = await apiFetch("/api/proxy-grpc", { method: "POST", credentials: "include", signal: args.signal, diff --git a/apps/web/src/lib/hibp.ts b/apps/web/src/lib/hibp.ts index 4599fdd9..0051689d 100644 --- a/apps/web/src/lib/hibp.ts +++ b/apps/web/src/lib/hibp.ts @@ -1,3 +1,5 @@ +import { isDesktop } from "@/lib/desktop/is-desktop" + async function sha1Hex(password: string): Promise { const encoded = new TextEncoder().encode(password) const hashBuffer = await crypto.subtle.digest("SHA-1", encoded) @@ -12,14 +14,22 @@ export async function checkPasswordBreach(password: string): Promise { const hash = await sha1Hex(password) const prefix = hash.slice(0, 5) const suffix = hash.slice(5) + const url = `https://api.pwnedpasswords.com/range/${prefix}` - const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, { - headers: { "Add-Padding": "true" }, - }) - - if (!res.ok) throw new Error(`HIBP request failed: ${res.status}`) + // Desktop routes through the Rust proxy (only the k-anonymity SHA-1 prefix + // leaves the machine — no plaintext, no user data). Web calls HIBP directly. + let text: string + if (isDesktop()) { + const { proxyGet } = await import("@/lib/desktop/proxy-get") + const r = await proxyGet(url, { headers: { "Add-Padding": "true" } }) + if (!r.ok) throw new Error(`HIBP request failed: ${r.status}`) + text = r.isBase64 ? atob(r.body) : r.body + } else { + const res = await fetch(url, { headers: { "Add-Padding": "true" } }) + if (!res.ok) throw new Error(`HIBP request failed: ${res.status}`) + text = await res.text() + } - const text = await res.text() for (const line of text.split("\n")) { const [lineSuffix, count] = line.trim().split(":") if (lineSuffix === suffix) return parseInt(count, 10) diff --git a/apps/web/src/lib/import/openapi-watcher.ts b/apps/web/src/lib/import/openapi-watcher.ts index 1a392bc9..844d9486 100644 --- a/apps/web/src/lib/import/openapi-watcher.ts +++ b/apps/web/src/lib/import/openapi-watcher.ts @@ -5,6 +5,7 @@ import { importOpenApiSpec } from "./openapi" import type { Collection } from "@/components/api-client/types" +import { isDesktop } from "@/lib/desktop/is-desktop" export interface OpenApiWatchHandle { stop: () => void @@ -32,10 +33,21 @@ export function watchOpenApi(opts: OpenApiWatchOptions): OpenApiWatchHandle { const tick = async () => { if (cancelled) return + // Offline: skip silently (the caller already tolerates missed ticks). + if (typeof navigator !== "undefined" && !navigator.onLine) return try { - const res = await fetch(opts.url) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const text = await res.text() + let text: string + if (isDesktop()) { + // Route through the Rust proxy (no CORS, no webview leak). + const { proxyGet } = await import("@/lib/desktop/proxy-get") + const r = await proxyGet(opts.url) + if (!r.ok) throw new Error(`HTTP ${r.status}`) + text = r.isBase64 ? atob(r.body) : r.body + } else { + const res = await fetch(opts.url) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + text = await res.text() + } const hash = await sha256Hex(text) if (hash !== lastHash) { lastHash = hash diff --git a/apps/web/src/lib/oauth2.ts b/apps/web/src/lib/oauth2.ts index 7ba8e1ff..d4321c67 100644 --- a/apps/web/src/lib/oauth2.ts +++ b/apps/web/src/lib/oauth2.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * OAuth 2.0 helpers for the API client. * Token requests go through `/api/proxy` so they benefit from the same SSRF @@ -73,7 +74,7 @@ async function postTokenRequest( bodyParams: Record, ): Promise { const form = new URLSearchParams(bodyParams).toString() - const proxyRes = await fetch("/api/proxy", { + const proxyRes = await apiFetch("/api/proxy", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/lib/perf/runner.ts b/apps/web/src/lib/perf/runner.ts index 44cda448..3e0ea19f 100644 --- a/apps/web/src/lib/perf/runner.ts +++ b/apps/web/src/lib/perf/runner.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * Performance runner — fires the same request N times through `/api/proxy` * with a configurable concurrency, returning latency samples for stats. @@ -123,7 +124,7 @@ function buildProxyPayload(tab: ApiRequestState, env: Record): u async function fireOne(i: number, payload: unknown, signal?: AbortSignal): Promise { const t0 = performance.now() try { - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", credentials: "include", signal, diff --git a/apps/web/src/lib/runner/runner.ts b/apps/web/src/lib/runner/runner.ts index d5e47c91..02d1d637 100644 --- a/apps/web/src/lib/runner/runner.ts +++ b/apps/web/src/lib/runner/runner.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * Collection runner — sequential, single-iteration-or-data-driven, with the * same pre-request + test script lifecycle as a normal send. Intentionally @@ -280,7 +281,7 @@ async function executeRequest(args: { error?: string } try { - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", credentials: "include", signal: args.signal, diff --git a/apps/web/src/lib/scim/scim-client.ts b/apps/web/src/lib/scim/scim-client.ts index c81e3e33..e0110cf2 100644 --- a/apps/web/src/lib/scim/scim-client.ts +++ b/apps/web/src/lib/scim/scim-client.ts @@ -1,3 +1,4 @@ +import { apiFetch } from "@/lib/desktop/api-fetch"; /** * SCIM 2.0 (RFC 7644) client wrapper. * @@ -62,7 +63,7 @@ async function send( if (init.body !== undefined) headers["Content-Type"] = "application/scim+json" if (opts.viaProxy) { - const res = await fetch("/api/proxy", { + const res = await apiFetch("/api/proxy", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, diff --git a/apps/web/src/lib/sse-client.ts b/apps/web/src/lib/sse-client.ts index fef9259e..ac9e053d 100644 --- a/apps/web/src/lib/sse-client.ts +++ b/apps/web/src/lib/sse-client.ts @@ -91,7 +91,69 @@ export function pullSseEvents(buf: string): { events: SseEvent[]; remaining: str return { events, remaining: buf.slice(cursor) } } +/** Desktop: relay the stream through the Rust http_request_stream command. */ +async function streamSseRequestDesktop(args: SseStreamArgs): Promise { + const { invoke, Channel } = await import("@tauri-apps/api/core") + let buf = "" + let streamId: number | null = null + let done: () => void = () => {} + const finished = new Promise((resolve) => { done = resolve }) + + const channel = new Channel<{ + kind: "meta" | "chunk" | "end" | "error" + status?: number + statusText?: string + headers?: Record + data?: string + message?: string + }>() + channel.onmessage = (msg) => { + if (msg.kind === "meta") { + args.onMeta?.({ + status: msg.status ?? 0, + statusText: msg.statusText ?? "", + headers: msg.headers ?? {}, + contentType: msg.headers?.["content-type"] ?? "", + }) + } else if (msg.kind === "chunk") { + buf += msg.data ?? "" + const { events, remaining } = pullSseEvents(buf) + for (const ev of events) args.onEvent(ev) + buf = remaining + } else { + // end or error — flush trailing partial event, then close + if (buf.trim()) { + const { events } = pullSseEvents(buf + "\n\n") + for (const ev of events) args.onEvent(ev) + buf = "" + } + args.onClose?.() + done() + } + } + + args.signal?.addEventListener( + "abort", + () => { + if (streamId !== null) { + void invoke("http_request_stream_cancel", { id: streamId }) + } + }, + { once: true } + ) + + streamId = await invoke("http_request_stream", { + input: { url: args.url, method: args.method, headers: args.headers, body: args.body }, + channel, + }) + await finished +} + export async function streamSseRequest(args: SseStreamArgs): Promise { + const { isDesktop } = await import("@/lib/desktop/is-desktop") + if (isDesktop()) { + return streamSseRequestDesktop(args) + } const res = await fetch("/api/proxy-stream", { method: "POST", credentials: "include", diff --git a/apps/web/src/utils/useAuth.tsx b/apps/web/src/utils/useAuth.tsx index 1e666444..e8b82d5c 100644 --- a/apps/web/src/utils/useAuth.tsx +++ b/apps/web/src/utils/useAuth.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { auth } from "../database/firebase"; import { User } from "firebase/auth"; +import { isDesktop } from "@/lib/desktop/is-desktop"; // Export the return type export interface AuthState { @@ -10,15 +11,48 @@ export interface AuthState { loading: boolean; } +/** + * Desktop offline mode has no Firebase session; tools are gated by the local + * master-vault unlock instead. A synthetic user keeps components that read + * uid/displayName/getIdToken working without network. + */ +function desktopLocalUser(): User { + return { + uid: "desktop-local", + email: null, + displayName: "Local User", + photoURL: null, + emailVerified: true, + isAnonymous: false, + providerData: [], + getIdToken: async () => "", + getIdTokenResult: async () => ({ token: "" }), + reload: async () => {}, + toJSON: () => ({ uid: "desktop-local" }), + } as unknown as User; +} + const useAuth = (requireAuth: boolean = false): AuthState => { // auth.currentUser is synchronously available once Firebase has resolved auth state. // On client-side navigation within the app it is already populated, so we avoid // a spurious full-screen loading flash on every route change. - const [user, setUser] = useState(() => auth.currentUser); - const [loading, setLoading] = useState(() => auth.currentUser === null); + const [user, setUser] = useState(() => + isDesktop() ? auth.currentUser ?? desktopLocalUser() : auth.currentUser + ); + const [loading, setLoading] = useState(() => + isDesktop() ? false : auth.currentUser === null + ); const router = useRouter(); useEffect(() => { + if (isDesktop()) { + // Desktop never redirects to /login — the local user is always signed + // in; a real Firebase user (cloud sign-in via deep link) takes priority. + const unsubscribe = auth.onAuthStateChanged((firebaseUser) => { + setUser(firebaseUser ?? desktopLocalUser()); + }); + return () => unsubscribe(); + } const unsubscribe = auth.onAuthStateChanged((firebaseUser) => { setUser(firebaseUser); setLoading(false); diff --git a/package.json b/package.json index 78942f95..5756b5ad 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "lint": "pnpm -r --if-present lint", "i18n:audit": "node scripts/i18n-audit.mjs", "i18n:sync": "node scripts/i18n-sync.mjs", - "clean-install": "rimraf node_modules apps/*/node_modules && pnpm install" + "clean-install": "rimraf node_modules apps/*/node_modules && pnpm install", + "dev:desktop": "pnpm --filter @mydevtools/desktop dev", + "build:desktop": "pnpm --filter @mydevtools/desktop build" }, "devDependencies": { "rimraf": "^6.1.0" @@ -21,4 +23,4 @@ "next-themes@0.4.6": "patches/next-themes@0.4.6.patch" } } -} \ No newline at end of file +} diff --git a/tauri-plan.md b/tauri-plan.md deleted file mode 100644 index e63df314..00000000 --- a/tauri-plan.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: Tauri Desktop App Plan -overview: Create a Tauri v2 desktop application (Mac/Windows/Linux) that runs offline-first with local encrypted SQLite storage, shares the existing Next.js frontend via webview, and offers optional sync to either the existing backend API or cloud storage providers. -todos: - - id: scaffold-tauri - content: Initialize Tauri v2 project in apps/desktop with Cargo.toml, tauri.conf.json, and basic window loading Next.js dev server - status: pending - - id: setup-sqlcipher - content: Set up SQLCipher database layer in Rust with migrations matching existing MongoDB collections schema - status: pending - - id: platform-adapter - content: Create platform detection + API adapter layer in frontend (isDesktop/isWeb branching) - status: pending - - id: rust-crypto - content: Implement master password key derivation and encryption in Rust (matching existing Web Crypto PBKDF2+AES-GCM scheme) - status: pending - - id: passwords-module - content: Implement password vault Tauri commands (CRUD) as proof of concept with full offline support - status: pending - - id: all-modules - content: "Implement remaining modules: notes, bookmarks, tasks, snippets, env manager, API client, S3 drive, SQL/NoSQL connections" - status: pending - - id: auth-desktop - content: Implement desktop-mode auth (local master password, skip Firebase, optional remote account link) - status: pending - - id: sync-engine - content: "Build sync engine: backend API sync + cloud storage export/import with per-app toggle" - status: pending - - id: shared-types - content: Extract shared TypeScript types into packages/shared-types for web and desktop to share - status: pending - - id: static-export - content: Configure Next.js static export build target for Tauri embedding - status: pending - - id: distribution - content: Set up multi-platform builds (macOS dmg, Windows msi, Linux AppImage) with GitHub Actions CI - status: pending -isProject: false ---- - -# Tauri Desktop App - Offline-First with Optional Sync - -## Current Architecture Summary - -The project is a pnpm monorepo with: -- **Frontend**: Next.js 16 app ([apps/web](apps/web)) with Zustand state, Radix UI, Tailwind, Firebase Auth -- **Backend**: FastAPI + MongoDB ([apps/backend](apps/backend)) storing user data in collections (passwords, notes, bookmarks, tasks, snippets, env vars, API client data, S3 connections, SQL connections, NoSQL connections) -- **Auth**: Firebase Auth on frontend, JWT session cookies for backend API -- **Encryption**: Client-side AES-256-GCM via Web Crypto API (PBKDF2 key derivation from master password). Encrypted data stored server-side; server never sees plaintext -- **Key Storage**: CryptoKey persisted in IndexedDB (browser) - -## Architecture for Tauri Desktop App - -```mermaid -graph TB - subgraph TauriApp ["Tauri Desktop App"] - WebView["Next.js Frontend (WebView)"] - RustCore["Rust Core Layer"] - SQLite["SQLite (encrypted)"] - FileStore["Local File Store"] - end - - subgraph SyncTargets ["Optional Sync Targets"] - BackendAPI["Existing Backend API"] - CloudStorage["Cloud Storage (S3/GDrive)"] - end - - WebView -->|"invoke()"| RustCore - RustCore --> SQLite - RustCore --> FileStore - RustCore -->|"optional"| BackendAPI - RustCore -->|"optional"| CloudStorage -``` - -## Key Design Decisions - -### 1. Offline-First Data Layer (Rust + SQLite) - -Replace backend API calls with a local Rust data layer using **SQLCipher** (encrypted SQLite): - -- **Confidential data** (passwords, env vars, S3 credentials, SQL/NoSQL connection strings): Encrypted locally with AES-256-GCM using the same Web Crypto derivation scheme, stored in SQLCipher DB -- **Non-confidential data** (bookmarks, tasks, notes, code snippets, API client history): Stored in plain SQLite tables locally -- **S3 Drive file cache**: Stored in app's local file system directory - -The Rust layer exposes Tauri commands that mirror the existing backend API interface, so the frontend's API layer can swap between `fetch()` (web) and `invoke()` (Tauri) with minimal changes. - -### 2. Frontend Adaptation Strategy - -Create a shared **platform adapter** layer: - -```typescript -// apps/web/src/lib/platform.ts -export const platform = { - isDesktop: () => Boolean(window.__TAURI__), - isWeb: () => !window.__TAURI__, -} - -// apps/web/src/lib/api-adapter.ts -export async function apiCall(endpoint, options) { - if (platform.isDesktop()) { - return invoke('api_call', { endpoint, ...options }) - } - return backendFetch(endpoint, options) -} -``` - -This keeps the existing web app working as-is while the Tauri build routes calls to the local Rust backend. - -### 3. Auth in Desktop Mode - -- **No Firebase dependency in desktop mode** - user sets a local master password on first launch -- The master password derives the SQLCipher encryption key (same PBKDF2 scheme already used) -- Optional: link a remote account for sync (Firebase auth or username/password to the existing backend) - -### 4. Sync Architecture - -Two sync modes, per-app configurable: - -| Data Type | Sync to Backend | Sync to Cloud | -|-----------|----------------|---------------| -| Passwords | Encrypted blobs synced | Encrypted export file | -| Notes | Full sync | JSON export | -| Bookmarks | Full sync | JSON export | -| Tasks | Full sync | JSON export | -| Env vars | Encrypted blobs synced | Encrypted export file | -| S3/SQL/NoSQL connections | Encrypted blobs synced | Encrypted export file | - -Sync protocol: -- **Last-write-wins** with `updatedAt` timestamps (already present on all records) -- Each record gets a `syncStatus` field: `local_only | synced | pending_push | conflict` -- Background sync worker in Rust polls/pushes on configurable interval -- Conflict resolution: show user a diff and let them choose - -### 5. Monorepo Structure Changes - -``` -mydevtools-monorepo/ -├── apps/ -│ ├── web/ # Existing Next.js web app (unchanged) -│ ├── backend/ # Existing FastAPI backend (unchanged) -│ └── desktop/ # NEW: Tauri app shell -│ ├── src-tauri/ -│ │ ├── Cargo.toml -│ │ ├── tauri.conf.json -│ │ ├── src/ -│ │ │ ├── main.rs -│ │ │ ├── commands/ # Tauri invoke handlers -│ │ │ │ ├── mod.rs -│ │ │ │ ├── passwords.rs -│ │ │ │ ├── notes.rs -│ │ │ │ ├── bookmarks.rs -│ │ │ │ ├── tasks.rs -│ │ │ │ ├── env_manager.rs -│ │ │ │ ├── snippets.rs -│ │ │ │ ├── api_client.rs -│ │ │ │ ├── s3_drive.rs -│ │ │ │ ├── sql_client.rs -│ │ │ │ └── nosql.rs -│ │ │ ├── db/ # SQLite/SQLCipher layer -│ │ │ │ ├── mod.rs -│ │ │ │ ├── migrations.rs -│ │ │ │ └── schema.rs -│ │ │ ├── sync/ # Optional sync engine -│ │ │ │ ├── mod.rs -│ │ │ │ ├── backend_sync.rs -│ │ │ │ └── cloud_sync.rs -│ │ │ ├── crypto.rs # Encryption helpers -│ │ │ └── config.rs # App configuration -│ │ └── icons/ -│ ├── src/ # Frontend entry (loads Next.js build) -│ │ └── index.html -│ └── package.json -├── packages/ -│ └── shared-types/ # NEW: Shared TypeScript types -│ ├── package.json -│ └── src/ -│ └── index.ts -└── pnpm-workspace.yaml # Updated to include packages/* -``` - -### 6. Tauri Configuration - -- **Tauri v2** (latest stable, Rust-based, better security model) -- Frontend: **Static export of Next.js** (`next export` or `output: 'export'`) embedded in Tauri -- Dev mode: Tauri points to `http://localhost:3000` (Next.js dev server) -- Permissions: filesystem (app data dir), network (for sync), shell (none) - -### 7. Data Migration Path - -For users of the web app who want to move to desktop: -1. Export all data from web app (encrypted JSON bundle) -2. Import into desktop app (decrypted with master password, re-encrypted locally) -3. Or: enable sync and let background sync pull all data - -## Implementation Phases - -### Phase 1: Tauri Scaffold + Local Storage (Core) -- Initialize Tauri v2 project in `apps/desktop` -- Set up SQLCipher database with schema matching all collections -- Implement master password / key derivation in Rust -- Create platform adapter in frontend (`isDesktop` detection) -- Wire up the first module (passwords) end-to-end locally - -### Phase 2: All Modules Offline -- Implement all Tauri commands for: notes, bookmarks, tasks, snippets, env manager, API client, S3 drive connections, SQL/NoSQL connections -- Frontend adapter for all API calls -- Local file storage for S3 drive cache - -### Phase 3: Sync Engine -- Backend sync: authenticate with existing backend, push/pull encrypted records -- Cloud sync: export/import encrypted bundles to S3 or local file -- Per-app sync toggle in settings UI -- Conflict resolution UI - -### Phase 4: Polish + Distribution -- App icons and branding -- Auto-updater (Tauri built-in) -- macOS notarization, Windows code signing -- Linux AppImage/deb/rpm -- CI/CD for multi-platform builds (GitHub Actions) - -## Key Files to Modify - -- [pnpm-workspace.yaml](pnpm-workspace.yaml) - add `packages/*` and `apps/desktop` -- [apps/web/src/lib/backend-auth.ts](apps/web/src/lib/backend-auth.ts) - add desktop adapter branching -- [apps/web/next.config.ts](apps/web/next.config.ts) - add static export config for Tauri builds -- [apps/web/src/utils/useAuth.tsx](apps/web/src/utils/useAuth.tsx) - bypass Firebase in desktop mode -- [apps/web/src/lib/key-storage.ts](apps/web/src/lib/key-storage.ts) - use Tauri secure storage in desktop mode - -## Technology Choices - -- **Tauri v2**: Latest, better permission model, smaller binary than Electron -- **SQLCipher** (via `rusqlite` with `bundled-sqlcipher` feature): Encrypted SQLite at rest -- **serde/serde_json**: Rust serialization matching existing JSON schemas -- **reqwest**: HTTP client for sync with backend API -- **tauri-plugin-store**: Secure key-value store for non-DB settings -- **tauri-plugin-fs**: File system access for S3 drive cache