From 3b694e4c689d8e251c0c8a8e27df7f643e6e608b Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Wed, 8 Jul 2026 13:50:50 -0700 Subject: [PATCH 1/2] chore: add prettier, eslint, husky + lint-staged tooling Mirrors lifecycle-ui: Prettier (with import sorting), ESLint flat config, Husky + lint-staged pre-commit running `pnpm validate`. Adds lint/format scripts and CI steps. Includes the minimal source fixes to pass lint (remove an unused import; annotate intentional control-char regexes). --- .editorconfig | 12 + .github/workflows/ci.yaml | 6 + .husky/pre-commit | 1 + .prettierignore | 4 + .prettierrc | 19 + eslint.config.js | 15 + lint-staged.config.mjs | 5 + package.json | 17 +- pnpm-lock.yaml | 983 ++++++++++++++++++++++++++++++++++++++ src/lib/doctor.ts | 5 +- tests/llms.test.ts | 1 + tests/output.test.ts | 14 +- 12 files changed, 1073 insertions(+), 9 deletions(-) create mode 100644 .editorconfig create mode 100644 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 eslint.config.js create mode 100644 lint-staged.config.mjs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f15441a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index efa7d4f..f4ea3f8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,6 +26,12 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check formatting + run: pnpm format:check + + - name: Lint + run: pnpm lint + - name: Run type check run: pnpm typecheck diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..cb2c84d --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c7fb7b8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +pnpm-lock.yaml +src/lib/generated/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..26fdf2e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,19 @@ +{ + "printWidth": 120, + "trailingComma": "all", + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "importOrder": [ + "", + "", + "", + "", + "^[.][.]/(.*)$", + "", + "^[.]/(.*)$", + "" + ], + "importOrderParserPlugins": ["typescript", "importAttributes"], + "plugins": ["@ianvs/prettier-plugin-sort-imports"] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0d879a4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,15 @@ +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**', 'src/lib/generated/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + // Node CLI: declare Node globals (URL, process, etc.) for all files. + { languageOptions: { globals: { ...globals.node } } }, + // eslint-config-prettier goes last: turns off any stylistic rules that would + // fight Prettier. Prettier owns formatting; ESLint owns correctness. + prettier, +); diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs new file mode 100644 index 0000000..b45588d --- /dev/null +++ b/lint-staged.config.mjs @@ -0,0 +1,5 @@ +// Mirrors lifecycle-ui: on any staged source file, run the full validate +// (format:write + lint + typecheck). The function form runs it once, not per file. +export default { + '*.{ts,js,mjs,cjs,json,md}': () => ['pnpm validate'], +}; diff --git a/package.json b/package.json index 4ada9cf..f032770 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,15 @@ "build": "tsup", "dev": "tsx src/index.ts", "generate:api": "rm -rf ./src/lib/generated && orval --config ./orval.config.ts && node ./scripts/fix-generated-imports.mjs", + "lint": "eslint .", + "format": "prettier \"**/*.{ts,js,mjs,cjs,json,md}\"", + "format:check": "pnpm format --check", + "format:write": "pnpm format --write", + "validate": "pnpm format:write && pnpm lint && pnpm typecheck", "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "prepare": "husky" }, "dependencies": { "@clack/prompts": "^0.11.0", @@ -44,14 +50,23 @@ "picocolors": "^1.1.1" }, "devDependencies": { + "@eslint/js": "^9.39.4", + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@types/archiver": "^6.0.3", "@types/js-yaml": "^4.0.9", "@types/node": "^24.0.0", "dotenv": "^17.2.3", + "eslint": "^9", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.7.0", + "husky": "^9.1.7", + "lint-staged": "^17.0.8", "orval": "7.4.1", + "prettier": "3.6.2", "tsup": "^8.5.0", "tsx": "^4.20.0", "typescript": "^5.8.0", + "typescript-eslint": "^8.63.0", "vitest": "^3.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35262d8..fe13e27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,12 @@ importers: specifier: ^1.1.1 version: 1.1.1 devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@ianvs/prettier-plugin-sort-imports': + specifier: ^4.7.1 + version: 4.7.1(prettier@3.6.2) '@types/archiver': specifier: ^6.0.3 version: 6.0.4 @@ -42,9 +48,27 @@ importers: dotenv: specifier: ^17.2.3 version: 17.4.2 + eslint: + specifier: ^9 + version: 9.39.4 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.4) + globals: + specifier: ^17.7.0 + version: 17.7.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^17.0.8 + version: 17.0.8 orval: specifier: 7.4.1 version: 7.4.1(openapi-types@12.1.3) + prettier: + specifier: 3.6.2 + version: 3.6.2 tsup: specifier: ^8.5.0 version: 8.5.1(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) @@ -54,6 +78,9 @@ importers: typescript: specifier: ^5.8.0 version: 5.9.3 + typescript-eslint: + specifier: ^8.63.0 + version: 8.63.0(eslint@9.39.4)(typescript@5.9.3) vitest: specifier: ^3.2.0 version: 3.2.6(@types/node@24.13.1)(tsx@4.22.4)(yaml@2.9.0) @@ -79,6 +106,43 @@ packages: '@asyncapi/specs@6.11.1': resolution: {integrity: sha512-A3WBLqAKGoJ2+6FWFtpjBlCQ1oFCcs4GxF7zsIGvNqp/klGUHjlA3aAcZ9XMMpLGE8zPeYDz2x9FmO6DSuKraQ==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} @@ -547,9 +611,85 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@ianvs/prettier-plugin-sort-imports@4.7.1': + resolution: {integrity: sha512-jmTNYGlg95tlsoG3JLCcuC4BrFELJtLirLAkQW/71lXSyOhVt/Xj7xWbbGcuVbNq1gwWgSyMrPjJc9Z30hynVw==} + peerDependencies: + '@prettier/plugin-oxc': ^0.0.4 || ^0.1.0 + '@vue/compiler-sfc': 2.7.x || 3.x + content-tag: ^4.0.0 + prettier: 2 || 3 || ^4.0.0-0 + prettier-plugin-ember-template-tag: ^2.1.0 + peerDependenciesMeta: + '@prettier/plugin-oxc': + optional: true + '@vue/compiler-sfc': + optional: true + content-tag: + optional: true + prettier-plugin-ember-template-tag: + optional: true + '@ibm-cloud/openapi-ruleset-utilities@1.9.2': resolution: {integrity: sha512-q9ZFdbtfxhaSeN6AykhVYk/EgkgV99dIW7ua15Wzy3s5BTH6tH1MZJA743QE9BQ+Iul/u1+XYNSaJm2/jiMQig==} engines: {node: '>=16.0.0'} @@ -897,6 +1037,65 @@ packages: '@types/urijs@1.19.26': resolution: {integrity: sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg==} + '@typescript-eslint/eslint-plugin@8.63.0': + resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.63.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.63.0': + resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.63.0': + resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.63.0': + resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.63.0': + resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.63.0': + resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.63.0': + resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.63.0': + resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.63.0': + resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.63.0': + resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} @@ -933,6 +1132,11 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -959,6 +1163,9 @@ packages: ajv: optional: true + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -966,6 +1173,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1132,6 +1343,10 @@ packages: call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1157,6 +1372,14 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -1237,6 +1460,9 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1274,6 +1500,9 @@ packages: emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1288,6 +1517,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es-abstract@1.24.2: resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} engines: {node: '>= 0.4'} @@ -1341,6 +1574,58 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1352,6 +1637,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events-universal@1.0.1: resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} @@ -1381,6 +1669,12 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-memoize@2.5.2: resolution: {integrity: sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==} @@ -1402,6 +1696,10 @@ packages: picomatch: optional: true + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1413,6 +1711,13 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -1448,6 +1753,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1468,11 +1777,23 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -1531,6 +1852,11 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1545,6 +1871,14 @@ packages: immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + inflected@2.1.0: resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} @@ -1599,6 +1933,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-generator-function@1.1.2: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} @@ -1679,6 +2017,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1690,9 +2031,23 @@ packages: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} engines: {node: '>= 10.16.0'} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonc-parser@2.2.1: resolution: {integrity: sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==} @@ -1711,6 +2066,9 @@ packages: jsonschema@1.5.0: resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -1719,6 +2077,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1729,6 +2091,15 @@ packages: linkify-it@5.0.1: resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + lint-staged@17.0.8: + resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} + engines: {node: '>=22.22.1'} + hasBin: true + + listr2@10.2.2: + resolution: {integrity: sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==} + engines: {node: '>=22.13.0'} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1744,6 +2115,9 @@ packages: lodash.isempty@4.4.0: resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.omit@4.18.0: resolution: {integrity: sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==} @@ -1768,6 +2142,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + loglevel-plugin-prefix@0.8.4: resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} @@ -1831,6 +2209,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@10.2.3: resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} engines: {node: 18 || 20 || >=22} @@ -1864,6 +2246,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nimma@0.2.3: resolution: {integrity: sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==} engines: {node: ^12.20 || >=14.13} @@ -1928,6 +2313,10 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + oniguruma-to-es@2.3.0: resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} @@ -1940,6 +2329,10 @@ packages: openapi3-ts@4.4.0: resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==} + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + orval@7.4.1: resolution: {integrity: sha512-pXg5g5gdAzFqCUURZsMJoVeiWynSNSzqi6lXI1Opw08ILtmzDTdiU7PoSOf7pKVTB6oEf82zQzWeJMqSk8nzuQ==} hasBin: true @@ -1959,6 +2352,10 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2030,6 +2427,15 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -2044,6 +2450,10 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2089,14 +2499,25 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.61.1: resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2126,6 +2547,11 @@ packages: safe-stable-stringify@1.1.1: resolution: {integrity: sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2200,6 +2626,14 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2236,6 +2670,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + string.prototype.trim@1.2.11: resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} engines: {node: '>= 0.4'} @@ -2269,6 +2711,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -2307,6 +2753,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -2337,6 +2787,12 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2380,6 +2836,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2409,6 +2869,13 @@ packages: peerDependencies: typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + typescript-eslint@8.63.0: + resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2446,6 +2913,9 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} @@ -2571,6 +3041,14 @@ packages: engines: {node: '>=8'} hasBin: true + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@10.0.0: + resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} + engines: {node: '>=20'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2579,6 +3057,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -2638,6 +3120,53 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -2880,8 +3409,81 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + '@exodus/schemasafe@1.3.0': {} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@ianvs/prettier-plugin-sort-imports@4.7.1(prettier@3.6.2)': + dependencies: + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + prettier: 3.6.2 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + '@ibm-cloud/openapi-ruleset-utilities@1.9.2': {} '@ibm-cloud/openapi-ruleset@1.33.10': @@ -3367,6 +3969,97 @@ snapshots: '@types/urijs@1.19.26': {} + '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/type-utils': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.63.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.63.0 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.63.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.63.0': + dependencies: + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 + + '@typescript-eslint/tsconfig-utils@8.63.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.63.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.63.0': {} + + '@typescript-eslint/typescript-estree@8.63.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.63.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@5.9.3) + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/visitor-keys': 8.63.0 + debug: 4.4.3 + minimatch: 10.2.3 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.63.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.63.0 + '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.63.0': + dependencies: + '@typescript-eslint/types': 8.63.0 + eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.1': {} '@vitest/expect@3.2.6': @@ -3415,6 +4108,10 @@ snapshots: dependencies: event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.16.0: {} ajv-draft-04@1.0.0(ajv@8.20.0): @@ -3429,6 +4126,13 @@ snapshots: optionalDependencies: ajv: 8.20.0 + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -3438,6 +4142,10 @@ snapshots: ansi-colors@4.1.3: {} + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -3595,6 +4303,8 @@ snapshots: call-me-maybe@1.0.2: {} + callsites@3.1.0: {} + ccount@2.0.1: {} chai@5.3.3: @@ -3620,6 +4330,15 @@ snapshots: dependencies: readdirp: 4.1.2 + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -3693,6 +4412,8 @@ snapshots: deep-eql@5.0.2: {} + deep-is@0.1.4: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -3729,6 +4450,8 @@ snapshots: emoji-regex-xs@1.0.0: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3740,6 +4463,8 @@ snapshots: entities@4.5.0: {} + environment@1.1.0: {} + es-abstract@1.24.2: dependencies: array-buffer-byte-length: 1.0.2 @@ -3921,6 +4646,78 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -3929,6 +4726,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} + events-universal@1.0.1: dependencies: bare-events: 2.9.1 @@ -3965,6 +4764,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + fast-memoize@2.5.2: {} fast-safe-stringify@2.1.1: {} @@ -3979,6 +4782,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3994,6 +4801,13 @@ snapshots: mlly: 1.8.2 rollup: 4.61.1 + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -4032,6 +4846,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4062,6 +4878,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -4071,6 +4891,10 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + globals@14.0.0: {} + + globals@17.7.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -4135,6 +4959,8 @@ snapshots: human-signals@2.1.0: {} + husky@9.1.7: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4143,6 +4969,13 @@ snapshots: immer@9.0.21: {} + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + inflected@2.1.0: {} inherits@2.0.4: {} @@ -4201,6 +5034,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + is-generator-function@1.1.2: dependencies: call-bound: 1.0.4 @@ -4279,6 +5116,8 @@ snapshots: joycon@3.1.1: {} + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} js-yaml@4.2.0: @@ -4287,8 +5126,16 @@ snapshots: jsep@1.4.0: {} + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + jsonc-parser@2.2.1: {} jsonfile@6.2.1: @@ -4307,12 +5154,21 @@ snapshots: jsonschema@1.5.0: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -4321,6 +5177,23 @@ snapshots: dependencies: uc.micro: 2.1.0 + lint-staged@17.0.8: + dependencies: + listr2: 10.2.2 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.2.4 + optionalDependencies: + yaml: 2.9.0 + + listr2@10.2.2: + dependencies: + cli-truncate: 5.2.0 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 10.0.0 + load-tsconfig@0.2.5: {} locate-path@6.0.0: @@ -4331,6 +5204,8 @@ snapshots: lodash.isempty@4.4.0: {} + lodash.merge@4.6.2: {} + lodash.omit@4.18.0: {} lodash.omitby@4.6.0: {} @@ -4347,6 +5222,14 @@ snapshots: lodash@4.18.1: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + loglevel-plugin-prefix@0.8.4: {} loglevel@1.9.2: {} @@ -4414,6 +5297,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-function@5.0.1: {} + minimatch@10.2.3: dependencies: brace-expansion: 5.0.6 @@ -4449,6 +5334,8 @@ snapshots: nanoid@3.3.12: {} + natural-compare@1.4.0: {} + nimma@0.2.3: dependencies: '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) @@ -4527,6 +5414,10 @@ snapshots: dependencies: mimic-fn: 2.1.0 + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + oniguruma-to-es@2.3.0: dependencies: emoji-regex-xs: 1.0.0 @@ -4543,6 +5434,15 @@ snapshots: dependencies: yaml: 2.9.0 + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + orval@7.4.1(openapi-types@12.1.3): dependencies: '@apidevtools/swagger-parser': 10.1.1(openapi-types@12.1.3) @@ -4591,6 +5491,10 @@ snapshots: package-json-from-dist@1.0.1: {} + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -4638,6 +5542,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + + prettier@3.6.2: {} + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -4646,6 +5554,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} readable-stream@2.3.8: @@ -4709,10 +5619,19 @@ snapshots: require-from-string@2.0.2: {} + resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.61.1: dependencies: '@types/estree': 1.0.9 @@ -4773,6 +5692,8 @@ snapshots: safe-stable-stringify@1.1.1: {} + semver@7.8.5: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -4876,6 +5797,16 @@ snapshots: slash@3.0.0: {} + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + source-map-js@1.2.1: {} source-map@0.7.6: {} @@ -4914,6 +5845,17 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string.prototype.trim@1.2.11: dependencies: call-bind: 1.0.9 @@ -4961,6 +5903,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-json-comments@3.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -5031,6 +5975,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -5052,6 +5998,10 @@ snapshots: trim-lines@3.0.1: {} + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-interface-checker@0.1.13: {} tsconfck@2.1.2(typescript@5.9.3): @@ -5096,6 +6046,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -5142,6 +6096,17 @@ snapshots: typescript: 5.9.3 yaml: 2.9.0 + typescript-eslint@8.63.0(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.63.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.63.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -5182,6 +6147,10 @@ snapshots: universalify@2.0.1: {} + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + urijs@1.19.11: {} util-deprecate@1.0.2: {} @@ -5333,6 +6302,14 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + word-wrap@1.2.5: {} + + wrap-ansi@10.0.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 8.2.1 + strip-ansi: 7.2.0 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5345,6 +6322,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + y18n@5.0.8: {} yaml@1.10.3: {} diff --git a/src/lib/doctor.ts b/src/lib/doctor.ts index 94f35bd..47abf10 100644 --- a/src/lib/doctor.ts +++ b/src/lib/doctor.ts @@ -10,7 +10,6 @@ import { PRIVATE_DIR_MODE, PRIVATE_FILE_MODE, tokensDir, - tokensPath, } from './config.js'; export type DoctorStatus = 'ok' | 'warn' | 'error'; @@ -262,13 +261,13 @@ export function runDoctor({ fix = false }: { fix?: boolean } = {}): DoctorReport path: path.join(tokensDir(), entry), expectedMode: PRIVATE_FILE_MODE, fix, - }) + }), ); } } return { - ok: checks.every((check) => check.status === 'ok'), + ok: checks.every(check => check.status === 'ok'), checks, }; } diff --git a/tests/llms.test.ts b/tests/llms.test.ts index f005209..4c10d06 100644 --- a/tests/llms.test.ts +++ b/tests/llms.test.ts @@ -20,6 +20,7 @@ describe('lfc llms instructions', () => { it('is plain text safe for non-TTY consumption', () => { // no ANSI escape codes and no template-literal escaping artifacts + // eslint-disable-next-line no-control-regex expect(LLMS_INSTRUCTIONS).not.toMatch(/\x1b\[/); expect(LLMS_INSTRUCTIONS).not.toContain('\\`'); expect(LLMS_INSTRUCTIONS.endsWith('\n')).toBe(true); diff --git a/tests/output.test.ts b/tests/output.test.ts index 3383d71..175eec1 100644 --- a/tests/output.test.ts +++ b/tests/output.test.ts @@ -11,11 +11,15 @@ describe('visibleLength', () => { describe('renderTable', () => { it('aligns columns including colored cells', () => { - const out = renderTable(['name', 'status'], [ - ['web', '\x1b[32mdeployed\x1b[0m'], - ['longer-name', 'error'], - ]); - const stripped = out.split('\n').map((l) => l.replace(/\x1b\[[0-9;]*m/g, '')); + const out = renderTable( + ['name', 'status'], + [ + ['web', '\x1b[32mdeployed\x1b[0m'], + ['longer-name', 'error'], + ], + ); + // eslint-disable-next-line no-control-regex + const stripped = out.split('\n').map(l => l.replace(/\x1b\[[0-9;]*m/g, '')); expect(stripped).toHaveLength(3); // the second column starts at the same visible offset on every line expect(stripped[1]!.indexOf('deployed')).toBe(stripped[0]!.indexOf('STATUS')); From 5af4e08a4534e9cf2cf770fe1f139dc01cd9528c Mon Sep 17 00:00:00 2001 From: Vigneshraj Sekar Babu Date: Wed, 8 Jul 2026 13:51:03 -0700 Subject: [PATCH 2/2] style: format the repository with prettier --- CONTRIBUTING.md | 2 +- README.md | 2 +- src/commands/auth.ts | 31 +++-- src/commands/builds.ts | 188 ++++++++++++++++------------ src/commands/config.ts | 20 +-- src/commands/doctor.ts | 9 +- src/commands/init.ts | 18 +-- src/commands/pods.ts | 38 +++--- src/commands/schema.ts | 4 +- src/commands/services.ts | 62 ++++----- src/commands/sites.ts | 53 +++++--- src/index.ts | 4 +- src/lib/api.ts | 40 +++--- src/lib/auth.ts | 10 +- src/lib/config.ts | 6 +- src/lib/context.ts | 10 +- src/lib/logs.ts | 4 +- src/lib/output.ts | 4 +- src/lib/resolve.ts | 7 +- src/lib/schema.ts | 14 +-- src/lib/schema/lifecycle-1.0.0.json | 146 +++++---------------- src/lib/telemetry.ts | 6 +- src/lib/types.ts | 21 +++- src/lib/zip.ts | 16 ++- tests/api.test.ts | 39 ++++-- tests/config.test.ts | 5 +- tests/doctor.test.ts | 6 +- tests/logs.test.ts | 4 +- tests/resolve.test.ts | 10 +- tests/schema.test.ts | 8 +- tests/zip.test.ts | 2 +- 31 files changed, 415 insertions(+), 374 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f76f34..2678aaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,7 @@ docs/plan.html # living plan/architecture/testing document - **API types**: generated schemas come from the same OpenAPI docs endpoint as lifecycle-ui. Set `NEXT_PUBLIC_API_URL` or `LIFECYCLE_API_URL`, then run `pnpm generate:api`. Keep CLI-specific nullability/backward-compatibility in `src/lib/types.ts`, not in generated files. -- **Output discipline**: data → stdout; progress/confirmation chatter → stderr; `--json` must emit *only* JSON on stdout. +- **Output discipline**: data → stdout; progress/confirmation chatter → stderr; `--json` must emit _only_ JSON on stdout. - **Interactivity**: prompts (`@clack/prompts`) only when stdin is a TTY; every prompt needs a flag escape hatch (`--yes`, `--device`, …) so agents can run non-interactively. - **Auth**: never log tokens; token files are 0600. Anything touching the live Keycloak realm must be additive-only. - Strict TypeScript; keep new code passing `pnpm typecheck` with `noUncheckedIndexedAccess`. diff --git a/README.md b/README.md index c185dbf..b880e4e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # lfc-cli (`lfc`) -Command-line interface for [Lifecycle](https://github.com/GoodRxOSS/lifecycle) — view and manage preview environments (builds), redeploy services, and host static sites, straight from your terminal. Built for humans *and* agents: every command supports `--json`. +Command-line interface for [Lifecycle](https://github.com/GoodRxOSS/lifecycle) — view and manage preview environments (builds), redeploy services, and host static sites, straight from your terminal. Built for humans _and_ agents: every command supports `--json`. ``` $ lfc builds list --mine diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 44b3646..4290d4a 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -2,7 +2,7 @@ import * as clack from '@clack/prompts'; import { Command } from 'commander'; import pc from 'picocolors'; -import { decodeJwt, endSession, loginDevice, loginPkce, AuthError } from '../lib/auth.js'; +import { AuthError, decodeJwt, endSession, loginDevice, loginPkce } from '../lib/auth.js'; import { clearTokens, loadTokens, saveTokens } from '../lib/config.js'; import { runAction, type Ctx } from '../lib/context.js'; import { link, printJson } from '../lib/output.js'; @@ -11,7 +11,7 @@ function requireKeycloak(ctx: Ctx) { if (!ctx.profile.authEnabled || !ctx.profile.keycloak) { throw new AuthError( `Profile "${ctx.profileName}" has auth disabled — nothing to log in to. ` + - 'Set profile.authEnabled=true and keycloak settings if this deployment enforces auth.' + 'Set profile.authEnabled=true and keycloak settings if this deployment enforces auth.', ); } return ctx.profile.keycloak; @@ -32,18 +32,20 @@ export function registerAuthCommands(program: Command): void { onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => { process.stderr.write( `\nOn any device, open ${link(verificationUriComplete ?? verificationUri)}\n` + - `and confirm the code: ${pc.bold(userCode)}\n\nWaiting for approval...\n` + `and confirm the code: ${pc.bold(userCode)}\n\nWaiting for approval...\n`, ); }, }); } else { tokens = await loginPkce(kc, { noBrowser: opts.browser === false, - onAuthUrl: (url) => { + onAuthUrl: url => { if (opts.browser === false) { process.stderr.write(`\nOpen this URL in your browser to log in:\n\n${url}\n\n`); } else { - process.stderr.write(`Opening your browser for SSO login... ${pc.dim('(--no-browser to print the URL)')}\n`); + process.stderr.write( + `Opening your browser for SSO login... ${pc.dim('(--no-browser to print the URL)')}\n`, + ); } }, }); @@ -53,27 +55,30 @@ export function registerAuthCommands(program: Command): void { const who = (claims.email as string) ?? (claims.preferred_username as string) ?? 'unknown'; if (ctx.json) printJson({ loggedIn: true, profile: ctx.profileName, user: who }); else process.stderr.write(`${pc.green('✓')} Logged in as ${pc.bold(who)} (profile: ${ctx.profileName})\n`); - }) + }), ); program .command('logout') .description('Log out and remove cached tokens for the active profile') .action( - runAction(async (ctx) => { + runAction(async ctx => { const tokens = loadTokens(ctx.profileName); if (tokens && ctx.profile.keycloak) await endSession(ctx.profile.keycloak, tokens); const removed = clearTokens(ctx.profileName); if (ctx.json) printJson({ loggedOut: removed, profile: ctx.profileName }); - else process.stderr.write(removed ? `${pc.green('✓')} Logged out (profile: ${ctx.profileName})\n` : 'No active session.\n'); - }) + else + process.stderr.write( + removed ? `${pc.green('✓')} Logged out (profile: ${ctx.profileName})\n` : 'No active session.\n', + ); + }), ); program .command('whoami') .description('Show the logged-in user and token details') .action( - runAction(async (ctx) => { + runAction(async ctx => { if (!ctx.profile.authEnabled) { if (ctx.json) printJson({ authEnabled: false, profile: ctx.profileName, apiUrl: ctx.api.baseUrl }); else process.stdout.write(`Auth is disabled for profile "${ctx.profileName}" (API: ${ctx.api.baseUrl})\n`); @@ -90,7 +95,7 @@ export function registerAuthCommands(program: Command): void { name: [claims.given_name, claims.family_name].filter(Boolean).join(' ') || null, username: claims.preferred_username ?? null, githubUsername: claims.github_username ?? null, - roles: realmAccess?.roles?.filter((r) => ['user', 'admin'].includes(r)) ?? [], + roles: realmAccess?.roles?.filter(r => ['user', 'admin'].includes(r)) ?? [], accessTokenExpires: new Date(tokens.expiresAt).toISOString(), hasRefreshToken: Boolean(tokens.refreshToken), }; @@ -107,9 +112,9 @@ export function registerAuthCommands(program: Command): void { ` profile ${info.profile}`, ` api ${info.apiUrl}`, ` token exp ${info.accessTokenExpires}${tokens.expiresAt < Date.now() ? pc.yellow(' (expired — auto-refreshes on use)') : ''}`, - ].join('\n') + '\n' + ].join('\n') + '\n', ); clack.outro(pc.dim('tokens cached in ~/.config/lifecycle-cli/tokens/')); - }) + }), ); } diff --git a/src/commands/builds.ts b/src/commands/builds.ts index 6c7956e..0142de5 100644 --- a/src/commands/builds.ts +++ b/src/commands/builds.ts @@ -12,18 +12,20 @@ function buildUiUrl(ctx: Ctx, uuid: string): string | undefined { return ctx.uiUrl ? `${ctx.uiUrl}/environments/${uuid}` : undefined; } -function prLabel(build: { pullRequest?: { fullName?: string; pullRequestNumber?: number; title?: string } | null }): string { +function prLabel(build: { + pullRequest?: { fullName?: string; pullRequestNumber?: number; title?: string } | null; +}): string { const pr = build.pullRequest; if (!pr) return ''; return `${pr.fullName}#${pr.pullRequestNumber}`; } function activeDeploys(build: Build): Deploy[] { - return (build.deploys ?? []).filter((d) => d.active); + return (build.deploys ?? []).filter(d => d.active); } function serviceRows(build: Build): string[][] { - return activeDeploys(build).map((d) => [ + return activeDeploys(build).map(d => [ d.deployable?.name ?? d.uuid, statusColor(d.status), d.branchName ?? '', @@ -33,15 +35,21 @@ function serviceRows(build: Build): string[][] { } function summarizeServices(deploys: Array<{ status: string; active: boolean }>): string { - const active = deploys.filter((d) => d.active); - const deployed = active.filter((d) => d.status === 'ready' || d.status === 'deployed').length; - const failed = active.filter((d) => d.status.includes('error') || d.status.includes('failed')).length; + const active = deploys.filter(d => d.active); + const deployed = active.filter(d => d.status === 'ready' || d.status === 'deployed').length; + const failed = active.filter(d => d.status.includes('error') || d.status.includes('failed')).length; let summary = `${deployed}/${active.length}`; if (failed > 0) summary += pc.red(` (${failed} failed)`); return summary; } -async function watchBuild(ctx: Ctx, uuid: string, intervalMs: number, timeoutMs: number, timeoutLabel: string): Promise { +async function watchBuild( + ctx: Ctx, + uuid: string, + intervalMs: number, + timeoutMs: number, + timeoutLabel: string, +): Promise { const deadline = Date.now() + timeoutMs; const isTty = process.stdout.isTTY; let firstRender = true; @@ -66,7 +74,9 @@ async function watchBuild(ctx: Ctx, uuid: string, intervalMs: number, timeoutMs: return; } if (BUILD_TERMINAL_FAILURE.has(build.status)) { - process.stdout.write(`${pc.red('✗')} ${build.uuid} ended in ${build.status}${build.statusMessage ? `: ${build.statusMessage}` : ''}\n`); + process.stdout.write( + `${pc.red('✗')} ${build.uuid} ended in ${build.status}${build.statusMessage ? `: ${build.statusMessage}` : ''}\n`, + ); process.exitCode = 1; return; } @@ -75,7 +85,7 @@ async function watchBuild(ctx: Ctx, uuid: string, intervalMs: number, timeoutMs: process.exitCode = 2; return; } - await new Promise((r) => setTimeout(r, intervalMs)); + await new Promise(r => setTimeout(r, intervalMs)); } } @@ -114,7 +124,9 @@ function renderBuildDetail(ctx: Ctx, build: Build): void { async function renderStatusOnce(ctx: Ctx, uuid: string): Promise { const build = await ctx.api.getBuild(uuid); const lines: string[] = []; - lines.push(`${pc.bold(build.uuid)} ${statusColor(build.status)}${build.statusMessage ? pc.dim(` ${build.statusMessage}`) : ''}`); + lines.push( + `${pc.bold(build.uuid)} ${statusColor(build.status)}${build.statusMessage ? pc.dim(` ${build.statusMessage}`) : ''}`, + ); const rows = serviceRows(build); if (rows.length > 0) lines.push('', renderTable(['service', 'status', 'branch', 'url', 'updated'], rows)); process.stdout.write(lines.join('\n') + '\n'); @@ -131,39 +143,50 @@ export function registerBuildsCommands(program: Command): void { .option('-m, --mine', 'only my environments (matched via GitHub username)') .option('--exclude ', 'comma-separated statuses to exclude', 'torn_down,pending') .option('--all', 'include all statuses (no exclusions)') - .option('-p, --page ', 'page number', (v) => Number(v), 1) - .option('-n, --limit ', 'items per page', (v) => Number(v), 25) + .option('-p, --page ', 'page number', v => Number(v), 1) + .option('-n, --limit ', 'items per page', v => Number(v), 25) .action( - runAction(async (ctx, opts: { search?: string; mine?: boolean; exclude: string; all?: boolean; page: number; limit: number }) => { - const { items, pagination } = await ctx.api.listBuilds({ - page: opts.page, - limit: opts.limit, - search: opts.search, - exclude: opts.all ? '' : opts.exclude, - myEnvs: opts.mine, - }); - if (ctx.json) { - printJson({ builds: items, pagination }); - return; - } - if (items.length === 0) { - process.stdout.write(pc.dim('No builds found.\n')); - return; - } - const rows = items.map((b) => [ - pc.bold(b.uuid), - statusColor(b.status), - prLabel(b), - b.pullRequest?.branchName ?? '', - b.pullRequest?.githubLogin ?? '', - summarizeServices(b.deploys ?? []), - formatAge(b.updatedAt), - ]); - process.stdout.write(renderTable(['uuid', 'status', 'pr', 'branch', 'author', 'services', 'updated'], rows) + '\n'); - if (pagination?.totalPages && Number(pagination.totalPages) > 1) { - process.stdout.write(pc.dim(`page ${pagination.page}/${pagination.totalPages} · ${pagination.totalItems} total · -p for more\n`)); - } - }) + runAction( + async ( + ctx, + opts: { search?: string; mine?: boolean; exclude: string; all?: boolean; page: number; limit: number }, + ) => { + const { items, pagination } = await ctx.api.listBuilds({ + page: opts.page, + limit: opts.limit, + search: opts.search, + exclude: opts.all ? '' : opts.exclude, + myEnvs: opts.mine, + }); + if (ctx.json) { + printJson({ builds: items, pagination }); + return; + } + if (items.length === 0) { + process.stdout.write(pc.dim('No builds found.\n')); + return; + } + const rows = items.map(b => [ + pc.bold(b.uuid), + statusColor(b.status), + prLabel(b), + b.pullRequest?.branchName ?? '', + b.pullRequest?.githubLogin ?? '', + summarizeServices(b.deploys ?? []), + formatAge(b.updatedAt), + ]); + process.stdout.write( + renderTable(['uuid', 'status', 'pr', 'branch', 'author', 'services', 'updated'], rows) + '\n', + ); + if (pagination?.totalPages && Number(pagination.totalPages) > 1) { + process.stdout.write( + pc.dim( + `page ${pagination.page}/${pagination.totalPages} · ${pagination.totalItems} total · -p for more\n`, + ), + ); + } + }, + ), ); builds @@ -180,9 +203,11 @@ export function registerBuildsCommands(program: Command): void { renderBuildDetail(ctx, build); if (opts.manifest && build.manifest) { process.stdout.write(`\n${pc.bold('manifest')}\n`); - process.stdout.write(typeof build.manifest === 'string' ? `${build.manifest}\n` : `${JSON.stringify(build.manifest, null, 2)}\n`); + process.stdout.write( + typeof build.manifest === 'string' ? `${build.manifest}\n` : `${JSON.stringify(build.manifest, null, 2)}\n`, + ); } - }) + }), ); builds @@ -195,7 +220,9 @@ export function registerBuildsCommands(program: Command): void { runAction(async (ctx, opts: { pr?: string; branch?: string; repo?: string }) => { const selector = parseSelector(opts); const target = - selector.prNumber !== undefined ? `${selector.repo}#${selector.prNumber}` : `${selector.repo}@${selector.branch}`; + selector.prNumber !== undefined + ? `${selector.repo}#${selector.prNumber}` + : `${selector.repo}@${selector.branch}`; const { matches, truncated } = await ctx.api.resolveBuild(selector); const chosen = pickBuild(matches); @@ -203,12 +230,12 @@ export function registerBuildsCommands(program: Command): void { if (truncated) { process.stderr.write( `${pc.yellow('!')} No build matched ${target} in the pages scanned — results may be truncated. ` + - `Double-check the repo/branch, or read the uuid from the PR comment.\n` + `Double-check the repo/branch, or read the uuid from the PR comment.\n`, ); process.exitCode = 1; } else { process.stderr.write( - `${pc.dim(`No build found for ${target}. If the PR was just opened, Lifecycle may not have created the environment yet.`)}\n` + `${pc.dim(`No build found for ${target}. If the PR was just opened, Lifecycle may not have created the environment yet.`)}\n`, ); process.exitCode = 3; } @@ -218,20 +245,20 @@ export function registerBuildsCommands(program: Command): void { // The list payload is trimmed; fetch the full build so output matches `builds get`. const build = await ctx.api.getBuild(chosen.uuid); - const others = matches.filter((m) => m.uuid !== chosen.uuid).map((m) => m.uuid); + const others = matches.filter(m => m.uuid !== chosen.uuid).map(m => m.uuid); if (others.length > 0) { process.stderr.write( - `${pc.yellow('!')} ${matches.length} builds matched ${target}; showing most recent ${pc.bold(chosen.uuid)}. Others: ${others.join(', ')}\n` + `${pc.yellow('!')} ${matches.length} builds matched ${target}; showing most recent ${pc.bold(chosen.uuid)}. Others: ${others.join(', ')}\n`, ); } if (ctx.json) { // Uniform envelope (`found` always present) so callers can branch on it, // and multiple matches are visible in JSON, not just on stderr. - printJson({ found: true, build, matches: matches.map((m) => m.uuid) }); + printJson({ found: true, build, matches: matches.map(m => m.uuid) }); return; } renderBuildDetail(ctx, build); - }) + }), ); builds @@ -249,7 +276,7 @@ export function registerBuildsCommands(program: Command): void { uuid: build.uuid, status: build.status, statusMessage: build.statusMessage, - services: activeDeploys(build).map((d) => ({ + services: activeDeploys(build).map(d => ({ name: d.deployable?.name, status: d.status, statusMessage: d.statusMessage, @@ -266,7 +293,7 @@ export function registerBuildsCommands(program: Command): void { } await watchBuild(ctx, uuid, parseDuration(opts.interval), parseDuration(opts.timeout), opts.timeout); - }) + }), ); builds @@ -282,10 +309,10 @@ export function registerBuildsCommands(program: Command): void { } process.stderr.write(`${pc.green('✓')} Redeploy queued for ${pc.bold(uuid)}\n`); if (opts.watch) { - await new Promise((r) => setTimeout(r, 3000)); + await new Promise(r => setTimeout(r, 3000)); await watchBuild(ctx, uuid, 5000, 30 * 60_000, '30m'); } - }) + }), ); builds @@ -296,7 +323,9 @@ export function registerBuildsCommands(program: Command): void { runAction(async (ctx, uuid: string, opts: { yes?: boolean }) => { if (!opts.yes) { if (!process.stdin.isTTY) throw new Error('Refusing to destroy without --yes in non-interactive mode'); - const ok = await clack.confirm({ message: `Tear down build ${uuid}? This deletes its namespace and services.` }); + const ok = await clack.confirm({ + message: `Tear down build ${uuid}? This deletes its namespace and services.`, + }); if (ok !== true) { process.stderr.write('Aborted.\n'); return; @@ -305,7 +334,7 @@ export function registerBuildsCommands(program: Command): void { const result = await ctx.api.destroyBuild(uuid); if (ctx.json) printJson({ uuid, destroy: result ?? 'queued' }); else process.stderr.write(`${pc.green('✓')} Teardown queued for ${uuid}\n`); - }) + }), ); builds @@ -320,7 +349,7 @@ export function registerBuildsCommands(program: Command): void { const ui = buildUiUrl(ctx, build.uuid); if (ui) process.stderr.write(` ${link(ui)}\n`); } - }) + }), ); builds @@ -340,19 +369,20 @@ export function registerBuildsCommands(program: Command): void { throw new Error('Nothing to change — pass --[no-]static and/or --[no-]track-defaults'); } const build = await ctx.api.patchBuild(uuid, patch); - if (ctx.json) printJson({ uuid: build.uuid, isStatic: build.isStatic, trackDefaultBranches: build.trackDefaultBranches }); + if (ctx.json) + printJson({ uuid: build.uuid, isStatic: build.isStatic, trackDefaultBranches: build.trackDefaultBranches }); else { process.stderr.write(`${pc.green('✓')} ${build.uuid} updated\n`); process.stdout.write(` static ${build.isStatic ? pc.green('yes') : 'no'}\n`); process.stdout.write(` track-defaults ${build.trackDefaultBranches ? pc.green('yes') : 'no'}\n`); } - }) + }), ); builds .command('webhooks ') .description('List webhook invocations for a build, or trigger them') - .option('--invoke', 'invoke the webhooks configured in the build\'s webhooksYaml', false) + .option('--invoke', "invoke the webhooks configured in the build's webhooksYaml", false) .action( runAction(async (ctx, uuid: string, opts: { invoke: boolean }) => { if (opts.invoke) { @@ -370,15 +400,9 @@ export function registerBuildsCommands(program: Command): void { process.stdout.write(pc.dim('No webhook invocations.\n')); return; } - const rows = invocations.map((w) => [ - w.name, - w.type, - statusColor(w.status), - w.runUUID, - formatAge(w.createdAt), - ]); + const rows = invocations.map(w => [w.name, w.type, statusColor(w.status), w.runUUID, formatAge(w.createdAt)]); process.stdout.write(renderTable(['name', 'type', 'status', 'run', 'when'], rows) + '\n'); - }) + }), ); builds @@ -395,7 +419,7 @@ export function registerBuildsCommands(program: Command): void { } process.stdout.write(`${link(url)}\n`); if (!opts.print) openBrowser(url); - }) + }), ); const env = builds.command('env').description('View and set comment env-var overrides on a build'); @@ -419,7 +443,7 @@ export function registerBuildsCommands(program: Command): void { }; print('runtime env overrides', data.runtime); print('init env overrides', data.init); - }) + }), ); env @@ -430,7 +454,9 @@ export function registerBuildsCommands(program: Command): void { runAction(async (ctx, uuid: string, pairs: string[], opts: { init?: boolean }) => { const build = await ctx.api.getBuild(uuid); const field = opts.init ? 'commentInitEnv' : 'commentRuntimeEnv'; - const merged: Record = { ...((opts.init ? build.commentInitEnv : build.commentRuntimeEnv) ?? {}) }; + const merged: Record = { + ...((opts.init ? build.commentInitEnv : build.commentRuntimeEnv) ?? {}), + }; for (const pair of pairs) { const eq = pair.indexOf('='); if (eq < 1) throw new Error(`Invalid pair "${pair}" — expected KEY=VALUE`); @@ -438,8 +464,11 @@ export function registerBuildsCommands(program: Command): void { } const updated = await ctx.api.patchBuild(uuid, { [field]: merged }); if (ctx.json) printJson({ runtime: updated.commentRuntimeEnv ?? {}, init: updated.commentInitEnv ?? {} }); - else process.stderr.write(`${pc.green('✓')} Updated ${opts.init ? 'init' : 'runtime'} env overrides on ${uuid} (${pairs.length} key${pairs.length > 1 ? 's' : ''})\n`); - }) + else + process.stderr.write( + `${pc.green('✓')} Updated ${opts.init ? 'init' : 'runtime'} env overrides on ${uuid} (${pairs.length} key${pairs.length > 1 ? 's' : ''})\n`, + ); + }), ); env @@ -450,11 +479,16 @@ export function registerBuildsCommands(program: Command): void { runAction(async (ctx, uuid: string, keys: string[], opts: { init?: boolean }) => { const build = await ctx.api.getBuild(uuid); const field = opts.init ? 'commentInitEnv' : 'commentRuntimeEnv'; - const current: Record = { ...((opts.init ? build.commentInitEnv : build.commentRuntimeEnv) ?? {}) }; + const current: Record = { + ...((opts.init ? build.commentInitEnv : build.commentRuntimeEnv) ?? {}), + }; for (const key of keys) delete current[key]; const updated = await ctx.api.patchBuild(uuid, { [field]: current }); if (ctx.json) printJson({ runtime: updated.commentRuntimeEnv ?? {}, init: updated.commentInitEnv ?? {} }); - else process.stderr.write(`${pc.green('✓')} Removed ${keys.join(', ')} from ${opts.init ? 'init' : 'runtime'} env overrides on ${uuid}\n`); - }) + else + process.stderr.write( + `${pc.green('✓')} Removed ${keys.join(', ')} from ${opts.init ? 'init' : 'runtime'} env overrides on ${uuid}\n`, + ); + }), ); } diff --git a/src/commands/config.ts b/src/commands/config.ts index a405694..4022733 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -36,7 +36,7 @@ export function registerConfigCommands(program: Command): void { .command('list') .description('List profiles and their settings') .action( - runAction(async (ctx) => { + runAction(async ctx => { if (ctx.json) { printJson({ currentProfile: ctx.config.currentProfile, profiles: ctx.config.profiles }); return; @@ -48,7 +48,7 @@ export function registerConfigCommands(program: Command): void { p.keycloak?.issuer ?? '', ]); process.stdout.write(renderTable(['profile', 'api url', 'auth', 'issuer'], rows) + '\n'); - }) + }), ); config @@ -64,7 +64,7 @@ export function registerConfigCommands(program: Command): void { const value = key.split('.').reduce((acc, part) => (acc as Record)?.[part], profile); if (ctx.json) printJson({ [key]: value ?? null }); else process.stdout.write(`${String(value ?? '')}\n`); - }) + }), ); config @@ -78,7 +78,7 @@ export function registerConfigCommands(program: Command): void { setKey(profile, key, value); saveConfig(cfg); process.stderr.write(`${pc.green('✓')} ${ctx.profileName}.${key} = ${value}\n`); - }) + }), ); config @@ -94,7 +94,7 @@ export function registerConfigCommands(program: Command): void { async ( _ctx, name: string, - opts: { apiUrl: string; uiUrl?: string; issuer?: string; clientId: string; auth?: boolean } + opts: { apiUrl: string; uiUrl?: string; issuer?: string; clientId: string; auth?: boolean }, ) => { const cfg = loadConfig(); if (cfg.profiles[name]) throw new Error(`Profile "${name}" already exists`); @@ -110,9 +110,11 @@ export function registerConfigCommands(program: Command): void { }; if (Object.keys(cfg.profiles).length === 1) cfg.currentProfile = name; saveConfig(cfg); - process.stderr.write(`${pc.green('✓')} Created profile "${name}" — switch with \`lfc config use-profile ${name}\`\n`); - } - ) + process.stderr.write( + `${pc.green('✓')} Created profile "${name}" — switch with \`lfc config use-profile ${name}\`\n`, + ); + }, + ), ); config @@ -125,6 +127,6 @@ export function registerConfigCommands(program: Command): void { cfg.currentProfile = name; saveConfig(cfg); process.stderr.write(`${pc.green('✓')} Active profile: ${name}\n`); - }) + }), ); } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index e21e13f..a32ac9b 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -30,11 +30,12 @@ export function registerDoctorCommand(program: Command): void { } else { process.stdout.write(`${pc.bold('lfc doctor')}\n`); for (const check of report.checks) printCheck(check); - const fixable = report.checks.some((check) => check.fixable && check.status !== 'ok'); - if (fixable && !opts.fix) process.stdout.write(pc.dim('\nRun `lfc doctor --fix` to repair fixable permission issues.\n')); + const fixable = report.checks.some(check => check.fixable && check.status !== 'ok'); + if (fixable && !opts.fix) + process.stdout.write(pc.dim('\nRun `lfc doctor --fix` to repair fixable permission issues.\n')); } - if (report.checks.some((check) => check.status === 'error')) process.exitCode = opts.fix ? 3 : 1; - else if (report.checks.some((check) => check.status === 'warn')) process.exitCode = 2; + if (report.checks.some(check => check.status === 'error')) process.exitCode = opts.fix ? 3 : 1; + else if (report.checks.some(check => check.status === 'warn')) process.exitCode = 2; }); } diff --git a/src/commands/init.ts b/src/commands/init.ts index d07f49b..e63fef7 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -2,14 +2,14 @@ import * as p from '@clack/prompts'; import { Command } from 'commander'; import pc from 'picocolors'; -import { decodeJwt, loginDevice, loginPkce, AuthError } from '../lib/auth.js'; +import { AuthError, decodeJwt, loginDevice, loginPkce } from '../lib/auth.js'; import { configDir, + DEFAULT_CLIENT_ID, + DEFAULT_PROFILE_NAME, loadConfig, saveConfig, saveTokens, - DEFAULT_CLIENT_ID, - DEFAULT_PROFILE_NAME, type Profile, } from '../lib/config.js'; import { link, printJson } from '../lib/output.js'; @@ -79,7 +79,7 @@ async function runInit(opts: InitOpts): Promise { const answer = await p.text({ message: 'Lifecycle app URL (serves /api/v2)', placeholder: 'https://app.lifecycle.example.com', - validate: (v) => validUrl(v ?? ''), + validate: v => validUrl(v ?? ''), }); if (cancelled(answer)) return; apiUrl = answer as string; @@ -92,7 +92,7 @@ async function runInit(opts: InitOpts): Promise { message: 'Lifecycle UI URL (optional, used by `lfc builds open`)', placeholder: 'https://ui.lifecycle.example.com — enter to skip', defaultValue: '', - validate: (v) => (v ? validUrl(v) : undefined), + validate: v => (v ? validUrl(v) : undefined), }); if (cancelled(answer)) return; uiUrl = (answer as string) || undefined; @@ -110,7 +110,7 @@ async function runInit(opts: InitOpts): Promise { const answer = await p.text({ message: 'Keycloak realm issuer URL', placeholder: 'https://auth.lifecycle.example.com/realms/lifecycle', - validate: (v) => validUrl(v ?? ''), + validate: v => validUrl(v ?? ''), }); if (cancelled(answer)) return; issuer = answer as string; @@ -146,13 +146,15 @@ async function runInit(opts: InitOpts): Promise { onPrompt: ({ userCode, verificationUri, verificationUriComplete }) => { process.stderr.write( `\nOn any device, open ${link(verificationUriComplete ?? verificationUri)}\n` + - `and confirm the code: ${pc.bold(userCode)}\n\nWaiting for approval...\n` + `and confirm the code: ${pc.bold(userCode)}\n\nWaiting for approval...\n`, ); }, }) : await loginPkce(kc, { onAuthUrl: () => { - process.stderr.write(`Opening your browser for SSO login... ${pc.dim('(lfc login --device for headless)')}\n`); + process.stderr.write( + `Opening your browser for SSO login... ${pc.dim('(lfc login --device for headless)')}\n`, + ); }, }); saveTokens(opts.name, tokens); diff --git a/src/commands/pods.ts b/src/commands/pods.ts index 9530263..89c30c9 100644 --- a/src/commands/pods.ts +++ b/src/commands/pods.ts @@ -8,7 +8,7 @@ import { printJson, renderTable, statusColor } from '../lib/output.js'; import type { PodInfo } from '../lib/types.js'; function podRows(pods: PodInfo[]): string[][] { - return pods.map((pod) => [ + return pods.map(pod => [ pod.podName, pod.serviceName ?? '', String(pod.ready), @@ -24,31 +24,31 @@ async function fetchPods(ctx: Ctx, uuid: string, service?: string): Promise { if (podName) { - const pod = pods.find((x) => x.podName === podName); + const pod = pods.find(x => x.podName === podName); if (!pod) throw new Error(`Pod ${podName} not found (run lfc pods list first)`); return pod; } if (pods.length === 1) return pods[0]!; if (!process.stdin.isTTY) { - throw new Error(`Multiple pods found — specify one: ${pods.map((x) => x.podName).join(', ')}`); + throw new Error(`Multiple pods found — specify one: ${pods.map(x => x.podName).join(', ')}`); } const picked = await p.select({ message: 'Select a pod', - options: pods.map((pod) => ({ + options: pods.map(pod => ({ value: pod.podName, label: pod.podName, hint: `${pod.serviceName ?? ''} ${pod.status} ${pod.ready} restarts:${pod.restarts}`.trim(), })), }); if (p.isCancel(picked)) throw new Error('cancelled'); - return pods.find((x) => x.podName === picked)!; + return pods.find(x => x.podName === picked)!; } async function pickContainer(pod: PodInfo, containerName?: string): Promise { const containers = pod.containers ?? []; if (containerName) { - if (containers.length > 0 && !containers.some((c) => c.name === containerName)) { - throw new Error(`Container ${containerName} not in pod (has: ${containers.map((c) => c.name).join(', ')})`); + if (containers.length > 0 && !containers.some(c => c.name === containerName)) { + throw new Error(`Container ${containerName} not in pod (has: ${containers.map(c => c.name).join(', ')})`); } return containerName; } @@ -62,7 +62,7 @@ async function pickContainer(pod: PodInfo, containerName?: string): Promise ({ value: c.name, label: c.name, hint: c.state })), + options: containers.map(c => ({ value: c.name, label: c.name, hint: c.state })), initialValue: containers[containers.length - 1]!.name, }); if (p.isCancel(picked)) throw new Error('cancelled'); @@ -70,7 +70,7 @@ async function pickContainer(pod: PodInfo, containerName?: string): Promise') @@ -87,14 +87,18 @@ export function registerPodsCommands(program: Command): void { process.stdout.write(pc.dim('No pods found.\n')); return; } - process.stdout.write(renderTable(['pod', 'service', 'ready', 'status', 'restarts', 'age'], podRows(list)) + '\n'); - const multi = list.filter((pod) => (pod.containers?.length ?? 0) > 1); + process.stdout.write( + renderTable(['pod', 'service', 'ready', 'status', 'restarts', 'age'], podRows(list)) + '\n', + ); + const multi = list.filter(pod => (pod.containers?.length ?? 0) > 1); for (const pod of multi) { process.stdout.write( - pc.dim(` ${pod.podName} containers: ${pod.containers.map((c) => `${c.name}(${c.state ?? '?'})`).join(', ')}\n`) + pc.dim( + ` ${pod.podName} containers: ${pod.containers.map(c => `${c.name}(${c.state ?? '?'})`).join(', ')}\n`, + ), ); } - }) + }), ); pods @@ -111,7 +115,7 @@ export function registerPodsCommands(program: Command): void { ctx, buildUuid: string, podName: string | undefined, - opts: { service?: string; container?: string; follow: boolean; tail: string; timestamps: boolean } + opts: { service?: string; container?: string; follow: boolean; tail: string; timestamps: boolean }, ) => { const build = await ctx.api.getBuild(buildUuid); const namespace = build.namespace || `env-${buildUuid}`; @@ -131,9 +135,9 @@ export function registerPodsCommands(program: Command): void { tailLines, timestamps: opts.timestamps, }, - { quiet: ctx.quiet || ctx.json } + { quiet: ctx.quiet || ctx.json }, ); - } - ) + }, + ), ); } diff --git a/src/commands/schema.ts b/src/commands/schema.ts index 7ac6b03..244c5ea 100644 --- a/src/commands/schema.ts +++ b/src/commands/schema.ts @@ -20,7 +20,7 @@ function reportResult(ctx: Ctx, source: string, result: SchemaValidationResult): } else { const n = result.errors.length; process.stdout.write( - `${pc.red('✗')} ${source} failed schema ${result.schemaVersion} validation (${n} error${n === 1 ? '' : 's'})\n` + `${pc.red('✗')} ${source} failed schema ${result.schemaVersion} validation (${n} error${n === 1 ? '' : 's'})\n`, ); for (const issue of result.errors) { process.stdout.write(` ${pc.yellow(issue.path)}: ${issue.message}\n`); @@ -43,7 +43,7 @@ async function validate(ctx: Ctx, target: string | undefined): Promise { } else { process.stdout.write( `${pc.red('✗')} ${opts.repo}@${opts.branch} failed validation ` + - `(run locally on a checkout for error details)\n` + `(run locally on a checkout for error details)\n`, ); } if (!valid) process.exitCode = 1; diff --git a/src/commands/services.ts b/src/commands/services.ts index e498f88..bac732a 100644 --- a/src/commands/services.ts +++ b/src/commands/services.ts @@ -12,16 +12,16 @@ export function registerServicesCommands(program: Command): void { services .command('list ') - .description('List a build\'s services with status and links') + .description("List a build's services with status and links") .option('--all', 'include inactive (disabled) services') .action( runAction(async (ctx, buildUuid: string, opts: { all?: boolean }) => { const build = await ctx.api.getBuild(buildUuid); - const deploys = (build.deploys ?? []).filter((d) => opts.all || d.active); + const deploys = (build.deploys ?? []).filter(d => opts.all || d.active); if (ctx.json) { printJson({ build: build.uuid, - services: deploys.map((d) => ({ + services: deploys.map(d => ({ name: d.deployable?.name, status: d.status, statusMessage: d.statusMessage, @@ -41,7 +41,7 @@ export function registerServicesCommands(program: Command): void { process.stdout.write(pc.dim('No services found.\n')); return; } - const rows = deploys.map((d) => [ + const rows = deploys.map(d => [ `${d.deployable?.name ?? d.uuid}${d.active ? '' : pc.dim(' (off)')}`, statusColor(d.status), d.branchName ?? '', @@ -51,7 +51,7 @@ export function registerServicesCommands(program: Command): void { ]); process.stdout.write(`${pc.bold(build.uuid)} ${statusColor(build.status)}\n\n`); process.stdout.write(renderTable(['service', 'status', 'branch', 'url', 'sha', 'updated'], rows) + '\n'); - }) + }), ); services @@ -64,9 +64,9 @@ export function registerServicesCommands(program: Command): void { else process.stderr.write( `${pc.green('✓')} Redeploy queued for service ${pc.bold(name)} in ${buildUuid}\n` + - pc.dim(` follow with: lfc builds status ${buildUuid} --watch\n`) + pc.dim(` follow with: lfc builds status ${buildUuid} --watch\n`), ); - }) + }), ); services @@ -77,7 +77,7 @@ export function registerServicesCommands(program: Command): void { const result = await ctx.api.patchServiceOverrides(buildUuid, [{ name, active: true }]); if (ctx.json) printJson(result); else process.stderr.write(`${pc.green('✓')} Enabled ${name} in ${buildUuid}\n`); - }) + }), ); services @@ -88,7 +88,7 @@ export function registerServicesCommands(program: Command): void { const result = await ctx.api.patchServiceOverrides(buildUuid, [{ name, active: false }]); if (ctx.json) printJson(result); else process.stderr.write(`${pc.green('✓')} Disabled ${name} in ${buildUuid}\n`); - }) + }), ); services @@ -107,7 +107,7 @@ export function registerServicesCommands(program: Command): void { process.stdout.write(pc.bold('build jobs\n')); if (buildJobs.length === 0) process.stdout.write(pc.dim(' (none)\n')); else { - const rows = buildJobs.map((j) => [ + const rows = buildJobs.map(j => [ j.jobName, statusColor(j.status), j.sha?.slice(0, 8) ?? '', @@ -121,7 +121,7 @@ export function registerServicesCommands(program: Command): void { process.stdout.write(pc.bold('\ndeploy jobs\n')); if (deployJobs.length === 0) process.stdout.write(pc.dim(' (none)\n')); else { - const rows = deployJobs.map((j) => [ + const rows = deployJobs.map(j => [ j.jobName, statusColor(j.status), j.sha?.slice(0, 8) ?? '', @@ -132,14 +132,14 @@ export function registerServicesCommands(program: Command): void { ]); process.stdout.write(renderTable(['job', 'status', 'sha', 'type', 'duration', 'started', ''], rows) + '\n'); } - const failed = [...buildJobs, ...deployJobs].filter((j) => j.error); + const failed = [...buildJobs, ...deployJobs].filter(j => j.error); for (const j of failed) process.stdout.write(pc.red(`\n${j.jobName}: ${j.error}\n`)); - }) + }), ); services .command('logs ') - .description('Show logs for a service\'s latest build or deploy job (archived or live)') + .description("Show logs for a service's latest build or deploy job (archived or live)") .option('--deploy', 'deploy-job logs instead of build-job logs', false) .option('--job ', 'a specific job (see lfc services history)') .option('-f, --follow', 'keep following while the job is running', false) @@ -151,7 +151,7 @@ export function registerServicesCommands(program: Command): void { ctx, buildUuid: string, name: string, - opts: { deploy: boolean; job?: string; follow: boolean; tail: string; timestamps: boolean } + opts: { deploy: boolean; job?: string; follow: boolean; tail: string; timestamps: boolean }, ) => { const kind = opts.deploy ? 'deploy' : 'build'; let jobName = opts.job; @@ -167,8 +167,8 @@ export function registerServicesCommands(program: Command): void { if (!jobName) throw new Error(`No ${kind} job selected for ${name}`); const info = await ctx.api.getJobLogInfo(buildUuid, name, jobName, kind); await printJobLogs(ctx, buildUuid, jobName, info, opts); - } - ) + }, + ), ); services @@ -180,7 +180,7 @@ export function registerServicesCommands(program: Command): void { throw new Error('services edit is interactive — use services enable/disable/set-branch in scripts'); } await editServicesInteractive(ctx, buildUuid); - }) + }), ); services @@ -191,7 +191,7 @@ export function registerServicesCommands(program: Command): void { const result = await ctx.api.patchServiceOverrides(buildUuid, [{ name, branchOrExternalUrl: branchOrUrl }]); if (ctx.json) printJson(result); else process.stderr.write(`${pc.green('✓')} ${name} in ${buildUuid} now tracks ${pc.bold(branchOrUrl)}\n`); - }) + }), ); } @@ -200,7 +200,7 @@ async function printJobLogs( buildUuid: string, jobName: string, info: LogStreamInfo, - opts: { follow: boolean; tail: string; timestamps: boolean } + opts: { follow: boolean; tail: string; timestamps: boolean }, ): Promise { if (info.archivedLogs !== undefined) { if (!ctx.quiet && !ctx.json) process.stderr.write(pc.dim(`— archived logs for ${jobName} —\n`)); @@ -225,16 +225,16 @@ async function printJobLogs( tailLines, timestamps: opts.timestamps, }, - { quiet: ctx.quiet || ctx.json } + { quiet: ctx.quiet || ctx.json }, ); } async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise { const build = await ctx.api.getBuild(buildUuid); - const deploys = (build.deploys ?? []).filter((d) => d.deployable?.name); + const deploys = (build.deploys ?? []).filter(d => d.deployable?.name); if (deploys.length === 0) throw new Error('No services found for this build'); - const current = deploys.map((d) => ({ + const current = deploys.map(d => ({ name: d.deployable!.name, active: d.active, branch: d.serviceOverride?.branchOrExternalUrl ?? d.branchName ?? '', @@ -244,8 +244,8 @@ async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise ({ value: s.name, label: s.name, hint: s.branch })), - initialValues: current.filter((s) => s.active).map((s) => s.name), + options: current.map(s => ({ value: s.name, label: s.name, hint: s.branch })), + initialValues: current.filter(s => s.active).map(s => s.name), required: false, }); if (p.isCancel(selected)) { @@ -256,12 +256,12 @@ async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise(); for (;;) { - const editable = current.filter((s) => activeSet.has(s.name)); + const editable = current.filter(s => activeSet.has(s.name)); const choice = await p.select({ message: 'Edit a service branch?', options: [ { value: '', label: 'No — apply changes' }, - ...editable.map((s) => ({ + ...editable.map(s => ({ value: s.name, label: s.name, hint: branchEdits.get(s.name) ?? s.branch, @@ -273,7 +273,7 @@ async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise s.name === choice)!; + const svc = current.find(s => s.name === choice)!; const branch = await p.text({ message: `Branch or external URL for ${choice}`, initialValue: branchEdits.get(choice as string) ?? svc.branch, @@ -299,7 +299,7 @@ async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise { + .map(o => { const parts: string[] = []; if (o.active !== undefined) parts.push(o.active ? pc.green('enable') : pc.red('disable')); if (o.branchOrExternalUrl !== undefined) parts.push(`branch → ${pc.bold(o.branchOrExternalUrl)}`); @@ -314,5 +314,7 @@ async function editServicesInteractive(ctx: Ctx, buildUuid: string): Promise', 'page number', (v) => Number(v), 1) - .option('-n, --limit ', 'items per page', (v) => Number(v), 25) + .option('-p, --page ', 'page number', v => Number(v), 1) + .option('-n, --limit ', 'items per page', v => Number(v), 25) .action( runAction(async (ctx, opts: { mine?: boolean; page: number; limit: number }) => { const user = opts.mine ? userEmail(ctx) : undefined; @@ -75,7 +90,7 @@ export function registerSitesCommands(program: Command): void { process.stdout.write(pc.dim('No sites found.\n')); return; } - const rows = items.map((s) => [ + const rows = items.map(s => [ pc.bold(s.id), s.name ?? '', statusColor(s.status), @@ -84,11 +99,15 @@ export function registerSitesCommands(program: Command): void { s.expiresAt ? formatAge(s.expiresAt).replace(' ago', '') : '∞', s.createdBy ?? '', ]); - process.stdout.write(renderTable(['id', 'name', 'status', 'url', 'size', 'expires in', 'created by'], rows) + '\n'); + process.stdout.write( + renderTable(['id', 'name', 'status', 'url', 'size', 'expires in', 'created by'], rows) + '\n', + ); if (pagination?.totalPages && Number(pagination.totalPages) > 1) { - process.stdout.write(pc.dim(`page ${pagination.page}/${pagination.totalPages} · ${pagination.totalItems} total\n`)); + process.stdout.write( + pc.dim(`page ${pagination.page}/${pagination.totalPages} · ${pagination.totalItems} total\n`), + ); } - }) + }), ); sites @@ -105,12 +124,12 @@ export function registerSitesCommands(program: Command): void { } finally { await upload.cleanup(); } - }) + }), ); sites .command('get ') - .description('Show a site\'s details') + .description("Show a site's details") .action( runAction(async (ctx, siteId: string) => { const site = await ctx.api.getSite(siteId); @@ -130,12 +149,12 @@ export function registerSitesCommands(program: Command): void { for (const [k, v] of fields) { if (v.trim()) process.stdout.write(` ${pc.dim(k.padEnd(8))} ${v}\n`); } - }) + }), ); sites .command('update ') - .description('Replace a site\'s content with a new .zip, .html file, or directory') + .description("Replace a site's content with a new .zip, .html file, or directory") .action( runAction(async (ctx, siteId: string, target: string) => { const upload = await prepareUpload(ctx, target); @@ -146,21 +165,21 @@ export function registerSitesCommands(program: Command): void { } finally { await upload.cleanup(); } - }) + }), ); sites .command('extend ') - .description('Extend a site\'s expiration (TTL)') + .description("Extend a site's expiration (TTL)") .action( runAction(async (ctx, siteId: string) => { const site = await ctx.api.extendSite(siteId); if (ctx.json) printJson(site); else process.stderr.write( - `${pc.green('✓')} Extended ${pc.bold(site.id)} — now expires ${site.expiresAt ? new Date(site.expiresAt).toLocaleString() : 'never'}\n` + `${pc.green('✓')} Extended ${pc.bold(site.id)} — now expires ${site.expiresAt ? new Date(site.expiresAt).toLocaleString() : 'never'}\n`, ); - }) + }), ); sites @@ -180,6 +199,6 @@ export function registerSitesCommands(program: Command): void { const site = await ctx.api.deleteSite(siteId); if (ctx.json) printJson(site); else process.stderr.write(`${pc.green('✓')} Deleted site ${siteId}\n`); - }) + }), ); } diff --git a/src/index.ts b/src/index.ts index 2168ecd..a8eebec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import { Command } from 'commander'; +import pkg from '../package.json' with { type: 'json' }; + import { registerAuthCommands } from './commands/auth.js'; import { registerBuildsCommands } from './commands/builds.js'; import { registerConfigCommands } from './commands/config.js'; @@ -11,8 +13,6 @@ import { registerSchemaCommands } from './commands/schema.js'; import { registerServicesCommands } from './commands/services.js'; import { registerSitesCommands } from './commands/sites.js'; -import pkg from '../package.json' with { type: 'json' }; - const program = new Command(); program diff --git a/src/lib/api.ts b/src/lib/api.ts index a0206ba..1ae5f76 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -22,7 +22,7 @@ export class ApiError extends Error { message: string, public readonly status: number, public readonly requestId?: string, - public readonly code?: string + public readonly code?: string, ) { super(message); } @@ -43,14 +43,19 @@ export class ApiClient { constructor( private readonly profileName: string, private readonly profile: Profile, - private readonly apiUrlOverride?: string + private readonly apiUrlOverride?: string, ) {} get baseUrl(): string { return (this.apiUrlOverride || process.env.LIFECYCLE_API_URL || this.profile.apiUrl).replace(/\/$/, ''); } - private async request(method: string, path: string, opts: RequestOptions = {}, retried = false): Promise> { + private async request( + method: string, + path: string, + opts: RequestOptions = {}, + retried = false, + ): Promise> { const url = new URL(this.baseUrl + path); for (const [k, v] of Object.entries(opts.query ?? {})) { if (v !== undefined) url.searchParams.set(k, String(v)); @@ -85,9 +90,7 @@ export class ApiClient { } const envelope = - parsed && - typeof parsed === 'object' && - ('request_id' in parsed || 'data' in parsed || 'error' in parsed) + parsed && typeof parsed === 'object' && ('request_id' in parsed || 'data' in parsed || 'error' in parsed) ? (parsed as ApiEnvelope) : ({ request_id: '', data: parsed as T, error: null } satisfies ApiEnvelope); @@ -96,7 +99,7 @@ export class ApiClient { envelope.error?.message ?? `${res.status} ${res.statusText}`, res.status, envelope.request_id, - envelope.error?.code + envelope.error?.code, ); } return envelope; @@ -132,7 +135,7 @@ export class ApiClient { */ async resolveBuild( selector: Selector, - opts: { limit?: number; maxPages?: number } = {} + opts: { limit?: number; maxPages?: number } = {}, ): Promise<{ matches: BuildListItem[]; truncated: boolean }> { const limit = opts.limit ?? 100; const maxPages = opts.maxPages ?? 20; @@ -171,19 +174,19 @@ export class ApiClient { async redeployService(uuid: string, service: string): Promise { const env = await this.request( 'PUT', - `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/redeploy` + `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/redeploy`, ); return env.data; } async patchServiceOverrides( uuid: string, - overrides: Array<{ name: string; active?: boolean; branchOrExternalUrl?: string }> + overrides: Array<{ name: string; active?: boolean; branchOrExternalUrl?: string }>, ): Promise<{ serviceOverrides: ServiceOverrideState[]; queued?: unknown }> { const env = await this.request<{ serviceOverrides: ServiceOverrideState[]; queued?: unknown }>( 'PATCH', `/api/v2/builds/${encodeURIComponent(uuid)}/services`, - { json: { serviceOverrides: overrides } } + { json: { serviceOverrides: overrides } }, ); return env.data as { serviceOverrides: ServiceOverrideState[]; queued?: unknown }; } @@ -198,7 +201,7 @@ export class ApiClient { async listServicePods(uuid: string, service: string): Promise { const env = await this.request<{ pods: PodInfo[] }>( 'GET', - `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/pods` + `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/pods`, ); return env.data?.pods ?? []; } @@ -206,7 +209,7 @@ export class ApiClient { async listBuildJobs(uuid: string, service: string): Promise { const env = await this.request<{ builds: BuildJobInfo[] }>( 'GET', - `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/build-jobs` + `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/build-jobs`, ); return env.data?.builds ?? []; } @@ -214,16 +217,21 @@ export class ApiClient { async listDeployJobs(uuid: string, service: string): Promise { const env = await this.request<{ deployments: DeployJobInfo[] }>( 'GET', - `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/deploy-jobs` + `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/deploy-jobs`, ); return env.data?.deployments ?? []; } - async getJobLogInfo(uuid: string, service: string, jobName: string, kind: 'build' | 'deploy'): Promise { + async getJobLogInfo( + uuid: string, + service: string, + jobName: string, + kind: 'build' | 'deploy', + ): Promise { const segment = kind === 'build' ? 'build-jobs' : 'deploy-jobs'; const env = await this.request( 'GET', - `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/${segment}/${encodeURIComponent(jobName)}` + `/api/v2/builds/${encodeURIComponent(uuid)}/services/${encodeURIComponent(service)}/${segment}/${encodeURIComponent(jobName)}`, ); return env.data as LogStreamInfo; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index adc3eb6..409fede 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -102,14 +102,14 @@ const SUCCESS_HTML = `lfc login */ export async function loginPkce( kc: KeycloakSettings, - opts: { noBrowser?: boolean; onAuthUrl: (url: string) => void } + opts: { noBrowser?: boolean; onAuthUrl: (url: string) => void }, ): Promise { const oidc = await discover(kc.issuer); const { verifier, challenge } = pkcePair(); const state = b64url(randomBytes(16)); const server = http.createServer(); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const port = (server.address() as AddressInfo).port; const redirectUri = `http://127.0.0.1:${port}/callback`; @@ -179,7 +179,7 @@ interface DeviceAuthResponse { */ export async function loginDevice( kc: KeycloakSettings, - opts: { onPrompt: (info: { userCode: string; verificationUri: string; verificationUriComplete?: string }) => void } + opts: { onPrompt: (info: { userCode: string; verificationUri: string; verificationUriComplete?: string }) => void }, ): Promise { const oidc = await discover(kc.issuer); if (!oidc.device_authorization_endpoint) { @@ -212,7 +212,7 @@ export async function loginDevice( let intervalMs = (device.interval ?? 5) * 1000; while (Date.now() < deadline) { - await new Promise((r) => setTimeout(r, intervalMs)); + await new Promise(r => setTimeout(r, intervalMs)); const token = await postForm(oidc.token_endpoint, { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', client_id: kc.clientId, @@ -251,7 +251,7 @@ export async function refreshTokens(kc: KeycloakSettings, tokens: TokenSet): Pro export async function getAccessToken( profileName: string, profile: Profile, - { forceRefresh = false }: { forceRefresh?: boolean } = {} + { forceRefresh = false }: { forceRefresh?: boolean } = {}, ): Promise { if (!profile.authEnabled) return null; if (!profile.keycloak) throw new AuthError('Profile has authEnabled but no keycloak settings'); diff --git a/src/lib/config.ts b/src/lib/config.ts index 15e2d9d..50d679d 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -74,7 +74,11 @@ export function saveConfig(config: ConfigFile): void { writePrivateJson(configPath(), config); } -export function resolveProfile(config: ConfigFile, name?: string, apiUrlOverride?: string): { name: string; profile: Profile } { +export function resolveProfile( + config: ConfigFile, + name?: string, + apiUrlOverride?: string, +): { name: string; profile: Profile } { const profileName = name || process.env.LFC_PROFILE || config.currentProfile; const profile = config.profiles[profileName]; if (!profile) { diff --git a/src/lib/context.ts b/src/lib/context.ts index eec856c..06f19d5 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -36,7 +36,7 @@ export function buildCtx(cmd: Command): Ctx { /** Wrap a command action: build ctx, run, and convert known errors into friendly exits. */ export function runAction( - fn: (ctx: Ctx, ...args: A) => Promise + fn: (ctx: Ctx, ...args: A) => Promise, ): (...args: [...A, Command]) => Promise { return async (...args) => { const cmd = args[args.length - 1] as Command; @@ -57,7 +57,13 @@ export function runAction( process.stderr.write(`${pc.red(`api error (${err.status}):`)} ${err.message}${reqId}\n`); const exitCode = err.status === 404 ? 3 : 1; process.exitCode = exitCode; - outcome = { status: 'error', exitCode, errorClass: 'ApiError', errorHttpStatus: err.status, errorCode: err.code }; + outcome = { + status: 'error', + exitCode, + errorClass: 'ApiError', + errorHttpStatus: err.status, + errorCode: err.code, + }; } else { process.stderr.write(`${pc.red('error:')} ${(err as Error).message}\n`); process.exitCode = 1; diff --git a/src/lib/logs.ts b/src/lib/logs.ts index 9dcab7a..bc76069 100644 --- a/src/lib/logs.ts +++ b/src/lib/logs.ts @@ -45,7 +45,7 @@ export function parseWsLogMessage(raw: string): WsLogMessage | null { export function streamPodLogs( apiBaseUrl: string, params: LogStreamParams, - opts: { prefix?: string; quiet?: boolean } = {} + opts: { prefix?: string; quiet?: boolean } = {}, ): Promise { const url = logStreamWsUrl(apiBaseUrl, params); const prefix = opts.prefix ? `${opts.prefix} ` : ''; @@ -71,7 +71,7 @@ export function streamPodLogs( process.stderr.write(pc.dim(`— streaming ${params.podName}/${params.containerName} (Ctrl-C to stop) —\n`)); } }; - ws.onmessage = (event) => { + ws.onmessage = event => { const msg = parseWsLogMessage(String(event.data)); if (!msg) return; if (msg.type === 'log') { diff --git a/src/lib/output.ts b/src/lib/output.ts index 1618818..29288dd 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -12,9 +12,7 @@ function pad(s: string, width: number): string { } export function renderTable(headers: string[], rows: string[][]): string { - const widths = headers.map((h, i) => - Math.max(visibleLength(h), ...rows.map((r) => visibleLength(r[i] ?? ''))) - ); + const widths = headers.map((h, i) => Math.max(visibleLength(h), ...rows.map(r => visibleLength(r[i] ?? '')))); const lines: string[] = []; lines.push(headers.map((h, i) => pc.bold(pad(h.toUpperCase(), widths[i] ?? 0))).join(' ')); for (const row of rows) { diff --git a/src/lib/resolve.ts b/src/lib/resolve.ts index 22106fc..96edcc7 100644 --- a/src/lib/resolve.ts +++ b/src/lib/resolve.ts @@ -21,7 +21,10 @@ const REPO_RE = /^[^/\s]+\/[^/\s]+$/; const TORN_DOWN = new Set(['torn_down', 'deleted']); function normalizeRepo(repo: string): string { - return repo.trim().replace(/\.git$/i, '').toLowerCase(); + return repo + .trim() + .replace(/\.git$/i, '') + .toLowerCase(); } function assertRepo(repo: string): string { @@ -84,7 +87,7 @@ export function parseSelector(input: SelectorInput): Selector { */ export function matchBuilds(builds: BuildListItem[], selector: Selector): BuildListItem[] { const repo = normalizeRepo(selector.repo); - return builds.filter((b) => { + return builds.filter(b => { const pr = b.pullRequest; if (!pr || !pr.fullName) return false; if (pr.fullName.toLowerCase() !== repo) return false; diff --git a/src/lib/schema.ts b/src/lib/schema.ts index f963321..cbc20b1 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -15,7 +15,7 @@ function stripComments(node: unknown): unknown { return Object.fromEntries( Object.entries(node) .filter(([key]) => key !== 'comment') - .map(([key, value]) => [key, stripComments(value)]) + .map(([key, value]) => [key, stripComments(value)]), ); } return node; @@ -61,12 +61,12 @@ const WEBHOOK_STATES = [ const DISK_ACCESS_MODES = ['readwriteonce', 'readonlymany', 'readwritemany', 'readwriteoncepod']; const CAPACITY_TYPES = ['on_demand', 'spot']; -JsonSchema.Validator.prototype.customFormats.webhookType = (input) => WEBHOOK_TYPES.includes(input); -JsonSchema.Validator.prototype.customFormats.webhookState = (input) => +JsonSchema.Validator.prototype.customFormats.webhookType = input => WEBHOOK_TYPES.includes(input); +JsonSchema.Validator.prototype.customFormats.webhookState = input => typeof input === 'string' && WEBHOOK_STATES.includes(input.toLowerCase()); -JsonSchema.Validator.prototype.customFormats.diskAccessMode = (input) => +JsonSchema.Validator.prototype.customFormats.diskAccessMode = input => typeof input === 'string' && DISK_ACCESS_MODES.includes(input.toLowerCase()); -JsonSchema.Validator.prototype.customFormats.capacityType = (input) => +JsonSchema.Validator.prototype.customFormats.capacityType = input => typeof input === 'string' && CAPACITY_TYPES.includes(input.toLowerCase()); /** @@ -91,7 +91,7 @@ export function findConfigFile(input?: string): string { if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate; } throw new ConfigFileNotFoundError( - `No lifecycle config found in ${target} (looked for ${CONFIG_FILE_CANDIDATES.join(', ')})` + `No lifecycle config found in ${target} (looked for ${CONFIG_FILE_CANDIDATES.join(', ')})`, ); } @@ -128,7 +128,7 @@ export function validateConfigContent(content: string): SchemaValidationResult { valid: result.valid, schemaVersion: '1.0.0', declaredVersion, - errors: result.errors.map((e) => ({ + errors: result.errors.map(e => ({ path: e.property.replace(/^instance\.?/, '') || '(root)', message: e.message, })), diff --git a/src/lib/schema/lifecycle-1.0.0.json b/src/lib/schema/lifecycle-1.0.0.json index b23d9ec..bcbd487 100644 --- a/src/lib/schema/lifecycle-1.0.0.json +++ b/src/lib/schema/lifecycle-1.0.0.json @@ -44,9 +44,7 @@ "type": "number" } }, - "required": [ - "name" - ] + "required": ["name"] } }, "optionalServices": { @@ -69,9 +67,7 @@ "type": "number" } }, - "required": [ - "name" - ] + "required": ["name"] } }, "webhooks": { @@ -124,9 +120,7 @@ "type": "number" } }, - "required": [ - "image" - ] + "required": ["image"] }, "command": { "type": "object", @@ -142,20 +136,13 @@ "type": "number" } }, - "required": [ - "image", - "script" - ] + "required": ["image", "script"] }, "env": { "type": "object" } }, - "required": [ - "state", - "type", - "env" - ] + "required": ["state", "type", "env"] } }, "agentSession": { @@ -235,11 +222,7 @@ "minLength": 1 } }, - "required": [ - "repo", - "branch", - "path" - ] + "required": ["repo", "branch", "path"] } } } @@ -279,9 +262,7 @@ "type": "string" } }, - "required": [ - "name" - ] + "required": ["name"] } }, "deploymentDependsOn": { @@ -342,9 +323,7 @@ } } }, - "required": [ - "name" - ] + "required": ["name"] }, "envLens": { "type": "boolean" @@ -377,11 +356,7 @@ "properties": { "engine": { "type": "string", - "enum": [ - "buildkit", - "kaniko", - "ci" - ] + "enum": ["buildkit", "kaniko", "ci"] }, "resources": { "type": "object", @@ -439,9 +414,7 @@ "minItems": 1 } }, - "required": [ - "dockerfilePath" - ] + "required": ["dockerfilePath"] }, "init": { "type": "object", @@ -460,22 +433,14 @@ "type": "object" } }, - "required": [ - "dockerfilePath" - ] + "required": ["dockerfilePath"] } }, - "required": [ - "defaultTag", - "app" - ] + "required": ["defaultTag", "app"] }, "deploymentMethod": { "type": "string", - "enum": [ - "native", - "ci" - ] + "enum": ["native", "ci"] }, "nativeHelm": { "type": "object", @@ -761,11 +726,7 @@ "type": "string" } }, - "required": [ - "name", - "mountPath", - "storageSize" - ] + "required": ["name", "mountPath", "storageSize"] } }, "node_selector": { @@ -781,10 +742,7 @@ } } }, - "required": [ - "repository", - "branchName" - ] + "required": ["repository", "branchName"] }, "github": { "type": "object", @@ -815,11 +773,7 @@ "properties": { "engine": { "type": "string", - "enum": [ - "buildkit", - "kaniko", - "ci" - ] + "enum": ["buildkit", "kaniko", "ci"] }, "resources": { "type": "object", @@ -877,9 +831,7 @@ "minItems": 1 } }, - "required": [ - "dockerfilePath" - ] + "required": ["dockerfilePath"] }, "init": { "type": "object", @@ -898,15 +850,10 @@ "type": "object" } }, - "required": [ - "dockerfilePath" - ] + "required": ["dockerfilePath"] } }, - "required": [ - "defaultTag", - "app" - ] + "required": ["defaultTag", "app"] }, "deployment": { "type": "object", @@ -1111,11 +1058,7 @@ "type": "string" } }, - "required": [ - "name", - "mountPath", - "storageSize" - ] + "required": ["name", "mountPath", "storageSize"] } }, "node_selector": { @@ -1134,11 +1077,7 @@ "type": "boolean" } }, - "required": [ - "repository", - "branchName", - "docker" - ] + "required": ["repository", "branchName", "docker"] }, "docker": { "type": "object", @@ -1365,11 +1304,7 @@ "type": "string" } }, - "required": [ - "name", - "mountPath", - "storageSize" - ] + "required": ["name", "mountPath", "storageSize"] } }, "node_selector": { @@ -1388,10 +1323,7 @@ "type": "boolean" } }, - "required": [ - "dockerImage", - "defaultTag" - ] + "required": ["dockerImage", "defaultTag"] }, "externalHttp": { "type": "object", @@ -1404,10 +1336,7 @@ "type": "string" } }, - "required": [ - "defaultInternalHostname", - "defaultPublicUrl" - ] + "required": ["defaultInternalHostname", "defaultPublicUrl"] }, "auroraRestore": { "type": "object", @@ -1420,10 +1349,7 @@ "type": "string" } }, - "required": [ - "command", - "arguments" - ] + "required": ["command", "arguments"] }, "configuration": { "type": "object", @@ -1436,10 +1362,7 @@ "type": "string" } }, - "required": [ - "defaultTag", - "branchName" - ] + "required": ["defaultTag", "branchName"] }, "dev": { "type": "object", @@ -1509,20 +1432,13 @@ "minLength": 1 } }, - "required": [ - "repo", - "branch", - "path" - ] + "required": ["repo", "branch", "path"] } } } } }, - "required": [ - "image", - "command" - ] + "required": ["image", "command"] }, "ignoreFiles": { "type": "array", @@ -1531,10 +1447,8 @@ } } }, - "required": [ - "name" - ] + "required": ["name"] } } } -} \ No newline at end of file +} diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index 6c54796..f971a71 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -2,12 +2,12 @@ import crypto from 'node:crypto'; import type { Command } from 'commander'; +import pkg from '../../package.json' with { type: 'json' }; + import { loadConfig, loadTokens, saveConfig } from './config.js'; import type { Ctx } from './context.js'; import type { CreateTelemetryEventBody } from './generated/index.js'; -import pkg from '../../package.json' with { type: 'json' }; - export const TELEMETRY_TIMEOUT_MS = 3000; export interface TelemetryOutcome { @@ -77,7 +77,7 @@ export async function reportInvocation( ctx: Ctx | undefined, cmd: Command, durationMs: number, - outcome: TelemetryOutcome + outcome: TelemetryOutcome, ): Promise { if (isTelemetryDisabled() || !ctx) return; diff --git a/src/lib/types.ts b/src/lib/types.ts index 500d4bd..3faa5d8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,16 +1,16 @@ import type { - Build as GeneratedBuild, - Deploy as GeneratedDeploy, - Deployable as GeneratedDeployable, DeploymentJobInfo, DeploymentPodContainerInfo, EnvironmentPodInfo, + Build as GeneratedBuild, + Deploy as GeneratedDeploy, + Deployable as GeneratedDeployable, + PaginationMetadata as GeneratedPaginationMetadata, + SitesConfig as GeneratedSitesConfig, LogStreamResponse, NativeBuildJobInfo, - PaginationMetadata as GeneratedPaginationMetadata, PullRequest, Site, - SitesConfig as GeneratedSitesConfig, SitesUploadConfig, WebhookInvocation, } from './generated/index.js'; @@ -61,7 +61,16 @@ export interface ServiceOverrideState { export type Deploy = Omit< Partial, - 'deployable' | 'repository' | 'serviceOverride' | 'status' | 'statusMessage' | 'cname' | 'branchName' | 'publicUrl' | 'dockerImage' | 'sha' + | 'deployable' + | 'repository' + | 'serviceOverride' + | 'status' + | 'statusMessage' + | 'cname' + | 'branchName' + | 'publicUrl' + | 'dockerImage' + | 'sha' > & { id?: number; uuid: string; diff --git a/src/lib/zip.ts b/src/lib/zip.ts index 8ede5d5..759e5ca 100644 --- a/src/lib/zip.ts +++ b/src/lib/zip.ts @@ -1,5 +1,5 @@ import { createWriteStream } from 'node:fs'; -import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -43,9 +43,11 @@ function extensionFor(filePath: string): string { function assertAllowedExtension(filePath: string, allowedExtensions: string[]): void { const ext = extensionFor(filePath); - const allowed = new Set(allowedExtensions.map((item) => item.toLowerCase().replace(/^\./, ''))); + const allowed = new Set(allowedExtensions.map(item => item.toLowerCase().replace(/^\./, ''))); if (!ext || !allowed.has(ext)) { - throw new Error(`Unsupported file type "${ext ? `.${ext}` : '(none)'}" — allowed extensions: ${allowedExtensions.join(', ')}`); + throw new Error( + `Unsupported file type "${ext ? `.${ext}` : '(none)'}" — allowed extensions: ${allowedExtensions.join(', ')}`, + ); } } @@ -66,7 +68,9 @@ async function loadSiteIgnore(root: string): Promise { } async function collectFiles(root: string, allowedExtensions: string[]): Promise { - const ig = createIgnore().add(DEFAULT_SITE_IGNORE_PATTERNS).add(await loadSiteIgnore(root)); + const ig = createIgnore() + .add(DEFAULT_SITE_IGNORE_PATTERNS) + .add(await loadSiteIgnore(root)); const files: SiteUploadFile[] = []; async function walk(dir: string): Promise { @@ -135,7 +139,9 @@ export async function prepareSiteUpload(target: string, config: SitesCliConfig): const files = await collectFiles(resolved, allowedExtensions); if (files.length === 0) throw new Error('No uploadable files found'); if (files.length > maxFiles) { - throw new Error(`Directory contains ${files.length} uploadable files, which exceeds the configured limit of ${maxFiles}`); + throw new Error( + `Directory contains ${files.length} uploadable files, which exceeds the configured limit of ${maxFiles}`, + ); } const extractedBytes = files.reduce((total, file) => total + file.size, 0); diff --git a/tests/api.test.ts b/tests/api.test.ts index 0a57eac..a2a8edb 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -6,7 +6,10 @@ import type { BuildListItem } from '../src/lib/types.js'; const PROFILE: Profile = { apiUrl: 'http://example.test', authEnabled: false }; -function mk(uuid: string, pr: { fullName: string; pullRequestNumber?: number; branchName?: string } | null): BuildListItem { +function mk( + uuid: string, + pr: { fullName: string; pullRequestNumber?: number; branchName?: string } | null, +): BuildListItem { return { uuid, status: 'deployed', @@ -44,33 +47,51 @@ describe('ApiClient.resolveBuild', () => { [mk('a', { fullName: 'acme/repo', branchName: 'x' }), mk('b', { fullName: 'acme/repo', branchName: 'y' })], [mk('c', { fullName: 'acme/repo', branchName: 'z' })], ]); - const { matches, truncated } = await client.resolveBuild({ repo: 'acme/repo', branch: 'z' }, { limit: 2, maxPages: 5 }); - expect(matches.map((m) => m.uuid)).toEqual(['c']); + const { matches, truncated } = await client.resolveBuild( + { repo: 'acme/repo', branch: 'z' }, + { limit: 2, maxPages: 5 }, + ); + expect(matches.map(m => m.uuid)).toEqual(['c']); expect(truncated).toBe(false); }); it('collects matches spread across multiple full pages', async () => { const client = clientWithPages([ - [mk('a', { fullName: 'acme/repo', pullRequestNumber: 7 }), mk('z', { fullName: 'other/repo', pullRequestNumber: 7 })], + [ + mk('a', { fullName: 'acme/repo', pullRequestNumber: 7 }), + mk('z', { fullName: 'other/repo', pullRequestNumber: 7 }), + ], [mk('c', { fullName: 'acme/repo', pullRequestNumber: 7 })], ]); const { matches } = await client.resolveBuild({ repo: 'acme/repo', prNumber: 7 }, { limit: 2, maxPages: 5 }); - expect(matches.map((m) => m.uuid).sort()).toEqual(['a', 'c']); + expect(matches.map(m => m.uuid).sort()).toEqual(['a', 'c']); }); it('flags truncated when the page cap is hit with full pages', async () => { const client = clientWithPages([ - [mk('a', { fullName: 'acme/repo', pullRequestNumber: 1 }), mk('b', { fullName: 'acme/repo', pullRequestNumber: 2 })], - [mk('c', { fullName: 'acme/repo', pullRequestNumber: 3 }), mk('d', { fullName: 'acme/repo', pullRequestNumber: 4 })], + [ + mk('a', { fullName: 'acme/repo', pullRequestNumber: 1 }), + mk('b', { fullName: 'acme/repo', pullRequestNumber: 2 }), + ], + [ + mk('c', { fullName: 'acme/repo', pullRequestNumber: 3 }), + mk('d', { fullName: 'acme/repo', pullRequestNumber: 4 }), + ], ]); - const { matches, truncated } = await client.resolveBuild({ repo: 'acme/repo', prNumber: 99 }, { limit: 2, maxPages: 2 }); + const { matches, truncated } = await client.resolveBuild( + { repo: 'acme/repo', prNumber: 99 }, + { limit: 2, maxPages: 2 }, + ); expect(matches).toEqual([]); expect(truncated).toBe(true); }); it('handles an empty first page', async () => { const client = clientWithPages([[]]); - const { matches, truncated } = await client.resolveBuild({ repo: 'acme/repo', prNumber: 1 }, { limit: 2, maxPages: 5 }); + const { matches, truncated } = await client.resolveBuild( + { repo: 'acme/repo', prNumber: 1 }, + { limit: 2, maxPages: 5 }, + ); expect(matches).toEqual([]); expect(truncated).toBe(false); }); diff --git a/tests/config.test.ts b/tests/config.test.ts index d075f21..4fe8f08 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -63,7 +63,10 @@ describe('config', () => { fs.chmodSync(configDir(), 0o755); fs.chmodSync(configPath(), 0o644); - saveConfig({ currentProfile: 'default', profiles: { default: { apiUrl: 'https://lc.example.com', authEnabled: false } } }); + saveConfig({ + currentProfile: 'default', + profiles: { default: { apiUrl: 'https://lc.example.com', authEnabled: false } }, + }); expect(fs.statSync(configDir()).mode & 0o777).toBe(0o700); expect(fs.statSync(configPath()).mode & 0o777).toBe(0o600); diff --git a/tests/doctor.test.ts b/tests/doctor.test.ts index aa5e3b2..1423315 100644 --- a/tests/doctor.test.ts +++ b/tests/doctor.test.ts @@ -33,7 +33,7 @@ describe('runDoctor', () => { const report = runDoctor(); expect(report.ok).toBe(false); - expect(report.checks.some((check) => check.id === 'config_profile' && check.status === 'warn')).toBe(true); + expect(report.checks.some(check => check.id === 'config_profile' && check.status === 'warn')).toBe(true); }); it('repairs loose config and token permissions with fix=true', () => { @@ -59,10 +59,10 @@ describe('runDoctor', () => { fs.chmodSync(tokensPath('default'), 0o644); const before = runDoctor(); - expect(before.checks.some((check) => check.fixable && check.status === 'warn')).toBe(true); + expect(before.checks.some(check => check.fixable && check.status === 'warn')).toBe(true); const after = runDoctor({ fix: true }); - expect(after.checks.filter((check) => check.fixed).length).toBeGreaterThanOrEqual(4); + expect(after.checks.filter(check => check.fixed).length).toBeGreaterThanOrEqual(4); expect(fs.statSync(configDir()).mode & 0o777).toBe(0o700); expect(fs.statSync(configPath()).mode & 0o777).toBe(0o600); expect(fs.statSync(tokensDir()).mode & 0o777).toBe(0o700); diff --git a/tests/logs.test.ts b/tests/logs.test.ts index a9bc8ac..c4867b4 100644 --- a/tests/logs.test.ts +++ b/tests/logs.test.ts @@ -13,7 +13,7 @@ describe('logStreamWsUrl', () => { follow: true, tailLines: 200, timestamps: false, - }) + }), ); expect(url.protocol).toBe('wss:'); expect(url.pathname).toBe('/api/logs/stream'); @@ -32,7 +32,7 @@ describe('logStreamWsUrl', () => { namespace: 'n', containerName: 'c', follow: false, - }) + }), ); expect(url.protocol).toBe('ws:'); expect(url.searchParams.has('tailLines')).toBe(false); diff --git a/tests/resolve.test.ts b/tests/resolve.test.ts index 00e4cad..d81c680 100644 --- a/tests/resolve.test.ts +++ b/tests/resolve.test.ts @@ -104,7 +104,7 @@ describe('parseSelector', () => { it('rejects a --repo that conflicts with the PR URL', () => { expect(() => parseSelector({ pr: 'https://github.com/acme/storefront/pull/9', repo: 'other/repo' })).toThrow( - /conflicts/ + /conflicts/, ); }); }); @@ -118,15 +118,15 @@ describe('matchBuilds', () => { ]; it('matches on repo + PR number only', () => { - expect(matchBuilds(builds, { repo: 'acme/storefront', prNumber: 10 }).map((b) => b.uuid)).toEqual(['a']); + expect(matchBuilds(builds, { repo: 'acme/storefront', prNumber: 10 }).map(b => b.uuid)).toEqual(['a']); }); it('matches on repo + branch only', () => { - expect(matchBuilds(builds, { repo: 'acme/storefront', branch: 'feat/y' }).map((b) => b.uuid)).toEqual(['b']); + expect(matchBuilds(builds, { repo: 'acme/storefront', branch: 'feat/y' }).map(b => b.uuid)).toEqual(['b']); }); it('is case-insensitive on repo', () => { - expect(matchBuilds(builds, { repo: 'ACME/StoreFront', prNumber: 11 }).map((b) => b.uuid)).toEqual(['b']); + expect(matchBuilds(builds, { repo: 'ACME/StoreFront', prNumber: 11 }).map(b => b.uuid)).toEqual(['b']); }); it('never matches a build without a pullRequest', () => { @@ -134,7 +134,7 @@ describe('matchBuilds', () => { }); it('does not match the wrong repo even when the number/branch coincide', () => { - expect(matchBuilds(builds, { repo: 'acme/storefront', branch: 'feat/x' }).map((b) => b.uuid)).toEqual(['a']); + expect(matchBuilds(builds, { repo: 'acme/storefront', branch: 'feat/x' }).map(b => b.uuid)).toEqual(['a']); }); }); diff --git a/tests/schema.test.ts b/tests/schema.test.ts index e772f48..98da8ea 100644 --- a/tests/schema.test.ts +++ b/tests/schema.test.ts @@ -50,14 +50,14 @@ describe('validateConfigContent', () => { it('rejects unknown top-level properties (additionalProperties: false)', () => { const result = validateConfigContent(`${VALID}\nbogusKey: true\n`); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.message.includes('bogusKey'))).toBe(true); + expect(result.errors.some(e => e.message.includes('bogusKey'))).toBe(true); }); it('reports the property path for nested type errors', () => { const bad = VALID.replace('branchName: main', 'branchName: [oops]'); const result = validateConfigContent(bad); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.path.includes('services[0].github.branchName'))).toBe(true); + expect(result.errors.some(e => e.path.includes('services[0].github.branchName'))).toBe(true); }); it('validates custom formats like webhook type', () => { @@ -68,11 +68,11 @@ describe('validateConfigContent', () => { - name: notify type: carrier-pigeon state: deployed - env: {}` + env: {}`, ); const result = validateConfigContent(withWebhook); expect(result.valid).toBe(false); - expect(result.errors.some((e) => e.path.includes('webhooks[0].type'))).toBe(true); + expect(result.errors.some(e => e.path.includes('webhooks[0].type'))).toBe(true); }); it('flags empty files', () => { diff --git a/tests/zip.test.ts b/tests/zip.test.ts index d65b4bd..0800ce3 100644 --- a/tests/zip.test.ts +++ b/tests/zip.test.ts @@ -75,7 +75,7 @@ describe('prepareSiteUpload', () => { prepareSiteUpload(dir, { ...config, upload: { ...config.upload, maxExtractedBytes: 2 }, - }) + }), ).rejects.toThrow('Directory contents'); }); });