commit dc23fac0577264e20e810c675ba28002511f276b Author: thomasabishop Date: Mon Jul 7 17:08:27 2025 +0100 chore: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..866fc4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +public/posts +public/post-index.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..d07b635 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": false, + "printWidth": 80, + "proseWrap": "always" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f46154 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Systems Obscure + +> Another software engineer with a blog + +![Screenshot of blog](./public/blog-screenshot.png) + +## Build + +```sh +npm run build +``` + +This runs `vite build` using [speedy web compiler](https://swc.rs/) for the +transpilation of TSX to JSX. I am using TypeScript so that I can use the +[shadcn](https://ui.shadcn.com/) component library however I don't actually care +about types in this project and all my components are written as JSX. SWC +transpiles very quickly without throwing TS warning and errors. + +I still use Vite's native `esbuild` for HMR transpilation in development. + +## Scripts + +### Generate post index + +```sh +npm run build:posts +``` + +This runs `scripts/generate-post-index.js` which reads all raw Markdown posts in +`/posts` and parses the body content and YAML front-matter. It then writes this +data to `public/post-index.json` which is read by the React application at +runtime. diff --git a/components.json b/components.json new file mode 100644 index 0000000..73afbdb --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..b628f77 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import tseslint from "typescript-eslint" + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + } +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..61388f6 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + Vite + React + TS + + + +
+ + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81d7736 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6757 @@ +{ + "name": "systems-obscure", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "systems-obscure", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-navigation-menu": "^1.2.12", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toggle": "^1.1.8", + "@shikijs/colorized-brackets": "^3.6.0", + "@tailwindcss/vite": "^4.1.6", + "@vitejs/plugin-react-swc": "^3.10.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gray-matter": "^4.0.3", + "i": "^0.3.7", + "lucide-react": "^0.509.0", + "marked": "^15.0.12", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.6.0", + "sharp": "^0.34.2", + "shiki": "^3.6.0", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.6" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/node": "^22.15.17", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "tw-animate-css": "^1.2.9", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", + "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", + "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", + "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.12.tgz", + "integrity": "sha512-iExvawdu7n6DidDJRU5pMTdi+Z3DaVPN4UZbAGuTs7nJA8P4RvvkEz+XYI2UJjb/Hh23RrH19DakgZNLdaq9Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.8.tgz", + "integrity": "sha512-hrpa59m3zDnsa35LrTOH5s/a3iGv/VD+KKQjjiCTo/W4r0XwPpiWQvAv6Xl1nupSoaZeNNxW6sJH9ZydsjKdYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", + "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", + "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/colorized-brackets": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/colorized-brackets/-/colorized-brackets-3.6.0.tgz", + "integrity": "sha512-vYAj5trQvNXrxp5gmvBX3SMQj5vKC9etfGZc01hjxQbgldKjmvXGoA/no1U5tuaqpjQGx0TBPyNdASg6mfvVFg==", + "license": "MIT", + "dependencies": { + "shiki": "3.6.0" + } + }, + "node_modules/@shikijs/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.6.0.tgz", + "integrity": "sha512-9By7Xb3olEX0o6UeJyPLI1PE1scC4d3wcVepvtv2xbuN9/IThYN4Wcwh24rcFeASzPam11MCq8yQpwwzCgSBRw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.6.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.6.0.tgz", + "integrity": "sha512-7YnLhZG/TU05IHMG14QaLvTW/9WiK8SEYafceccHUSXs2Qr5vJibUwsDfXDLmRi0zHdzsxrGKpSX6hnqe0k8nA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.6.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.6.0.tgz", + "integrity": "sha512-nmOhIZ9yT3Grd+2plmW/d8+vZ2pcQmo/UnVwXMUXAKTXdi+LK0S08Ancrz5tQQPkxvjBalpMW2aKvwXfelauvA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.6.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.6.0.tgz", + "integrity": "sha512-IdZkQJaLBu1LCYCwkr30hNuSDfllOT8RWYVZK1tD2J03DkiagYKRxj/pDSl8Didml3xxuyzUjgtioInwEQM/TA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.6.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.6.0.tgz", + "integrity": "sha512-Fq2j4nWr1DF4drvmhqKq8x5vVQ27VncF8XZMBuHuQMZvUSS3NBgpqfwz/FoGe36+W6PvniZ1yDlg2d4kmYDU6w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.6.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.6.0.tgz", + "integrity": "sha512-cLWFiToxYu0aAzJqhXTQsFiJRTFDAGl93IrMSBNaGSzs7ixkLfdG6pH11HipuWFGW5vyx4X47W8HDQ7eSrmBUg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.7.tgz", + "integrity": "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.23" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.12.7", + "@swc/core-darwin-x64": "1.12.7", + "@swc/core-linux-arm-gnueabihf": "1.12.7", + "@swc/core-linux-arm64-gnu": "1.12.7", + "@swc/core-linux-arm64-musl": "1.12.7", + "@swc/core-linux-x64-gnu": "1.12.7", + "@swc/core-linux-x64-musl": "1.12.7", + "@swc/core-win32-arm64-msvc": "1.12.7", + "@swc/core-win32-ia32-msvc": "1.12.7", + "@swc/core-win32-x64-msvc": "1.12.7" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.7.tgz", + "integrity": "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.7.tgz", + "integrity": "sha512-jN6LhFfGOpm4DY2mXPgwH4aa9GLOwublwMVFFZ/bGnHYYCRitLZs9+JWBbyWs7MyGcA246Ew+EREx36KVEAxjA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.7.tgz", + "integrity": "sha512-rHn8XXi7G2StEtZRAeJ6c7nhJPDnqsHXmeNrAaYwk8Tvpa6ZYG2nT9E1OQNXj1/dfbSFTjdiA8M8ZvGYBlpBoA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.7.tgz", + "integrity": "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.7.tgz", + "integrity": "sha512-jxyINtBezpxd3eIUDiDXv7UQ87YWlPsM9KumOwJk09FkFSO4oYxV2RT+Wu+Nt5tVWue4N0MdXT/p7SQsDEk4YA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.7.tgz", + "integrity": "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.7.tgz", + "integrity": "sha512-zy7JWfQtQItgMfUjSbbcS3DZqQUn2d9VuV0LSGpJxtTXwgzhRpF1S2Sj7cU9hGpbM27Y8RJ4DeFb3qbAufjbrw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.7.tgz", + "integrity": "sha512-52PeF0tyX04ZFD8nibNhy/GjMFOZWTEWPmIB3wpD1vIJ1po+smtBnEdRRll5WIXITKoiND8AeHlBNBPqcsdcwA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.7.tgz", + "integrity": "sha512-WzQwkNMuhB1qQShT9uUgz/mX2j7NIEPExEtzvGsBT7TlZ9j1kGZ8NJcZH/fwOFcSJL4W7DnkL7nAhx6DBlSPaA==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.7.tgz", + "integrity": "sha512-R52ivBi2lgjl+Bd3XCPum0YfgbZq/W1AUExITysddP9ErsNSwnreYyNB3exEijiazWGcqHEas2ChiuMOP7NYrA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", + "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.6.tgz", + "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.29.2", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", + "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-arm64": "4.1.6", + "@tailwindcss/oxide-darwin-x64": "4.1.6", + "@tailwindcss/oxide-freebsd-x64": "4.1.6", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.6", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.6", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.6", + "@tailwindcss/oxide-linux-x64-musl": "4.1.6", + "@tailwindcss/oxide-wasm32-wasi": "4.1.6", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.6", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.6" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.6.tgz", + "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.6.tgz", + "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.6.tgz", + "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.6.tgz", + "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.6.tgz", + "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.6.tgz", + "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.6.tgz", + "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.6.tgz", + "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.6.tgz", + "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.6.tgz", + "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.9", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.6.tgz", + "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.6.tgz", + "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.6.tgz", + "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.6", + "@tailwindcss/oxide": "4.1.6", + "tailwindcss": "4.1.6" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", + "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", + "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", + "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/type-utils": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", + "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", + "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", + "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.0", + "@typescript-eslint/utils": "8.32.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", + "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", + "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/visitor-keys": "8.32.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", + "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.0", + "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/typescript-estree": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", + "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", + "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.11", + "@swc/core": "^1.11.31" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001717", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", + "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.151", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz", + "integrity": "sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==", + "dev": true, + "license": "ISC" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "zod": "^3.24.2" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", + "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.2", + "lightningcss-darwin-x64": "1.29.2", + "lightningcss-freebsd-x64": "1.29.2", + "lightningcss-linux-arm-gnueabihf": "1.29.2", + "lightningcss-linux-arm64-gnu": "1.29.2", + "lightningcss-linux-arm64-musl": "1.29.2", + "lightningcss-linux-x64-gnu": "1.29.2", + "lightningcss-linux-x64-musl": "1.29.2", + "lightningcss-win32-arm64-msvc": "1.29.2", + "lightningcss-win32-x64-msvc": "1.29.2" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz", + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.2.tgz", + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.2.tgz", + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.2.tgz", + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.2.tgz", + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.2.tgz", + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.2.tgz", + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.2.tgz", + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.2.tgz", + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.2.tgz", + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.509.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.509.0.tgz", + "integrity": "sha512-xCJHn6Uh5qF6PGml25vveCTrHJZcqS1G1MVzWZK54ZQsOiCVJk4fwY3oyo5EXS2S+aqvTpWYIfJN+PesJ0quxg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz", + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.6.0.tgz", + "integrity": "sha512-tKn/Y0MGBTffQoklaATXmTqDU02zx8NYBGQ+F6gy87/YjKbizcLd+Cybh/0ZtOBX9r1NEnAy/GTRDKtOsc1L9w==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.6.0", + "@shikijs/engine-javascript": "3.6.0", + "@shikijs/engine-oniguruma": "3.6.0", + "@shikijs/langs": "3.6.0", + "@shikijs/themes": "3.6.0", + "@shikijs/types": "3.6.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", + "integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tw-animate-css": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.9.tgz", + "integrity": "sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.32.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.0.tgz", + "integrity": "sha512-UMq2kxdXCzinFFPsXc9o2ozIpYCCOiEC46MG3yEh5Vipq6BO27otTtEBZA1fQ66DulEUgE97ucQ/3YY66CPg0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.32.0", + "@typescript-eslint/parser": "8.32.0", + "@typescript-eslint/utils": "8.32.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f9639a --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "systems-obscure", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:posts": "node scripts/generate-post-index.js", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-navigation-menu": "^1.2.12", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toggle": "^1.1.8", + "@shikijs/colorized-brackets": "^3.6.0", + "@tailwindcss/vite": "^4.1.6", + "@vitejs/plugin-react-swc": "^3.10.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "gray-matter": "^4.0.3", + "i": "^0.3.7", + "lucide-react": "^0.509.0", + "marked": "^15.0.12", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.6.0", + "sharp": "^0.34.2", + "shiki": "^3.6.0", + "tailwind-merge": "^3.2.0", + "tailwindcss": "^4.1.6" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/node": "^22.15.17", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "tw-animate-css": "^1.2.9", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/posts/a-human-being-is.md b/posts/a-human-being-is.md new file mode 100644 index 0000000..e614647 --- /dev/null +++ b/posts/a-human-being-is.md @@ -0,0 +1,11 @@ +--- +title: "A human being is..." +slug: /a-human-being-is/ +date: 2024-08-18 +tags: ["quotes"] +--- + +> A human being is a packet of propagating information that dissipates at death. + +I can't remember who said this or where it comes from but I just found it in an +old notebook. diff --git a/posts/activity-log-backend.md b/posts/activity-log-backend.md new file mode 100644 index 0000000..bdf7290 --- /dev/null +++ b/posts/activity-log-backend.md @@ -0,0 +1,194 @@ +--- +title: "Activity Log backend" +slug: /activity-log-backend/ +date: 2024-07-19 +tags: ["log", "aws", "typescript", "python", "bash"] +--- + +I have just added a new feature to the site: +[Activity Log](https://systemsobscure.blog/activity-log/), which presents my +personal time tracking entries. This is how I configured the backend. + +![](./img/activity-log-diagram-darker.png) + +On my local machine I use [TimeWarrior](https://timewarrior.net/docs/what/) to +record the time I spend on different activities. It runs from the terminal and +can be exported to JSON. + +To access the data remotely, I needed to store it in a database and be able to +regularly export the time entries from my machine to the remote DB. + +I created a simple AWS Lambda with a few endpoints: + +- A POST that receives data and adds it to the database. +- A GET at `/count` that returns a count of the entries for each day for the + last twelve months +- A GET at `/month` that returns the entries for the last month +- A GET at `/day` that returns the entries for the specified single date + +The idea is that the frontend will first present all the entries for the current +month in a table. Along with this there will be histogram similar to GitHub's +commit graph that will present a colour coded representation of the amount of +activity for each day in the last year. When the user clicks on a day, a request +is made for the entries for that day. That way I don't have to load all the +entries into memory at once. + +In development, I used MySQL but it turns out that production SQL databases are +prohibitively expensive on AWS. This figures as MySQL requires a permanently +running server, contravening the serverless architecture that makes AWS +otherwise affordable on the Free Tier. + +After some research I decided to use DynamoDB. This didn't immediately occur to +me because, in my ignorance, I assumed a key-value database only permits one key +to one value and my table contains multiple fields for each row. In fact, after +specifying a unique _primary key_, you can associate it with any number of +values (known as "attributes"), thus creating a "table-like" data structure. + +The nomenclature for DynamoDB is quite confusing but I was able to create a +simple table-like structure with the following schema: + +```json +{ + "TableName": "TimeEntries", + "AttributeDefinitions": [ + { "AttributeName": "activity_start_end", "AttributeType": "S" }, + { "AttributeName": "year", "AttributeType": "S" } + ], + "KeySchema": [{ "AttributeName": "activity_start_end", "KeyType": "HASH" }], + "GlobalSecondaryIndexes": [ + { + "IndexName": "YearIndex", + "KeySchema": [ + { "AttributeName": "year", "KeyType": "HASH" }, + { "AttributeName": "start", "KeyType": "RANGE" } + ] + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 1, + "WriteCapacityUnits": 1 + } +} +``` + +The attribute `activity_start_end` is my primary key which is basically +equivalent to the primary key in SQL: the value I use to uniquely individuate +each entry in the table. It is a simple concatenation of the fields +`activity_type`, `start`, `end`. The latter two are ISO timestamps, ensuring +uniqueness. + +A _global secondary index_ (GSI) is an attribute in addition to the primary key +that you may use to group entries. By suppling a GSI with your query you can +reduce the compute required for lookups. By using `year` as a GSI I ensure that +the query will only run against values matching the specified year. + +When working locally, I used Docker to create a DynamoDB image. As the AWS SAM +software (used for running the Lambda and API Gateway locally) also uses a +Docker container (that you can't inspect or modify) it was necessary to specify +a bridging network in the `docker-compose.yml` so that each container could +communicate: + +```yml +services: + dev: + image: amazon/dynamodb-local + container_name: timetracking_dynamodb_dev + ports: + - "8000:8000" + volumes: + - "/home/thomas/repos/lambdas/node-js/time-tracking/data/dev:/home/dynamodblocal/data/dev" + command: + "-jar DynamoDBLocal.jar -dbPath /home/dynamodblocal/data/dev --sharedDb" + networks: + - sam-local + +networks: + sam-local: + driver: bridge +``` + +With the server running I could then view the database using Amazon's _NoSQL +Workbench_ client: + +![NoSQL workbench](./img/no-sql-workbench.png) + +Creating the Lambda with TypeScript was simple enough using the AWS SDK. Here is +the function that I use to return the entries for the month and specific dates: + +```ts +import { DynamoDBClient } from "@aws-sdk/client-dynamodb" +import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb" +import { generateDates, TPeriod } from "./generateDates" + +interface ITimeEntry { + activity_start_end: string + year: string + start: string + end: string + activity_type: string + duration: number + description: string +} + +const getTimeEntries = async ( + client: DynamoDBClient, + timePeriod: TPeriod +): Promise => { + const documentClient = DynamoDBDocumentClient.from(client) + const dateParams = generateDates()[timePeriod] + + const params = { + TableName: "TimeEntries", + IndexName: "YearIndex", + KeyConditionExpression: + "#yr = :year AND #start BETWEEN :start_date AND :end_date", + ExpressionAttributeNames: { + "#yr": "year", + "#start": "start", + }, + ExpressionAttributeValues: { + ":year": dateParams.year, + ":start_date": dateParams.start, + ":end_date": dateParams.end, + }, + } + + const command = new QueryCommand(params) + const response = await documentClient.send(command) + return (response?.Items as ITimeEntry[]) || [] +} + +export { getTimeEntries, ITimeEntry } +``` + +(The `DynamoDBDocumentClient` proved essential. It ensures that each row comes +back as an array of objects matching the shape of `ITimeEntry`. Without this, +the data returns in a nested format specific to DynamoDB that is cumbersome to +work with.) + +Outside of the Lambda and setting up the database, it was necessary to write +some scripts to glue the different parts together: + +- a Python + [script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/export_timewarrior_entries.py) + that exports the entries from TimeWarrior, parsing them into the format the + database expects +- a Python + [script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/upload_daily_entries.py) + (running on a cron timer) that runs the preceding export script every hour and + uploads to the remote DB using the Lambda's POST endpoint, sending a + notification to my + [Slack channel](https://systemsobscure.blog/slack-notification-center/) +- a Bash + [ script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/migrate.sh) + to migrate the contents of the production database to my local Docker instance + (useful when working on the frontend locally) +- a Bash + [script](https://github.com/thomasabishop/lambdas/blob/main/node-js/time-tracking/scripts/seed.sh) + to seed the production and local databases from a CSV + +That's basically it. The biggest pain point was getting my DynamoDB Docker +instance to communicate with the default SAM instance. That, and converting my +SQL version into DynamoDB. I wouldn't say I find DynamoDB particularly +straightforward or fun to use but I'll put up with it because it costs like +£00.02 per month compared to the £54.00 they were asking for MySQL. diff --git a/posts/alien-blood-vscode-theme.md b/posts/alien-blood-vscode-theme.md new file mode 100644 index 0000000..40d30cf --- /dev/null +++ b/posts/alien-blood-vscode-theme.md @@ -0,0 +1,42 @@ +--- +title: "Alien Blood VS Code theme" +slug: /alien-blood-vscode-theme/ +date: 2022-01-01 +tags: ["project", "productivity"] +--- + +I'm endlessly toggling through colour themes in VS Code, combining them with +different fonts and icons to approximate a Platonic ideal of the optimal working +environment. I am never satisfied. There is always some aspect that displeases +me. + +![The Alien Blood terminal colours](./img/alien_blood_terminal_colours.png) + +Over the last month or so I decided to resolve the issue once and for all by +creating my own custom theme. + +I have always really enjoyed the Alien Blood colours that are one of the default +themes of the iTerm2 terminal emulator for Mac. I decided I would use this as my +base and try to construct a full syntax and UI scheme from the terminal +pallette. This wasn't easy because the colour palettes of terminal themes are +more austere than those designed for syntax-highlighting. It took lots of +tweaking to try and make the resulting theme visually harmonious but not so +homogenous that you cannot distinguish the syntax at glance. + +![Alien Blood VS Code theme showing TypeScript highlighting](./img/alien_blood_syntax_demo.png) + +I was able to achieve this without introducing any additional colours to the +default Alien Blood palette. The theme can be demoed in the browser +[here](https://vscode.dev/theme/ThomasBishop.alien-blood/Alien%20Blood%20) and +downloaded from the +[extension marketplace](https://marketplace.visualstudio.com/items?itemName=ThomasBishop.alien-blood). +You can also view the theme on +[GitHub](https://github.com/thomasabishop/alien-blood-vscode). + +I'm particularly happy with the rendering of TypeScript and Angular components +which is what I am editing the majority of the time at work, similarly with HTML +and CSS. However I think there is room for improvement when it comes to plain +JavaScript and other filetypes such as shell scripts. In these cases the +highlighting is too homogenous and dark but now I have the basic palette +completed I can add additional rules for specific programming languages in +future releases. diff --git a/posts/backing-up-primary-machine.md b/posts/backing-up-primary-machine.md new file mode 100644 index 0000000..916c24d --- /dev/null +++ b/posts/backing-up-primary-machine.md @@ -0,0 +1,195 @@ +--- +title: "How I backup my primary machine" +slug: /backing-up-primary-machine/ +date: 2023-01-08 +tags: ["log", "linux"] +--- + +I use the `rsnapshots` package to run sequenced backups of my ThinkPad T15 +running Arch Linux. + +`rsnapshots` is based on the `rsync` utility and makes it easy to maintain +backups over several timeframes. I run hourly, daily, weekly, and monthly +backups, sequenced using `systemd` timers. + +## Preparing the external disk + +I store my backups on an external 500GB SSD. + +First I partition the disk: + +``` +fdisk /dev/sda +d # delete the existing partitions +n # start a new partition ++500GB # specify size +w # write the partition to the disk +``` + +I now have a 500GB partition at `/dev/sda/sda1` + +Next I create the file system: + +``` +mkfs -t ext4 /dev/sda1 +``` + +And I label my disk so that I it has a readable name rather than the default +GUID that Linux will apply. + +``` +e2label /dev/sda1 archbish_backups +``` + +At this point you would create or specify a mount directory and mount the +partition with `mount`, also adding it to your `fstab` file to ensure that the +disk mounts to the same location in future. + +I haven't chosen to do this because I use the KDE Plasma desktop environment and +it automatically mounts any connected drives to `/run/media`. However in order +to check the partition label and ensure the above processes have been successful +I will disconnect and reconnect the device. + +Now when I run `lsblk` to list the block devices on the machine, I see my new +disk and partition: + +``` +lsblk +NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS +sda 8:0 0 465.8G 0 disk +└─sda1 8:1 0 465.8G 0 part /run/media/thomas/archbish_backups +nvme0n1 259:0 0 476.9G 0 disk +├─nvme0n1p1 259:1 0 512M 0 part +├─nvme0n1p2 259:2 0 11.2G 0 part [SWAP] +└─nvme0n1p3 259:3 0 465.3G 0 part / +``` + +Typically you won't have access to the partition or mount directory yet. You can +run the following against your username to ensure access: + +``` +sudo chown -R :users /run/media// +``` + +## Configure snapshot backups with rsnapshot + +I install `rsnapshot` and `rsync` as a dependency: + +``` +pacman -Sy rsync rsnapshot +``` + +`rsnapshot` is setup entirely through its config file: `/etc/rsnapshot.conf`. +When you install the package the default config file contains lots of +instructions and it's mostly a case of removing the comments for the +functionality you require. The +[Arch wiki](https://wiki.archlinux.org/title/rsnapshot) provides an exhaustive +account but the key parts are as follows: + +``` +# Set the snapshot root directory (the external HDD you plan to use) + +snapshot_root /run/media/thomas/archbish_backups + +# Set the backup intervals + +retain hourly 24 +retain daily 7 +retain weekly 4 +retain monthly 12 + +# Name the dir you want to snapshot and what it should be called on the external disk + +backup /home/ localhost +``` + +So, obviously I am taking hourly, daily, weekly and monthly snapshots. In total, +this gives me a years worth of retention whilst minimising the space taken by +the older backups. (If I wanted to keep a record spanning several years, I could +just make a copy the latest monthly backup in a year's time.) + +The numbers next to the names indicate the retention period. After 24 hours, the +oldest hourly backup becomes the daily backup; after seven days the oldest daily +backup becomes the weekly backup; after four weeks the oldest weekly backup +becomes the monthly backup and so on. + +Now we need to automate the execution of `rsnapshot` at the times designated in +the configuration file. We'll do this with a `systemd` service file and several +timers. Each file created will be located at `/etc/systemd/system/` + +First we create the service file. This will be a oneshot service that is run by +the timers: + +``` +[Unit] +Description=rsnapshot (%I) backup + +[Service] +Type=oneshot +Nice=19 +IOSchedulingClass=idle +ExecStart=/usr/bin/rsnapshot %I +``` + +Then we have to create a timer file for each of the intervals: hourly, daily, +weekly, and monthly. Here's the hourly and monthly files to give an idea: + +``` +# /etc/systemd/system/rsnapshot-hourly.timer +[Unit] +Description=rsnapshot hourly backup + +[Timer] +# Run every hour at 15mins past the hour +OnCalendar=*:15 +Persistent=true +Unit=rsnapshot@hourly.service + +[Install] +WantedBy=timers.target +``` + +``` +# /etc/systemd/system/rsnapshot-monthly.timer +[Unit]Description=rsnapshot monthly backup + +[Timer] +# Run once per month at 3:30 local time +OnCalendar=*-*-1 03:30:00 +Persistent=true +Unit=rsnapshot@monthly.service + +[Install] +WantedBy=timers.target +``` + +Let's check one of our timers: + +``` +systemd-analyze calendar "*:15" + +Original form: *:15 +Normalized form: *-*-* *:15:00 + Next elapse: Sun 2023-01-08 15:15:00 GMT + (in UTC): Sun 2023-01-08 15:15:00 UTC + From now: 27min left +``` + +Nice. Now we need to enable and start the timers. For each timer run: + +``` +systemctl enable rsnapshot-[interval].timer +systemctl start rsnapshot-[interval].timer +``` + +Oh it's 15:21, let's check the first hourly snapshot was taken: + +``` +journalctl -u rsnapshot@hourly +Jan 08 15:15:04 archbish systemd[1]: Starting rsnapshot (hourly) backup... +``` + +Great. After a few hours we can see the different snapshots mounting up in the +backup directory: + +![](img/rnapshot-dolphin-file-viewr.png) diff --git a/posts/beckett-quote.md b/posts/beckett-quote.md new file mode 100644 index 0000000..924acdb --- /dev/null +++ b/posts/beckett-quote.md @@ -0,0 +1,13 @@ +--- +title: "Beckett quote" +slug: /beckett-quote/ +date: 2023-10-19 +tags: ["quotes", "reading"] +--- + +> For what is this shadow of the going in which we come, this shadow of the +> coming in which we go, this shadow of the coming and the going in which we +> wait, if not the shadow of purpose, of the purpose that budding withers, that +> withering buds, whose blooming is a budding withering. + +Samuel Beckett, _Watt_ (1953) diff --git a/posts/bletchley-park-tnmoc-holiday.md b/posts/bletchley-park-tnmoc-holiday.md new file mode 100644 index 0000000..13d6892 --- /dev/null +++ b/posts/bletchley-park-tnmoc-holiday.md @@ -0,0 +1,96 @@ +--- +title: "Visit to Bletchley Park and The National Museum of Computing" +slug: /bletchley-park-tnmoc-holiday/ +date: 2024-08-07 +tags: ["personal", "beige", "gruvbox"] +--- + +As part of our holiday this year my girlfriend and I went to visit +[Bletchley Park](https://bletchleypark.org.uk/) and +[The National Museum of Computing](https://www.tnmoc.org/). + +With our customary taste for luxury, we stayed in one of the three Premier Inns +in Milton Keynes. Milton Keynes is strange: a car park in search of a town. +However, we had the good fortune to overlook one of its supermalls. This is +notable only because it _looks exactly like a sandworm in Dune_: + +![The Milton Keynes sandworm](./img/mk-worm.jpg) + +Bletchley was superb. A model of how to run a heritage project and museum. We +spent about five hours on site stopping for lunch and an excellent cup of tea. + +As you work your way through the huts you pass through each region of what was a +global signals intelligence factory: collection, decryption, evaluation, and +finally dissemination. + +The huts concerned with decryption were naturally the most compelling. There was +life size replica of the Bombe computer used to derive the daily settings of the +Enigma machine. It had spinning and clicking rotars however this was a +simulation rather than a working reconstruction. + +![Replica of the wartime Bombe Computer at Bletchley](../posts/img/bombe-at-bletchley.jpg) + +![Author viewing Turing's notes on the Bombe computer](../posts/img/turing-notes-bombe.jpg) + +Probably the biggest highlight for me was standing at "the birthplace of the +modern computer": the hut where the Colossus computer was used to decipher the +Lorenz messages of the German high command. + +![Standing outside the hut where the Colosus code breaking computer was +housed](../posts/img/colossus-birthplace-bletcley.jpg) + +Although not a modern computer in the sense of being general-purpose (it could +only be used for breaking this type of cipher and and was not programmable), it +was the first to use vacuum-tubes for logic operations, rather than +electro-mechanical switches and relays. This made it fully electronic and +therefore much quicker and with greater combinatorial range. + +![Reconstruction of the Colossus computer](../posts/img/colossos-reconstruction.jpg) + +This was the insight of +[Tommy Flowers](https://en.wikipedia.org/wiki/Tommy_Flowers) (a working class +hero if ever there was), who designed and built it. He proposed using +vacuum-tubes from his experience with telephony at the Post Office Research +Station. This was met with scepticism and at one point he resorted to using his +own money (never properly remunerated) to build it. He was vindicated. Not only +did the machine prove critical in the final stages of the War (confirming the +Nazis had bought the D-Day deception), it proved the speed and viability of +purely-electronic components that would ultimately lead to the transistor and +integrated circuit in later decades. + +![Sculpture of Alan Turing at Bletchley Park](./img/turing_sculpture.jpg) + +The next day we went to the National Museum of Computing which is unaffiliated +with Bletchley but located on the same site. + +![Outside The National Museum of Computing](./img/outside-tmoc.jpg) + +This was a different experience. Certainly less polished and perhaps a bit +forbidding for those not already well versed in computer lore. + +This said, it had it's own scruffy charm and is clearly a labour of love. During +our visit there were OG volunteer computer engineers actively working on the +reconstructions. + +It was a complete cornicopia of retro computers and we had a high time +marvelling at the sheer amount of beige and retro-futurist design. + +Here are some of my highlights... + +![The Elliott 900](./img/elliott-900.jpg) + +![An array of vacuum-tubes (thermionic valves)](./img/thermionic-valves.jpg) + +![Looking at circuit boards](./img/circuit-boards.jpg) + +![The first IBM Personal Computer](./img/ibm-pc.jpg) + +![Nice keycaps](./img/keycaps.jpg) + +![Gaby with BBC Micro](./img/gaby-bbc-micro.jpg) + +![Gruvbox keycaps!](./img/gruvbox_calculator.jpg) + +![Gaby with an array of PCs](./img/gaby-with-pcs-again.jpg) + +![PDP-11 programming instructions](./img/pdp-decompressed.jpg) diff --git a/posts/converting-pixel-phone-to-dap.md b/posts/converting-pixel-phone-to-dap.md new file mode 100644 index 0000000..2e88bad --- /dev/null +++ b/posts/converting-pixel-phone-to-dap.md @@ -0,0 +1,110 @@ +--- +title: "Converting a Pixel 2 XL into a private DAP" +slug: /converting-pixel-phone-to-dap/ +date: 2024-12-15 +tags: ["projects"] +--- + +I recently read +[_The Age of Surveillance Capitalism_](https://en.wikipedia.org/wiki/The_Age_of_Surveillance_Capitalism) +. This book and initiatives like the +[Opt Out Project](https://www.optoutproject.net) have motivated me to try and +improve my digital privacy. + +Some will see this as a fool's errand and I agree to some extent. However it +isn't something I expect to complete overnight. I view the journey as just as +important as the destination as it provides opportunities to acquire new skills, +discover new technologies, and work on projects that have utility in my daily +life. + +My first objective was to do away with my Spotify subscription and consume music +from a dedicted offline device that plays albums I actually own. + +I researched modern-day MP3 players but this is a pretty dead market these days +and all the devices were ugly or surplus to my requirements. There is a +community of hard-core audiophiles who have rechristened MP3 players _digital +audio players_ (DAPs) but most of the recommended devices are very expensive and +my goal is to try and keep the overall anti-surveillance project affordable, +recycling and building my own solutions where I can. + +It occurred to me that I could just repurpose an old phone into a single-purpose +computer for playing music. Appropriately enough, I have an old Google Pixel 2 +XL from 2017 - the surveillance capitalist device _par excellence_. + +It is able to play FLAC files natively and its 128GB harddrive should be more +than sufficient for my library. + +My first task was to "de-Google" the phone by finding a version of Android that +respects privacy and doesn't bundle surveillance-ware. This was trickier that I +expected. Not because there isn't such software but because phones have a more +specific hardware set with greater variation than laptops of desktop computers. +You have to find an OS that is compatible with your hardware and which is still +regularly maintained even though the hardware is, in my case, eight years old +and long since superseded by more recent devices. + +Luckily [LineageOS](https://lineageos.org) offers a build that works with the +Pixel 2 XL and which is fairly lightweight, allowing me to preserve greater disk +space for the audio files. + +Having enabled "Developer Mode" on the Pixel, I needed to install a few CLI +tools on my Arch Linux machine that would enable me to interface with the +device. + +I installed `android-tools` and `android-udev`. `android-tools` includes `adb` +which allows me to communicate with an Android device over USB, transfer files, +and access the shell from another Linux device. It also includes `fastboot` +which unlocks the Pixel bootloader, necessary to install third-party recovery +software through which I can flash a new OS ROM to the device. I used +`android-udev` to grant myself access to the Pixel from my Linux PC. + +![Bootloader unlocked](./img/lineage-bootloader.jpeg) + +Having gained access to the device remotely I used `adb` to unlock the +bootloader and then transferred over the [TWRP](https://twrp.me/about/) recovery +software and the latest build of LineageOS, tagged to Android v.12. + +Then, on the device, I booted into TWRP. From here I wiped the data and +installed LineageOS. This took a few attempts to work. It kept booting into the +recovery menu for some reason but eventually it just worked. + +![Running the recovery software](./img/lineage-recovery.jpeg) + +Once the new OS was installed there wasn't much else to do. I deleted the few +apps included with LineageOS that I didn't need and installed F-Droid. F-Droid +is a de-Googled version of Google's Play Store that serves as a package manager +for FOSS Android apps. + +![LineageOS installed](./img/lineage.jpeg) + +Using F-Droid, I installed a few music apps to experiment with but ended up +finding the default LineageOS player satisfactory. I also installed Duck Duck Go +as my browser and KDE Connect. Although I intend to mostly keep the device +offline, it's handy to have a browser to source images for the music player. + +KDE Connect allows me to connect to the Pixel from my PC over WiFi. This is +necessary for transferring the audio files. It also allows me to control media +on the PC from the phone although I doubt I will have much use for this. + +I am still in the process of recreating my Spotify library with albums I own. I +have a lot of albums in MP3 on an old external HD from the pre-streaming era. I +have also been able to use the Internet Archive as well as buying albums direct +from artists on Bandcamp. For very obscure stuff that was originally released on +tape and circulated online I am usually able to find torrents, although some +stuff is very hard to track down. I'm planning on getting a CD drive so that I +can rip CDs I find in second-hand shops and fairs. If I go full +"digital-hoarder", I may even get a vinyl-to-digital converter turnable +eventually, then I can exploit my friends' LP collections. + +![Album list within the DAP](./img/lineage-library.jpeg) + +One annoying thing is that the metadata for the music files will often be +missing or incomplete. For instance the tracks on an album might be out of order +or lacking the album cover. I use [kid3](https://kid3.kde.org) to view and edit +the metadata so that the tracks are recognised properly by the player with +release year and album art. + +![kid3 for managing album metadata](./img/kid3-screenshot.png) + +My next challenge will be tackling audiobooks and podcasts. Ideally I would like +to download them as files to the DAP and avoid streaming services. More on this +to follow. diff --git a/posts/delillo-alignment.md b/posts/delillo-alignment.md new file mode 100644 index 0000000..f7959be --- /dev/null +++ b/posts/delillo-alignment.md @@ -0,0 +1,21 @@ +--- +title: "DeLillo, alignment" +slug: /delillo-alignment/ +date: 2023-04-16 +tags: ["quotes", "reading"] +--- + +I am reading Don DeLillo's _Underworld_ (1997) at the moment. As we worry about +the [problem of alignment](https://en.wikipedia.org/wiki/AI_alignment) in the +wake of GPT-4 the following passage seemed especially apposite. + +> I was driving a Lexus through a rustling wind. This is a car assembled in a +> work area that's completely free of human presence. Not a spot of mortal sweat +> except, okay, for the guys who drive the product out of the plant — +> allow a little moisture where they grip the wheel. The system flows forever +> onward, automated to priestly nuance, every gliding movement back-referenced +> for prime performance. Hollow bodies coming in endless sequence. There's +> nobody on the line with caffeine nerves or a history of clinical depression. +> Just the eerie weave of chromium alloys carried in interlocking arcs, block +> iron and asphalt sheeting, soaring ornaments of coachwork fitted and merged. +> Robots tightening bolts, programmed drudges that do not dream of family dead. diff --git a/posts/gruvbox-image-filters.md b/posts/gruvbox-image-filters.md new file mode 100644 index 0000000..2b94e16 --- /dev/null +++ b/posts/gruvbox-image-filters.md @@ -0,0 +1,23 @@ +--- +title: "Gruvbox image filters" +slug: /gruvbox-image-filters/ +date: 2024-11-16 +tags: ["gruvbox", "beige", "log"] +--- + +I've been experimenting with an +ImageMagick script to +create filters for images that I use as my screensaver. I applied a +Gruvbox-themed colour swap and manipulated the dithering to create a retro +effect with highlights. I was quite surprised how easy and effective this proved +to be. Below are some of the ones I like best. + +![The composer Éliane Radigue manipulating an ARP synthesiser.](./img/radigue_gruvbox.png) + +![Operators in front of the MANIAC computer.](./img/Operators_in_front_of_the_MANIAC_gruvbox.png) + +![Power station control panel image I found on Unsplash.](./img/control-panel_gruvbox.png) + +![Composer Laurie Speigel working with a computer at Bell Labs.](./img/laurie-burn_gruvbox.png) + +![The Twin Towers](./img/tt-two_gruvbox.png) diff --git a/posts/how-I-deploy-this-site.md b/posts/how-I-deploy-this-site.md new file mode 100644 index 0000000..120ac55 --- /dev/null +++ b/posts/how-I-deploy-this-site.md @@ -0,0 +1,133 @@ +--- +title: "How I deploy this site" +slug: /how-I-deploy-this-site/ +date: 2023-01-02 +tags: ["log", "linux"] +--- + +I want to make a note of how this blog is maintained and deployed for future +reference as the AWS process is quite involved. + +I use AWS because I have to know at least one cloud/serverless provider well and +we use AWS at work. I intend to take the AWS Certified Developer exam eventually +so knowing how to deploy a frontend application is useful. + +## Frontend: Gatsby.js + +Nothing special here. I am using the React-based [Gatsby]() framework to create +the frontend and as you can see, there isn't much to the site. Just a homepage +and a bunch of posts. I used the Gatsby starter template, changed the fonts and +styled it in the manner of my [AlienBlood]() theme. + +## Deployment: AWS S3, CloudFront, Route 53, Certificate Manager + +- First I uploaded the build directory to a bucket on S3 making sure to set the + permissions to public and to specify that the bucket hosts a static website. +- Then I created a hosted zone using AWS Route 53. This is necessary for the + bucket to be publicly accessible as a domain on the internet. I purchased the + domain name from GoDaddy and uploaded the AWS nameservers there. Cue 24 hours + propagation time. +- All sites should be served over `https` so I requested a public SSL + certificate from AWS Certificate Manager. +- You can't serve an S3 bucket as `https` by default. The solution is to use + CloudFront as a CDN and then specify a redirection rule from `http` to + `https`. So you create the CloudFont instance and generate an endpoint. This + endpoint is then added to the Route 53 specifications as an A record. That + propagates in a matter of seconds and now the site is securely hosted. + +## Middlewear: AWS Lambda + +An annoying thing about CloudFront is that it won't recognise pages other than +the root `index.html` on refresh. So if I went to this page +(https://systemsobscure.blog/how-I-deploy-this-site/), on refresh it becomes a +403 because the file format is not `how-I-deploy_this_site/index.html`. + +To get around this the custom is to use a Lambda function that intercepts +requests and add the trailing `index.html`: + +```js +exports.handler = (event, context, callback) => { + // Extract the request from the CloudFront event that is sent to Lambda@Edge + var request = event.Records[0].cf.request + + // Extract the URI from the request + var olduri = request.uri + + // Match any '/' that occurs at the end of a URI. Replace it with a default index + var newuri = olduri.replace(/\/$/, "/index.html") + + // Replace the received URI with the URI that includes the index page + request.uri = newuri + + // Return to CloudFront + return callback(null, request) +} +``` + +You then set this function to trigger on page requests to the CloudFront +distribution that is handling the routing and the problem is resolved. + +## Continuous deployment: GitHub Actions + +It's slightly onerous to have to manually trigger a deploy to S3 every time I +write a new post. A better scenario would be to trigger a build and a deployment +to S3 every time I push to the `main` branch on GitHub. I could do this from +within AWS but I've chosen to have GitHub communicate with AWS rather than the +other way around. That way I get some experience of using Actions. + +My GitHub Action declaration runs the standard `gatsby build` command on push +and also runs the following NPM script once the build completes: + +```bash +gatsby-plugin-s3 deploy --yes; aws cloudfront create-invalidation --distribution-id --paths '/*';", +``` + +This uses a Gatsby plugin to deploy to S3 and clear the Cloudfront cache. + +In order for this command to run from GitHub I had to create a "GitHub" user and +custom permissions file in AWS IAM. This gives me an Access Key ID and secret +which the GitHub Action can use to authenticate the deployment. I save these as +secrets within the repository settings in GitHub and now the whole Action +declaration works. + +Here is the GitHub Action in full: + +```yaml +name: Deploy systemsobscure.blog +on: + push: + branches: + - main +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 18 + - name: Caching Gatsby + id: gatsby-cache-build + uses: actions/cache@v2 + with: + path: | + public + .cache + node_modules + key: ${{ runner.os }}-systemsobscure-site-build-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-systemsobscure-site-build- + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Set AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-1 + - name: Deploy to S3 + run: npm run deploy +``` diff --git a/posts/image-deletion-bash-script.md b/posts/image-deletion-bash-script.md new file mode 100644 index 0000000..e294530 --- /dev/null +++ b/posts/image-deletion-bash-script.md @@ -0,0 +1,32 @@ +--- +title: "Bash script: delete unused images" +slug: /image-deletion-bash-script/ +date: 2023-04-16 +tags: ["log", "bash"] +--- + +When I'm working on my technical notes I often insert images, diagrams, +screenshots etc. Often, I end up creating multiple images that I end up not +using. I don't like commiting dead bytes to the repo so I wrote a short script +that reads each file in the image directory and looks to see if it is referenced +in any Markdown files. If it isn't, the image is deleted. + +```bash +find /home/thomas/repos/eolas/_img -type f | while read filename; do + rg "${filename##*/}" ../ --type markdown >/dev/null 2>&1 + if [ "$?" -eq 1 ]; then + echo "Deleted unused image: ${filename##*/}" + rm $filename + fi +done +``` + +Example: + +```sh +./clean_image_directory.sh +Deleted unused image: multiplication_03.gif +Deleted unused image: Pasted_image_20220319174839.png +Deleted unused image: multiplication_04.gif +Deleted unused image: multiplication_02.gif +``` diff --git a/posts/img/BJT_NPN_symbol.png b/posts/img/BJT_NPN_symbol.png new file mode 100644 index 0000000..9aa8df3 Binary files /dev/null and b/posts/img/BJT_NPN_symbol.png differ diff --git a/posts/img/BJT_NPN_symbol.svg b/posts/img/BJT_NPN_symbol.svg new file mode 100644 index 0000000..3f19c89 --- /dev/null +++ b/posts/img/BJT_NPN_symbol.svg @@ -0,0 +1,124 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + C + E + B + + diff --git a/posts/img/BJT_transistor_symbol.svg b/posts/img/BJT_transistor_symbol.svg new file mode 100644 index 0000000..b665d24 --- /dev/null +++ b/posts/img/BJT_transistor_symbol.svg @@ -0,0 +1,160 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + B + C + E + + + diff --git a/posts/img/BJT_transistor_symbol_two.png b/posts/img/BJT_transistor_symbol_two.png new file mode 100644 index 0000000..8b86172 Binary files /dev/null and b/posts/img/BJT_transistor_symbol_two.png differ diff --git a/posts/img/Operators_in_front_of_the_MANIAC_gruvbox.png b/posts/img/Operators_in_front_of_the_MANIAC_gruvbox.png new file mode 100644 index 0000000..2920b32 Binary files /dev/null and b/posts/img/Operators_in_front_of_the_MANIAC_gruvbox.png differ diff --git a/posts/img/access-api-permission.png b/posts/img/access-api-permission.png new file mode 100644 index 0000000..48d24f1 Binary files /dev/null and b/posts/img/access-api-permission.png differ diff --git a/posts/img/activity-log-diagram-darker.png b/posts/img/activity-log-diagram-darker.png new file mode 100644 index 0000000..0b68748 Binary files /dev/null and b/posts/img/activity-log-diagram-darker.png differ diff --git a/posts/img/alien_blood_syntax_demo.png b/posts/img/alien_blood_syntax_demo.png new file mode 100644 index 0000000..72974e9 Binary files /dev/null and b/posts/img/alien_blood_syntax_demo.png differ diff --git a/posts/img/alien_blood_terminal_colours.png b/posts/img/alien_blood_terminal_colours.png new file mode 100644 index 0000000..b200a59 Binary files /dev/null and b/posts/img/alien_blood_terminal_colours.png differ diff --git a/posts/img/and-or-not-gates.png b/posts/img/and-or-not-gates.png new file mode 100644 index 0000000..ed346c7 Binary files /dev/null and b/posts/img/and-or-not-gates.png differ diff --git a/posts/img/api-gateway-pocket-api.png b/posts/img/api-gateway-pocket-api.png new file mode 100644 index 0000000..e7f2503 Binary files /dev/null and b/posts/img/api-gateway-pocket-api.png differ diff --git a/posts/img/aws-console-cloud-formation-new-entry.png b/posts/img/aws-console-cloud-formation-new-entry.png new file mode 100644 index 0000000..f70f975 Binary files /dev/null and b/posts/img/aws-console-cloud-formation-new-entry.png differ diff --git a/posts/img/basic-led-circuit.jpeg b/posts/img/basic-led-circuit.jpeg new file mode 100644 index 0000000..a7333f3 Binary files /dev/null and b/posts/img/basic-led-circuit.jpeg differ diff --git a/posts/img/basic-led-circuit.png b/posts/img/basic-led-circuit.png new file mode 100644 index 0000000..4dcf467 Binary files /dev/null and b/posts/img/basic-led-circuit.png differ diff --git a/posts/img/bbc-leave-gruv-small.png b/posts/img/bbc-leave-gruv-small.png new file mode 100644 index 0000000..87269a6 Binary files /dev/null and b/posts/img/bbc-leave-gruv-small.png differ diff --git a/posts/img/bbc-leave-gruv.png b/posts/img/bbc-leave-gruv.png new file mode 100644 index 0000000..39fa3e3 Binary files /dev/null and b/posts/img/bbc-leave-gruv.png differ diff --git a/posts/img/bjt-again.png b/posts/img/bjt-again.png new file mode 100644 index 0000000..fd92e6b Binary files /dev/null and b/posts/img/bjt-again.png differ diff --git a/posts/img/bjt-diagram.png b/posts/img/bjt-diagram.png new file mode 100644 index 0000000..3b142e5 Binary files /dev/null and b/posts/img/bjt-diagram.png differ diff --git a/posts/img/bjt.png b/posts/img/bjt.png new file mode 100644 index 0000000..74202bb Binary files /dev/null and b/posts/img/bjt.png differ diff --git a/posts/img/bombe-at-bletchley.jpg b/posts/img/bombe-at-bletchley.jpg new file mode 100644 index 0000000..8623e2e Binary files /dev/null and b/posts/img/bombe-at-bletchley.jpg differ diff --git a/posts/img/certbot_confirm.png b/posts/img/certbot_confirm.png new file mode 100644 index 0000000..e76ce79 Binary files /dev/null and b/posts/img/certbot_confirm.png differ diff --git a/posts/img/chat-gpt-explanation-bug.png b/posts/img/chat-gpt-explanation-bug.png new file mode 100644 index 0000000..2fc6ea8 Binary files /dev/null and b/posts/img/chat-gpt-explanation-bug.png differ diff --git a/posts/img/circuit-boards.jpg b/posts/img/circuit-boards.jpg new file mode 100644 index 0000000..193d2e6 Binary files /dev/null and b/posts/img/circuit-boards.jpg differ diff --git a/posts/img/circuit-simple.png b/posts/img/circuit-simple.png new file mode 100644 index 0000000..f4fb3fa Binary files /dev/null and b/posts/img/circuit-simple.png differ diff --git a/posts/img/circuit-two-switch.png b/posts/img/circuit-two-switch.png new file mode 100644 index 0000000..5f3cc0c Binary files /dev/null and b/posts/img/circuit-two-switch.png differ diff --git a/posts/img/cloud-formation-cli-confirmation.png b/posts/img/cloud-formation-cli-confirmation.png new file mode 100644 index 0000000..971c803 Binary files /dev/null and b/posts/img/cloud-formation-cli-confirmation.png differ diff --git a/posts/img/colossos-reconstruction.jpg b/posts/img/colossos-reconstruction.jpg new file mode 100644 index 0000000..9ff2d50 Binary files /dev/null and b/posts/img/colossos-reconstruction.jpg differ diff --git a/posts/img/colossus-birthplace-bletcley.jpg b/posts/img/colossus-birthplace-bletcley.jpg new file mode 100644 index 0000000..b37187d Binary files /dev/null and b/posts/img/colossus-birthplace-bletcley.jpg differ diff --git a/posts/img/commit-table-workbench.png b/posts/img/commit-table-workbench.png new file mode 100644 index 0000000..0c8c06e Binary files /dev/null and b/posts/img/commit-table-workbench.png differ diff --git a/posts/img/computer.svg b/posts/img/computer.svg new file mode 100644 index 0000000..e081864 --- /dev/null +++ b/posts/img/computer.svg @@ -0,0 +1,50 @@ + + + + diff --git a/posts/img/control-panel_gruvbox.png b/posts/img/control-panel_gruvbox.png new file mode 100644 index 0000000..42c5448 Binary files /dev/null and b/posts/img/control-panel_gruvbox.png differ diff --git a/posts/img/correcting-chatgpt.png b/posts/img/correcting-chatgpt.png new file mode 100644 index 0000000..d166430 Binary files /dev/null and b/posts/img/correcting-chatgpt.png differ diff --git a/posts/img/create-iam-user-aws-console.png b/posts/img/create-iam-user-aws-console.png new file mode 100644 index 0000000..0befda1 Binary files /dev/null and b/posts/img/create-iam-user-aws-console.png differ diff --git a/posts/img/create-iam-user.png b/posts/img/create-iam-user.png new file mode 100644 index 0000000..028f463 Binary files /dev/null and b/posts/img/create-iam-user.png differ diff --git a/posts/img/cropped-eolas-fe.png b/posts/img/cropped-eolas-fe.png new file mode 100644 index 0000000..6c41f77 Binary files /dev/null and b/posts/img/cropped-eolas-fe.png differ diff --git a/posts/img/ddb-data-in-builder.png b/posts/img/ddb-data-in-builder.png new file mode 100644 index 0000000..721a1a0 Binary files /dev/null and b/posts/img/ddb-data-in-builder.png differ diff --git a/posts/img/dns-records-detail.png b/posts/img/dns-records-detail.png new file mode 100644 index 0000000..1c8fca3 Binary files /dev/null and b/posts/img/dns-records-detail.png differ diff --git a/posts/img/docker-compose-up.png b/posts/img/docker-compose-up.png new file mode 100644 index 0000000..79ca22e Binary files /dev/null and b/posts/img/docker-compose-up.png differ diff --git a/posts/img/docker-net-ls.png b/posts/img/docker-net-ls.png new file mode 100644 index 0000000..74d4445 Binary files /dev/null and b/posts/img/docker-net-ls.png differ diff --git a/posts/img/docker-network-inspect.png b/posts/img/docker-network-inspect.png new file mode 100644 index 0000000..a0050e7 Binary files /dev/null and b/posts/img/docker-network-inspect.png differ diff --git a/posts/img/docker-network-ls.png b/posts/img/docker-network-ls.png new file mode 100644 index 0000000..b0def16 Binary files /dev/null and b/posts/img/docker-network-ls.png differ diff --git a/posts/img/double-and-circ.png b/posts/img/double-and-circ.png new file mode 100644 index 0000000..0fdde7e Binary files /dev/null and b/posts/img/double-and-circ.png differ diff --git a/posts/img/elliott-900.jpg b/posts/img/elliott-900.jpg new file mode 100644 index 0000000..03a4638 Binary files /dev/null and b/posts/img/elliott-900.jpg differ diff --git a/posts/img/eolas-static-diag.png b/posts/img/eolas-static-diag.png new file mode 100644 index 0000000..8a07785 Binary files /dev/null and b/posts/img/eolas-static-diag.png differ diff --git a/posts/img/file-launcher.png b/posts/img/file-launcher.png new file mode 100644 index 0000000..728a0f2 Binary files /dev/null and b/posts/img/file-launcher.png differ diff --git a/posts/img/full-note-view.png b/posts/img/full-note-view.png new file mode 100644 index 0000000..47ad9eb Binary files /dev/null and b/posts/img/full-note-view.png differ diff --git a/posts/img/function-diagram-new.png b/posts/img/function-diagram-new.png new file mode 100644 index 0000000..fd37506 Binary files /dev/null and b/posts/img/function-diagram-new.png differ diff --git a/posts/img/function-diagram.png b/posts/img/function-diagram.png new file mode 100644 index 0000000..b53b50f Binary files /dev/null and b/posts/img/function-diagram.png differ diff --git a/posts/img/gaby-bbc-micro.jpg b/posts/img/gaby-bbc-micro.jpg new file mode 100644 index 0000000..9184bd3 Binary files /dev/null and b/posts/img/gaby-bbc-micro.jpg differ diff --git a/posts/img/gaby-with-pcs-again.jpg b/posts/img/gaby-with-pcs-again.jpg new file mode 100644 index 0000000..b403fea Binary files /dev/null and b/posts/img/gaby-with-pcs-again.jpg differ diff --git a/posts/img/grafana-login-screen.png b/posts/img/grafana-login-screen.png new file mode 100644 index 0000000..fb8d0bb Binary files /dev/null and b/posts/img/grafana-login-screen.png differ diff --git a/posts/img/grafana-server-metrics-dashboard.png b/posts/img/grafana-server-metrics-dashboard.png new file mode 100644 index 0000000..0833282 Binary files /dev/null and b/posts/img/grafana-server-metrics-dashboard.png differ diff --git a/posts/img/grafana-varlogs-loki.png b/posts/img/grafana-varlogs-loki.png new file mode 100644 index 0000000..44b4393 Binary files /dev/null and b/posts/img/grafana-varlogs-loki.png differ diff --git a/posts/img/gruvbox95.png b/posts/img/gruvbox95.png new file mode 100644 index 0000000..ebf6b27 Binary files /dev/null and b/posts/img/gruvbox95.png differ diff --git a/posts/img/gruvbox_calculator.jpg b/posts/img/gruvbox_calculator.jpg new file mode 100644 index 0000000..ff0a849 Binary files /dev/null and b/posts/img/gruvbox_calculator.jpg differ diff --git a/posts/img/hardware-simulator-basic-use.png b/posts/img/hardware-simulator-basic-use.png new file mode 100644 index 0000000..ccbe9db Binary files /dev/null and b/posts/img/hardware-simulator-basic-use.png differ diff --git a/posts/img/hardware-simulator-three.png b/posts/img/hardware-simulator-three.png new file mode 100644 index 0000000..94790b6 Binary files /dev/null and b/posts/img/hardware-simulator-three.png differ diff --git a/posts/img/hardware-simulator.png b/posts/img/hardware-simulator.png new file mode 100644 index 0000000..641b1da Binary files /dev/null and b/posts/img/hardware-simulator.png differ diff --git a/posts/img/hetzner-console.png b/posts/img/hetzner-console.png new file mode 100644 index 0000000..df2b5a8 Binary files /dev/null and b/posts/img/hetzner-console.png differ diff --git a/posts/img/hetzner-server.png b/posts/img/hetzner-server.png new file mode 100644 index 0000000..ed0a618 Binary files /dev/null and b/posts/img/hetzner-server.png differ diff --git a/posts/img/httpie-showing-data.png b/posts/img/httpie-showing-data.png new file mode 100644 index 0000000..fdb1b0c Binary files /dev/null and b/posts/img/httpie-showing-data.png differ diff --git a/posts/img/ibm-pc.jpg b/posts/img/ibm-pc.jpg new file mode 100644 index 0000000..9e8304f Binary files /dev/null and b/posts/img/ibm-pc.jpg differ diff --git a/posts/img/keycaps.jpg b/posts/img/keycaps.jpg new file mode 100644 index 0000000..664d891 Binary files /dev/null and b/posts/img/keycaps.jpg differ diff --git a/posts/img/kid3-screenshot.png b/posts/img/kid3-screenshot.png new file mode 100644 index 0000000..3baeba8 Binary files /dev/null and b/posts/img/kid3-screenshot.png differ diff --git a/posts/img/lambda-diagram-new.png b/posts/img/lambda-diagram-new.png new file mode 100644 index 0000000..648ffd4 Binary files /dev/null and b/posts/img/lambda-diagram-new.png differ diff --git a/posts/img/lambda-schematic-diagram.png b/posts/img/lambda-schematic-diagram.png new file mode 100644 index 0000000..c03f9d6 Binary files /dev/null and b/posts/img/lambda-schematic-diagram.png differ diff --git a/posts/img/laurie-burn_gruvbox.png b/posts/img/laurie-burn_gruvbox.png new file mode 100644 index 0000000..f8c2d4c Binary files /dev/null and b/posts/img/laurie-burn_gruvbox.png differ diff --git a/posts/img/lets-encrypt-certs.png b/posts/img/lets-encrypt-certs.png new file mode 100644 index 0000000..fd369d1 Binary files /dev/null and b/posts/img/lets-encrypt-certs.png differ diff --git a/posts/img/lineage-bootloader.jpeg b/posts/img/lineage-bootloader.jpeg new file mode 100644 index 0000000..2589883 Binary files /dev/null and b/posts/img/lineage-bootloader.jpeg differ diff --git a/posts/img/lineage-library.jpeg b/posts/img/lineage-library.jpeg new file mode 100644 index 0000000..405c19a Binary files /dev/null and b/posts/img/lineage-library.jpeg differ diff --git a/posts/img/lineage-recovery.jpeg b/posts/img/lineage-recovery.jpeg new file mode 100644 index 0000000..66cfed9 Binary files /dev/null and b/posts/img/lineage-recovery.jpeg differ diff --git a/posts/img/lineage.jpeg b/posts/img/lineage.jpeg new file mode 100644 index 0000000..b0c01ee Binary files /dev/null and b/posts/img/lineage.jpeg differ diff --git a/posts/img/meridian-monument.jpg b/posts/img/meridian-monument.jpg new file mode 100644 index 0000000..33ac6a1 Binary files /dev/null and b/posts/img/meridian-monument.jpg differ diff --git a/posts/img/mk-worm.jpg b/posts/img/mk-worm.jpg new file mode 100644 index 0000000..fe07db9 Binary files /dev/null and b/posts/img/mk-worm.jpg differ diff --git a/posts/img/nand-fork.png b/posts/img/nand-fork.png new file mode 100644 index 0000000..4bc455c Binary files /dev/null and b/posts/img/nand-fork.png differ diff --git a/posts/img/nand-with-and-not.png b/posts/img/nand-with-and-not.png new file mode 100644 index 0000000..efaecd9 Binary files /dev/null and b/posts/img/nand-with-and-not.png differ diff --git a/posts/img/neuron-eolas-screenshot-small.png b/posts/img/neuron-eolas-screenshot-small.png new file mode 100644 index 0000000..1fd7b52 Binary files /dev/null and b/posts/img/neuron-eolas-screenshot-small.png differ diff --git a/posts/img/new-circuit-photo-again.jpg b/posts/img/new-circuit-photo-again.jpg new file mode 100644 index 0000000..835fee6 Binary files /dev/null and b/posts/img/new-circuit-photo-again.jpg differ diff --git a/posts/img/new-cloud-formation-application.png b/posts/img/new-cloud-formation-application.png new file mode 100644 index 0000000..54bb0a6 Binary files /dev/null and b/posts/img/new-cloud-formation-application.png differ diff --git a/posts/img/newly-created-pocket-lambda.png b/posts/img/newly-created-pocket-lambda.png new file mode 100644 index 0000000..1eb5374 Binary files /dev/null and b/posts/img/newly-created-pocket-lambda.png differ diff --git a/posts/img/no-sql-workbench.png b/posts/img/no-sql-workbench.png new file mode 100644 index 0000000..46f53f0 Binary files /dev/null and b/posts/img/no-sql-workbench.png differ diff --git a/posts/img/not-by-ai-alternative--dark.png b/posts/img/not-by-ai-alternative--dark.png new file mode 100644 index 0000000..42ee747 Binary files /dev/null and b/posts/img/not-by-ai-alternative--dark.png differ diff --git a/posts/img/not-by-ai-alternative--light.png b/posts/img/not-by-ai-alternative--light.png new file mode 100644 index 0000000..71c191e Binary files /dev/null and b/posts/img/not-by-ai-alternative--light.png differ diff --git a/posts/img/note-view.png b/posts/img/note-view.png new file mode 100644 index 0000000..2b39b94 Binary files /dev/null and b/posts/img/note-view.png differ diff --git a/posts/img/obsidian-backlinks.png b/posts/img/obsidian-backlinks.png new file mode 100644 index 0000000..8fc879a Binary files /dev/null and b/posts/img/obsidian-backlinks.png differ diff --git a/posts/img/obsidian-graph-view.png b/posts/img/obsidian-graph-view.png new file mode 100644 index 0000000..26ae35e Binary files /dev/null and b/posts/img/obsidian-graph-view.png differ diff --git a/posts/img/obsidian-tags.png b/posts/img/obsidian-tags.png new file mode 100644 index 0000000..9653900 Binary files /dev/null and b/posts/img/obsidian-tags.png differ diff --git a/posts/img/outside-tmoc.jpg b/posts/img/outside-tmoc.jpg new file mode 100644 index 0000000..8c2573a Binary files /dev/null and b/posts/img/outside-tmoc.jpg differ diff --git a/posts/img/pdp-decompressed.jpg b/posts/img/pdp-decompressed.jpg new file mode 100644 index 0000000..2f7974b Binary files /dev/null and b/posts/img/pdp-decompressed.jpg differ diff --git a/posts/img/pdp-programming-card.jpg b/posts/img/pdp-programming-card.jpg new file mode 100644 index 0000000..857de9e Binary files /dev/null and b/posts/img/pdp-programming-card.jpg differ diff --git a/posts/img/query-pocket-lambda-response-postman.png b/posts/img/query-pocket-lambda-response-postman.png new file mode 100644 index 0000000..0d5cc4f Binary files /dev/null and b/posts/img/query-pocket-lambda-response-postman.png differ diff --git a/posts/img/query-remote-endpoint-with-auth.png b/posts/img/query-remote-endpoint-with-auth.png new file mode 100644 index 0000000..9300de1 Binary files /dev/null and b/posts/img/query-remote-endpoint-with-auth.png differ diff --git a/posts/img/radigue_gruvbox.png b/posts/img/radigue_gruvbox.png new file mode 100644 index 0000000..da13e81 Binary files /dev/null and b/posts/img/radigue_gruvbox.png differ diff --git a/posts/img/reverse-proxy.png b/posts/img/reverse-proxy.png new file mode 100644 index 0000000..88b3d56 Binary files /dev/null and b/posts/img/reverse-proxy.png differ diff --git a/posts/img/rnapshot-dolphin-file-viewr.png b/posts/img/rnapshot-dolphin-file-viewr.png new file mode 100644 index 0000000..6e95fb7 Binary files /dev/null and b/posts/img/rnapshot-dolphin-file-viewr.png differ diff --git a/posts/img/running-neuron-generator.png b/posts/img/running-neuron-generator.png new file mode 100644 index 0000000..9ee7b91 Binary files /dev/null and b/posts/img/running-neuron-generator.png differ diff --git a/posts/img/save-articles-architecture.png b/posts/img/save-articles-architecture.png new file mode 100644 index 0000000..392c45f Binary files /dev/null and b/posts/img/save-articles-architecture.png differ diff --git a/posts/img/saved_tech_articles.png b/posts/img/saved_tech_articles.png new file mode 100644 index 0000000..79d9020 Binary files /dev/null and b/posts/img/saved_tech_articles.png differ diff --git a/posts/img/secrets-manager-aws-console.png b/posts/img/secrets-manager-aws-console.png new file mode 100644 index 0000000..9cbd1a5 Binary files /dev/null and b/posts/img/secrets-manager-aws-console.png differ diff --git a/posts/img/secrets-manager-key-pairs.png b/posts/img/secrets-manager-key-pairs.png new file mode 100644 index 0000000..d35e99e Binary files /dev/null and b/posts/img/secrets-manager-key-pairs.png differ diff --git a/posts/img/single-and-gate.png b/posts/img/single-and-gate.png new file mode 100644 index 0000000..36ebdef Binary files /dev/null and b/posts/img/single-and-gate.png differ diff --git a/posts/img/single-switch-and-circuit.png b/posts/img/single-switch-and-circuit.png new file mode 100644 index 0000000..fab7714 Binary files /dev/null and b/posts/img/single-switch-and-circuit.png differ diff --git a/posts/img/slack-notification-center.png b/posts/img/slack-notification-center.png new file mode 100644 index 0000000..5e0574e Binary files /dev/null and b/posts/img/slack-notification-center.png differ diff --git a/posts/img/thermionic-valves.jpg b/posts/img/thermionic-valves.jpg new file mode 100644 index 0000000..1a51f17 Binary files /dev/null and b/posts/img/thermionic-valves.jpg differ diff --git a/posts/img/timekeeping-diagram-new.png b/posts/img/timekeeping-diagram-new.png new file mode 100644 index 0000000..f544f8e Binary files /dev/null and b/posts/img/timekeeping-diagram-new.png differ diff --git a/posts/img/timer-module.png b/posts/img/timer-module.png new file mode 100644 index 0000000..7b682e2 Binary files /dev/null and b/posts/img/timer-module.png differ diff --git a/posts/img/toolbar-one.png b/posts/img/toolbar-one.png new file mode 100644 index 0000000..8bac122 Binary files /dev/null and b/posts/img/toolbar-one.png differ diff --git a/posts/img/toolbar-three.png b/posts/img/toolbar-three.png new file mode 100644 index 0000000..d32c202 Binary files /dev/null and b/posts/img/toolbar-three.png differ diff --git a/posts/img/toolbar-two.png b/posts/img/toolbar-two.png new file mode 100644 index 0000000..f58ecfb Binary files /dev/null and b/posts/img/toolbar-two.png differ diff --git a/posts/img/transistor-diag.svg b/posts/img/transistor-diag.svg new file mode 100644 index 0000000..fb67854 --- /dev/null +++ b/posts/img/transistor-diag.svg @@ -0,0 +1,4 @@ + + + +
Base
Base
Collector
Collector
Emitter
Emitter
Negative-Positive-Negative (NPN) Bipolar Junction Transistor
Negative-Positive-Negative (NPN...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/posts/img/tt-two_gruvbox.png b/posts/img/tt-two_gruvbox.png new file mode 100644 index 0000000..965c3d6 Binary files /dev/null and b/posts/img/tt-two_gruvbox.png differ diff --git a/posts/img/turing-notes-bombe.jpg b/posts/img/turing-notes-bombe.jpg new file mode 100644 index 0000000..dfed5ea Binary files /dev/null and b/posts/img/turing-notes-bombe.jpg differ diff --git a/posts/img/turing_sculpture.jpg b/posts/img/turing_sculpture.jpg new file mode 100644 index 0000000..7288130 Binary files /dev/null and b/posts/img/turing_sculpture.jpg differ diff --git a/posts/img/tux-icon.png b/posts/img/tux-icon.png new file mode 100644 index 0000000..bf8f0be Binary files /dev/null and b/posts/img/tux-icon.png differ diff --git a/posts/img/tux.jpg b/posts/img/tux.jpg new file mode 100644 index 0000000..9352c01 Binary files /dev/null and b/posts/img/tux.jpg differ diff --git a/posts/img/wakatime-waybar.jpg b/posts/img/wakatime-waybar.jpg new file mode 100644 index 0000000..91c1005 Binary files /dev/null and b/posts/img/wakatime-waybar.jpg differ diff --git a/posts/img/waybar-full.png b/posts/img/waybar-full.png new file mode 100644 index 0000000..885e00c Binary files /dev/null and b/posts/img/waybar-full.png differ diff --git a/posts/img/workbench-create-conn.png b/posts/img/workbench-create-conn.png new file mode 100644 index 0000000..f16ea59 Binary files /dev/null and b/posts/img/workbench-create-conn.png differ diff --git a/posts/img/workbench-data-modeller.png b/posts/img/workbench-data-modeller.png new file mode 100644 index 0000000..db4618a Binary files /dev/null and b/posts/img/workbench-data-modeller.png differ diff --git a/posts/img/zk-tags.png b/posts/img/zk-tags.png new file mode 100644 index 0000000..1931ca8 Binary files /dev/null and b/posts/img/zk-tags.png differ diff --git a/posts/informal-concepts-propositional-logic.md b/posts/informal-concepts-propositional-logic.md new file mode 100644 index 0000000..2dd1fd0 --- /dev/null +++ b/posts/informal-concepts-propositional-logic.md @@ -0,0 +1,442 @@ +--- +title: "Informal definitions of key concepts in propositional logic" +slug: /informal-concepts-propositional-logic/ +date: 2021-11-27 +tags: ["article"] +--- + +## Introduction + +This post is the first of a series on propositional logic that introduces the +foundational logical concepts of validity, soundness, truth, falsity, +possibility and indeterminacy. These concepts are defined here informally and +will receive formal articulation in later posts. + +## What is propositional logic? + +The chief unit of propositional logic is the _proposition_. It is typically said +that the sentences of a language _express_ propositions. Thus, in natural +languages, propositions are expressed via declarative sentences: a statement +that _such and such is the case_. For example, in English: _snow is white_, +_London is the capital of the United Kingdom_, _John is travelling to Stockholm_ +etcetera. Whilst this is true it is important not to assume that a proposition +reduces to its expression by a given sentence. There are numerous examples of +why this assumption proves problematic. For example, _il pleut_ and _it is +raining_ express the same proposition but do so via different sentences in +different languages. Similarly the semantically ambiguous English sentence _Two +cars were reported stolen by the police yesterday_ expresses two possible +propositions (the police reported the car stolen, the police stole the car) that +are left underdetermined by the surface grammar of the sentence. For simplicity +we will talk about propositions exclusively in terms of sentences but it is +important to note that the two are not straightforwardly interchangeable. + +Not every sentence in a language has a propositional form. Consider for example +_Áchtung!_ or _thanks_. It is not obvious that these sentences express a +proposition in the manner of declarative sentences. Philosophers and linguists +have called expressions that do not satisfy propositional criteria +[speech acts](https://en.wikipedia.org/wiki/Speech_act). The difference between +declarative sentences and speech acts (like thanking someone or issuing an +order) centers on the fact that the former possess clear _truth-conditions_ that +reduce to a given _truth-value_. In the case of _il pleut_ the sentence is true +if it is raining and false otherwise. Those are its truth-conditions. Its +truth-value is the particular assignment that is made on the basis of these +conditions. Assume it is not raining; in this case the truth-value of _il pleut_ +is false. + +Although declarative sentences are a subset of the totality of possible +grammatical expressions in any natural language, they are clearly an important +subset. They form the basis of all scientific and mathematical discourse and are +our primary means of spreaking and reasoning about the world. The scope of +propositional logic is limited to sentences that have this declarative property. +There are no questions, commands or exhortations in propositional logic, only +statements which may be true or false.1 + +The purpose of propositional logic is to analyse propositions in terms of their +truth conditions and to derive rules governing their proper application and +combination in arguments. Equipped with these rules, we are able to demonstrate +for example that an argument is valid or that it displays sound reasoning. On +the other hand, the same rules will allow us to demonstrate when an argument is +invalid or that it leads to contradiction. + +## Arguments and consistency + +Propositional logic proceeds upon two interconnected axes: + +
    +
  1. Analysing compound propositions in terms of their constituent parts
  2. +
  3. Analysing a proposition in relation to other propositions
  4. +
+ +In this post we are focused on the second axis. The first will be covered in a +future post on logical connectives and truth-tables. + +When we analyse a series of propositions and the relations between them we call +the group a _set_. Sets possess logical _properties_. The first such property we +will define is **consistency**. + +> A given set of propositions is consistent if and only if it is possible for +> each member of the set to be true at the same time. It is inconsistent just if +> this is not the case. + +The following set of propositions form an inconsistent set.2 Can you +spot the inconsistency? + +``` +(1) Anyone who takes astrology seriously is crazy. +(2) Jane is my sister and no sister of mine has a crazy person for a husband. +(3) Richard is Jane's husband and he checks his horoscope every morning. +(4) Anyone who checks their horoscope takes astrology seriously. +``` + +The set is inconsistent because it is not the case that all the propositions can +be true at once. Specifically: if (1), (3), and (4) are true, (2) cannot be. +Alternatively, if (2), (3) and (4) are true (1) cannot be. + +Let's illuminate the first instance of inconsistency. On the one hand we assert +that taking astrology seriously is crazy and we assume that checking your +horoscope means that you take astrology seriously. Richard is Jane's husband and +he checks his horoscope each morning. By definition then, Richard is crazy and +Jane is married to him, but if I believe this, I cannot believe that my sister +would not marry a crazy person. My beliefs are inconsistent. + +Now, the second instance of inconsistency. Jane, my sister, is married to +Richard. None of my sisters have a crazy husband. Richard checks his horoscope +each day and therefore takes astrology seriously. If this is the case, I cannot +believe that taking astrology seriously is crazy because otherwise my sister +cannot be married to Richard. + +In deconstructing the inconsistencies in the scenario of Richard and Jane we +have been concerned to point out that one proposition _implies_ or _follows_ +another proposition. Intuitively we have been invoking the logical notion of an +**argument**. This is not so different from what we mean by an argument in +ordinary life. If we are arguing with someone, we believe that they are wrong +about something where 'something' is a proposition and 'wrong' means _false_. +For example _the prime minister is a liar_. A more logical way to put this is +that we believe their beliefs about a set of propositions are inconsistent. In +order to make assertions about the relative consistency or inconsistency of a +set of propositions we advance arguments. This is like seeking to change the +person's viewpoint by showing that their belief _A_ conflicts with their other +belief _B_ or that if _A_ is true, they cannot believe _B_. + +In the example above each proposition in the set has equal footing; we have not +distinguished one type of proposition from any other. When we construct an +argument however, we distinguish the propositions by type. We say that one or +more propositions are _premises_ and one proposition is the _conclusion_: + +> An argument is a set of propositions comprising one or more premises and a +> conclusion. The conclusion is taken to be supported by the premises. + +Let's demonstrate how this works by making an implicit argument from previous +example explicit: + +``` +(P1) Anyone who checks their horoscope takes astrology seriously. +(P2) Richard checks his horoscope. +(C) Richard takes astrology seriously. +``` + +This constitutes a logical argument because of how the propositions are +arranged: we are asserting that the conclusion (C) is supported by premises P1 +and P2. We call such arguments **syllogisms**. + +## Evaluation criteria for arguments + +In what sense do the premises support the conclusion of an argument? What is the +relation between these two types of proposition? The word 'support' is rather +vague. In logic there are different ways to assess the qualility of an argument: +**inductive strength** and **deductive validity**. + +Consider the following argument: + +``` +(P1) When a cat scratches itself it can mean it has fleas. +(P2) Our tabby, Carrot has been scratching himself a lot lately. +(C) Carrot has fleas. +``` + +To ask ourselves whether this is a strong argument is to reflect on whether we +have good grounds for believing the conclusion given the premises. My intuition +that this is a reasonable argument but not a particularly strong one. It doesn't +strain credulity but it is by no means watertight. + +Contrast it with this argument: + +``` +(P1) Every day the sun rises in the east. +(P2) The sun rose in the east today. +(C) The sun will rise in the east tomorrow. +``` + +This strikes me as a stronger argument than the first. If you had to bet on +Carrot having fleas or the sun rising in the East you would put your money on +the latter although you would probably get better returns on the former. With +arguments of this nature we are proceeding on the basis of likelihood. The +technical term for this is _induction_ : given some background context of +beliefs (for instance the typical behaviour of cats and the planet) there are +stronger or weaker grounds for accepting the conclusion of an argument bases on +its premises. + +> An argument is inductively strong if and only if the conclusion is probably +> true given the premises. + +Although the arguments differ in their relative strength they are both inductive +arguments. This is because they are each falsifiable. In the first case, this is +obvious: Carrot might not have fleas and could be scratching for some other +reason. Perhaps surprisingly, the second argument is also falsifiable. The +magnetic field of the Earth could switch polarity meaning that while the Earth +would not change its position relative to the sun, our compass would be inverted +and therefore indicate that the sun rising in the west. This is very unlikely to +happen imminently but it _will_ happen at some point in the next 10,000 years. +Therefore the conclusion could prove false. + +The next obvious is whether all arguments are like this. Is probability the best +we can hope for? Fortunately not. Propositional logic is a deductive schema +which means it aims for truths that are not falsifiable in the manner of the two +examples above. It is this criterion of evaluation that we mainly interested in +when we use logic as a formal discipline. This is the domain of deductive +validity. Validity is our second key logical property. + +> An argument is deductively valid if and only if it is not possible for the +> premises to be true and the conclusion false. + +## Validity and soundness + +The following syllogism is an example of a valid argument: + +``` +(P1) All fish live in the sea and only in the sea. +(P2) Cod are fish. +(C) Cod live in the sea. +``` + +And here is an invalid argument: + +``` +(P1) All fish live in the sea and only in the sea. +(P2) Cod are fish. +(C) Cod live on land. +``` + +In the valid instance, there is no sense to the idea that we might accept each +premise and yet deny the conclusion. In the invalid instance this is not the +case: we can accept each premise and deny the conclusion. We can relate this +back to the the notion of consistency. Recall that a set of propositions is +consistent if and only if it is possible for each member to be true at once. +With the first argument we cannot consistently accept the premises and deny the +conclusion whereas in the case of the second argument we can quite consistently +accept the premises and deny the conclusion since the propositions do not +comprise a consistent set. + +In contrast to the previous inductive arguments, with valid arguments the +conclusion is supported purely in virtue of the terms used in the premises and +the propositions they express. In order to assess the validity of an argument +like the first it is not neccessary to aquaint oneself with cod and to study +their behaviour so as to determine whether they do in fact live in the sea. All +that is necessary is to understand the propositions expressed. Philosophers +refer to statements of this sort as _analytic_. They are true or false 'by +definition'. More specifically: the concept of the predicate is contained within +the concept of the subject for example _all brothers are male_. In the case of +the argument, we have defined at (P1) that fish are creatures that live in the +sea so given this definition, the conclusion is bound to follow from the +premises since it is just a specific instance of the general property already +defined. Validity is therefore an entirely formal notion that exists over and +above any facts of the matter. If I have defined 'fish' universally as +sea-dwellers I cannot without inconsistency say that they are not sea-dwellers. + +This is further exemplified with an argument like the following: + +``` +(P1) Manchester is the capital of the UK. +(P2) Manchester is north of Birmingham. +(C) The capital of the UK is north of Birmingham. +``` + +Is this a valid argument? To answer this question remember that invalidity means +that it is possible for the premises to be true and the conclusion false. In the +strict logical sense, this is valid argument since _were_ the premises true, the +conclusion would also be true. The point is that validity is a function of +truth-conditions not truth-values. The truth value of the first premise and the +conclusion in the above argument happen to be false but this does not affect its +validity. There is no necessity to London being the capital of the UK and not +Manchester. We can imagine things being otherwise which is to say that we can +entertain the truth-conditions of the proposition and make judgements in +accordance with it being true or false quite independently of whether it is in +actuality true/false. + +We can take this back to the earlier example of the invalid argument about cod: +in order to judge the argument invalid it was not necessary for us to look for +cod that live on land and come back empty. Rather we just had to assume that if +all fish live in the sea then it must be the case that if something is a fish, +it is a sea-dweller. We made no commitment to fish actually living in the sea. + +Does this mean that actual truth does not matter to logic? No, it just means +that validity as a property is decoupled from truth as a property although we +cannot of course have a grasp of the notion of validity without possessing a +prior notion of truth. A proposition being true in fact is a property it may or +not possess in addition to its membership within a valid sequence of reasoning. +If an argument is both valid and its premises are true in fact we say that it is +a **sound** argument. This is a stronger criterion of evaluation than validity +alone. + +> An argument is sound if and only if it is deductively valid and all of its +> premises are true. + +It follows from this definition of soundness that: + +- an argument cannot be sound if it is not also valid +- an argument can be valid without being sound +- if an argument is sound its conclusion must be true + +(The last point follows from the fact that soundness means the premises are true +and validity requires that if the premises are true the conclusion must also be +true.) + +We have already seen examples of arguments that are valid but not sound in the +Manchester example, let's close this section with an example where both premises +and conclusion are true yet the argument is invalid, demonstrating that truth +alone is not sufficient for soundness. + +``` +(P1) London is the capital of the UK. +(P2) The capital of the UK is in the southern part of the country. +(P3) Cambridge is not the capital of the United Kingdom +(C) London is south of Cambridge +``` + +This argument is deductively invalid because we can consistently assert the +premises but deny the conclusion. Specifically: there isn't anything about the +premises that makes the denial of the conclusion inconsistent. From the point of +view of the premises alone, London could be north of Cambridge whilst still +being in the southern part of the country. + +## Logical possibility + +In distinguishing the properties of logical consistency and validity we have +been making much tacit use of the notion of _possibility_. This is because when +we consider the validity of an argument we are assessing truth-conditions and +this consists in asking ourselves what could or could not be the case: were it +such that _P_, then it would be the case that _Q_. It is important to understand +what possibility means in the context of logic and how it differs from what we +might mean ordinarily when we use the term. + +It is evident from the case of arguments that are valid but not sound that logic +operates with a specialised notion of possibility. For example it has to be the +case that the proposition _Every woman can levitate_ is logically possible since +the following argument is valid: + +``` +(P1) Ellen is a woman. +(P2) Every woman can levitate. +(C) Ellen can levitate. +``` + +But we know of course that women cannot levitate. When we assert that this is +impossible we are relying on a stronger notion of possibility than logical +possibility. It follows that the concept of possibility can have different +degrees. The scope of the concept of possibility has been the concern of +logicians and philosophers since at least the time of Plato and numerous +different formulations exist. The notion that we mostly work with unreflectively +in everyday life is nomological possibility. This means 'governed by the +application of laws' where these laws pertain to our current understanding of +the natural world as determined by physics. Levitation is therefore +nomologically impossible but logically possible. + +If logical possibility is not contrained by the laws of physics does it place +any restrictions on what is possible? Logic applies a single restriction, the +law of non-contradiction: a proposition cannot both be true and false at once. +The following propositions are examples of a contradictory propositions. + +``` +There is a dog that is not a dog. +Today is Tuesday and today is not Tuesday. +The cat that is dead is alive. +``` + +From this we can derive the following property of logical possibility: + +> A proposition is logically possible just if it does not imply a contradiction. + +## Logical truth, falsity and indeterminacy + +What are the truth-conditions of a contradictory proposition? We know that a +logically possible proposition such as _every woman can levitate_ could be true +or false. It has to be so because we are capable of constructing valid arguments +where it features as a premise and a valid argument implies the possible truth +of its premises. + +In the case of a contradiction there are no conditions under which it could be +judged to be true. For this reason, contradictions are classified as **logically +false**. This is distinct from ordinary falsity where a proposition _could_ be +true but happens to be false. Logically false propositions are universally false +and could never be true. This is consistent with our previous observation of the +law of non-contradiction: if a proposition cannot be both true and false at once +we are saying that something _cannot be the case_ which is of course to say _is +false_. + +Logical falsity is therefore another property that a proposition may possess and +it is a property that is possessed by all propositions that are contradictions: + +> A proposition is logically false if and only if it is not possible for the +> sentence to be true. + +Complementing logical falsity is **logical truth**: + +> A proposition is logically true if and only if it is not possible for the +> sentence to be false. + +We call logically true propositions tautologies. Some examples: + +``` +An apple is an apple. +Today is Tuesday or today is not Tuesday. +The cat is dead or alive. +``` + +The properties of logical truth and falsity are alike in their universality. +Propositions that are logically true do not exclude any possibility (today is +Tuesday or it is not Tuesday; there is no possible state outside of this) +whereas logically false propositions exclude all possibilities (there is no +scenario where today is both Tuesday and not Tuesday). + +We class all propositions that are not contradictions or tautologies **logically +indeterminate** propositions. This means that their truth-value is not assigned +purely on the basis of the meanings of the terms of which they are comprised. +_It is raining_ for example, is logically indeterminate because we cannot know +its truth-value just by reflecting on the meaning of the predicate _is raining_. +It may be true under certain conditions and false under others and in order to +know the specific truth-value at a given moment, we must look to states of +affairs beyond the sentence. The vast majority of propositions expressed in +natural and formal languages are indeterminate in this manner. + +## Summary + +In this post we introduced propositions as descriptions of states of affairs +that possess truth-conditions. We noted that propositions are expressed in +language through the medium of declarative sentences and that not every +expression in a language possesses a propositional form. Two key properties that +pertain to sets of propositions were introduced and exemplified: consistency and +validity. In addition we considered different evaluative criteria for logical +arguments comparing inductive strength with deductive validity. We distinguished +logical possibility from nomological possibility and explained how the law of +non-contradiction places bounds on what is logically possible. Equipped with the +concept of logical possibility we were able to introduce logical truth and +falsity, analysing the truth-conditional form of tautologies and contradictions +and noting that propositions that are neither logically true or false are +logically indeterminate. + +## Notes + +
    +
  1. These all being standard instances of speech acts which are resistant to truth-conditional analyses as noted above.
  2. +
  3. This example comes from (Bergmann, Moor and Nelson, 2014).
  4. +
+ +## References + +Bergmann, M., Moor, J. and Nelson, J. (2014). The logic book. Boston: +Mcgraw-Hill/Connect Learn Succeed. + +Wikipedia Contributors (2019). Speech-act. [online] Wikipedia. Available at: +https://en.wikipedia.org/wiki/Speech_act. + +‌ diff --git a/posts/jest-parameterization.md b/posts/jest-parameterization.md new file mode 100644 index 0000000..11e3be5 --- /dev/null +++ b/posts/jest-parameterization.md @@ -0,0 +1,157 @@ +--- +title: "Jest parameterization" +slug: /jest-parameterization/ +date: 2023-11-20 +tags: ["learning", "javascript", "unit-testing"] +--- + +At work I am in the process of upgrading our AWS Lambdas to use the v.18 Node +runtime (most of them are still using v.14). It has been a good opportunity to +carry out refactoring and address technical debt. + +Many of the lambdas lack unit tests whilst others have tests that haven't been +maintained. Thus I've been spending time adding and optimising tests in Jest. + +I've been harnessing parameterization when improving unit test coverage. The +lambdas I'm currently working on are subroutines within a broader +[AWS Step Function](https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html) +that constitutes the backend of one of our internal content management systems. +Much of the functionality consists in generating and parsing properties from XML +and JSON. As I am testing the same code under different conditions, the tests +are highly repetitive and can be readily parameterized. + +Here's one example: + +```js +describe("handler", () => { + let mockApiGatewayEvent = { + propertyId: "1234", + isCrossPublished: false, + } + describe("exit conditions", () => { + it("should throw an error if a user attempts to unpublish an alpha file", async () => { + event = { + ...event, + fileId: "alpha:1234", + } + await expect(handler(event)).rejects.toThrow( + "Not allowed to unpublish file alpha:1234" + ) + }) + it("should throw an error if user attempts to unpublish a beta file", async () => { + event = { + ...event, + fileId: "beta:1234", + } + await expect(handler(event)).rejects.toThrow( + "Not allowed to unpublish file beta:1234" + ) + }) + // and so on... + }) +}) +``` + +I've anonymised the specifics of the data but the process is straightforward: +I'm asserting that the correct error text is returned if a user attempts to +delete a certain filetype. I reduced the verbiage of countless `it` blocks by +utilising parameterization: + +```js +describe("exit conditions", () => { + let mockApiGatewayEvent = { + projectId: "1234", + preview: false, + } + + it.each([["alpha:1234"], ["beta:1234"]])( + "should throw an error if user attempts to unpublish %s file", + async (fileId) => { + mockApiGatewayEvent = { + ...mockApiGatewayEvent, + fileId, + } + await expect(handler(mockApiGatewayEvent)).rejects.toThrow( + `Not allowed to unpublish file ${fileId}` + ) + } + ) +}) +``` + +Instead of multiple `it` clauses, there is a single `each` expression that loops +through each file variant, executing the same test each time, changing only the +file name that is output. + +The `%s` symbol is a placeholder for string substitution. This lets me include +the name of each variant in the test description. This is important because +there is a risk of obscuring the specifics of each test iteration when using +parameterization. It's essential to be able to trace a failure to the specific +iteration. + +In the example below, the process is functionally the same but there are more +parameters in the mix: + +```js +describe("deletePageFromS3()", () => { + beforeEach(() => { + process.env.AWS_ENV = "live" + s3ClientMock.reset() + }) + + const parameters = [ + { + previouslyPublished: false, + isDraft: false, + bucket: "bucket:alpha", + key: "key:alpha", + }, + { + previouslyPublished: true, + isDraft: false, + bucket: "bucket:beta", + key: "key:beta", + }, + { + previouslyPublished: false, + isDraft: true, + bucket: "bucket:gamma", + key: "key:gamma", + }, + { + previouslyPublished: true, + isDraft: true, + bucket: "bucket:delta", + key: "key:delta", + }, + ] + + it.each(parameters)( + "should return page for deletion, given: previouslyPublished is %s, isDraft is %s", + async ({ previouslyPublished, isDraft, bucket, key }) => { + await deleteFileFromS3("url", previouslyPublished, isDraft) + const deleteObjectCommand = s3ClientMock.calls()[0].args[0] + expect(deleteObjectCommand.input).toEqual({ + Bucket: bucket, + Key: key, + }) + } + ) +}) +``` + +The process under test is a function that uses the AWS SDK to delete objects +from an S3 bucket. I am checking that the (mocked) S3 client is called with the +correct parameters. + +This time I am passing in an array of objects to the `each` function rather than +a multi-dimensional array. This makes it easier to destructure the specific +properties in the individual test cases. Again I use `%s` to interpolate a +subset of the parameters into each test description. `%s` applies to the each +value in the `each` array in sequence so you just repeat it to individuate the +different params. + +This has been a brief sketch of some applied examples of parameterization. For a +better account, see +[Parameterized tests in JavaScript with Jest](https://blog.codeleak.pl/2021/12/parameterized-tests-with-jest.html) +by Rafał Borowiec. diff --git a/posts/local-dynamodb-setup-sam.md b/posts/local-dynamodb-setup-sam.md new file mode 100644 index 0000000..f6b0217 --- /dev/null +++ b/posts/local-dynamodb-setup-sam.md @@ -0,0 +1,166 @@ +--- +title: "Getting a local DynamoDB instance working" +slug: /local-dynamodb-setup-sam/ +date: 2024-06-08 +tags: ["log", "aws", "dynamodb"] +--- + +I wasted most of an afternoon trying to get the following set-up: + +- Two local instances of DynamoDB running in Docker +- The ability to add table data to either instance via NoSQL Workbench and query + it via an AWS lambda + +If you are coming from SQLPro or DBeaver, Amazon's NoSQL workbench is a bit +unintuitive. Creating the lambdas and the Docker containers was easy, it was +getting them recognised by this client that was tricky. Anyway this is the +process. + +I want to set up two Docker instances of the `amazon/dynamodb-local` image, +along with a bridging network so that the Docker container that SAM runs in can +communicate with the containers that DynamoDB runs in. Hence `docker-compose` is +the way to go: + +```yml +# docker-compose.yaml + +version: "3.8" +services: + dev: + image: amazon/dynamodb-local + container_name: timetracking_dynamodb_dev + ports: + - "8000:8000" + volumes: + - "./data:/home/dynamodblocal/data" + networks: + - sam-local + stage: + image: amazon/dynamodb-local + container_name: timetracking_dynamodb_stage + ports: + - "8001:8001" + volumes: + - "./data:/home/dynamodblocal/data" + networks: + - sam-local + +networks: + sam-local: + driver: bridge +``` + +My two DDB instances are `dev` and `stage` running on ports 8000 and 8001 +respectively. Their shared network is called `sam local`. + +After running `docker-compose up`. I run `docker network ls` and it confirms the +network has been created (it prepends the network name with the repo name): + +![](./img/docker-network-ls.png) + +Next comes SAM. I've just created the default Typescript Lambda template using +the SAM CLI. For now this will only expose a single API Gateway endpoint, +`/fetch`, which I will use to GET my DynamoDB table data. The key info from the +template: + +```yml +# template.yaml + +Resources: + TimeTrackingFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: index.handler + Runtime: nodejs20.x + Architectures: + - x86_64 + Events: + Fetch: + Type: Api + Properties: + Path: /fetch + Method: get +``` + +I'll use the following command to start the local APIGateway server: + +```sh +sam build && sam local start-api --docker-network time-tracking_sam-local +``` + +If I run `docker network inspect time-tracking_sam-local`, I can confirm that +the SAM Docker instance (which AWS creates and which is effectively a black box +to me) is on the same network as the two DDB instances that I set up earlier: + +![](./img/docker-network-inspect.png) + +Next I want to view my containers in NoSQL workbench so I can create the table +schema and start adding data. I'm just going to worry about the `dev` container +here. I go to the _Operation builder_ and add a new connection: + +![](./img/workbench-create-conn.png) + +This automatically generates an _Access key ID_ and _Secret access key_ even +though this is a local service that won't interact with the production version +in any way. You _must_ use these. Some guides say you can put anything when +connecting locally but in my experience this is _not true_. + +Using these keys, I write a skeletal lambda just to check the connection via the +AWS SDK for DynamoDB: + +```ts +import { DynamoDBClient, ListTablesCommand } from "@aws-sdk/client-dynamodb" + +const client = new DynamoDBClient({ + region: "localhost", + endpoint: "http://dev:8000", + credentials: { + accessKeyId: "xxxx", + secretAccessKey: "xxxx", + }, +}) + +export const handler = async (event: APIGatewayProxyEvent): Promise => { + try { + const command = new ListTablesCommand({}) + const response = await client.send(command) + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: "Successfully retrieved tables", + tableNames: response.TableNames, + }), + } +... +``` + +The first problem because I wasn't providing a region. Why would I, this is +local? But it needs to be `localhost`. And again, you must put the actual +credential values that the Workbench gives you. + +The next issue was that this function was returning an empty array when I +queried the `/fetch` endpoint. Even though I had set up a table in Workbench: + +![](./img/workbench-data-modeller.png) + +You must click _Commit to Amazon DynamoDB_ even though this is counter-intuitive +and suggests you are going to auto-provision paid services! You can then select +the local instance that you are going to seed with the table from the model: + +![](./img/commit-table-workbench.png) + +Now if you back to _Operation builder_, the table is associated with the given +instance and the data is displayed: + +![](./img/ddb-data-in-builder.png) + +On reflection, this makes a lot of sense as it decouples the data model from any +particular instance (local or production) that uses the data and makes it easy +to seed different tables to different database instances. + +So finally, if I now query the `fetch` endpoint, I get the table name returned, +demonstrating that all the different parts have joined up: + +![](./img/httpie-showing-data.png) diff --git a/posts/nand-to-tetris-unit-one.md b/posts/nand-to-tetris-unit-one.md new file mode 100644 index 0000000..d0d0cae --- /dev/null +++ b/posts/nand-to-tetris-unit-one.md @@ -0,0 +1,756 @@ +--- +title: "N2T Unit One: Boolean functions and gate logic" +slug: /nand-to-tetris-unit-one/ +date: 2023-01-13 +tags: ["article", "learning", "computer-architecture"] +--- + +I have recently started [Nand to Tetris](https://www.nand2tetris.org/course). +This course teaches the foundations of hardware and computer achitecture and is +based on the textbook _The Elements of Computing Systems_ by Noam Nisan and +Shimon Schocken. As the course proceeds you build a functioning general-purpose +computer that is eventually capable of running Tetris and any number of other +programs. The hardware is built primarily through simulation software running on +Hack, a simplified +[hardware description language](https://en.wikipedia.org/wiki/Hardware_description_language) +similar to Verilog and VHDL. + +In this post I outline what I learned in the first unit. This broadly follows +the curriculum but adds extra details I have acquired elsewhere to give the +fullest account. + +## Bits and functions + +The workings of a classical computer can be reduced to a series of operations on +the binary digits (bits) 0 and 1. A computational process can be represented as +a function: data (a series of bits) enters the function in one state and exits +in another state. This new state is a product of the function. + +![](./img/function-diagram-new.png) + +The most primitive bit operations are equivalent to the truth-conditions of the +logical connectives of Boolean algebra. In logic, the Boolean values are _true_ +and _false_ but we will use 1 and 0, to represent these states as bits. There +are multiple logical connectives but we will mostly focus on AND, OR, and NOT +for simplicity. + +The logic of each Boolean connective can be expressed as a function. + +NOT can be represented as follows; + +$$ +f(x) = \lnot (x) +$$ + +NOT (¬) is a unary operator which means it takes one operand (_x_). We pass a +single bit as the operand and the function inverts its value. We can utilise +logical [truth tables]() to represent all possible inputs and outputs for the +operator: + +| x | f(x) = NOT(x) | +| --- | ------------- | +| 1 | 0 | +| 0 | 1 | + +If NOT receives 0 as an input it will return 1 as the output. If it receives 1 +as the input it will return 0 as the output. + +AND ($\land$) and OR($\lor$) are binary operators: they receive two operands as +input. Therefore we pass two bits to the function. As a bit can be one of two +values (0 or 1), there are four possible inputs. + +For AND this gives us: + +| x | y | f(x) = x AND y | +| --- | --- | -------------- | +| 1 | 1 | 1 | +| 0 | 1 | 0 | +| 1 | 0 | 0 | +| 0 | 0 | 0 | + +AND returns 1 if both input bits are 1, otherwise it returns 0. + +OR returns 1 if one or both bits are 1, otherwise it returns 0: + +| x | y | f(x) = x OR y | +| --- | --- | ------------- | +| 1 | 1 | 1 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 0 | 0 | 0 | + +## Logic gates + +The logical function of each of the Boolean operators is implemented at the +level of computer hardware by logic gates. Each gate is represented with one or +more input pins and a single output pin through which the bits enter and exit. +The pins feed into and out of a chip which executes the function. + +The diagram below shows the logic gates for AND, OR, and NOT. + +![](./img/and-or-not-gates.png) + +Logic gates are an abstraction. In reality, you cannot _see_ a logic gate in a +computer. If we were to look inside a chip that implements gate logic we would +see an arrangement of transistors, capacitors and resistors connected by wires. +These components are configured to mimic the behaviour of the logical operators. + +_Nand to Tetris_ starts at the level of gates and does not discuss how the +individual gates are realised in electronic circuits. However, I think it is +useful to understand the basics of the electrical engineering so we might begin +to grasp how it is possible to go from an inert block of metal and silicon to +fully functioning computer. + +### The electronic implementation of gate logic + +We can start with the concept of a switch. Consider what is happening when we +turn on a light using a wall switch. + +When the switch is off, the electrical circuit that connects the bulb to the +voltage source is broken. As a result, there is no potential difference between +the terminals and the current cannot reach the bulb. When the switch is on, the +circuit is complete and the current flows freely to the bulb. + +This is represented by the following simple schematic: + +![](./img/single-switch-and-circuit.png) + +This circuit embodies the logic of a NOT gate. Think of the light as 1 and the +absence of light as 0. When the switch is on, 1 is the output and 0 is inverted. +When the switch is off, 0 is the output and 1 is inverted. + +This scenario can be developed to represent the logical behaviour of an AND +gate. Imagine now that this room is a bit strange and there are two switches +controlling the operation of the bulb. If both switches are off the bulb will +not emit light. If one of the switches is on and the other is off the bulb will +not emit light. The bulb will only emit light when _both_ switches are on. + +The schematic for this scenario embodies the logic of the Boolean AND +connective: + +![](./img/double-and-circ.png) + +Switch-controlled circuits are functionally equivalent to what actually happens +inside a computer when logical conditions are implemented via gates. Electrical +charge is directed along different routes depending on the value of an on/off +condition. However, in modern computers the actual component that controls the +flow of current is not a switch. + +Whilst we could construct primitive computers with switches, the average CPU has +more than a million logic gates. Controlling this number of gates with +mechanical switches would be practically impossible and even if it were +achieved, it would result in extremely slow processing times. + +This is an important point because the computational power of logic gates +emerges from their behaviour at scale. Collections of gates are combined to +express complex logical conditions that are a function of their individual +parts. For this to be possible, the output of a collection of gates needs to be +able to be fed into another collection and this it would be difficult to achieve +in a mechancial switch-based system. + +Instead of mechanical switches, computers use transistors. Transistors are +semi-conductors: components that possess an electrical conductivity between that +of a conductor and an insulator. This property means they can both impede and +expede the flow of electrical charge. + +There are different types of transistors but we will focus on basic Bipolar +Junction Transistors: + +![](./img/bjt-again.png) + +Applying a small amount of current at the base terminal (B) of a BJT allows a +larger current to flow from the collector (C) to the emitter (E). Removing +current at the base terminal reduces the flow from collector to emitter. This is +because the the emitter and collector are composed of a semi-conductor that has +a surplus of electrons (negatively charged) whereas the base has a deficiency of +electrons (positively charged). This state creates a modifiable potential +difference, reducing or increasing the current based on the voltage. + +The base terminal of a transistor is another way of implementating the gate-like +behaviour we previously achieved with mechanical switches. However, there is an +important difference. With a switch, the circuit is actually broken when it is +in the "off" state and there is no current flowing at all. With a transistor, +the current drops in the "off" state but a voltage remains. + +Because a continuous circuit is an analogue system, the quantities of +resistance, voltage and current are not discrete values, they will vary over a +given range. Thus "off" corresponds to "low" voltage and "on" corresponds to +"high" voltage. The specific stipulation will depend on the circuit design but +it is typically the case that a state of 1 or "on" is within the range 2-5V +whereas a state of 0 or "off" is within the range 0.0 - 0.8V. + +## Boolean function synthesis + +To recap, elementary computational processes can be represented as logical +functions. A function consists in one or more Boolean operators processing bits. +For each Boolean operator we can construct a chip that represents its truth +conditions. We call these chips logic gates. Logic gates are built with +transistors that either block or permit the flow of electric current. This makes +them behave in a manner almost identical to mechanical switches. + +Now that we know how the individual logic gates work and how they are +implemented electronically, we will explore how they can be applied in +combination to represent complex logical states that more closely resemble +actual computer programs. This process is known as **Boolean function +synthesis**. + +We will construct a logic circuit that represents the truth conditions for the +following state of affairs: + +> The team plays on either Monday or Thursday and not at weekends + +Let's call this _P_ for ease of reference. + +This complex comprises several simpler atomic expressions: + +
    +
  1. The team plays on Monday
  2. +
  3. The team plays on Thursday
  4. +
  5. The team plays at weekends
  6. +
+ +The first step is to construct a truth table. On the left-hand side we list all +the possible truth values for each individual expression. On the right-hand +side, we assign an overall truth value for their combination, based on whether +or not they reflect the truth conditions for _P_. + +| x | y | z | _P_ | +| --- | --- | --- | --- | +| 1 | 1 | 1 | 0 | +| 1 | 1 | 0 | 1 | +| 1 | 0 | 1 | 0 | +| 1 | 0 | 0 | 1 | +| 0 | 1 | 1 | 0 | +| 0 | 1 | 0 | 1 | +| 0 | 0 | 1 | 0 | +| 0 | 0 | 0 | 0 | + +We are only interested in the cases where _P_ is true, so we can discount any +lines that result in a truth value of 0 for the complex expresssion. This leaves +us with: + +| x | y | z | _P_ | +| --- | --- | --- | --- | +| 1 | 1 | 0 | 1 | +| 1 | 0 | 0 | 1 | +| 0 | 1 | 0 | 1 | + +Parsing each line, the truth table tells us that our complex expression (_P_) is +true in the following scenarios: + +- If the team plays on both Mondays and Thursdays but not at weekends +- If the team plays on Mondays but not on Thursdays and not at weekends +- If the team plays on Thursdays but not on Mondays and not at weekends + +We can formalise each case: + +- _(x AND y) AND NOT z_ +- _(x AND NOT y) AND NOT z_ +- _(NOT x and y) AND NOT z_ + +We now have three logical expressions that if constructed with logic gates would +result in a partial representation of _P_. The representation would be partial +because each individual expression only conveys a single aspect of the truth of +_P_, not its totality. For example, if we constructed a circuit that represents +_(x AND NOT y) AND NOT z_, this would only cover occasions where the team plays +on Mondays but not on Thursdays (or the weekend). It wouldn't cover the case +where the team plays on Thursdays but not Mondays (or the weekend). + +We want a circuit that captures all possible instances where _P_ returns true. +There are practical benefits to seeking a single implementation. In order to +maximise our computational resources we want to use the minimum number of gates +in the simplest configuration possible. + +We start by concatenating each individual expression into a single disjunctive +expression using logical OR: + +$$ +((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z) +$$ + +Next, we look for opportunities to simplify this complex expression. This is +similar to simplifying equations in mathematical algebra. It is a heuristic +process; there is no formal or automated procedure that will work in every case. + +_NOT z_ occurs in each of the individual disjunctive expressions. Therefore we +can reduce the repetition by using it only once: + +$$ +(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y) \land \lnot z +$$ + +Now we need to consider how we can simplify the remaining expressions: + +$$ +(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y) +$$ + +If we look closely we can see that this expression is displaying the truth +conditions for OR. The truth conditions for _x_ and _y_ are: + +- true if _x_ and _y_ are true +- true if _x_ is true and _y_ is false +- true if _x_ is false and _y_ is true + +This recalls our earlier definition of OR: + +| x | y | f(x) = x OR y | +| --- | --- | ------------- | +| 1 | 1 | 1 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 0 | 0 | 0 | + +Thus we can reduce: + +$$ +(x \land y) \lor (x \land \lnot y) \lor (\lnot x \land y) +$$ + +to: + +$$ +x \lor y +$$ + +The reduction is now complete, allowing us to reduce: + +$$ +((x \land y) \land \lnot z) \lor ((x \land \lnot y) \land \lnot z) \lor ((\lnot x \land y) \land \lnot z) +$$ + +to: + +$$ +(x \lor y) \land \lnot z +$$ + +If we construct a truth table for the original expression (_P_) and its +simplification (_P'_) we see that they are true under the same logical +conditions which demonstrates their equivalence: + +| x | y | z | _P_ | _P'_ | +| --- | --- | --- | --- | ---- | +| 1 | 1 | 0 | 1 | 1 | +| 1 | 0 | 0 | 1 | 1 | +| 0 | 1 | 0 | 1 | 1 | + +### Constructing the digital circuit + +Now that we have reduced _P_ to its simplest form using the connectives AND, OR +and NOT we can construct a circuit using the logic gates for these connectives +to represent the overall state of affairs expressed by _P_. + +We will have three input bits which correspond to _x_, _y_, _z_, and a single +output bit that will reflect the truth value of _P_ based on the inputs. The +input bits will be fed into an arrangement of logic gates that that matches the +logical connectives in _(x OR y) AND NOT z_. + +
+ +
+ +We can confirm that the circuit implementation is an accurate representation of +_P_ by toggling the input values to confirm that the output is only 1 when +either _x_ or _y_ is true and _z_ is false. + +### Further simplification with NAND + +Our circuit uses three different types of logic gate. This is satisfactory but +it would better if we could simplify the circuit even further and use a single +gate rather than three. To do so we need to further reduce our logic and +introduce another type of logic gate: NAND. + +NAND stands for "NOT AND" and its truth conditions are the inversion of AND: + +| x | y | f(x) = x NAND y | +| --- | --- | --------------- | +| 1 | 1 | 0 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 0 | 0 | 1 | + +NAND returns 1 whenever _x_ and _y_ are not both true. We will represent NAND in +our formulae with the symbol: + +$$ +\tilde\land +$$ + +NAND is a _universal logic gate_. This means that by using NAND gates and only +NAND gates, we can represent the truth function of every other logic gate (AND +can be expressed with just NAND gates, as can OR and so on). It follows that we +can create every possible logical circuit using NAND gates alone. + +Let's demonstrate this by reformulating _(x OR y) AND NOT z_ with just NANDs: + +$$ +( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) ) \tilde\land ( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) ) +$$ + +This is quite difficult to parse, so let's look at the circuit representation +and derive its equivalence to _(x OR y) AND NOT z_: + + + + +You will notice that there is repeated forking pattern to most of the inputs. +This occurs when the same input value is used for both input pins, equivalent to +_x NAND x_ in the equation: + +![](./img/nand-fork.png) + +When a NAND is wired to receive the same value for each input, it embodies the +truth conditions for NOT: in the diagram above when _a_ is 1 the output is 0 and +when _a_ is 0 the output is 1. + +If we feed two of these sub-circuits into a NAND, we observe that the output is +consistent with the truth conditions for OR: + +
+ +
+ +This is equivalent to the _(x NAND x) NAND (y NAND y)_ section of our NAND +equation which we can see is equivalent to _x OR y_. + +The final part of the NAND equation is dedicated to: _NOT z_. This is achieved +by using another forking NAND and applying it to _z_, this is then joined with +the existing fragment via a NAND to give: + +$$ +( [(x \tilde\land x) \tilde\land (y \tilde\land y)] \tilde\land (z \tilde\land z) ) +$$ + +Which is then itself forked into a NAND to give the final output. + +This is harder to parse than the implementation that used three different +operators but the point is just to demonstrate that such a reduction is possible +and that complex abstract states can be constructed from the concatenation of +primitive electronic components. + +## Hardware Description Language + +Digital circuits can be designed using a Hardware Description Language (HDL) and +simulation software. An HDL is a declarative programming language used to +describe the behaviour and structure of digital circuits. In _Nand To Tetris_ +the HDL is Hack, a simplified HDL for teaching purposes. + +An HDL file uses specialised syntax to describe the function and implementation +of a given chip. When it is fed into a simulator, we can test the chip's outputs +against a variety of inputs to check it is working as intended. + +Below is an HDL specification file for the NAND logic gate written in Hack: + +``` +CHIP Nand { + IN a,b; + OUT out; + + PARTS: + And(a=a,b=b,out=w); + Not(in=w1,out=out); +} +``` + +The code contains two sections: + +- the interface (`CHIP`, `IN`, `OUT`) +- the implementation (`PARTS`) + +The interface names the chip and designates its input and output pins. In the +example, the interface specifies two input pins (`a` and `b`) and a single +output pin (`out`). + +The interface abstracts the actual implementation of the chip. It only tells us +the inputs and output, not how the output is generated from the input. This is +provided by the implementation section which details the internal workings of +the chip. + +The NAND implementation invokes two other gates, AND and NOT. We are simply +taking the output of AND and inverting it with NOT. The HDL specification +describes the following circuit: + +![](./img/nand-with-and-not.png) + +Having defined the gate we can load it into the simulator and test its +behaviour. + +![](./img/hardware-simulator-three.png) + +We can change the values of the input pins and observe how this affects both the +output and the interim outputs of the implementation (ie. _w1_). + +To be more efficient we can create a test file that runs through all our +expected outputs: + +``` +# Nand.tst + +load Nand.hdl, +output-file Nand.out, +compare-to Nand.cmp, +output-list a%B3.1.3 b%B3.1.3 out%B3.1.3; + +set a 0, +set b 0, +eval, +output; + +set a 0, +set b 1, +eval, +output; + +set a 1, +set b 0, +eval, +output; + +set a 1, +set b 1, +eval, +output; +``` + +We feed the test file into the simulator along with the following comparison +file and it will compute whether the chip conforms to our expectations: + +``` +# Nand.cmp + +| a | b | out | +| 0 | 0 | 1 | +| 0 | 1 | 1 | +| 1 | 0 | 1 | +| 1 | 1 | 1 | +``` + +## Coursework + +The task for the first unit was to use Hack to create the set of logic gates and +chips that will later be utilised in the construction of the computer. You are +provided with NAND as a primitive and from this you build the other gates. Once +a working gate has been constructed from NAND you are permitted to use it in the +construction of subsequent gates. For example if you have made an OR gate solely +out of NANDs, you may then use OR along with NAND to create XOR. + +Below I have listed the HDL files for each gate along with a simulation of the +circuit implementation. + +### Gates + +#### NOT + +``` +CHIP Not { + IN in; + OUT out; + + PARTS: + Nand(a=in,b=in,out=out); +} +``` + +
+ + +
+ +#### AND + +``` +CHIP And { + IN a, b; + OUT out;CHIP DMux { + IN in, sel; + OUT a, b; + + PARTS: + Not(in=sel,out=nsel); + And(a=in,b=nsel,out=a); + And(a=in,b=sel,out=b); +} + Not(in=w1,out=out); +} +``` + +
+ +
+ +#### OR + +``` +CHIP Or { + IN a, b; + OUT out; + + PARTS: + Nand(a=a,b=a,out=w1); + Nand(a=b,b=b,out=w2); + Nand(a=w1,b=w2,out=out); +} +``` + +
+ +
+ +#### XOR + +``` +CHIP Xor { + IN a, b; + OUT out; + + PARTS: + And(a=a,b=notb,out=w1); + Not(in=b,out=notb); + And(a=b,b=nota,out=w2); + Not(in=a,out=nota); + Or(a=w1,b=w2,out=out); +} +``` + +
+ +
+ +### Chips + +As well as the basic logic gates, the first unit introduced additional chips +that are essential for constructing a working computer. These chips represent +more complex states that the logical operators but proceed on the same +functional and modular basis: input values are processed internally to produce +output values and the implementation can utilise previously constructed logic +gates. + +#### MUX (Multiplexer) + +A multiplexer selects one of several input pins and forwards the selection to a +single output pin. There are three pins: two input bits (`A`, `B`) and a +selection bit (`SEL`). When `SEL` is applied the output bit is toggled between +`A` and `B`. Multiplexers are essential to the construction of large digital +circuits as they implement data selection and switching on the basis of logical +conditions. + +``` +CHIP Mux { + IN a, b, sel; + OUT out; + + PARTS: + Not(in=sel,out=w1); + And(a=w1,b=a,out=w2); + And(a=sel,b=b,out=w3); + Or(a=w2,b=w3,out=out); +} +``` + +
+ +
+ +#### DMUX (Demultiplexer) + +As the name suggests, a demultiplexer reverses the functionality of a +multiplexer. It receives a single input, and based on the `SEL` value channels +it to either an `A` or `B` output. + +``` +CHIP DMux { + IN in, sel; + OUT a, b; + + PARTS: + Not(in=sel,out=nsel); + And(a=in,b=nsel,out=a); + And(a=in,b=sel,out=b); +} +``` + +
+ +
+ +### Multi-bit chips + +Multi-bit chips are variants of the chips and gates already produced. The logic +of a multi-bit AND is the same as the logic for a normal AND gate. They differ +only in the number of bits they can receive and output. + +In a real computer, passing single 1s and 0s into chips would be inefficient +since very little information can be represented or encoded in a single bit. +When we build the computer we will be passing values with a bit-length of 8-bits +(a byte) as a miniumum (e.g. 10101100) and we need chips that can handle bits of +this length. + +For illustration, here is the HDL implementation of an AND-16: + +``` +CHIP And16 { + IN a[16], b[16]; + OUT out[16]; + + PARTS: + And(a=a[0],b=b[0],out=out[0]); + And(a=a[1],b=b[1],out=out[1]); + And(a=a[2],b=b[2],out=out[2]); + And(a=a[3],b=b[3],out=out[3]); + And(a=a[4],b=b[4],out=out[4]); + And(a=a[5],b=b[5],out=out[5]); + And(a=a[6],b=b[6],out=out[6]); + And(a=a[7],b=b[7],out=out[7]); + And(a=a[8],b=b[8],out=out[8]); + And(a=a[9],b=b[9],out=out[9]); + And(a=a[10],b=b[10],out=out[10]); + And(a=a[11],b=b[11],out=out[11]); + And(a=a[12],b=b[12],out=out[12]); + And(a=a[13],b=b[13],out=out[13]); + And(a=a[14],b=b[14],out=out[14]); + And(a=a[15],b=b[15],out=out[15]); +} +``` + +Instead of a single-bit AND gate that takes two single-bit inputs and produces a +single-bit output, the 16-bit AND takes two 16-bit inputs and produces a single +16-bit output. Each bit of the output is determined by the AND operation which +is executed on each of the input bits. + +Don't be confused by the base-10 numbers: we are still working with binary +values however we use denary digits to individuate each bit in the 16-bit +number. For example if `a = 10101111`, `a[4]` refers to the fourth bit in `a` +counting from the right-hand side (`0`). + +In addition to `And16` I created multi-bit variants of OR, NOT, MUX and DMUX. + +### Multi-way chips + +I also produced _multi-way_ variants of some of the main gates and chips. These +versions accept more than the standard one or two input pins but execute the +same logic. For example instead of a standard 2-pin input AND gate, a 3-pin +input AND gate would take three inputs and produce a `1` output when all three +inputs are `1`. + +An example of a multi-way chip that I constructed is OR-8-WAY. This chip outputs +1 when any of its 8 inputs is 1. If all inputs are 0, it outputs 0: + +``` +CHIP Or8Way { + IN in[8]; + OUT out; + + PARTS: + Or(a=in[0],b=in[1],out=a); + Or(a=a,b=in[2],out=b); + Or(a=b,b=in[3],out=c); + Or(a=c,b=in[4],out=d); + Or(a=d,b=in[5],out=e); + Or(a=e,b=in[6],out=f); + Or(a=f,b=in[7],out=out); +} + +``` diff --git a/posts/neuron-eolas-frontend.md b/posts/neuron-eolas-frontend.md new file mode 100644 index 0000000..33c48ff --- /dev/null +++ b/posts/neuron-eolas-frontend.md @@ -0,0 +1,36 @@ +--- +title: "Frontend for Eòlas" +slug: /neuron-eolas-frontend/ +date: 2024-10-24 +tags: ["projects", "log", "eolas"] +--- + +![Diagram of service](./img/eolas-static-diag.png) + +I'm now publishing [my zettelkasten](https://github.com/thomasabishop/eolas) +publicly at +[https://thomasabishop.github.io/eolas/](https://thomasabishop.github.io/eolas/). +I wanted to be able to access my notes from different devices and also have them +in a nicer format with syntax highlighting and LaTeX support. + +I'm using the [Neuron](https://neuron.zettel.page/) static site generator which +is designed specifically zettelkasten-type projects. It requires that the links +and some other metadata are in a specific format. However, I wanted the +publishing process to be decoupled from my actual notes and not have to make +arbitrary changes to suit Neuron. + +So I wrote a simple Python application +([neuron-zk-generator](https://github.com/thomasabishop/neuron-zk-generator)) +that copies the source notes into a specific directory and applies all necessary +formatting to this copy. This generator executes locally when I push to the +remote and creates a Neuron directory which is then built (via a GitHub Action) +and deployed via GitHub pages. + +![Neuron generator output](./img/running-neuron-generator.png) + +
+It applies some basic CSS changes and also generates dynamic content for the +home page, displaying the build ID, publication date, a page count and the most +recently updated files along with an index: + +![Screenshot of Neuron static site](./img/cropped-eolas-fe.png) diff --git a/posts/new-job.md b/posts/new-job.md new file mode 100644 index 0000000..6d8a372 --- /dev/null +++ b/posts/new-job.md @@ -0,0 +1,27 @@ +--- +title: "New job" +slug: /new-job/ +date: 2024-11-12 +tags: ["personal"] +--- + +After two years and a couple of months I am leaving the BBC for ITV. I enjoyed +my time at the BBC. I was fortunate to have a really supportive and personable +manager who was great to work with and who took a real interest in my +professional development. + +![](./img/bbc-leave-gruv-small.png) + +I worked with a variety of stakeholders on applications used internally within +the Corporation. I was able to work across the frontend and the backend in a +"full-stack" capacity, enhancing my understanding of AWS serverless, Docker, +GraphQL and other backend technologies. + +In my new role I will be working with a similar tech stack also on internal +solutions, however this will be purely backend which better suits my technical +interests. + +After the BBC, ITV is the second biggest broadcaster in the UK, so I think there +will be a lot of overlap. It will be interesting to see how the technical +culture differs and I am looking forward to working in a new environment on new +projects. diff --git a/posts/note-taking-routine.md b/posts/note-taking-routine.md new file mode 100644 index 0000000..3ed9eaa --- /dev/null +++ b/posts/note-taking-routine.md @@ -0,0 +1,127 @@ +--- +title: "Note-taking routine" +slug: /note-taking-routine/ +date: 2024-03-01 +tags: ["log", "productivity", "zettelkasten", "personal", "eolas"] +--- + +![Notes view](./img/full-note-view.png) + +I keep my notes in a repository called [Eólas](). Eólas (_awlus_) is an Irish +word meaning knowledge or information, especially knowledge gained by experience +or practice. + +I have designed it as a technical knowledge management system or "second-brain" +comprising notes from my autodidactic study of software engineering and computer +science. + +It is a [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten) work in +progress. It previously had a hierarchical structure made up of topic-based +subdirectories however I have recently converted this into a single flat +directory structure organised by tags. I'm in the process of partitioning longer +notes into smaller informational units. + +I use the [zk](https://github.com/zk-org/zk) CLI package to help with indexing +and task automation alongside its [zk-nvim](https://github.com/zk-org/zk-nvim) +Neovim wrapper (my main editor). I occassionally utilise +[Obsidian](https://obsidian.md/) alongside Neovim for when I want to view my +notes as a knowledge graph or read them alongside their rich content (images, +videos etc). + +![Viewing backlinks in Telescope](./img/zk-tags.png) + +## Commands + +On my local machine I have aliased several commands to help me manage the +knowledge base: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliasCommandOutput
zcd $HOME/repos/eolasAccess Zettelkasten
znzk new --title ...Create new entry from template
<leader> zk:ZkNotesAccess Zettelkasten from anywhere within nvim
<leader> zi:ZkIndexIndex Zettelkasten within nvim
<leader> zt:ZkTagsView tags via Telescope within nvim
<leader> ztt:ObsidianTagsView tags in a Vim buffer via within nvim using obsidian-nvim
<leader> zl:ZkLinksView links in current entry via Telescope within nvim, using obsidian-nvim
<leader> zb:ZkBacklinksView backlinks to current entry via Telescope within nvim, using obsidian-nvim
+ +## Frontmatter + +When I run the `zn` command this generates a new Zettelkasten entry with the +following frontmatter template: + +```yaml +--- +id: o8yzcrtv +title: test +tags: [] +created: Saturday, February 17, 2024 | 17:44 +--- +``` + +## Scripts + +The [scripts](https://github.com/thomasabishop/eolas/tree/master/scripts) +directory contains several Bash and Python scripts I use for general +housekeeping, such as formatting image URLs, removing unused assets, and +autosaving. + +## Autosave + +I use a +[bash script](https://github.com/thomasabishop/eolas/blob/master/scripts/auto_save.sh) +to create autosave functionality via Git. This script runs every 15 minutes via +a `cron` timer. It tidies up the directory (removes unused images, ensures all +file names use underscores rather than spaces and hyphens etc) and commits and +pushes to GitHub. + +## Usage + +I want to be able to access my notes instantly whatever I am working on so I +typically have them open, alongside my knowledge graph and GPT-4 client in a +secret [Hyprland](https://hyprland.org/) window overlay (pictured above). diff --git a/posts/recent-courses.md b/posts/recent-courses.md new file mode 100644 index 0000000..ee99985 --- /dev/null +++ b/posts/recent-courses.md @@ -0,0 +1,70 @@ +--- +title: "Recent courses" +slug: /recent-courses/ +date: 2023-05-03 +tags: ["log", "personal"] +--- + +Below are the recent courses I have completed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CourseProviderDate completed
GraphQL Essential TrainingLinkedIn Learning19-11-2022
Graph Developer AssociateApollo GraphQL25-11-2022
Learning AWS for DevelopersLinkedIn Learning03-12-2022
MySQL Essential TrainingLinkedIn Learning11-01-2023
NodeJS Essential TrainingLinkedIn Learning23-01-2023
Python Programming (3 Day)BBC Internal Training15-02-2023
Learning Bash ScriptingLinkedIn Learning16-03-2023
Git Intermediate TechniquesLinkedIn Learning06-04-2023
Learning AWS LambdaLinkedIn Learning21-04-2023
Essential Docker (3 Day)BBC Internal Training26-04-2023
diff --git a/posts/recommended-articles-integration.md b/posts/recommended-articles-integration.md new file mode 100644 index 0000000..3497225 --- /dev/null +++ b/posts/recommended-articles-integration.md @@ -0,0 +1,694 @@ +--- +title: "AWS Lambda: recommended articles" +slug: /recommended-articles-integration +date: 2023-07-07 +tags: ["log", "project", "aws", "productivity"] +--- + +I've just added a new feature to the blog: a page that lists links to articles +that I have found interesting. Here I will walk through the development process +as it is a good example of setting up a simple AWS Lambda and will be useful for +future reference. + +## Implementation + +![](./img/lambda-diagram-new.png) + +
+ +I use Pocket to manage my reading list. When I save an article that I want to +share to the blog I tag it with `website`. When querying the Pocket API I use +this tag to distinguish articles I wish to share from my other saves. I deploy a +Lambda function (written in TypeScript) that is accessible through an +[AWS API Gateway](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) +endpoint. This function simply sends a fetch request to the Pocket API and +returns the response via the Gateway endpoint. + +The Lambda is just a wrapper around the API request - I am not storing any data +in a database. Using a Lambda means that I can access my Pocket API credentials +securely on the backend and also sidestep the issue of Pocket not allowing CORS +requests from a frontend. + +## Backend + +I will use the AWS +[Single Application Model](https://aws.amazon.com/serverless/sam/) (SAM) for +development. This will allow me to develop and provision the Lambda locally and +then deploy my specifications as a template to AWS via the terminal. Once +deployed this template will be used by AWS CloudFormation to create a manage the +resources I have specified. This simplifies and programatises much of the work +involved in creating and running a serverless application on AWS, since you +don't have to fiddle too much with different AWS services in the AWS web +console. + +### Create Lambda template + +I use the SAM CLI to bootstrap a NodeJS Lambda written in TypeScript: + +```sh +sam init --runtime nodejs16.x +``` + +### Update the defaults + +This creates a basic _hello world_ template that I will adapt for my project. I +will also change the naming conventions and file structure as I dislike the +defaults. + +My directory stucture is as follows: + +``` +. +├── pocket-api-lambda/ +│ └── src/ +│ └── query-pocket/ +│ ├── index.ts +│ ├── package.json +│ ├── tsconfig.json +│ └── tests/ +└── template.yml + +``` + +### Create an IAM role for the Lambda function + +Next I will provision for AWS to create a dedicated IAM role for my function. +This is an executive role that will allow my function to access other services +on my behalf. At deploy time, AWS will create the role and assign it an Amazon +Resource Name (ARN). This is will uniquely identify the function's role within +AWS. + +Within the `Resources` object of the YAML template I add: + +```yml +QueryPocketFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - "sts:AssumeRole" + Policies: + - PolicyName: lambda-execution-policy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "*" +``` + +This gives the function basic execution rights. I also need to add the IAM role +to the `Outputs` object in the template: + +```yml +QueryPocketFunctionIamRole: + Description: "IAM Role created for Query Pocket function" + Value: !GetAtt QueryPocketFunctionRole.Arn +``` + +This is necessary for AWS to create a dedicated ARN for the function when the +application is deployed. + +### Storing API credentials in AWS Secret Manager + +In order to access the Pocket API you need to send HTTP POST requests to the +endpoint with the following credentials supplied in the body: + +```json +{ + "consumer_key": "[consumer_key_value_here]", + "access_token": "[access_token_value_here]" +} +``` + +Rather than hard code these credentials in my Lambda code, I will follow best +practice and store them in +[AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) which the Lambda +will retrieve at runtime. This adds an extra layer of security and encrypts the +credentials. + +First I add the two values to Secrets Manager: + +![](./img/secrets-manager-aws-console.png) + +Then I add a resource permission to the existing `Policies` array for the +`QueryPocketFunctionIamRole` to allow the Lambda to access the secret, +specifying the ARN of the secret just created: + +```yml +- PolicyName: secrets-manager-access + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_REF] +``` + +> I have put redacted private info by using square brackets: this isn't SAM +> syntax + +### Accessing credentials + +Now that the function is permitted to access the given secret. I will write a +function that will retrieve the credentials from Secrets Manager, which I can +invoke from the Lambda: + +```typescript +const getPocketCredentials = async (): Promise => { + const secretsManager = new AWS.SecretsManager() + const response = await secretsManager + .getSecretValue({ SecretId: process.env.SECRET_ARN as string }) + .promise() + const secretValues = JSON.parse(response.SecretString as string) + + if (secretValues) { + return { + accessToken: secretValues.POCKET_ACCESS_TOKEN, + consumerKey: secretValues.POCKET_CONSUMER_KEY, + } + } else { + throw new Error("Failed to return Pocket credentials") + } +} + +type PocketCredentials = { + accessToken: string + consumerKey: string +} +``` + +This function invokes the `SecretsManager` module of the `aws-sdk` to retrieve +the secret and then exposes each key for destructuring within the main body of +the Lambda function. In order to do so, it needs access to the ARN for the given +secret which I have set as an environment variable in my `template.yml`: + +```yml +... +Resources: + QueryPocketFunction: + ... + Environment: + Variables: + SECRET_ARN: + arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN] +``` + +This is great for production but when I am working locally, I don't want to keep +making requests to Secrets Manager as this is unnecessary and could become +costly. Instead I will utilise environment variables for the different +deployment context and reserve calls to Secrets Manager for when the Lambda is +executing in production. When working locally, I will source the Pocket +credentials from an env file. + +First I create this file at `env/local.env.json`: + +```json +{ + "QueryPocketFunction": { + "NODE_ENV": "development", + "POCKET_CREDENTIALS": "{\"POCKET_ACCESS_TOKEN\": \"[ACCESS_TOKEN_HERE]\",\"POCKET_CONSUMER_KEY\": \"[CONSUMER_KEY_VALUE_HERE]\"}" + } +} +``` + +This sets the Node runtime environment to `development` and ensures that the +`POCKET_CREDENTIALS` are sourced from this file rather than Secrets Manager. I +will add this file to the `.gitignore` so that it remains local. You'll notice +that even though this is JSON, I pass `POCKET_CREDENTIALS` as a string rather +than a nested object. This is because environment variables are a feature of the +operating system's shell and most OSs treat them as simple strings rather than +complex data structures. + +Next I will update the `Environment` field in the `template.yml` so that it sets +the Node runtime to `production` when it is deployed. I will also add an empty +variable for `POCKET_CREDENTIALS` so that it can be set with the Secrets Manager +values in the production context. + +```yml +... +Resources: + QueryPocketFunction: + ... + Environment: + Variables: + SECRET_ARN: + arn:aws:secretsmanager:eu-west-2:[MY_ACCOUNT_ID]:secret:[MY_SECRET_ARN] + NODE_ENV: production + POCKET_CREDENTIALS: "" +``` + +Now I need to update the earlier `getPocketCredentials` function to distinguish +between deployment environments and source the credentials from each location +depending on the runtime: + +```ts +const getPocketCredentials = async (): Promise => { + if (process.env.NODE_ENV === "production") { + const secretsManager = new AWS.SecretsManager() + const response = await secretsManager + .getSecretValue({ SecretId: process.env.SECRET_ARN as string }) + .promise() + const secretValues = JSON.parse(response.SecretString as string) + + if (secretValues) { + return { + accessToken: secretValues.POCKET_ACCESS_TOKEN, + consumerKey: secretValues.POCKET_CONSUMER_KEY, + } + } else { + throw new Error("Failed to return Pocket credentials") + } + } else { + const localCredentials = JSON.parse( + process.env.POCKET_CREDENTIALS as string + ) + if (localCredentials) { + return { + accessToken: localCredentials.POCKET_ACCESS_TOKEN, + consumerKey: localCredentials.POCKET_CONSUMER_KEY, + } + } else { + throw new Error("Failed to return Pocket credentials") + } + } +} +``` + +Now when I run `sam build && sam local start-api` it will run the production +version, sourcing the credentials from Secrets Manager. When I run the same +command but specify the local environment variables, it will bypass Secrets +Manager and get the credentials from the JSON file: + +```sh +sam build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json +``` + +This command is a bit unwieldy to paste everytime, so I will write a Makefile +that stores it: + +``` +.PHONY: clean build + +clean: + rm -rf .aws-sam + +build: + sam build + +start-local: + make build && sam local start-api --env-vars /home/thomas/repos/lambdas/pocket-api-lambda/env/local.env.json + +``` + +Now I can just run `make start-local` to get the local API endpoint up and +running. + +### Lambda handler function + +Finally I will write the actual handler function: + +```ts +export const handler = async (event: APIGatewayProxyEvent): Promise => { + let response: APIGatewayProxyResult + const endpoint = `https://getpocket.com/v3/get` + + try { + const { accessToken, consumerKey } = await getPocketCredentials() + const tag = event.queryStringParameters?.tag + const responseHeaders = { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + } + + const requestBody = { + consumer_key: consumerKey, + access_token: accessToken, + state: "all", + tag: tag, + } + + const options: RequestInit = { + method: "POST", + headers: { + "Content-Type": "application/json; charset=UTF8", + }, + body: JSON.stringify(requestBody), + } + + const request: Response = await fetch(endpoint, options) + const requestJson: any = await request.json() + + response = { + statusCode: 200, + headers: responseHeaders, + body: JSON.stringify({ + data: requestJson, + }), + } + } catch (err: unknown) { + console.error(err) + response = { + statusCode: 500, + header: responseHeaders + body: JSON.stringify({ + message: err instanceof Error ? err.message : "some error happened", + }), + } + } + + return response +} +``` + +I'm sending a POST request to the Pocket API endpoint. If successful, I return +the data as the response, else I return an error code. You'll notice I am +retrieving `queryStringParameter` from the `event` object. This means I can pass +in different tags later, in case I want to retrieve different articles. + +Locally, I would call the endpoint using the following URL: +`http://127.0.0.1:3000/query-pocket/get-articles-by-tag?tag=website` + +### The template file in full + +Now the configuration is complete and the Lambda is written let's take a look at +the final `template.yml`: + +```yml +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: > + pocket-api-lambda +Globals: + Function: + Timeout: 10 + MemorySize: 256 +Resources: + QueryPocketFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - "sts:AssumeRole" + Policies: + - PolicyName: lambda-execution-policy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "*" + - PolicyName: secrets-manager-access + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID] + QueryPocketFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/query-pocket/ + Role: !GetAtt QueryPocketFunctionRole.Arn + Handler: index.handler + Runtime: nodejs16.x + Architectures: + - x86_64 + Events: + QueryPocketApi: + Type: Api + Properties: + Path: /query-pocket/get-articles-by-tag + Method: get + Environment: + Variables: + SECRET_ARN: arn:aws:secretsmanager:eu-west-2:[ACCOUNT_ID]:secret:[SECRET_ID] + NODE_ENV: production + POCKET_CREDENTIALS: "" + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Sourcemap: true + EntryPoints: + - index.ts +Outputs: + QueryPocketApi: + Description: + "API Gateway endpoint URL for Prod stage for Query Pocket function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/query-pocket/get-articles-by-tag" + QueryPocketFunction: + Description: "Query Pocket Lambda Function ARN" + Value: !GetAtt QueryPocketFunction.Arn + QueryPocketFunctionIamRole: + Description: "IAM Role created for Query Pocket function" + Value: !GetAtt QueryPocketFunctionRole.Arn +``` + +To summarise, and cover the parts of the template I have not already explained: + +- I am creating provisions for three entities: + + - A Lambda function + - An API Gateway enpoint that will trigger the function + - An IAM role for the Lambda function + +- In addition I am setting permissions so that my Lambda function can access a + value stored in Secrets Manager, along with associated environment variables. + +- I have a single endpoint: `/query-pocket/get-articles-by-tag`. I have given it + it's own path rather than just using the root `/query-pocket` because I may + add additional endpoints in the future + +- My Lambda function is defined in `/query-pocket/index.ts` + +Now when I start the server locally and call the endpoint from Postman I can see +the data returned: + +![](./img/query-pocket-lambda-response-postman.png) + +### Deploying the function + +I start by validating my template to ensure that I do not introduce problems at +the outset of the deployment: + +``` +sam validate +/home/thomas/repos/lambdas/pocket-api-lambda/template.yaml is a valid SAM Template +``` + +Then I run a build: + +``` +sam build +``` + +Next I package the application. CloudFront can only receive one file as an +input. When we package the application we create this single file. The packaging +proces will first archive all of the project artefacts into a zip file and then +upload that to an [AWS S3](https://aws.amazon.com/s3/) bucket. A reference to +this S3 entity is then provided to CloudFormation and used to retrieve the +requisite artefacts. + +``` +sam package +--template-file template.yaml +--output-template-file pkg.yaml +--region eu-west-2 +``` + +This outputs the following, confirming the creation of the zipped resources: + +``` +Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xc8swn6xu4km +A different default S3 bucket can be set in samconfig.toml +Or by specifying --s3-bucket explicitly. +Uploading to 67c241b6f81524ee092adab3ba9998c5 50691677 / 50691677 (100.00%) + +Successfully packaged artifacts and wrote output template to file pkg.yaml. +Execute the following command to deploy the packaged template +sam deploy --template-file /home/thomas/repos/lambdas/pocket-api-lambda/pkg.yaml --stack-name +``` + +Now that we have our packaged application, the last step is to deploy it: + +``` +sam deploy --guided +``` + +This presents me with options for the deployment defaults. The most important +one is `Allow SAM CLI IAM role creation`. By affirming this, AWS will create the +`QueryPocketFunctionIamRole` and generate an ARN for it. + +This takes a few minutes but the CLI will keep you up to date with the +deployment process. Then, if successful, it will confirm the resources it has +created: + +![](./img/cloud-formation-cli-confirmation.png) + +If I go to CloudFormation in the AWS I will see that the `pocket-api-lambda` +stack now exists: + +![](./img/new-cloud-formation-application.png) + +And a new function is added to AWS Lambda with the API Gateway endpoint as the +trigger: + +![](./img/newly-created-pocket-lambda.png) + +And in API Gateway the API has also been set up: + +![](./img/api-gateway-pocket-api.png) + +## Frontend + +My frontend is built with the GatsbyJS library and so is React-based. + +### Environment variables + +Early I took to trouble to run both development and production versions of the +API. When I am working locally on my site I want to be querying the local +endpoint rather than the production version since this will accrue fees and also +requires me to deploy after every change. + +I will use an environment variable to distinguish the local and production +endpoints. I do this using the [dotenv](https://www.npmjs.com/package/dotenv) +Node package and create the following two files: + +```sh +# .env.production +GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=https://[HASH].execute-api.eu-west-2.amazonaws.com/Prod/query-pocket/get-articles-by-tag +``` + +```sh +# .env.development +GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=http://127.0.0.1:3000/query-pocket/get-articles-by-tag +``` + +By prepending the variable name with `GATSBY`, Gatsby will know to inject these +values at runtime. This way, and by adding my `.env` files to the `.gitignore`, +I can avoid hardcoding my endpoints and can use a single reference in my React +component. + +When working locally I can run `NODE_ENV=development npm run start` to source +the local endpoint variable and `NODE_ENV=production npm run start` to source +the production endpoint variable. In the deployed production context however, +`.env.production` will not be available because it is an ignored file. Thus to +source the variables when `npm run build` is run remotely, it is necessary to +store them as secrets that can be accessed in my CI/CD pipeline. I use a +[GitHub Action](https://systemsobscure.blog/how-I-deploy-this-site/) to build my +site and deploy it to AWS when I push changes, so I add the following line to my +`main.file` build script: + +```yml +steps: + - name: Set environment variables + run: | + echo "GATSBY_POCKET_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV + echo "GATSBY_METRICS_AWS_LAMBDA_ENDPOINT=${{ secrets.GATSBY_METRICS_AWS_LAMBDA_ENDPOINT }}" >> $GITHUB_ENV +``` + +### React component + +Below is the React component: + +```jsx +const ENDPOINT = process.env.GATSBY_POCKET_AWS_LAMBDA_ENDPOINT + +const ArticleListing = ({ article }) => { + return ( + + + + {article?.resolved_title} + + + {formatUnixTimestamp(article?.time_added)} + + ) +} + +export default function RecommendedArticlesPage() { + const [loading, setLoading] = useState(false) + const [data, setData] = useState({}) + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true) + const response = await axios.get(`${ENDPOINT}?tag=website`) + setData(response?.data?.data?.list) + } catch (error) { + console.error(error) + } finally { + setLoading(false) + } + } + fetchData() + }, []) + + const articles = Object.keys(data).map((key) => data[key]) + return ( +
+ +

+ Articles written by others that I have learned from or which present + interesting viewpoints. +

+ + + + + + + + + + {articles?.map((article) => ( + + ))} + +
TitleDate added
+
+ ) +} +``` + +Here I source my environment variable from the Node runtime context and use it +to query the API via the React `useEffect` hook that runs once on load. Then I +loop through the data to populate the rows of an HTML table. + +## Addendum: authentication + +I have set up a basic AWS Gateway API with two execution contexts (local and +production) that trigger a Lambda function. In closing I want to note that a +considerable oversight is the lack of authentication for the remote endpoint. In +its current state anyone with access to the production URL can call the API. + +In this context it's not that big of a deal since the information is not +sensitive or particularly interesting however if I was following best practice I +would need to provide a way for AWS to authenticate requests. I did attempt this +using an IAM role and an AWS Cognito user pool however I was unable to get this +working despite my best efforts. This is something I will return to in a future +post. diff --git a/posts/requiem-for-gruvbox-95.md b/posts/requiem-for-gruvbox-95.md new file mode 100644 index 0000000..d571eaf --- /dev/null +++ b/posts/requiem-for-gruvbox-95.md @@ -0,0 +1,23 @@ +--- +title: "Requiem for Gruvbox95" +slug: /requiem-for-gruvbox-95/ +date: 2024-07-31 +tags: ["projects", "gruvbox", "beige"] +--- + +![Gruvbox95 screenshot](./img/gruvbox95.png) + +Back in April I was working on a full system rice. It was going to be called +_Gruvbox95_ and would basically apply the aesthetics of Windows95/98 to the +Hyprland window manager but with the seductive beige of the Gruvbox light theme. + +Alas I was ultimately defeated by box-shadow and my own limitations but I came +across a screenshot when I was clearing out my phone. + +![Gruvbox95 detail: menubar](./img/toolbar-three.png) + +![Gruvbox95 detail: menubar](./img/toolbar-one.png) + +![App launcher](./img/file-launcher.png) + +![Pixel art icon intended for the launch button.](./img/tux-icon.png) diff --git a/posts/revision-bash-script.md b/posts/revision-bash-script.md new file mode 100644 index 0000000..12d4337 --- /dev/null +++ b/posts/revision-bash-script.md @@ -0,0 +1,38 @@ +--- +title: "Bash script: random revision topic" +slug: /revision-bash-script +date: 2022-12-28 +tags: ["log", "productivity", "bash"] +--- + +I keep all my study notes in Markdown format in a +[single repository]('https://github.com/thomasabishop/computer_science'). +Because I study a lot of varied topics it can sometimes seem that I am just +writing notes and forgetting about them. This is particularly true of topics +that I don't draw on in my everyday work. + +As a corrective I have written a script at the root of my notes repository that +selects a random revision topic from the categories I specify. When I start +studying each morning (I study from 6am-8am before work) I run the script and +spend ten minutes consolidating the topic it selects. Sometimes this will just +mean re-reading the notes but often I will rewrite or add ideas that I have +gleaned in the period since I originally studied the topic, extending my +understanding of the subject matter. + +```bash +#!/bin/bash + +# Choose source directories... +DIRS_TO_PARSE="../Computer_Architecture ../Electronics_and_Hardware ../Operating_Systems ../Programming_Languages/Shell ../Logic" + +# Return array of all files belonging to source dirs... +for ele in $DIRS_TO_PARSE; do +FILE_MATCHES+=( $(find $ele -name "\*.md" -type f) ) +done + +# Generate a random integer between 0 and the match array length... +RANDOM_FILE_INDEX=$(( $RANDOM % ${#FILE_MATCHES[@]} + 0 )) + +# Return file matching that index... +echo "Revise this topic: ${FILE_MATCHES[$RANDOM_FILE_INDEX]}" +``` diff --git a/posts/save-articles-lambda.md b/posts/save-articles-lambda.md new file mode 100644 index 0000000..22f95b6 --- /dev/null +++ b/posts/save-articles-lambda.md @@ -0,0 +1,27 @@ +--- +title: "AWS Lambda: save articles" +slug: /save-articles-lambda +date: 2023-09-07 +tags: ["log", "project", "productivity", "aws", "python"] +--- + +I've just finished a new +[AWS Lambda](https://github.com/thomasabishop/lambdas/tree/main/save-articles) +written in Python. It retrieves entries from my saved articles in Pocket, parses +the key metadata and saves it to a Google Sheet. + +I've done this because I find the Pocket app to be really uncongenial and +confusing to use. However I've used Pocket for a long time and the alternatives +are worse or have bad APIs. + +Now I keep my interactions with Pocket minimal. I use the Pocket browser +extension only to save and tag articles. My lambda function retrieves articles +by tag and saves them to a Google Sheet. This allows me to access the articles +from anywhere without using a database. + +Each tag has a corresponding sheet that lists the articles in reverse +chronological order. The lambda executes on a cron timer once a day. + +Here are my saved technical articles, for example: + +![](./img/saved_tech_articles.png) diff --git a/posts/self-hosting-1-initial-setup.md b/posts/self-hosting-1-initial-setup.md new file mode 100644 index 0000000..f436e5b --- /dev/null +++ b/posts/self-hosting-1-initial-setup.md @@ -0,0 +1,319 @@ +--- +title: "Self-hosting: initial setup" +slug: /self-hosting-1-initial-setup/ +date: 2025-02-23 +tags: ["projects", "self-hosting"] +--- + +With the way the Internet (and the world) is going, the benefits of self-hosting +the services you rely on is becoming increasingly apparent. + +I have been using AWS to run a limited set microservices on the Free Tier but +doing anything interesting with an actual server and database becomes expensive +quickly. While I hope to eventually run my own physical server, for now I have +opted to maintain a remote virtual private server. The skills I gain here will +obviously transfer to the physical context. + +I intend to run a series of third-party FOSS applications on different +subdomains via Docker containers. I will also be deploying my own software using +Docker. + +This post goes over the initial setup and configuration of the server. + +## Choosing a provider + +Jurisdiction, location and price were the main considerations. I want the server +to be based in a country with good privacy laws but not so far away that latency +would be noticeable. I also don't want to pay that much initially given that I +am just starting out. + +![The Hetzner Cloud Console](./img/hetzner-console.png) + +I opted for the German company, Hetzner. Being in the EU means their digital +privacy laws are better than the UK and Germany's recent history has created +more of a culture of resistance to surveillance than other EU states. + +I chose their CX22 Cloud package which provides the following for just under €4 +a month: + +- 2 virtual CPUs (Intel) +- 4 GB RAM +- 40 GB SSD +- IPv4 and free IPv6 + +This is modest but given that it's virtual, I can scale up later as necessary. + +![My server](./img/hetzner-server.png) + +## Creating a server + +Once you've paid you get access to the Hetzner Cloud Console and you just click +"Add server" and specify the location, hostname and OS. I opted for Nuremberg as +it's nearest and decided on Debian as my Linux distro. + +I set up a basic firewall using the Hetzner console, allowing incoming traffic +on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). This will apply at the Network +Layer. In a moment I'll explain how I also created a firewall at the Application +Layer, over SSH. + +## Connecting + +Once the server was spun up the obvious next step was to connect to it and start +configuring from the inside. To do this I needed SSH access. This was +straightforward, I just added my local machine's public key via the Hetzner +console and then connected with: + +```sh +ssh root@ +``` + +## Create non-root user + +Once I had access as root the first thing I did was update my packages: + +```sh +apt update +apt upgrade +``` + +Next I created a non-root user with `sudo` privileges. This is the account I +will use for administrating the server which is obviously safer than doing stuff +as root: + +```sh +apt install sudo +add user +usermod -aG sudo +``` + +I then switched to this user: + +```sh +su - +``` + +I'm connecting to the server over SSH but I also want to login as `my_username` +using SSH keys instead of a password as this is less secure. + +I set up the necessary SSH directory and permissions for `my_username`: + +```sh +mkdir /home//.ssh +chown : /home//.ssh +chmod 700 /home//.ssh +``` + +Then I transferred my local machine's public key, that I added to the server via +the Hetzner Console earlier, to the known hosts for `my_username`. + +```sh +cp /root/.ssh/authorized_keys /home//.ssh/ +chown : /home//.ssh/authorized_keys +chmod 600 /home//.ssh +``` + +To confirm, I closed the connection and then connected to the server again but +this time as `my_username`: + +```sh +ssh @ +``` + +Having confirmed SSH login I installed a lightweight version of vim +([vim-tiny](https://www.baeldung.com/linux/vim-tiny-properties)) to make +text-editing easier. + +```sh +sudo apt install vim-tiny +``` + +## Disable root login and access + +An important server-hardening routine is to prevent password logins as root. + +To do this I edited the root SSH config: + +```sh +sudo vim /etc/sshd_config +``` + +And added the following: + +``` +PasswordAuthentication no +``` + +Then I restarted the SSH daemon: + +``` +sudo systemctl restart sshd +``` + +Then when I attempted `ssh root@` I was met with "Permission +denied", as intended. + +As I side note, it's actually not possible for me to login as root at all, which +is best practice. I haven't set a root password, so if I attempt to login as +root when logged in as `my_username` (with `su -`), all attempts will fail. + +When I need to run processes as root, I will do this exclusively by assuming +`sudo` and to do this, I need to enter the password for `my_username`. + +## Firewall + +Earlier, I set up a firewall from outside of the server, using the Hetzner +console. I'm now going to do the same thing but from within the server as +`my_username`. It will share the same rules. This means I have a firewall at the +Network Layer (Hetzner) blocking requests before they reach the server and a +firewall operating at the Application Layer for those requests that get to the +server. + +This doesn't add much in terms of security, especially as the rules are +identical, but having a peripheral firewall on the Network and a firewall on the +server provides a degree of "defence in depth". It will also potentially reduce +the load on the server if blocked requests are deflected at the Network Layer +before reaching the server. + +It would be better to block by IP, and only open the SSH port to requests from +my local network (HTTP/HTTPS is obviously public and open to all). This isn't an +option because my my broadband package doesn't include a static IP. One way +around this would be to deploy another Hetzner server and set it up as a VPN. I +would then restrict port 22 to requests from that server's IP and connect to the +VPN server first. This might be a project further down the line. + +To set up the host firewall I used `ufw` (Uncomplicated Firewall): + +```sh +sudo apt install ufw +``` + +This is a wrapper for the venerable `iptables` that simplifies the process. + +I applied the same basic rules as earlier: + +```sh +sudo ufw default deny incoming +sudo ufw default allow outgoing +sudo ufw allow 22/tcp +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable +``` + +To confirm: + +```sh +sudo ufw status + +``` + +``` +To Action From +-- ------ ---- +22/tcp ALLOW Anywhere +80/tcp ALLOW Anywhere +443/tcp ALLOW Anywhere +22/tcp (v6) ALLOW Anywhere (v6) +80/tcp (v6) ALLOW Anywhere (v6) +443/tcp (v6) ALLOW Anywhere (v6) + +``` + +(This automatically applies the rules for IPv6 as well as IPv4.) + +## Fail2Ban + +As an additional security measure I installed `fail2ban`. This software is +designed to work alongside your firewall to detect and block brute-force login +attempts. You apply it to services running on specific ports at the Application +Layer. + +Debian does not come with the `rsyslog` package installed and `fail2ban` works +best with this logging program rather than `systemd` and its `journalctl` logs. +I tried to get it working with `systemd` but kept facing errors I couldn't +resolve, so I just installed `rsyslog` along with `fail2ban`: + +```sh +sudo apt install fail2ban +sudo apt install rsyslog + +sudo systemctl start rsyslog +sudo systemctl enable rsyslog +sudo systemctl start fail2ban +sudo systemctl enable fail2ban +``` + +I didn't change any of the `fail2ban` defaults. On Debian it runs on the `sshd` +server automatically and this is the only service I have running currently. + +I waited a while and then checked the status: + +```sh +sudo fail2ban-client status sshd +``` + +Which gave me: + +``` +Status for the jail: sshd +|- Filter +| |- Currently failed: 2 +| |- Total failed: 55 +| `- File list: /var/log/auth.log +`- Actions + |- Currently banned: 0 + |- Total banned: 6 + `- Banned IP list: + +``` + +The IP addresses of actors that make repeated failed login attempts are put in a +temporary "jail" by `fail2ban` which means they are blocked for a limited time +period. The data above tells me that there have been 55 total attempted failed +logins and this has resulted in 6 IPs being put in jail! However this is just +normal background noise in serverland and nothing to particularly worry about. + +When I take a look in `var/log/auth.log`, I see entries like: + +``` +Disconnected from authenticating user root 218.92.0.236 port 22272 [preauth] + +``` + +This indicates a malicious actor trying gain access as root but being blocked by +the earlier root protections. + +There are also entries like this: + +``` + Connection closed by invalid user postgres 195.211.190.228 port 47454 [preauth] + +``` + +Which shows a speculative attempt to try and connect to a non-existing +PostgreSQL service. + +These will typically be bots trying their luck at scale rather than actual +people. + +Alongside the malicious attempts I see my own legimate logins and invocations of +root as `sudo`: + +``` +pam_unix(sudo:session): session opened for user root(uid=0) by (uid=1000) +``` + +This was enough for one Sunday! In my next post I'll detail how I set up DNS and +SSL for the new server. + +## Resources + +In researching how to do the above, I created or expanded the following entries +in my Zettelkasten: + +- [Firewalls](https://thomasabishop.github.io/eolas/Firewalls) +- [UFW firewall management](https://thomasabishop.github.io/eolas/UFW_firewall_management) +- [Disable non-root SSH access](https://thomasabishop.github.io/eolas/Disable_non-root_ssh_access) +- [IP addresses](https://thomasabishop.github.io/eolas/IP_addresses) +- [Internet fundamentals](https://thomasabishop.github.io/eolas/Internet_fundamentals) +- [The Application Layer of the Internet Protocol](https://thomasabishop.github.io/eolas/Application_Layer_of_Internet_Protocol) diff --git a/posts/self-hosting-2-dns-tls.md b/posts/self-hosting-2-dns-tls.md new file mode 100644 index 0000000..967403f --- /dev/null +++ b/posts/self-hosting-2-dns-tls.md @@ -0,0 +1,118 @@ +--- +title: "Self-hosting: setting up DNS and TLS" +slug: /self-hosting-2-dns-tls/ +date: 2025-03-03 +tags: ["projects", "self-hosting"] +--- + +In my [previous post](./self-hosting-1-initial-setup.md) I described how I +purchased my VPS package, initialised a server and applied some basic security +safeguards. In this post I'll record how I set up DNS and the certification +necessary to enable encrypted client-server communication over HTTPS. + +## DNS + +As it stands, my server exists on the Internet but can only be reached via its +IPv4 address. This is sufficient to connect via SSH but eventually I want to +host HTTP services and for this, I'll need a domain name. + +I purchased the domain `systemsobscure.net` from Namecheap. I'm not mad about +using a US company but it is indeed cheap and has a better record when it comes +to privacy than its main competitor, Cloudflare. + +Once purchased, I needed to set up the A Records that associate my server's IP +address with the domain name: + +![Setting up A Records in Namecheap](./img/dns-records-detail.png) + +`@` sets the root domain. `*` is a wildcard that represents all subdomains at +the root level. This means I can dedicate specific services to different +subdomains on the same server, e.g _grafana.systemsobscure.net_, +_wallabag.systemsobscure.net_ etc. + +I've done this for both my IPv4 address and IPv6 addresses (pixelated). + +The DNS resolution propagated very quickly and now instead of using the IP, I +can SSH with the domain name, e.g: + +```sh +ssh my_username@systemsobscure.net +``` + +## TLS certification + +Next I needed to generate a TLS certificate for the domain. Certificates provide +a mechanism for independently verifying domain ownership. This serves as a +safeguard against impersonation and man-in-the-middle attacks. (Imagine if +someone was able to impersonate the web address of your online banking provider; +they could intercept and steal your account details.) + +Establishing trust through a certificate is a prerequisite for the initiation of +encrypted communication between client and server. Once the client has validated +the server's certificate, it can use the server's public cryptographic key +(supplied with the certificate) to create a shared cipher that will encrypt the +subsequent HTTP messages. + +The process essentially works as follows. A client will request a resource on my +server. The server will offer its TLS certificate which contains its public key. +The client will confirm the certificate is valid and issued by a reputable +Certificate Authority. It will then use the public key to send the server an +encrypted message. This will be decrypted by the server (using its private key) +and used to create the shared key that will encrypt communication between the +two hosts. + +All of this happens silently at the Transport Layer - a layer lower in the +network stack that the actual applications running over HTTPS (TLS stands for +_Transport Layer Security_). You only become aware of it when your browser +alerts you to the fact that a website has an invalid certificate or is using +unencrypted HTTP. + +Thankfully, this algorithmic complexity is not the immediate concern of the +server administrator! You can use a tool called `certbot` to generate the +certificate and prove your domain ownership in a matter of minutes. + +[certbot](https://certbot.eff.org/) is a free and open-source ACME client. ACME +stands for _Automatic Certificate Management Environment_. It generates the +initial certificate and automatically renews it after 90 days. + +I installed `certbot`: + +```sh +sudo apt install certbot +``` + +Then generated the certificate: + +```sh +sudo certbot certonly --stanalone -d systemsobscure.net +``` + +This command does quite a lot. It creates a temporary web server on port 80 and +then sends a request to [Let's Encrypt](https://letsencrypt.org/) (the body +responsible for generating the certificates), asking for a "challenge" to prove +domain ownership. + +Let's Encrypt will send a token to `certbot` and `certbot` will dutifully place +the token in a file that is served at a URL over port 80. Let's Encrypt will +then make an HTTP request to this URL ( e.g +`http://systemsobscure.net/.well-known/acme-challenge/the-token`) confirming +ownership and conferring the certificate. + +`certbot` saves the certificate (and several associated cryptographic tokens) to +the `/etc/letsencrypt/live` directory. + +![Let's Encrypt certificates](./img/certbot_confirm.png) + +As I currently don't have any services running on HTTP, the certificates won't +actually be used yet, but they are ready to go. In my next post I will finally +create my first self-hosted service which can now be served using my dedicated +domain name over HTTPS! + +## Resources + +In researching how to do the above, I created or expanded the following entries +in my Zettelkasten: + +- [Certificates and Certificate Authorities](https://thomasabishop.github.io/eolas/Certificate_authorities) +- [Let's Encrypt](https://thomasabishop.github.io/eolas/Let's_Encrypt) +- [HTTPS](https://thomasabishop.github.io/eolas/HTTPS) diff --git a/posts/self-hosting-3-reverse-proxy-grafana.md b/posts/self-hosting-3-reverse-proxy-grafana.md new file mode 100644 index 0000000..b421c15 --- /dev/null +++ b/posts/self-hosting-3-reverse-proxy-grafana.md @@ -0,0 +1,479 @@ +--- +title: "Self-hosting: setting up reverse proxy and first service" +slug: /self-hosting-3-reverse-proxy-grafana/ +date: 2025-03-12 +tags: ["projects", "self-hosting"] +--- + +In my [previous post](https://systemsobscure.blog/self-hosting-2-dns-tls/) in +the [series](link_to_tag) I explained how I configured the DNS settings for the +server and set up its TLS certificate. I am now in a position to start hosting +services. + +## Architecture + +![The basic client-server architecture I will be using with my reverse proxy.](./img/reverse-proxy.png) + +There are three core components to my self-hosting architecture: + +- a public subdomain +- a reverse proxy +- software running in Docker containers + +I will use this structure for the majority of my services. Let me explain each +part. + +### Subdomain + +This is the easiest bit to understand. A subdomain is just an address on a +larger server that groups together a set of related processes or resources. For +example, on my server, _service-a_ and _service-b_ would be accessible at the +following subdomains: + +- `service-a.systemsobscure.net` +- `service-b.systemsobscure.net` + +To access a given resource over the public Internet, it needs to be reachable +via a URL - subdomains provide this in a clear, hierarchical manner. + +Back when I set up the +[DNS records](https://systemsobscure.blog/self-hosting-2-dns-tls/) for the +server, I created an A Record with the wildcard character (`*`). This means that +I can create multiple subdomains and have them be served off of the main +`systemsobscure` domain. + +### Reverse proxy + +The subdomain is the public address of the resource or service. Clients will +send HTTP requests to this address and to handle them, I need a way to map the +public subdomain to the resource on the server that the client has requested. +This is the role of the reverse proxy. + +A reverse proxy serves as a buffer between the server and the incoming client +requests. It inspects the request and directs it to the port of the relevant +running process. + +Reverse proxies have other uses and advantages - they can be used as load +balancers and as a security measure to block certain traffic. However my use +will be mostly administrative - directing incoming requests to the right +services. + +### Docker + +[Docker](https://www.docker.com/) is fairly complex but I want to go into a bit +of detail because I think it is one of the few genuinely innovative technologies +of the last couple of decades. It's remarkable how simple it is to use and how +quickly you can provision complex resources with just a few lines of +configuration. + +Docker exploits a capacity native to the Linux kernel: containerisation. +Containers allow you to isolate running processes from their specific runtime +environment into self-contained virtual runtimes. + +All operating system processes ultimately share the same computational +resources: memory, disk-space, processor etc. This means that any one process +can monopolise those resources at the expense of the others. For example, you +might be running software that has hit a bug and as a result it freezes and your +mouse movement slows down. + +Prior to the advent of containerisation, server management mostly consisted in +balancing the competing resource needs of different processes. With containers, +you group a set of related processes into an isolated group and assign them a +specific amount of virtual memory, disk-space etc. This grouping is partitioned +from other processes on the native OS and the container is ignorant of the +specific machine it is running on. This means it is confined to its container +and cannot unduly affect other system processes. It can also be activated and +deactivated, as needed. + +You can provision software just as selectively as hardware, using specific +runtimes and dependencies as required. This practically eradicates the common +issue of conflicts arising between, say, the version of Python you have on your +local machine, and the version required by a third-party software. Moreover, as +containers are _portable_ - they can, in principle, be shared between machines - +you can share software as a container and be confident that it will run on any +machine that can leverage containerisation. + +This is where Docker comes in. It's a particular implementation of container +technology that is designed to simplify and standardise the creation and +exchange of containerised software. + +A Docker _image_ is a blueprint for creating a specific container. For example, +you might use a MySQL image to create a database within your application. The +image contains everything necessary to run the application: binaries, libraries, +resources, and additional dependencies. + +You can combine several images into a single container. In this scenario, you +might also include an OS image to manage the different components. These are +typically stripped-down versions of common Linux distributions. + +Docker images are defined in a declarative file (Dockerfile) that specifies the +software to be used, the directory within the container where it should execute, +and, usually, an initialisation command. + +To demonstrate, the Dockerfile below sets up a basic Python application using +the public `python:3.8` image. It transfers source files from a directory on the +local machine into to the container, installs dependencies and then starts the +application: + +``` +FROM python:3.8 +WORKDIR /app +COPY . /app +RUN pip install -r requirements.txt +CMD ["python", "./my_script.py"] +``` + +Docker maintains a [public registry](https://hub.docker.com/) of images that you +can download and use via the Docker CLI. + +Hopefully my intended architecture is starting to become apparent: I will run +software on my server using Docker images. Requests will arrive at a given +subdomain and the reverse proxy will channel them the port where the Docker +container is running. + +That's enough background, let's get started... + +## Grafana + +The first service I am going to host is +[Grafana](https://en.wikipedia.org/wiki/Grafana). This is a good software to +start with because it will allow me to easily access server logs and build +dashboards that display performance and capacity metrics about the server and +the services I'm running. + +I am going to host Grafana at the `grafana.systemsobscure.net` subdomain and, as +explained, I will run it as a Docker container. + +### Code management + +All of my services will live in a single monorepo on GitHub. This simplifies +deployment. I will configure the software in this repository on my local machine +and test it on a local server. Then, when I want to deploy , I'll simply push my +changes to the remote and pull them down to the production server over SSH. + +To manage each service I will use a Docker Compose file. This is just a more +elaborate Dockerfile that you use to manage multi-container Docker applications +that require more advanced functionality than a single container can provide. +For example, when you have multiple containers, they may need access to a shared +storage device and shared network in order to communicate. You define all this +in the Docker Compose then use a single command (`docker compose up`) to start +all the processes. + +The directory structure of the monorepo is as follows: + +``` +├── proxy +│   └── nginx +│   ├── conf.d +│   │   └── grafana.conf +│   └── docker-compose.yml +└── services + └── grafana + ├── docker-compose.yml + ├── prometheus + │   └── prometheus.yml + ├── promtail + │   └── promtail-config.yml + └── README.md +``` + +Each service will have its own subdirectory (for example `grafana/` ) containing +a Docker Compose file that configures the software. Certain images within the +Docker Compose (eg. `prometheus`, `promtail`) may require custom configuration +in addition to the Docker Compose file - this will be handled in a dedicated +config file, e.g. `prometheus/prometheus.yml`. + +### Building the Grafana Docker container + +I don't want to get too bogged down in the details of how I set Grafana up, as +my objective in this pose is to demonstrate the generic architecture. However a +few parts require clarification. + +Grafana itself is a sort of parent to different tracking and logging tools. It +provides an integrated interface for a wide variety of tools and you only +install the ones you wish to use. + +Each of these sub-services require their own Docker image in addition to the +Grafana Docker image. I will be using the following: + +- Loki + - To display server logs +- Promtail + - For collecting logs on the host server +- Prometheus + - Backend service that presents the Grafana frontend with data and which can + be queried +- Node Exporter + - A service that can be used by Prometheus to gather hardware and OS metrics + about the host environment + +In `/services/grafana/docker-compose.yml`, I create an entry for each service +(including Grafana itself) specifying the image I want to use: + +```yml +services: + prometheus: + image: prom/prometheus:latest + + node-exporter: + image: prom/node-exporter:latest + + loki: + image: grafana/loki:latest + + promtail: + image: grafana/promtail:latest + + grafana: + image: grafana/grafana:latest + container_name: grafana +``` + +Certain containers are going to require their own Docker volumes. A Docker +volume is a form of persistent virtual storage within the container for storing +application data. This will be necessary for Prometheus, Loki, and Grafana so I +add the following to the Docker Compose: + +```yml +volumes: + prometheus_data: + loki_data: + grafana_data: +``` + +This establishes the volumes, then for each image I specify a `volumes` field +that maps the volume to the requisite directory within the container: + +```yml +grafana: + image: grafana/grafana:latestki + container_name: grafana + volumes: + - grafana_data:/var/lib/grafana +``` + +The Grafana application will look for its data at `/var/lib/grafana` at runtime, +hence the volume is mapped to this location. + +In addition to volumes, I need to provision Docker networks. I need a network +because the individual applications will need to be able to communicate with +each other and the `grafana` container - which is the parent of the other +applications - will need to communicate with processes outside of itself. +Namely, the reverse proxy that will connect client requests to the Grafana +service. + +The following section of the Compose file specifies two networks for this +purpose: `default` which runs internally within the the container, and `web` +which is external to the container which `grafana` and other separate Docker +containers will be able to hook into: + +```yml +networks: + default: + web: + external: true +``` + +Then in my `grafana` declaration, I specify its membership of these networks: + +```yml +grafana: + image: grafana/grafana:latestki + container_name: grafana + ports: + - "3000:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} + volumes: + - grafana_data:/var/lib/grafana + networks: + - default + - web +``` + +You'll notice I also include an environment variable for the password. This will +be injected automatically via the presence of a `.env` file on the server +containing the password. + +Additionally I specify port `3000` for Grafana to run on. This will be important +later when I connect it to the reverse proxy. + +### Deploying the container + +I'm now ready to pull the changes from the server and test out the container in +the live environment. + +Once I've SSH'd into the server I install Docker: + +```sh +sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +``` + +I clone my monorepo on the server and then `cd` into the `/services/grafana` +directory and run the Docker Compose start script: + +```sh +docker compose up -d +``` + +The containers comprising the Docker Compose start fine: + +![](./img/docker-compose-up.png) + +I also confirm that the two networks associated with Grafana (`web` and +`default`) are running as expected: + +![](./img/docker-net-ls.png) + +## nginx + +So far I have succeeded in getting my `grafana` container up and running on port +3000 but I have no way yet of accessing its frontend. To do this I need to set +up and configure the reverse proxy. + +I'll use [nginx](https://nginx.org/en/) for this and I'll also run this as a +Docker service, configured in the `proxy/` directory of the monorepo that I +demonstrated earlier. + +As with Grafana, everything necessary is detailed in the Docker Compose: + +```yml +# proxy/nginx/docker-compose.yml + +services: + nginx: + image: nginx:latest + ports: + - "80:80" + - "443:443" + volumes: + - ./conf.d:/etc/nginx/conf.d + - /etc/letsencrypt:/etc/letsencrypt:ro + restart: unless-stopped + networks: + - web + +networks: + web: + external: true +``` + +Key points to note: + +- `nginx` will have access to the shared `web` Docker network that we saw + earlier when configuring Grafana. This will enable it to communicate with + other Docker services. +- I pass through the actual location of my TLS certificates on the server + (`/etc/letsencrypt`) to the nginx container, specifying that they are read + only. This is a bit different to Grafana where we were mapping virtual Docker + volumes to locations within the container. +- I map both the server's HTTP (`80`) and HTTPS (`443`) ports through to the + equivalent ports in the `nginx` container. + +For each Docker service that `nginx` will proxy, I need to provide a +configuration file within the `nginx/conf.d` directory. (These too are copied +into the container.) + +The core process is quite simple. You specify: + +- The port you want `nginx` to listen to +- The specific subdomain being requested on that port +- The location of the running server process to which you want to direct the + incoming request + +To demonstrate with the Grafana config file: + +``` +# proxy/nginx/conf.d/grafana.conf + +server { + listen 443 ssl; + server_name grafana.systemsobscure.net; + + location / { + proxy_pass http://grafana:3000; + } +} +``` + +This is the basic idea. In fact it's a bit more complicated because we need to +enforce HTTPS and make it such that `grafana.systemsobscure.net` is only served +over HTTPS. + +To do this I specify the location of my TLS certificate and set the necessary +response header: + +``` +# proxy/nginx/conf.d/grafana.conf + +server { + listen 443 ssl; + server_name grafana.systemsobscure.net; + + ssl_certificate /etc/letsencrypt/live/systemsobscure.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/systemsobscure.net/privkey.pem; + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + ... +} +``` + +This is fine so long as people always use `https://grafana.systemsobscure.net` +but of course they won't always do this, so we need to redirect any requests to +the HTTP port (80) to the HTTPS service. This is achieved with a simple 301 +redirect in the same file: + +``` +server { + listen 80; + server_name grafana.systemsobscure.net; + location / { + return 301 https://$host$request_uri; + } +} +``` + +Now any requests to `grafana.systemsobscure.net` will automatically be served at +`https://grafana.systemsobscure.net`. + +The final step is to start the `nginx` container: + +```sh +cd proxy/nginx +docker compose up -d +``` + +## Results + +With the Grafana container running and the reverse-proxy configured we can +finally navigate to `https://grafana.systemsobscure.net` and check out the live +service! + +![](./img/grafana-login-screen.png) + +Once I've logged in, I use a Grafana template code to create a default template +that outputs the system metrics provided by the Node Exporter service: + +![](./img/grafana-server-metrics-dashboard.png) + +I can also go to Loki and look at the server connection logs and track the +activity of the `fail2ban` software I installed in the last post: + +![](./img/grafana-varlogs-loki.png) + +Later, when I have additional services, I'll add additional dashboards and Loki +queries so I can analyse everything that is going on. + +## Resources + +In researching how to do the above, I created or expanded the following entries +in my Zettelkasten: + +- [Proxies](https://thomasabishop.github.io/eolas/Proxies) +- [Containerization](https://thomasabishop.github.io/eolas/Containerization) +- [Docker images](https://thomasabishop.github.io/eolas/Docker_images) +- [Creating a Docker image](https://thomasabishop.github.io/eolas/Creating_a_Docker_image) +- [Docker containers](https://thomasabishop.github.io/eolas/Docker_containers) +- [Docker storage](https://thomasabishop.github.io/eolas/Docker_storage) +- [Docker Compose and Dockerfile difference](https://thomasabishop.github.io/eolas/Docker_compose_and_Dockerfile_difference) diff --git a/posts/slack-notification-center.md b/posts/slack-notification-center.md new file mode 100644 index 0000000..bfb3f58 --- /dev/null +++ b/posts/slack-notification-center.md @@ -0,0 +1,114 @@ +--- +title: "Using Slack as a desktop notification center" +slug: /slack-notification-center/ +date: 2024-04-10 +tags: ["log", "project", "productivity", "javascript"] +--- + +My current desktop environment is very bare-bones and consists basically of a +window manager and launcher. One difficulty of this is having to manage +notifications manually. + +Triggering notifications is fine, I use +[swaync](https://github.com/ErikReider/SwayNotificationCenter). The problem is +logging them in one place so I can see at a glance what has been going on over a +given time period. There are solutions for the Wayland compositor that I use but +the list of notifications won't persist beyond the current session. They are +also quite ugly with limited options for style customisation. + +When I talk about notifcations, I mainly mean alerts triggered by the automated +scripts I have running on my system: backups, auto-commits to my Zettelkasten, +time-tracking etc. I don't care about email notifications, Spotify updates etc +and suppress these. + +I thought a good way to solve this would be to forgo a desktop-specific +notification center and instead use Slack. Each notification type has a +dedicated channel which I can post to via a specific webhook URL. On the +free-plan the data will persist for up to three months which is more than +sufficient for my purposes. + +This solution has the added benefit that I can access my notification center +from other devices. I can also display notifications from more than one machine +and everything will be syndicated in one place. Additionally, I can leverage +Slack's extensions like the GitHub bot. Finally, using Slack's block +architecture means that I have lots of options when it comes to outputting the +notification contents. + +The image below shows the notification center with the channel for this blog +displayed: + +![](./img/slack-notification-center.png) + +(The channel is called "Ripley" because I am an Alien fan! "processes" is the +parent app to which the different webhooks belong) + +I invoke the NodeJS script below when I want to set up a notification: + +```js +#!/usr/bin/env node + +const process = require("process") +const { exec } = require("child_process") +const axios = require("axios") + +const notificationSound = + "mpv /home/thomas/dotfiles/gruvbox-95/sounds/st-notification.mp3" + +const slackNotifier = async (channel, { message = "", block = null } = {}) => { + try { + const webhooks = { + test: process.env.SLACK_WEBHOOK_TEST, + backups: process.env.SLACK_WEBHOOK_BACKUPS, + eolas: process.env.SLACK_WEBHOOK_EOLAS, + systems_obscure: process.env.SLACK_WEBHOOK_SYSTEMS_OBSCURE, + time_tracking: process.env.SLACK_WEBHOOK_TIME_TRACKING, + } + + const webhookUrl = webhooks[channel] + + let payload + + if (message) { + payload = { text: message, channel: channel } + } else if (block) { + payload = { blocks: block, channel: channel } + } else { + throw new Error("Either a message or a block must be provided.") + } + + const response = await axios.post(webhookUrl, payload) + + if (response.status === 200) { + exec(notificationSound) + console.log(`Message successfully sent to ${channel}`) + } else { + console.error( + `Slack API returned non-200 response: ${response.status}`, + response.data + ) + } + } catch (error) { + console.error(`Failed to send message to ${channel}:`, error) + } +} + +if (require.main === module) { + const [, , channel, message] = process.argv + slackNotifier(channel, { message }).catch(console.error) +} + +module.exports = { slackNotifier } +``` + +An example of it being invoked (from within a script that manages my hourly +backups): + +```sh +./slack-notifier/src/index.js 'backups' '💾 Error: backup drive not mounted.' + +``` + +One drawback is that there is sometimes latency between the execution of the +script which contains the notificaton trigger and the appearance of the +notification in Slack. This is because the former happens instantly on my +machine and the latter depends on the network. I can live with this though. diff --git a/posts/some-nice-typescript.md b/posts/some-nice-typescript.md new file mode 100644 index 0000000..6ca3d69 --- /dev/null +++ b/posts/some-nice-typescript.md @@ -0,0 +1,103 @@ +--- +title: Some nice TypeScript +slug: /some-nice-typescript/ +date: 2023-09-26 +tags: ["log", "typescript"] +--- + +I wanted to share a snippet of a TypeScript function I wrote recently. Although +it's not a particularly interesting scenario, I like it because it is simple and +elegant, if I say so myself. + +At the moment I am working on an AWS Lambda that queries the API of the Toggl +time-tracking service to retrieve a list of time entries for a given date range. +The API returns an array of time entries with the following structure: + +```json +{ + "id": 3136598124, + "workspace_id": 2360906, + "project_id": 193325937, + "task_id": null, + "billable": false, + "start": "2023-09-21T20:10:02+00:00", + "stop": "2023-09-21T20:39:54Z", + "duration": 1792, + "description": "pytest blog post", + "tags": [], + "tag_ids": [], + "duronly": true, + "at": "2023-09-21T20:39:54+00:00", + "server_deleted_at": null, + "user_id": 3700888, + "uid": 3700888, + "wid": 2360906, + "pid": 193325937 +} +``` + +Notice that the `project_id` is a number rather than the human-readable string +that appears in the Toggl UI. When I return the data to the frontend I want to +have the name of the project rather than this number. So I wrote the following +transformer: + +```ts +interface IProject { + id: number + name: string + [key: string]: unknown +} + +type TProjectMap = Record + +const getProjects = async (): Promise => { + const workspace = process.env.TOGGL_WORKSPACE_ID + const togglClient = new TogglClient() + const projects: IProject[] = await togglClient.get( + `workspaces/${workspace}/projects` + ) + return parseProjects(projects) +} + +const parseProjects = (projects: IProject[]): TProjectMap => { + return projects.reduce((projectMap: TProjectMap, project: IProject) => { + projectMap[project.id] = project.name + return projectMap + }, {}) +} +``` + +`getProjects` obviously calls the Toggl API to retrieve the list of projects for +the workspace. The `parseProjects` function takes the array of projects and +returns an object with the project IDs as keys and the project names as values. +In my main controller function, I will then be able to transform the project IDs +like so: + +```ts +const projects = await getProjects() +const projectName = projects[193325937] +console.log(projectName) +// "Practical study" +``` + +What I like: + +- The time-entry returns several properties that I am not interested in - I only + want `id` and `name`. I could manually type the other properties to ensure + full type-safety but life is short and I can't be bothered. One get-out would + be to use `any` for the other properties but this is obviously self-defeating + in TS. Instead I use `unknown` and then type-assert the return value of + `parseProjects` to `TProjectMap` which is a record of `number` keys and + `string` values. This means that if I try to access a property of the + `projectMap` object that is not a number, I will get a type error with the + added benefit that I make it transparent to the reader that I am solely + interested in the `id` and `name` keys. + +- Rather than using a `forEach` loop or a `map` function combined with a + `filter`, I make good use of `reduce`, _reduce_ being a very semantic account + of what I am seeking to do. + +More generally, I feel like I have purposefully harnessed the explanatory value +of the type system to make the code more readable and self-documenting. This is +a good example of where TypeScript can reduce the need for boilerplate and +comments, in addition to the obvious benefits of type-safety. diff --git a/posts/suppressing-logs-errors-jest.md b/posts/suppressing-logs-errors-jest.md new file mode 100644 index 0000000..6a2f323 --- /dev/null +++ b/posts/suppressing-logs-errors-jest.md @@ -0,0 +1,61 @@ +--- +title: "Suppressing logs and errors in Jest" +slug: /suppressing-logs-errors-jest/ +date: 2024-09-10 +tags: ["javascript", "unit-testing"] +--- + +It annoys me when I am working on a project and a previous developer has left +logs and/or thrown errors in their unit tests. To be clear: I mean when the +developer is testing that an error is thrown in the right circumstances, not a +test failure arising from regression. The former makes the latter harder to +detect by polluting the output. + +This can be so easily prevented. + +Take the following function: + +```js +function sillyFunction(int) { + console.info(`Now handling ${int}`) + if (int > 2) throw new Error(`Error: int ${int} is greater than two`) + else return +} +``` + +To avoid pointlessly logging to the console and to confirm the error without +actually throwing it: + +```js +import { jest } from "@jest/globals" + +describe("sillyFunction", () => { + beforeEach(() => { + jest.spyOn(console, "info").mockImplementation(() => {}) + }) + + afterEach(() => { + console.info.mockRestore() + }) + + it("throws error if `int` is less than 2", () => { + expect(() => { + sillyFunction(3) + }).toThrow("Error: int 3 is greater than two") + }) +}) +``` + +The `spyOn` method silences the output of `console.error` by returning nothing. +If we wish, we can still confirm that the log is acting as expected during the +runtime of the test by again using a spy: + +```js +const consoleInfoSpy = jest.spyOn(console, "info") +expect(consoleInfoSpy).toHaveBeenCalledWith("Now handling x") +``` + +The `toThrow` method catches the error before it hits the console and allows us +to interrogate it. If it was an asynchronous function under test, we would need +to use the matcher `rejects.toThrow`. This waits for the promise to resolve +before checking if it has been rejected. diff --git a/posts/the-world-knew-him-not.md b/posts/the-world-knew-him-not.md new file mode 100644 index 0000000..844a7fd --- /dev/null +++ b/posts/the-world-knew-him-not.md @@ -0,0 +1,44 @@ +--- +title: "And the world knew him not (lossless compression)" +slug: /the-world-knew-him-not/ +date: 2023-02-16 +tags: ["random", "reading"] +--- + +Last night I was reading 'Three Versions of Judas' from Juan Luis Borges' +masterpiece _Fictions_ (1944). The structure is typical Borges: a cool precis of +a series of fabricated texts, replete with tokens of mock facticity (publication +dates, references to other commentaries, footnotes on errata and so forth). + +The story describes the work of an obscure theologian who developed a +revisionist account of the role of Judas in the Passion, broadly in keeping with +the (real) Gnostic +[Gospel of Judas](https://en.wikipedia.org/wiki/Gospel_of_Judas). Far from being +the perennial embodiment of self-interest and betrayal, Judas is reimagined as +an agent of the Holy Spirit. He becomes the essential catalyst in the +fullfilment of Christ's prophesy and thus the redemption of humanity. + +The argument is compelling and centers on why it would be necessary to _betray_ +Jesus in the first place. His activities were well known to both the Jewish and +Roman authorities. If the same Gospels that cast Judas as the betrayer are to be +believed, Jesus was constantly performing miracles, challenging social and +religious hierarchies and being a major-league nuisance. They could've come for +him at any point. + +According to the scholar, Judas was the most significant disciple because he had +a presentiment of the Crucifixion and its cosmic significance, and was compelled +to "betray" Christ and shoulder eternal infamy out of this knowledge. His +suicide by hanging was a pale mirroring of Christ's own death. Hence the kiss +(an act of love) and discarding the silver at the temple (a repudiation of a +simplistic reading of the betrayal). Hence also, Jesus's to Judas at the Last +Supper: "what you are going to do, do quickly". + +Anyway, this elaborate preamble has been merely to draw attention to a passage +in John that Borges quotes. Maybe it is just that I am a lapsed Catholic but I +think it is very beautiful and profound: a lossless compression of the entirety +of Christianity to a single sentence: + +> He was in the world, and the world was made by him, and the world knew him +> not. + +— John 1: 10, King James Version diff --git a/posts/two-strategies-for-handling-exceptions-in-python.md b/posts/two-strategies-for-handling-exceptions-in-python.md new file mode 100644 index 0000000..23cd3c0 --- /dev/null +++ b/posts/two-strategies-for-handling-exceptions-in-python.md @@ -0,0 +1,88 @@ +--- +title: "Two strategies for handling exceptions in Python" +slug: /two-strategies-for-handling-exceptions-in-python/ +date: 2023-10-03 +tags: ["learning", "python"] +--- + +The following function illustrates two strategies for handling exceptions in +Python: + +```py +def parse_articles(articles: Dict[str, Any]) -> List[List]: + + if not articles: + raise ValueError("No articles to parse") + + articles_list = [] + + for article in articles.values(): + try: + time_added, given_title, resolved_url = ( + article["time_added"], + article["given_title"], + article["resolved_url"], + ) + except KeyError as e: + logging.warning( + f"Article missing {e} property. Skipping article: {given_title}." + ) + continue + + articles_list.append([time_added, given_title, resolved_url]) + + return articles_list +``` + +The function is a basic transformer: it receives a dictionary and loops through +a subset of its properties which are also dictionaries, returning selected keys +from each as a multidimensional array. + +The first exception handler checks whether the `articles` dictionary is +populated or `None`. If either are the case, the function exits and returns a +`ValueError`. This approach _propagates_ the error and is deliberately and +transparently obstructive: the function exits before it can execute the main +code. If this exception is not adequately handled by the caller, it will cause a +runtime error. + +The second instance of exception handling occurs in the loop. If any of the +three specified properties (`time_added`, `given_title`, `resolved_url`) are +absent from the `articles` dictionary, a `KeyError` exception will be thrown. +The outcome is quite different from the first exception however. The function +will not exit immediately, it simply won't append this set of properties to the +list, and will move on to the next iteration of the loop. + +This behaviour, known as _graceful degradation_ is afforded by the _try, except_ +scaffolding. It literally allows us to `try` an execution before an exception +can be raised. And if an exception is raised, we can augment the code with a +`finally` block to return something regardless of the exception, such as an +empty list. + +In the scenario above, `finally` isn't used since the `continue` ensures that +the execution proceeds to the next iteration of the loop. In addition, even if +none of the `articles` contain the requisite keys, an empty list will be still +be returned. + +Both strategies, propagation and graceful degradation, have their merits. + +Propagation is useful because it identifies the error immediately and blocks the +code execution, meaning that failures do not occur silently or go unchecked, +forcing the caller to implement a handling routine. This is particulaly useful +for genuine exceptions - cases that _should not_ occur in regular execution. If +they occur, they are so severe that they should be addressed immediately, not +deferred. One such scenario might be a memory-intensive script or lambda running +in a severless cloud provider: you would not want it to waste compute time on a +process that is not going to return the expected value. + +The obvious benefit of graceful degradation is that it doesn't halt the program +straight away. This is most suitable for cases where the exception in question +is fairly common. In the context of the code example this would be not returning +articles that lack the requisite keys. Providing most of the articles are +parsable, the fact that some are skipped is unlikely to be a terminal fault in +the application. + +Finally, the two methods differ in the amount of work required of the developer. +Propagation requires explicit error handling from the function or method that +receives the error. In contrast, graceful degradation allows for silent +failure - the error is not immediately apparent and may only be detected in logs +at a later stage. diff --git a/posts/waybar-customisation.md b/posts/waybar-customisation.md new file mode 100644 index 0000000..19f450c --- /dev/null +++ b/posts/waybar-customisation.md @@ -0,0 +1,197 @@ +--- +title: "Waybar customisation" +slug: /waybar-customisation/ +date: 2024-03-03 +tags: ["log", "productivity", "python", "linux"] +--- + +On my local machine I am currently replacing my desktop environment (Gnome) with +a tile-based window manager ([Hyprland](https://github.com/hyprwm/Hyprland)). +I'm trying to strip out the bloat and have a lightweight and highly customised +Arch workhorse. + +A key component is a useful status bar that displays metrics and provides a +means of quickly executing common tasks. Previously, when I was using X, I opted +for Polybar but now that I am on Wayland I'm using +[waybar](https://github.com/Alexays/Waybar). So far, I've added two custom +modules: one for displaying code metrics and another for managing my +time-tracking. + +![](./img/waybar-full.png) + +
+Full status bar. Click to enlarge. +
+ +## Code metrics module + +I use WakaTime to collate metrics on my coding activity (time coding, +programming languages, projects etc.) I already have a dashboard on this site +that displays some of this data but I thought it would be useful to see my stats +from the status bar as I am working. + +Below is an image of the module. The permanent display outputs the coding +duration for the current day. When you hover you see a summary of the main +languages used and the main projects I've been working on. + +![](./img/wakatime-waybar.jpg) + +It's a straightforward Python script that queries the WakaTime API and parses +the data and forwards it to the module: + +```py +#! /usr/local/bin/python3 +import requests +import os +import json +import textwrap + +WAKATIME_API_KEY = os.getenv("WAKATIME_API_KEY") +WAKATIME_ENDPOINT = "https://wakatime.com/api/v1/users/current/status_bar/today" + + +def get_data(url): + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + raise Exception( + f"Failed to fetch data from API. Status code: {response.status_code}" + ) + + +def generate_tooltip(time, languages, projects): + return textwrap.dedent( + f"""\ + Time coding: {time} + Languages: {languages} + Projects: {projects}""" + ) + + +def format_metric(metrics): + return ", ".join( + [f'{metric["name"]} ({metric["percent"]}%)' for metric in metrics[:3]] + ) + + +def main(): + output = {} + try: + data = get_data(WAKATIME_ENDPOINT + "?api_key=" + WAKATIME_API_KEY) + digital_time = data["data"]["grand_total"]["digital"] + human_time = data["data"]["grand_total"]["text"] + langs = data["data"]["languages"] + projects = data["data"]["projects"] + tooltip = generate_tooltip( + human_time, format_metric(langs), format_metric(projects) + ) + output["text"] = digital_time + output["tooltip"] = tooltip + + except Exception as e: + output["text"] = "Error" + + print(json.dumps(output)) + + +if __name__ == "__main__": + main() +``` + +Here's the module declaration in the Waybar config: + +``` +"custom/wakatime": { + "exec": "source $HOME/dotfiles/.env && python3 $HOME/.config/waybar/resources/custom_modules/wakatime_waybar.py", + "format": "󰅱 {}", + "return-type": "json", + "interval": 600 + }, +``` + +## Time-tracking module + +I've recently started using [time warrior](https://timewarrior.net/) to track my +extra-curricular coding and study. This is a command-line time-tracker so it +integrates really well with Waybar. + +The module highlights green when there is an active timer. This helps to remind +me to stop the timer! When you click the current timer stops and when you +right-click it resumes. This saves me going into the terminal when I stopping +and starting an ongoing piece of work. + +![](./img/timer-module.png) + +
+Time warrior module in its active state. +
+ +Again I've used Python for the scripting: + +```py +#! /usr/local/bin/python3 + +import subprocess +import json + + +def invoke_shell(proc): + try: + result = subprocess.run( + proc, + shell=True, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + return e.stderr.strip() + + +def timer_active() -> bool: + status = invoke_shell("timew get dom.active") + if status == "1": + return True + else: + return False + + +def generate_tooltip(): + tooltip = invoke_shell("timew summary :week") + return tooltip + + +def main(): + output = {} + try: + if timer_active(): + output["text"] = "󱫐 Timer" + output["class"] = "active" + else: + output["text"] = "󱫦 Timer" + output["class"] = "inactive" + except Exception as e: + output["text"] = "Error" + + print(json.dumps(output)) + + +if __name__ == "__main__": + main() +``` + +And here is the module declaration in the Waybar config: + +``` +"custom/timewarrior": { + "exec": "python3 $HOME/.config/waybar/resources/custom_modules/time_warrior_waybar.py", + "format": "{}", + "on-click": "timew stop && notify-send 'Time Warrior' 'Timer stopped'", + "on-click-right": "timew continue && notify-send 'Time Warrior' 'Timer resumed'", + "return-type": "json", + "interval": 5 + }, +``` diff --git a/public/blog-screenshot.png b/public/blog-screenshot.png new file mode 100644 index 0000000..61e2831 Binary files /dev/null and b/public/blog-screenshot.png differ diff --git a/scripts/generate-post-index.js b/scripts/generate-post-index.js new file mode 100644 index 0000000..7f0199a --- /dev/null +++ b/scripts/generate-post-index.js @@ -0,0 +1,71 @@ +import fs from "fs" +import matter from "gray-matter" +import { marked } from "marked" +import { createHighlighter } from "shiki" +import { transformerColorizedBrackets } from "@shikijs/colorized-brackets" +import { processBlogImages } from "./process-blog-imgs.js" +import { transformImagePaths } from "./transform-img-paths.js" + +const renderer = { + image({ href, text }) { + return `
${text}
` + }, +} + +const highlighter = await createHighlighter({ + themes: ["github-dark-dimmed"], + langs: [ + "javascript", + "typescript", + "jsx", + "tsx", + "html", + "css", + "json", + "markdown", + "bash", + "python", + "yaml", + ], +}) + +// Copy blog images to /public and compress +processBlogImages() + +const files = fs.readdirSync("posts").filter((x) => x.endsWith(".md")) + +const posts = files.map((file) => { + const raw = fs.readFileSync(`posts/${file}`, "utf8") + const { data, content: markdown } = matter(raw) + + // Convert img urls to source from /public whilst markdown + const transformedMarkdown = transformImagePaths(markdown) + + // Appy syntax highlighting + let html = transformedMarkdown.replace( + /```(\w+)?\n([\s\S]*?)```/g, + (match, lang, code) => { + return highlighter.codeToHtml(code.trim(), { + lang: lang || "text", + theme: "github-dark-dimmed", + transformers: [transformerColorizedBrackets()], + }) + } + ) + + // Convert img tags to figure tags + marked.use({ renderer }) + + console.info(`Processing ${file}...`) + + return { + slug: file.replace(".md", ""), + title: data.title, + date: data.date, + tags: data.tags, + html: marked(html), + } +}) + +fs.writeFileSync("./public/post-index.json", JSON.stringify(posts, null, 2)) +console.info(`✅ Generated ${posts.length} posts.`) diff --git a/scripts/process-blog-imgs.js b/scripts/process-blog-imgs.js new file mode 100644 index 0000000..97bc91c --- /dev/null +++ b/scripts/process-blog-imgs.js @@ -0,0 +1,52 @@ +import fs from "fs" +import path from "path" +import sharp from "sharp" + +const processBlogImages = async () => { + const srcDir = "posts/img" + const destDir = "public/posts/img" + + if (!fs.existsSync(srcDir)) { + console.error("⚠️ No posts/img directory found") + return + } + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }) + } + + // Copy all images + const files = fs.readdirSync(srcDir) + const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"] + + // Use for...of loop instead of forEach to work with async/await + for (const file of files) { + const ext = path.extname(file).toLowerCase() + if (imageExtensions.includes(ext)) { + const inputPath = path.join(srcDir, file) // Define inputPath and outputPath + const outputPath = path.join(destDir, file) + + if (ext === ".svg") { + // SVGs just get copied + fs.copyFileSync(inputPath, outputPath) + console.info(`📸 Copied ${file}`) + } else { + // Process other images + await sharp(inputPath) + .resize(1200, 800, { + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: 85, progressive: true }) + .png({ compressionLevel: 8 }) + .webp({ quality: 85 }) + .toFile(outputPath) + console.info(`🖼️ Processed ${file}`) + } + } + } + + console.log(`✅ Processed images from ${srcDir}`) +} + +export { processBlogImages } diff --git a/scripts/transform-img-paths.js b/scripts/transform-img-paths.js new file mode 100644 index 0000000..c6c348a --- /dev/null +++ b/scripts/transform-img-paths.js @@ -0,0 +1,9 @@ +const transformImagePaths = (markdown) => { + // Transform ./img/image-name to /posts/img/image-name + return markdown.replace( + /!\[([^\]]*)\]\(\.\/img\/([^)]+)\)/g, + "![$1](/posts/img/$2)" + ) +} + +export { transformImagePaths } diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..4ee4e55 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,105 @@ +// @ts-nocheck +import { Button } from "@/components/ui/button" +import { useTheme } from "@/context/ThemeProvider" +import { MoonStar } from "lucide-react" +import { Link } from "react-router" +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, + NavigationMenuViewport, +} from "@/components/ui/navigation-menu" + +import { Toggle } from "@/components/ui/toggle" +const Menu = () => { + return ( + + + {/* Desktop menu - hidden on mobile, visible on md+ */} +
+ + + Posts + + + + + About + + +
+ + {/* Mobile dropdown - visible only on small screens */} + + Menu + + + + Posts + + + + + About + + + + +
+ +
+ ) +} + +const Header = () => { + const { theme, setTheme } = useTheme() + return ( +
+
+
+ +
+
+ + + setTheme(theme === "dark" ? "light" : "dark") + } + > + + Dark theme + + + {/* +
+ + +
+ + */} +
+
+
+ ) +} + +export { Header } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..1199945 --- /dev/null +++ b/src/components/ui/navigation-menu.tsx @@ -0,0 +1,168 @@ +import * as React from "react" +import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" +import { cva } from "class-variance-authority" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function NavigationMenu({ + className, + children, + viewport = true, + ...props +}: React.ComponentProps & { + viewport?: boolean +}) { + return ( + + {children} + {viewport && } + + ) +} + +function NavigationMenuList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const navigationMenuTriggerStyle = cva( + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1" +) + +function NavigationMenuTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + {children}{" "} + + ) +} + +function NavigationMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuViewport({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ +
+ ) +} + +function NavigationMenuLink({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function NavigationMenuIndicator({ + className, + ...props +}: React.ComponentProps) { + return ( + +
+ + ) +} + +export { + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuContent, + NavigationMenuTrigger, + NavigationMenuLink, + NavigationMenuIndicator, + NavigationMenuViewport, + navigationMenuTriggerStyle, +} diff --git a/src/components/ui/pagination.tsx b/src/components/ui/pagination.tsx new file mode 100644 index 0000000..0d18541 --- /dev/null +++ b/src/components/ui/pagination.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import { + ChevronLeftIcon, + ChevronRightIcon, + MoreHorizontalIcon, +} from "lucide-react" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Pagination({ className, ...props }: React.ComponentProps<"nav">) { + return ( +