From 70a41699f2175a47c3de5df6a1a822f0c0a0f3a2 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sat, 6 Jun 2026 13:19:56 +0200 Subject: [PATCH 01/17] fix(lint): remove useless assignments and unnecessary type assertions (#89) The Dependabot dev-dependency bump upgraded @typescript-eslint/eslint-plugin to ^8.60.0 in package.json but did not regenerate the lock file, so npm ci on CI installed a stale version that lacked the no-useless-assignment rule. Regenerated the lock file to pin the plugin at 8.60.0, then fixed the 20 errors it surfaced. Two config menu loops assigned running = false before an immediate return - simplified to while (true) with exits via return only. Eighteen redundant as-cast assertions in the commitlint-sync and constants test files were removed since TypeScript already infers the correct types. --- package-lock.json | 10 ---------- src/commands/config.ts | 20 ++++--------------- tests/unit/commitlint-sync.test.ts | 32 +++++++++++++++--------------- tests/unit/constants.test.ts | 8 ++------ 4 files changed, 22 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97d62f9..3790bd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3928,16 +3928,6 @@ "yaml": "^2.8.4" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/listr2": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", diff --git a/src/commands/config.ts b/src/commands/config.ts index 3b55084..ffe2525 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -640,8 +640,7 @@ async function runProjectConfigMenu(repoRoot: string): Promise<"back" | "exit"> // so the menu immediately reflects a newly-written config file. let commitlintConfigFile = await detectCommitlintConfig(repoRoot); - let running = true; - while (running) { + while (true) { const config = loadConfig(repoRoot); const messages = getMessages(repoRoot); const projectConfig = loadProjectConfig(repoRoot); @@ -694,7 +693,6 @@ async function runProjectConfigMenu(repoRoot: string): Promise<"back" | "exit"> } if (choice === "exit") { - running = false; console.log(colors.muted(`\n${messages.config.exit}\n`)); return "exit"; } @@ -748,12 +746,8 @@ async function runProjectConfigMenu(repoRoot: string): Promise<"back" | "exit"> } } - if (running) { - console.log(); - } + console.log(); } - - return "exit"; } /** @@ -761,8 +755,7 @@ async function runProjectConfigMenu(repoRoot: string): Promise<"back" | "exit"> * wants to return to the scope selector, or "exit" to quit entirely. */ async function runUserConfigMenu(repoRoot: string | null): Promise<"back" | "exit"> { - let running = true; - while (running) { + while (true) { const config = loadConfig(repoRoot); const messages = getMessages(repoRoot); @@ -793,19 +786,14 @@ async function runUserConfigMenu(repoRoot: string | null): Promise<"back" | "exi console.log(colors.muted(`\n${messages.config.back}`)); return "back"; case "exit": - running = false; console.log(colors.muted(`\n${messages.config.exit}\n`)); return "exit"; default: break; } - if (running) { - console.log(); - } + console.log(); } - - return "exit"; } /** diff --git a/tests/unit/commitlint-sync.test.ts b/tests/unit/commitlint-sync.test.ts index 9b28a8b..6480124 100644 --- a/tests/unit/commitlint-sync.test.ts +++ b/tests/unit/commitlint-sync.test.ts @@ -22,7 +22,7 @@ const mockWriteFile = vi.mocked(writeFile); const FILE_NOT_FOUND = new Error("ENOENT"); function makeConfig(overrides: Partial = {}): Required { - return { ...DEFAULT_CONFIG, ...overrides } as Required; + return { ...DEFAULT_CONFIG, ...overrides }; } const CUSTOM_TYPES: CommitType[] = [ @@ -117,7 +117,7 @@ describe("detectCommitlintConfig", () => { skipCandidates(4); // skip to .commitlintrc.js mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasMarker: true, hasExtends: true, hasRules: true }) as never + makeJsContent({ hasMarker: true, hasExtends: true, hasRules: true }) ); const result = await detectCommitlintConfig("/repo"); @@ -129,7 +129,7 @@ describe("detectCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasExtends: true, hasRules: false }) as never + makeJsContent({ hasExtends: true, hasRules: false }) ); const result = await detectCommitlintConfig("/repo"); @@ -141,7 +141,7 @@ describe("detectCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasExtends: false, hasRules: true }) as never + makeJsContent({ hasExtends: false, hasRules: true }) ); const result = await detectCommitlintConfig("/repo"); @@ -153,7 +153,7 @@ describe("detectCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - "module.exports = { parserPreset: 'conventional-changelog-atom' };" as never + "module.exports = { parserPreset: 'conventional-changelog-atom' };" ); const result = await detectCommitlintConfig("/repo"); @@ -165,7 +165,7 @@ describe("detectCommitlintConfig", () => { skipCandidates(7); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasMarker: true, hasExtends: true }) as never + makeJsContent({ hasMarker: true, hasExtends: true }) ); const result = await detectCommitlintConfig("/repo"); @@ -256,7 +256,7 @@ describe("syncToCommitlintConfig", () => { it("syncs to JSON config and returns success", async () => { mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - JSON.stringify({ extends: ["@commitlint/config-conventional"] }) as never + JSON.stringify({ extends: ["@commitlint/config-conventional"] }) ); const result = await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -268,7 +268,7 @@ describe("syncToCommitlintConfig", () => { it("JSON sync: reports all applied rule names", async () => { mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - JSON.stringify({ extends: ["@commitlint/config-conventional"] }) as never + JSON.stringify({ extends: ["@commitlint/config-conventional"] }) ); const result = await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -283,7 +283,7 @@ describe("syncToCommitlintConfig", () => { rules: { "header-max-length": [2, "always", 100] }, }); mockAccess.mockResolvedValue(undefined); - mockReadFile.mockResolvedValue(originalJson as never); + mockReadFile.mockResolvedValue(originalJson); await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -298,7 +298,7 @@ describe("syncToCommitlintConfig", () => { extends: ["@commitlint/config-conventional"], }); mockAccess.mockResolvedValue(undefined); - mockReadFile.mockResolvedValue(originalJson as never); + mockReadFile.mockResolvedValue(originalJson); await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -313,7 +313,7 @@ describe("syncToCommitlintConfig", () => { rules: { "subject-max-length": [2, "always", 100] }, }); mockAccess.mockResolvedValue(undefined); - mockReadFile.mockResolvedValue(originalJson as never); + mockReadFile.mockResolvedValue(originalJson); await syncToCommitlintConfig(makeConfig({ maxSubjectLength: 50 }), "/repo"); @@ -326,7 +326,7 @@ describe("syncToCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasMarker: true, hasExtends: true }) as never + makeJsContent({ hasMarker: true, hasExtends: true }) ); const result = await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -340,7 +340,7 @@ describe("syncToCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasMarker: true, hasExtends: true }) as never + makeJsContent({ hasMarker: true, hasExtends: true }) ); await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -355,7 +355,7 @@ describe("syncToCommitlintConfig", () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - makeJsContent({ hasMarker: true, hasExtends: true }) as never + makeJsContent({ hasMarker: true, hasExtends: true }) ); await syncToCommitlintConfig(makeConfig({ types: CUSTOM_TYPES }), "/repo"); @@ -404,7 +404,7 @@ describe("syncToCommitlintConfig", () => { it("js-custom: returns failure with manual instructions", async () => { skipCandidates(4); mockAccess.mockResolvedValue(undefined); - mockReadFile.mockResolvedValue(makeJsContent({ hasRules: true }) as never); + mockReadFile.mockResolvedValue(makeJsContent({ hasRules: true })); const result = await syncToCommitlintConfig(makeConfig(), "/repo"); @@ -416,7 +416,7 @@ describe("syncToCommitlintConfig", () => { it("type-enum appears in appliedRuleNames for JSON sync when types are custom", async () => { mockAccess.mockResolvedValue(undefined); mockReadFile.mockResolvedValue( - JSON.stringify({ extends: ["@commitlint/config-conventional"] }) as never + JSON.stringify({ extends: ["@commitlint/config-conventional"] }) ); const result = await syncToCommitlintConfig( diff --git a/tests/unit/constants.test.ts b/tests/unit/constants.test.ts index e9c6dae..286a217 100644 --- a/tests/unit/constants.test.ts +++ b/tests/unit/constants.test.ts @@ -167,12 +167,8 @@ describe("STANDARD_MESSAGES", () => { return result; } - const wizardStructure = getKeyStructure( - WIZARD_MESSAGES as unknown as Record - ); - const standardStructure = getKeyStructure( - STANDARD_MESSAGES as unknown as Record - ); + const wizardStructure = getKeyStructure(WIZARD_MESSAGES); + const standardStructure = getKeyStructure(STANDARD_MESSAGES); expect(standardStructure).toEqual(wizardStructure); }); From 0c480453f2dc65b0fd2aaf3d1f784f52f6db0c41 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 21 Jun 2026 17:07:44 +0200 Subject: [PATCH 02/17] chore(knip): add unused-code detection to tooling and CI (#94) The three-layer architecture with barrel re-exports in types/index.ts makes orphaned exports and unused dependencies easy to accumulate unnoticed, since nothing in the build fails when an export loses its last importer. Add knip to trace the real import graph from src and flag unreachable files, exports, and dependencies. Wire it into the CI pipeline and the pre-push hook so issues are caught before they reach the remote, and configure it to ignore the husky binary and the config-only packages (@commitlint/cli, lint-staged) that are never imported in source. Document the script in the contributing guide and add it to the pre-PR quality check chain. --- .github/workflows/ci.yml | 3 + .husky/pre-push | 1 + docs/development/contributing.md | 5 +- knip.json | 6 + package-lock.json | 917 ++++++++++++++++++++++++++++++- package.json | 4 +- 6 files changed, 922 insertions(+), 14 deletions(-) create mode 100644 knip.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecce65a..ccb9e1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - name: Type check run: npm run type-check + - name: Knip (unused code and dependencies) + run: npm run knip + - name: Test run: npm test diff --git a/.husky/pre-push b/.husky/pre-push index 560ffde..10f8fd8 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,5 @@ #!/bin/bash +npm run knip npm run test npm run build diff --git a/docs/development/contributing.md b/docs/development/contributing.md index a304893..0250321 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -119,6 +119,7 @@ See [Architecture](architecture.md) for a detailed description of each layer's r | Lint | `npm run lint` | Check code style (also runs in pre-commit hook) | | Format | `npm run format` | Auto-format with Prettier | | Type check | `npm run type-check` | Verify TypeScript types without emitting | +| Knip | `npm run knip` | Detect unused files, exports, and dependencies (also runs in pre-push hook) | | Test | `npm run test` | Run full test suite once | | Test watch | `npm run test:watch` | Run tests on file changes | | Coverage | `npm run test:coverage` | Run tests with coverage report | @@ -178,10 +179,10 @@ If you are unsure which doc to update, check `docs/index.md` - it links every gu Run the full quality check before opening a pull request: ```bash -npm run lint && npm run format && npm run type-check && npm run test && npm run build +npm run lint && npm run format && npm run type-check && npm run knip && npm run test && npm run build ``` -All five must pass. +All six must pass. --- diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..151ce9e --- /dev/null +++ b/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "project": ["src/**/*.ts"], + "ignoreBinaries": ["husky"], + "ignoreDependencies": ["@commitlint/cli", "lint-staged"] +} diff --git a/package-lock.json b/package-lock.json index 3790bd7..a3f871f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-import-x": "^4.16.2", "globals": "^17.6.0", + "knip": "^6.17.1", "lint-staged": "^17.0.5", "prettier": "^3.8.3", "rimraf": "^6.1.3", @@ -1117,6 +1118,367 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.135.0.tgz", + "integrity": "sha512-sHeZItACNcA5WRAWqF6ixriR4GkZDyY10gVgnZU7pXku1DjHFATSqnwZM809jl0gXPHxb6fKzYQCK7bNK5cACQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.135.0.tgz", + "integrity": "sha512-wPte+SzgzWWFgMSF8YZDNM+tBXtJg0AXBi7+tU3yS2z1f2Af9kRLZLKuJojADmuD/cZexmnMHHC3SDItTW77Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.135.0.tgz", + "integrity": "sha512-BmKz3lHIsqVos+9aPcdYCT9MG3APoUyM43KlEFhJMWNVDOGG8FKyiFz81Bc+mGz2o0hpuQ3PfXLfVWJrKXjo2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.135.0.tgz", + "integrity": "sha512-dM8BS+8+Br1fNvmh2QZbGiHaYttwLebRa6J4Uz9vuFzMNmvsdRYwf7993ptOaV0JTrR63AaoVLjX7nhWbijxjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.135.0.tgz", + "integrity": "sha512-xlZnvvJdR9bGu2pOhvR5hMuKPHCE6Sa9owK5A484mzjHdm75VRV5nCs5w/jkmGODMMTFc+KN7EnZqEieM813kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.135.0.tgz", + "integrity": "sha512-PSR8LmBK/H/PQRiN8g7RebQgZX/ntVCrdT/JBfNxE5ezdHG1s2i4rbazsRJYD83TTI1MmgTpC0MGL42PLtskQQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.135.0.tgz", + "integrity": "sha512-I85GJXzfUsigkkk7Ngdz95C217M4FdUi1Z2HrX5UyPmURobwQZ7m2bbUvwFkz4VGZd+lymFGKHvDZ3RQC9qOzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.135.0.tgz", + "integrity": "sha512-zqEY0npz0g0aGZj/8a5BclunjVDytsBQHYtIC10Gd26HcrLwbVF6YDbqRQjunMGYdSo97u6xOBl05aTDI2diDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.135.0.tgz", + "integrity": "sha512-mWAfprP819gQ2qYst1RxgTI8b/z0b29OpoKfRflIXLHde2dZLihQD4g47Onuvtpo5GPIkMYPRlX9QoeZfs/GnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.135.0.tgz", + "integrity": "sha512-gri8c2AOmJKJwOux2KTHFBfUaXoJURuVMKhmKEi/2hTF55cQteTDV2XNfTiE5oCC+Tnem1Y4/MWzcyDadtsSag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.135.0.tgz", + "integrity": "sha512-Y2tkupCG5wo0SxH2rMLG4d4Kmv6DaM3sBp+GuM5lox0S8Za6VxKgQrY2Mut088QQxKkEE89n/4CCCgmw2o0e3Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.135.0.tgz", + "integrity": "sha512-xDRJq6i6WTynjeP+ISbDpyH4p9BaJ0wuQcL0lCSDkt9qOXC9dmwpOu1VG/TlwmPI3KpYntmO9nJCuc3TMTsNBA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.135.0.tgz", + "integrity": "sha512-V4MoUuiCRNvihxhIufRxvK+ka013V4joTSK0FAGA1KEjLuNprfH6N/Qw2uxQEVIFuNYMhD/hV6xJ/ptbzlKdHg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.135.0.tgz", + "integrity": "sha512-JCFZ7zM7KXOKoPAbK/ZB4wY0M1jxRECiem2UQuiXLjzGqS9+hno7mtX+qyK2F7HWK2xPhyJb+frpcOtk5DKOtg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.135.0.tgz", + "integrity": "sha512-9jSVS1b3hOV7sdKH4aA2DFfnTz0RgQd0v2BefR+LYbH8yIlmSM22JJZbAAjVeVXmFgUAk3zJQ1tpE/Nd+Vi2YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.135.0.tgz", + "integrity": "sha512-M857ZLBSdn1Uy/SJJz5zh0qGu67B4P9omCgXGBU2LLqTzraX6ZjVNaKq5yW1PDw/LgJXDXR/dbZfgmB310f11Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.135.0.tgz", + "integrity": "sha512-2w6DVcntQZX9U5RhXtgiWb3FLWFB5EcwI1U8yr3htOCJUJjagN4BFUHz/Y/d9ZsumndZ6ByxxWEtbUZNE1bfFw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.135.0.tgz", + "integrity": "sha512-rX1U8+IH2Z37EJjDXKa1iifvUQAdba+vZ4Ewj1iaG5eA/QaSybzclCOwtWa0/5BuUQnnK/T2JHUEFrwhL6Ck2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.135.0.tgz", + "integrity": "sha512-9FAisBbH1QICGAjlJobiuKGd/jOuVmyqniWdQMwTa5SkCl6hhuotBCJf1n46B0flYbSOR5TzfV9HZCWSyb3c/Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.135.0.tgz", + "integrity": "sha512-wYF+A2AzJ2n7ul6q+Z2G/ia0S2+8cUp0AgWZzoFvF4WmUcl1P7p+o6se1Gdr5wGnWuF0iAMIkGddrjCarNr2yA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxc-project/types": { "version": "0.132.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", @@ -1124,9 +1486,333 @@ "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.3.tgz", + "integrity": "sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.3.tgz", + "integrity": "sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.3.tgz", + "integrity": "sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.3.tgz", + "integrity": "sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.3.tgz", + "integrity": "sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.3.tgz", + "integrity": "sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.3.tgz", + "integrity": "sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.3.tgz", + "integrity": "sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.3.tgz", + "integrity": "sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.3.tgz", + "integrity": "sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.3.tgz", + "integrity": "sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.3.tgz", + "integrity": "sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.3.tgz", + "integrity": "sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.3.tgz", + "integrity": "sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.3.tgz", + "integrity": "sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.3.tgz", + "integrity": "sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.3.tgz", + "integrity": "sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.3.tgz", + "integrity": "sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.3.tgz", + "integrity": "sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@package-json/types": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@package-json/types/-/types-0.0.12.tgz", @@ -1472,9 +2158,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -3075,6 +3761,16 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "walk-up-path": "^4.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3159,6 +3855,22 @@ "dev": true, "license": "ISC" }, + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, + "engines": { + "node": ">=18.3.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3213,9 +3925,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3621,6 +4333,55 @@ "json-buffer": "3.0.1" } }, + "node_modules/knip": { + "version": "6.17.1", + "resolved": "https://registry.npmjs.org/knip/-/knip-6.17.1.tgz", + "integrity": "sha512-HcQsZSQ4Ymhuay4BVzJtM5pFZNDSomYYqcNCZOSITPQh9g18a09DqziWAxSt2G+BH9wGlG+0ZjWpEnaFlnKseQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/knip" + } + ], + "license": "ISC", + "dependencies": { + "fdir": "^6.5.0", + "formatly": "^0.3.0", + "get-tsconfig": "4.14.0", + "jiti": "^2.7.0", + "oxc-parser": "^0.135.0", + "oxc-resolver": "^11.20.0", + "picomatch": "^4.0.4", + "smol-toml": "^1.6.1", + "strip-json-comments": "5.0.3", + "tinyglobby": "^0.2.17", + "unbash": "^4.0.1", + "yaml": "^2.9.0", + "zod": "^4.1.11" + }, + "bin": { + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/knip/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4306,6 +5067,85 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxc-parser": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.135.0.tgz", + "integrity": "sha512-/DaPStu0s2zzNSRRniKyTPM6Z/o+DapOp2JYNKDL8AsgaBGPK2IdZyB87SQjVH+xeQPz+Qr9mrjglfkYgtbVRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.135.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.135.0", + "@oxc-parser/binding-android-arm64": "0.135.0", + "@oxc-parser/binding-darwin-arm64": "0.135.0", + "@oxc-parser/binding-darwin-x64": "0.135.0", + "@oxc-parser/binding-freebsd-x64": "0.135.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.135.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.135.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.135.0", + "@oxc-parser/binding-linux-arm64-musl": "0.135.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.135.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.135.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.135.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.135.0", + "@oxc-parser/binding-linux-x64-gnu": "0.135.0", + "@oxc-parser/binding-linux-x64-musl": "0.135.0", + "@oxc-parser/binding-openharmony-arm64": "0.135.0", + "@oxc-parser/binding-wasm32-wasi": "0.135.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.135.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.135.0", + "@oxc-parser/binding-win32-x64-msvc": "0.135.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.135.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.135.0.tgz", + "integrity": "sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.3", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.3.tgz", + "integrity": "sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.3", + "@oxc-resolver/binding-android-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-arm64": "11.21.3", + "@oxc-resolver/binding-darwin-x64": "11.21.3", + "@oxc-resolver/binding-freebsd-x64": "11.21.3", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.3", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.3", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.3", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.3", + "@oxc-resolver/binding-linux-x64-musl": "11.21.3", + "@oxc-resolver/binding-openharmony-arm64": "11.21.3", + "@oxc-resolver/binding-wasm32-wasi": "11.21.3", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.3", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.3" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -4715,6 +5555,19 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4814,6 +5667,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4845,9 +5711,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -4919,6 +5785,16 @@ "node": ">=14.17" } }, + "node_modules/unbash": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.1.tgz", + "integrity": "sha512-1ajSo3813sDoVIHx4inJdUS4l5L2ic5cFiddemPiyjb/PZEoBAhFwHtbaEdRDFxbAKy7FCG7s5ww3/uCFawuIA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", @@ -5151,6 +6027,16 @@ } } }, + "node_modules/walk-up-path": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", + "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5245,7 +6131,6 @@ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", - "optional": true, "bin": { "yaml": "bin.mjs" }, @@ -5326,6 +6211,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 6c1dddf..13879a3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "test:watch": "vitest", "test:coverage": "vitest run --coverage", "prepublishOnly": "npm run build", - "prepare": "husky || true" + "prepare": "husky || true", + "knip": "knip" }, "repository": { "type": "git", @@ -73,6 +74,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-import-x": "^4.16.2", "globals": "^17.6.0", + "knip": "^6.17.1", "lint-staged": "^17.0.5", "prettier": "^3.8.3", "rimraf": "^6.1.3", From 6fd9fe179a2332020ceb583076a832171f1b53d9 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 21 Jun 2026 17:32:07 +0200 Subject: [PATCH 03/17] style(docs): realign development scripts table in contributing guide (#95) Adding the knip row to the development scripts table widened its "When to use" column, but the surrounding rows merged without being re-padded, so the committed table failed prettier --check. Re-pad every row to the new column width. This is a whitespace-only change with no edits to the table's text content. --- docs/development/contributing.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 0250321..fcd005d 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -112,18 +112,18 @@ See [Architecture](architecture.md) for a detailed description of each layer's r ## Development Scripts -| Script | Command | When to use | -| ---------- | ----------------------- | ----------------------------------------------- | -| Build | `npm run build` | Before testing locally, before committing | -| Watch | `npm run dev` | During active development | -| Lint | `npm run lint` | Check code style (also runs in pre-commit hook) | -| Format | `npm run format` | Auto-format with Prettier | -| Type check | `npm run type-check` | Verify TypeScript types without emitting | +| Script | Command | When to use | +| ---------- | ----------------------- | --------------------------------------------------------------------------- | +| Build | `npm run build` | Before testing locally, before committing | +| Watch | `npm run dev` | During active development | +| Lint | `npm run lint` | Check code style (also runs in pre-commit hook) | +| Format | `npm run format` | Auto-format with Prettier | +| Type check | `npm run type-check` | Verify TypeScript types without emitting | | Knip | `npm run knip` | Detect unused files, exports, and dependencies (also runs in pre-push hook) | -| Test | `npm run test` | Run full test suite once | -| Test watch | `npm run test:watch` | Run tests on file changes | -| Coverage | `npm run test:coverage` | Run tests with coverage report | -| Clean | `npm run clean` | Delete `dist/` | +| Test | `npm run test` | Run full test suite once | +| Test watch | `npm run test:watch` | Run tests on file changes | +| Coverage | `npm run test:coverage` | Run tests with coverage report | +| Clean | `npm run clean` | Delete `dist/` | --- From 0bc2b95fe18a3c92e9b5963b44062aeaa87684b7 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 21 Jun 2026 17:46:55 +0200 Subject: [PATCH 04/17] build(prettier): broaden format scope to docs and tests (#96) The CI format check and the format script only covered src, so Prettier violations in docs and tests were never caught. A contributing-guide edit had already merged with a misaligned table because of this gap, and the script and check used different globs, so running format locally could not always fix what CI verified. Reformat the existing offenders (five docs files, .prettierrc, and eslint.config.mjs) so the wider check passes, then extend both the format script and the CI check to the same scope: src, tests, and docs markdown. Keeping the two globs identical means npm run format fixes exactly what CI checks. Root-level markdown is left out to avoid matching untracked files. --- .github/workflows/ci.yml | 2 +- .prettierrc | 7 +-- docs/GLOSSARY.md | 50 ++++++++--------- docs/development/testing.md | 77 +++++++++++++++------------ docs/guides/commitlint-integration.md | 22 ++++---- docs/index.md | 40 +++++++------- docs/usage/editor-integration.md | 10 ++-- eslint.config.mjs | 41 ++++---------- package.json | 2 +- 9 files changed, 117 insertions(+), 134 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb9e1d..122ab15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: run: npm run lint - name: Format check - run: npx prettier --check "src/**/*.ts" "tests/**/*.ts" + run: npx prettier --check "src/**/*.ts" "tests/**/*.ts" "docs/**/*.md" - name: Type check run: npm run type-check diff --git a/.prettierrc b/.prettierrc index daf0cdc..a012aec 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,11 +9,6 @@ "jsxSingleQuote": false, "arrowParens": "always", "plugins": ["@ianvs/prettier-plugin-sort-imports"], - "importOrder": [ - "", - "", - "", - "^[./]" - ], + "importOrder": ["", "", "", "^[./]"], "importOrderTypeScriptVersion": "5.7.0" } diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index cb3fc32..83cab10 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -23,17 +23,17 @@ and examples from the codebase. ### Core Actions -| Term | Meaning | Usage | -| ------------ | ------------------------------------ | ----------------------------------------------- | -| **cast** | Create a commit | "Shall Merlin cast this spell?" | -| **inscribe** | Write, save, or create a file/config | "Inscribing project enchantments" | -| **summon** | Install or fetch (npm install) | "Summoning dependencies from the npm realm" | -| **invoke** | Run a command | "Invoke git init to create a sacred realm" | -| **awaken** | Initialize (a tool or service) | "Awakening the husky guardian" | -| **weave** | Write detailed content (commit body) | "Would you like to weave a detailed tale?" | -| **shatter** | Break (as in breaking changes) | "Does this spell shatter ancient contracts?" | -| **peer** | Preview or inspect | "Merlin peers into possible futures" | -| **bind** | Create an alias | "Bind 'git merlin' to your spellbook" | +| Term | Meaning | Usage | +| ------------ | ------------------------------------ | -------------------------------------------- | +| **cast** | Create a commit | "Shall Merlin cast this spell?" | +| **inscribe** | Write, save, or create a file/config | "Inscribing project enchantments" | +| **summon** | Install or fetch (npm install) | "Summoning dependencies from the npm realm" | +| **invoke** | Run a command | "Invoke git init to create a sacred realm" | +| **awaken** | Initialize (a tool or service) | "Awakening the husky guardian" | +| **weave** | Write detailed content (commit body) | "Would you like to weave a detailed tale?" | +| **shatter** | Break (as in breaking changes) | "Does this spell shatter ancient contracts?" | +| **peer** | Preview or inspect | "Merlin peers into possible futures" | +| **bind** | Create an alias | "Bind 'git merlin' to your spellbook" | ### Objects & Artifacts @@ -64,7 +64,7 @@ and examples from the codebase. | --------------- | ----------------------------------------------- | -------------------------------------------------------------------- | | **ritual** | The commit workflow/process | "The ritual has been cancelled" | | **enchantment** | A configuration setting applied to a project | "Realm enchantments inscribed", "Which realm enchantment to modify?" | -| **blessing** | An init/setup operation applied to a repository | "Your realm is now blessed!", "Existing blessings detected" | +| **blessing** | An init/setup operation applied to a repository | "Your realm is now blessed!", "Existing blessings detected" | | **binding** | A git alias | "A binding for 'git merlin' already exists" | | **quest** | A GitHub/tracker issue | "Does this resolve any quests?" | | **vision** | A preview / dry run | "The vision fades - no spell was cast" | @@ -74,7 +74,7 @@ and examples from the codebase. | Term | Meaning | Usage | | ------------- | --------------------------------------- | ------------------------------------- | -| **sacred** | Important, protected (repository) | "Verifying the sacred realm" | +| **sacred** | Important, protected (repository) | "Verifying the sacred realm" | | **ancient** | Established, existing (contracts/texts) | "The ancient texts demand this field" | | **woven** | Committed, saved to history | "Your spell is woven into history" | | **forbidden** | Invalid, not allowed | "This spell contains forbidden runes" | @@ -166,18 +166,18 @@ These emojis prefix wizard-themed messages to provide visual context. #### Config Menu (wizard theme) -| Emoji | Context | Meaning | -| ----- | ------- | ------------------------ | -| 🎨 | Palette | Theme setting | -| 📏 | Ruler | Max subject length | -| 🎯 | Target | Max scope length | -| 📝 | Pencil | Editor setting | -| 🔄 | Refresh | Auto-add setting | -| 🔢 | Numbers | Show character counter | -| 🔗 | Link | Sync to commitlint | -| 👁️ | Eye | Show config | -| 🗑️ | Trash | Reset config | -| 👋 | Wave | Exit | +| Emoji | Context | Meaning | +| ----- | ------- | ---------------------- | +| 🎨 | Palette | Theme setting | +| 📏 | Ruler | Max subject length | +| 🎯 | Target | Max scope length | +| 📝 | Pencil | Editor setting | +| 🔄 | Refresh | Auto-add setting | +| 🔢 | Numbers | Show character counter | +| 🔗 | Link | Sync to commitlint | +| 👁️ | Eye | Show config | +| 🗑️ | Trash | Reset config | +| 👋 | Wave | Exit | --- diff --git a/docs/development/testing.md b/docs/development/testing.md index 4749a3f..5f1af1a 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -9,11 +9,13 @@ Testing guide for contributors. Covers the testing philosophy, how to write test **Plain tests over mocks.** Write real assertions against real logic first. Introduce mocks only when the code under test touches something external: the file system, git commands, or interactive prompts. **What to mock:** + - `execa` - avoids creating real git commits during tests - `fs` - avoids reading/writing real config files - `@inquirer/prompts` - avoids requiring a real TTY **What not to mock:** + - Pure utility functions - test them directly with inputs and expected outputs - Internal module logic - if you are mocking the code under test, you are not testing it @@ -21,11 +23,11 @@ Testing guide for contributors. Covers the testing philosophy, how to write test ## Test Stack -| Tool | Purpose | -|---|---| +| Tool | Purpose | +| ----------------------------- | -------------------------------- | | [Vitest](https://vitest.dev/) | Test runner, assertions, mocking | -| `vi.mock()` | Module-level mock injection | -| `vi.fn()` | Spy and mock functions | +| `vi.mock()` | Module-level mock injection | +| `vi.fn()` | Spy and mock functions | Tests use Vitest's global API (`describe`, `it`, `expect`, `beforeEach`, `vi`) without explicit imports, configured in `vitest.config.ts` with `globals: true`. @@ -33,14 +35,15 @@ Tests use Vitest's global API (`describe`, `it`, `expect`, `beforeEach`, `vi`) w ## Coverage Targets -| Metric | Target | -|---|---| -| Lines | 80% | -| Statements | 80% | -| Functions | 80% | -| Branches | 75% | +| Metric | Target | +| ---------- | ------ | +| Lines | 80% | +| Statements | 80% | +| Functions | 80% | +| Branches | 75% | Run coverage: + ```bash npm run test:coverage ``` @@ -147,13 +150,13 @@ Mock external dependencies. Assert the right functions were called with the righ ### Mocking execa (git operations) ```typescript -import { vi, beforeEach } from "vitest"; +import { execa } from "execa"; +import { beforeEach, vi } from "vitest"; + import { commit } from "../../src/lib/git.js"; vi.mock("execa"); -import { execa } from "execa"; - describe("commit", () => { beforeEach(() => { vi.clearAllMocks(); @@ -172,14 +175,20 @@ describe("commit", () => { }); it("should pass --no-verify when noVerify is true", async () => { - vi.mocked(execa).mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 } as any); + vi.mocked(execa).mockResolvedValue({ + stdout: "", + stderr: "", + exitCode: 0, + } as any); await commit("feat: test", true); - expect(execa).toHaveBeenCalledWith( - "git", - ["commit", "-m", "feat: test", "--no-verify"] - ); + expect(execa).toHaveBeenCalledWith("git", [ + "commit", + "-m", + "feat: test", + "--no-verify", + ]); }); }); ``` @@ -187,13 +196,13 @@ describe("commit", () => { ### Mocking the file system (config I/O) ```typescript -import { vi } from "vitest"; import * as fs from "fs"; - -vi.mock("fs"); +import { vi } from "vitest"; import { loadUserConfig } from "../../src/lib/config-loader.js"; +vi.mock("fs"); + describe("loadUserConfig", () => { it("should return parsed config from ~/.merlinrc.json", () => { vi.mocked(fs.existsSync).mockReturnValue(true); @@ -221,7 +230,14 @@ describe("loadUserConfig", () => { Commands orchestrate workflows. Test the coordination logic - that the right lib functions are called in the right order with the right arguments. ```typescript -import { vi, beforeEach } from "vitest"; +import { beforeEach, vi } from "vitest"; + +import { commitCommand } from "../../src/commands/commit.js"; +import { getMessages, loadConfig } from "../../src/lib/config-loader.js"; +import { commit, hasStagedChanges, isGitRepo } from "../../src/lib/git.js"; +import { buildCommitMessage } from "../../src/lib/message.js"; +import { promptUser } from "../../src/lib/prompt.js"; +import { DEFAULT_CONFIG, WIZARD_MESSAGES } from "../../src/utils/constants.js"; // Mock all dependencies before imports vi.mock("../../src/lib/git.js"); @@ -229,13 +245,6 @@ vi.mock("../../src/lib/prompt.js"); vi.mock("../../src/lib/message.js"); vi.mock("../../src/lib/config-loader.js"); -import { isGitRepo, hasStagedChanges, commit } from "../../src/lib/git.js"; -import { promptUser } from "../../src/lib/prompt.js"; -import { buildCommitMessage } from "../../src/lib/message.js"; -import { loadConfig, getMessages } from "../../src/lib/config-loader.js"; -import { commitCommand } from "../../src/commands/commit.js"; -import { DEFAULT_CONFIG, WIZARD_MESSAGES } from "../../src/utils/constants.js"; - function setupHappyPath() { vi.mocked(isGitRepo).mockResolvedValue(true); vi.mocked(hasStagedChanges).mockResolvedValue(true); @@ -285,10 +294,10 @@ describe("commitCommand", () => { ### Mocking `@inquirer/prompts` ```typescript -vi.mock("@inquirer/prompts"); - import { confirm, input, select } from "@inquirer/prompts"; +vi.mock("@inquirer/prompts"); + vi.mocked(confirm).mockResolvedValue(true); vi.mocked(input).mockResolvedValue("my input"); vi.mocked(select).mockResolvedValue("feat"); @@ -327,9 +336,9 @@ it("should resolve with result", () => { ```typescript it.each([ - ["short", "short", 72, true], - ["exact limit", "a".repeat(72), 72, true], - ["one over", "a".repeat(73), 72, false], + ["short", "short", 72, true], + ["exact limit", "a".repeat(72), 72, true], + ["one over", "a".repeat(73), 72, false], ])("validateSubjectLength: %s", (_, input, max, expected) => { const validate = createMaxLengthValidator(max); const result = validate(input); diff --git a/docs/guides/commitlint-integration.md b/docs/guides/commitlint-integration.md index 1ce3e0b..b3721fd 100644 --- a/docs/guides/commitlint-integration.md +++ b/docs/guides/commitlint-integration.md @@ -14,11 +14,11 @@ When you trigger the sync, merlin: **Rules written:** -| Config field | Commitlint rule | -|---|---| -| `maxSubjectLength` | `subject-max-length: [2, "always", ]` | -| `maxScopeLength` | `scope-max-length: [2, "always", ]` | -| `types` (only if custom) | `type-enum: [2, "always", []]` | +| Config field | Commitlint rule | +| ------------------------ | -------------------------------------------- | +| `maxSubjectLength` | `subject-max-length: [2, "always", ]` | +| `maxScopeLength` | `scope-max-length: [2, "always", ]` | +| `types` (only if custom) | `type-enum: [2, "always", []]` | `type-enum` is only written when you have defined custom types. When using the 11 default types, commitlint's `@commitlint/config-conventional` already covers conventional type validation. @@ -41,12 +41,12 @@ Re-run sync whenever you change `maxSubjectLength`, `maxScopeLength`, or `types` Merlin detects the format of your existing commitlint config and handles each differently: -| Format | File examples | Sync behavior | -|---|---|---| -| JSON | `.commitlintrc`, `.commitlintrc.json` | Merges rules directly into the JSON `rules` object | -| JS (merlin-managed) | `commitlint.config.js` with `// @merlin-managed` | Regenerates the entire file | -| JS (user-authored) | `commitlint.config.js` without the marker | Returns manual instructions - merlin does not overwrite custom JS | -| YAML | `.commitlintrc.yaml`, `.commitlintrc.yml` | Returns manual instructions | +| Format | File examples | Sync behavior | +| ------------------- | ------------------------------------------------ | ----------------------------------------------------------------- | +| JSON | `.commitlintrc`, `.commitlintrc.json` | Merges rules directly into the JSON `rules` object | +| JS (merlin-managed) | `commitlint.config.js` with `// @merlin-managed` | Regenerates the entire file | +| JS (user-authored) | `commitlint.config.js` without the marker | Returns manual instructions - merlin does not overwrite custom JS | +| YAML | `.commitlintrc.yaml`, `.commitlintrc.yml` | Returns manual instructions | The `// @merlin-managed` comment is how merlin identifies files it generated (or files you have explicitly marked as safe to regenerate). diff --git a/docs/index.md b/docs/index.md index 090d7ad..a76d095 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,12 +2,12 @@ **Merlin Commit** is an interactive CLI for creating [Conventional Commits](https://www.conventionalcommits.org/). It guides you through each part of a commit message with a prompt sequence, validates your input in real time, and produces consistently formatted messages your whole team can rely on. -| Property | Value | -| -------- | -------------------------- | +| Property | Value | +| -------- | --------------- | | Package | `merlin-commit` | -| Binary | `merlin` | -| Node.js | >= 18.0.0 | -| License | MIT | +| Binary | `merlin` | +| Node.js | >= 18.0.0 | +| License | MIT | --- @@ -51,35 +51,35 @@ merlin --dry-run You want to create commits and understand what merlin-commit can do. -| Document | Description | -| ------------------------------------------- | ------------------------------------------- | -| [Installation](getting-started/installation.md) | Requirements, install methods, uninstalling | -| [Quick Start](getting-started/quickstart.md) | Your first commit, step by step | +| Document | Description | +| ------------------------------------------------- | ------------------------------------------- | +| [Installation](getting-started/installation.md) | Requirements, install methods, uninstalling | +| [Quick Start](getting-started/quickstart.md) | Your first commit, step by step | | [Commit Types](usage/commit-types.md) | All 11 built-in types with usage guidance | | [Themes](usage/themes.md) | Wizard (default) and standard themes | | [Editor Integration](usage/editor-integration.md) | VS Code, Neovim, Emacs, Nano setup | | [Auto-Add](usage/auto-add.md) | Interactive file staging before committing | -| [CLI Reference](reference/cli-reference.md) | Every command, flag, and option | +| [CLI Reference](reference/cli-reference.md) | Every command, flag, and option | ### Team Leads You want to roll out merlin-commit across a repository with consistent standards. -| Document | Description | -| --------------------------------------------------- | ------------------------------------------ | +| Document | Description | +| ---------------------------------------------------------- | ------------------------------------------ | | [Team Setup](guides/team-setup.md) | Step-by-step team rollout guide | -| [Configuration](reference/configuration.md) | Config files, merge strategy, common tasks | +| [Configuration](reference/configuration.md) | Config files, merge strategy, common tasks | | [Commitlint Integration](guides/commitlint-integration.md) | Syncing merlin rules to commitlint | -| [Config Schema] | Every config field documented | +| [Config Schema] | Every config field documented | ### Advanced Users You want to customize behavior beyond the defaults. -| Document | Description | -| ------------------------------------------- | ------------------------------------------ | -| [Config Schema] | Custom types, all field details | -| [Configuration](reference/configuration.md) | Three-tier merge, project overrides | +| Document | Description | +| ------------------------------------------------- | ------------------------------------------ | +| [Config Schema] | Custom types, all field details | +| [Configuration](reference/configuration.md) | Three-tier merge, project overrides | | [Editor Integration](usage/editor-integration.md) | Advanced editor configuration | | [Themes](usage/themes.md) | Theme internals and terminal compatibility | @@ -87,8 +87,8 @@ You want to customize behavior beyond the defaults. You want to contribute code or understand how the project is built. -| Document | Description | -| ------------------------------- | ---------------------------------------- | +| Document | Description | +| ------------------------------------------- | ---------------------------------------- | | [Contributing](development/contributing.md) | Development setup, PR process | | [Architecture](development/architecture.md) | Code layers, data flow, design decisions | | [Testing](development/testing.md) | Test patterns, running tests, coverage | diff --git a/docs/usage/editor-integration.md b/docs/usage/editor-integration.md index c2f1443..6095035 100644 --- a/docs/usage/editor-integration.md +++ b/docs/usage/editor-integration.md @@ -24,11 +24,11 @@ Comment lines (lines beginning with `#`) are stripped on read-back, just as git ## Which Fields Use the Editor -| Field | Prompt | -|---|---| -| Body | "Would you like to weave a detailed tale?" (wizard) / "Add a detailed description?" (standard) | +| Field | Prompt | +| ---------------- | ---------------------------------------------------------------------------------------------------- | +| Body | "Would you like to weave a detailed tale?" (wizard) / "Add a detailed description?" (standard) | | Breaking changes | "Does this spell shatter ancient contracts?" (wizard) / "Are there any breaking changes?" (standard) | -| Issue references | "Does this resolve any quests?" (wizard) / "Reference any issues?" (standard) | +| Issue references | "Does this resolve any quests?" (wizard) / "Reference any issues?" (standard) | Answer Yes (type `y` and press Enter) to open the editor for that field. Answer No or press Enter (the default for all three is No) to skip it. @@ -105,6 +105,7 @@ Use `emacsclient` with the `--wait` flag to block until the buffer is closed: ``` Start an Emacs server first if it is not already running: + ```bash emacs --daemon ``` @@ -243,6 +244,7 @@ merlin config --show project ### `editor exited with error` The editor exited with a non-zero exit code. This can happen if: + - The editor command is wrong or the binary is not installed - The editor was force-quit (e.g., `:q!` in Vim without saving discards changes but exits cleanly; closing the terminal mid-edit may exit non-zero) diff --git a/eslint.config.mjs b/eslint.config.mjs index bd6dd15..34570ce 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -51,13 +51,11 @@ const mainConfig = { paths: [ { name: "child_process", - message: - "Use execa instead for safe command execution.", + message: "Use execa instead for safe command execution.", }, { name: "node:child_process", - message: - "Use execa instead for safe command execution.", + message: "Use execa instead for safe command execution.", }, ], }, @@ -146,14 +144,8 @@ const mainConfig = { "object-shorthand": ["error", "always"], "prefer-rest-params": "error", "prefer-spread": "error", - "prefer-destructuring": [ - "error", - { object: true, array: false }, - ], - "prefer-arrow-callback": [ - "error", - { allowNamedFunctions: false }, - ], + "prefer-destructuring": ["error", { object: true, array: false }], + "prefer-arrow-callback": ["error", { allowNamedFunctions: false }], "arrow-body-style": ["error", "as-needed"], "no-useless-rename": "error", "@typescript-eslint/no-useless-constructor": "error", @@ -171,32 +163,17 @@ const mainConfig = { fixStyle: "inline-type-imports", }, ], - "@typescript-eslint/consistent-type-definitions": [ - "error", - "type", - ], + "@typescript-eslint/consistent-type-definitions": ["error", "type"], "@typescript-eslint/prefer-nullish-coalescing": "warn", "@typescript-eslint/prefer-optional-chain": "warn", - "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_" }, - ], + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], "@typescript-eslint/explicit-function-return-type": "off", // ── Style (non-formatting) ───────────────────────────── - camelcase: [ - "error", - { properties: "never", ignoreDestructuring: false }, - ], + camelcase: ["error", { properties: "never", ignoreDestructuring: false }], "no-nested-ternary": "error", - "no-unneeded-ternary": [ - "error", - { defaultAssignment: false }, - ], - "new-cap": [ - "error", - { newIsCap: true, capIsNew: false }, - ], + "no-unneeded-ternary": ["error", { defaultAssignment: false }], + "new-cap": ["error", { newIsCap: true, capIsNew: false }], "one-var": ["error", "never"], "spaced-comment": ["error", "always"], "no-lonely-if": "error", diff --git a/package.json b/package.json index 13879a3..4044d94 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "dev": "tsc --watch", "clean": "rimraf dist", "lint": "eslint src/ tests/", - "format": "prettier --write \"src/**/*.ts\"", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"docs/**/*.md\"", "type-check": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", From f90500702386c96ba6b3a6081f60b805d72bbdcf Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 21 Jun 2026 17:55:03 +0200 Subject: [PATCH 05/17] chore(license): correct copyright holder to Maksym Bukator (#97) The MIT license named "Avilo Labs" as the copyright holder, which is not the actual author of the project. Update the copyright line to Maksym Bukator. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 85d7b66..074865f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Avilo Labs +Copyright (c) 2026 Maksym Bukator Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From d65148017ed1e5d785bb327a4caed881fb31b67d Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 5 Jul 2026 01:49:19 +0200 Subject: [PATCH 06/17] chore(deps): bump dev dependencies to resolve security advisories (#101) npm audit and Dependabot flagged three advisories in transitive development dependencies: a high and moderate in vite (server.fs.deny bypass and launch-editor NTLM disclosure), a high in flatted (prototype pollution), and a moderate in js-yaml (quadratic-complexity DoS). Update the lock file to pull patched versions: vite 8.1.3, flatted 3.4.2, and js-yaml 4.3.0. --- package-lock.json | 236 +++++++++++++++++++++++++++------------------- 1 file changed, 140 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3f871f..2f3f5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1480,9 +1480,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.132.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", - "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", "dev": true, "license": "MIT", "funding": { @@ -1821,9 +1821,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", - "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", "cpu": [ "arm64" ], @@ -1838,9 +1838,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", "cpu": [ "arm64" ], @@ -1855,9 +1855,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", - "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", "cpu": [ "x64" ], @@ -1872,9 +1872,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", - "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", "cpu": [ "x64" ], @@ -1889,9 +1889,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", "cpu": [ "arm" ], @@ -1906,9 +1906,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", "cpu": [ "arm64" ], @@ -1923,9 +1923,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", "cpu": [ "arm64" ], @@ -1940,9 +1940,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", - "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", "cpu": [ "ppc64" ], @@ -1957,9 +1957,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", - "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", "cpu": [ "s390x" ], @@ -1974,9 +1974,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", "cpu": [ "x64" ], @@ -1991,9 +1991,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", - "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", "cpu": [ "x64" ], @@ -2008,9 +2008,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", - "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", "cpu": [ "arm64" ], @@ -2025,9 +2025,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", - "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", "cpu": [ "wasm32" ], @@ -2035,23 +2035,57 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" }, "funding": { "type": "github", @@ -2063,9 +2097,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", "cpu": [ "arm64" ], @@ -2080,9 +2114,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", "cpu": [ "x64" ], @@ -2158,9 +2192,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", "optional": true, @@ -3849,9 +3883,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4270,10 +4304,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4932,9 +4976,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -5293,9 +5337,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -5446,13 +5490,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", - "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.132.0", + "@oxc-project/types": "=0.138.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -5462,21 +5506,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.2", - "@rolldown/binding-darwin-arm64": "1.0.2", - "@rolldown/binding-darwin-x64": "1.0.2", - "@rolldown/binding-freebsd-x64": "1.0.2", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", - "@rolldown/binding-linux-arm64-gnu": "1.0.2", - "@rolldown/binding-linux-arm64-musl": "1.0.2", - "@rolldown/binding-linux-ppc64-gnu": "1.0.2", - "@rolldown/binding-linux-s390x-gnu": "1.0.2", - "@rolldown/binding-linux-x64-gnu": "1.0.2", - "@rolldown/binding-linux-x64-musl": "1.0.2", - "@rolldown/binding-openharmony-arm64": "1.0.2", - "@rolldown/binding-wasm32-wasi": "1.0.2", - "@rolldown/binding-win32-arm64-msvc": "1.0.2", - "@rolldown/binding-win32-x64-msvc": "1.0.2" + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, "node_modules/safer-buffer": { @@ -5860,17 +5904,17 @@ } }, "node_modules/vite": { - "version": "8.0.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", - "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.2", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5886,7 +5930,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.18", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", From 365ab8569457df3ef103c7262de0c4cc87517c55 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 5 Jul 2026 02:09:40 +0200 Subject: [PATCH 07/17] ci(dependabot): target develop instead of default branch (#102) Dependabot had no target-branch configured, so it opened every update PR against the default branch (main), conflicting with the develop-first workflow and pushing dependency changes onto main ahead of develop. Set target-branch: develop on both the npm and github-actions update blocks so future PRs open against develop and follow the normal flow. --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ee1fc3c..a4e3923 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,6 +3,7 @@ version: 2 updates: - package-ecosystem: npm directory: / + target-branch: develop schedule: interval: weekly day: monday @@ -21,6 +22,7 @@ updates: - package-ecosystem: github-actions directory: / + target-branch: develop schedule: interval: weekly day: monday From 75dbd1c5a80dee36ccf03e115fd4e882bcca7e39 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 5 Jul 2026 02:35:49 +0200 Subject: [PATCH 08/17] chore: sync release v1.0.1 to develop (#106) Back-merge the v1.0.1 release commit from main, bringing the version bump and changelog entry into develop so the branches stay reconciled. --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3077ce3..034e322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] - 2026-07-05 + +### Fixed + +- Correct the LICENSE copyright holder to Maksym Bukator + +### Changed + +- Add knip unused-code detection to CI and the pre-push hook +- Broaden the Prettier format check to cover docs and test files +- Update development dependencies to resolve security advisories (vite, flatted, js-yaml) +- Retarget Dependabot at the develop branch + ## [1.0.0] - 2026-05-29 ### Added diff --git a/package-lock.json b/package-lock.json index 2f3f5c5..64606c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "merlin-commit", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "merlin-commit", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.5.0", diff --git a/package.json b/package.json index 4044d94..44de61b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "merlin-commit", - "version": "1.0.0", + "version": "1.0.1", "description": "🧙‍♂️ Magical, interactive CLI for creating conventional commits", "type": "module", "main": "./dist/cli.js", From 2dbc1c005f2709638bc849f015c88f752aff6e9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:18:00 +0200 Subject: [PATCH 09/17] chore(deps): bump actions/setup-node from 6 to 7 (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
Release notes

Sourced from actions/setup-node's releases.

v7.0.0

What's Changed

Enhancements:

Bug fixes:

Documentation updates:

Dependency update:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v6...v7.0.0

v6.5.0

What's Changed

Full Changelog: https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0

v6.4.0

What's Changed

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v6...v6.4.0

v6.3.0

What's Changed

Enhancements:

... (truncated)

Commits
  • 8207627 Migrate to ESM and upgrade dependencies (#1574)
  • 04be95c Add cache-primary-key and cache-matched-key as outputs (#1577)
  • 7c2c68d docs: Update caching recommendations to mitigate cache poisoning risks (#1567)
  • 6a61c03 Merge pull request #1569 from jasongin/update-actions-cache-5.1.0
  • 30eb73b Resolve high-severity audit issues
  • 4e1a87a Update dist
  • 360237f Strict equality
  • 4f8aac5 Bump @​actions/cache to 5.1.0, log cache write denied
  • f4a67bb Only use mirrorToken in getManifest if it's provided (#1548)
  • 0355742 Remove dummy NODE_AUTH_TOKEN export (#1558)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 122ab15..1ed8576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 20 cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e53f263..05daed0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: 20 cache: npm From c74d66e9a7fd77be95838a625c98f2ff60cc37a7 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sat, 22 Aug 2026 18:26:31 +0200 Subject: [PATCH 10/17] ci(dependabot): ignore typescript major bumps (#111) The weekly dev-dependencies group PR was permanently failing npm ci. Dependabot bundles every dev dependency into unsatisfiable bump blocked 13 unrelated safe updates. The group bumped typescript to ^7.0.2, which no stable typescript-eslint release supports - @typescript-eslint/eslint-plugin@8.67.0, the current latest, still declares a peer range of typescript ">=4.8.4 <6.1.0". Only 8.67.1-alpha.* prereleases exist beyond that, so the bump cannot resolve until upstream ships TypeScript 7 support. Scope the ignore rule to version-update:semv patch and minor typescript updates keep flowing. The rationale and the removal condition are documented inline in the config. Closing #107 alone would not have helped - Dependabot would recreate the identical failing PR on the next weekly run. Relates to #107 --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a4e3923..42c30b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,13 @@ updates: dependency-type: development production-dependencies: dependency-type: production + ignore: + # typescript-eslint has no stable release supporting TypeScript 7 yet: + # @typescript-eslint/eslint-plugin@8.67.0 still peers on typescript <6.1.0. + # Without this, the whole dev-dependencies group PR fails npm ci on ERESOLVE. + # Remove once typescript-eslint ships TypeScript 7 support. + - dependency-name: "typescript" + update-types: ["version-update:semver-major"] - package-ecosystem: github-actions directory: / From c68a70609860fd21276b8640f3111828ed80cd38 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sat, 22 Aug 2026 18:28:39 +0200 Subject: [PATCH 11/17] chore(deps): bump production dependencies and correct node floor (#112) Update chalk 5 to 6, execa 9 to 10, @inquirer/prompts to 8.6.0 and ora to 9.4.1. Both majors raise their Node floor to 22, so correct the declared engines range alongside them. The >=18 claim was already inaccurate: commander@15 requires Node >=22.12.0, so v1.0.1 could not install or run on Node 18 or 20 despite advertising support. This corrects a false declaration rather than removing working functionality; Node 18 and 20 are both end-of-life. Three safeguards hid the drift: npm ci only warns on EBADENGINE, CI pinned a single Node 20 job below the real floor, and execa is mocked throughout the test suite. Move CI and release workflows to Node 22 so the supported floor is actually exercised. No source changes were needed. Only chalk colour helpers are used, and every execa call site uses the execa(file, argsArray) form - neither the removed execaCommand() nor the relocated ChildProcess methods are touched. @inquirer/prompts 8.5.2 hardens external-editor temp files, which back the editor flow in lib/editor-wrapper.ts. Verified against real git with execa 10 rather than the test mocks: isGitRepo, getRepoRoot and getStagingCandidates all behave correctly, and the CLI renders help and version. CI run 32578611999 confirms Node v22.23.2 with zero EBADENGINE warnings across the run. Relates to #110 --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 22 ++ docs/development/contributing.md | 2 +- docs/getting-started/installation.md | 2 +- docs/guides/team-setup.md | 2 +- docs/index.md | 2 +- package-lock.json | 301 +++++++++++++++------------ package.json | 10 +- 9 files changed, 200 insertions(+), 145 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ed8576..c77a91c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: npm - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05daed0..3001d8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: npm registry-url: https://registry.npmjs.org diff --git a/CHANGELOG.md b/CHANGELOG.md index 034e322..ef7c655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Raise the minimum supported Node.js version to 22 + + The declared `engines.node` range of `>=18` was already inaccurate: + `commander@15` requires Node `>=22.12.0`, so v1.0.1 could not install or + run on Node 18 or 20 despite claiming support. This corrects the declared + range to match reality rather than removing working functionality. Node 18 + and 20 are both past end-of-life. + +- Update production dependencies: `chalk` 5 to 6, `execa` 9 to 10, + `@inquirer/prompts` to 8.6.0, `ora` to 9.4.1 + + `@inquirer/prompts` 8.5.2 hardens temp-file handling in `external-editor`, + which backs the external editor flow. No merlin code changes were required: + only chalk colour helpers are used, and every `execa` call site uses the + `execa(file, argsArray)` form that both majors support. + +- Run CI and release workflows on Node 22 so the supported floor is actually exercised + ## [1.0.1] - 2026-07-05 ### Fixed diff --git a/docs/development/contributing.md b/docs/development/contributing.md index fcd005d..2054caf 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -23,7 +23,7 @@ This project follows the [Contributor Covenant Code of Conduct](../../CODE_OF_CO ### Prerequisites -- Node.js >= 18.0.0 +- Node.js >= 22.0.0 - npm >= 9.0.0 - Git diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 40fc59e..0701b1f 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -6,7 +6,7 @@ | Requirement | Version | | ----------- | -------------------------------- | -| Node.js | >= 18.0.0 | +| Node.js | >= 22.0.0 | | npm | >= 9.0.0 (bundled with Node 18+) | | Git | Any recent version | diff --git a/docs/guides/team-setup.md b/docs/guides/team-setup.md index 78f6656..051df4f 100644 --- a/docs/guides/team-setup.md +++ b/docs/guides/team-setup.md @@ -6,7 +6,7 @@ This guide walks a team lead through setting up merlin-commit for a repository s ## Prerequisites -- Node.js >= 18.0.0 on all developer machines +- Node.js >= 22.0.0 on all developer machines - A git repository with a `package.json` (any Node.js project) - npm, yarn, or pnpm diff --git a/docs/index.md b/docs/index.md index a76d095..230d105 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ | -------- | --------------- | | Package | `merlin-commit` | | Binary | `merlin` | -| Node.js | >= 18.0.0 | +| Node.js | >= 22.0.0 | | License | MIT | --- diff --git a/package-lock.json b/package-lock.json index 64606c3..4509dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "1.0.1", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^8.5.0", - "chalk": "^5.6.2", + "@inquirer/prompts": "^8.6.0", + "chalk": "^6.0.0", "commander": "^15.0.0", - "execa": "^9.6.1", - "ora": "^9.4.0" + "execa": "^10.0.1", + "ora": "^9.4.1" }, "bin": { "merlin": "bin/merlin.js", @@ -739,27 +739,27 @@ } }, "node_modules/@inquirer/ansi": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.6.tgz", - "integrity": "sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/checkbox": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.0.tgz", - "integrity": "sha512-1HJt+3fqxblp/GQjdntSyoSHYBc0e3CzXVgjFpKA6qFLd9FHBBqwN8Co0xYH6t2JVUZrtFwZ4bBiwptkiLxyOg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.2.tgz", + "integrity": "sha512-Y5/bAScMy5Y+9isCx0SKbyJebMCaXXX5em0kxkj115eZNscgV9srOHrgyfS0e5xAVymIfOh9piYBKDILktsMMg==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.6", - "@inquirer/core": "^11.2.0", - "@inquirer/figures": "^2.0.6", - "@inquirer/type": "^4.0.6" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^12.0.0", + "@inquirer/figures": "^2.0.8", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -771,16 +771,16 @@ } }, "node_modules/@inquirer/confirm": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.0.tgz", - "integrity": "sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.2.0.tgz", + "integrity": "sha512-SKXarWrYhtpqOEctf9XGCGy29QjsvJAM0Aq9ZR9z4Ns94OmpqudOly+aSEfNqUf9SwsQaUgY9+Z8hyzG0xX8fw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -792,21 +792,21 @@ } }, "node_modules/@inquirer/core": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.0.tgz", - "integrity": "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.0.tgz", + "integrity": "sha512-+nnvFEXIB08CZNVXpvW3B+zHW96QXvGUjNKJ8NJIPqAZi5Kd4WhYt2S3C234ReepG1qw2HOlEUbjYVHBowXObA==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.6", - "@inquirer/figures": "^2.0.6", - "@inquirer/type": "^4.0.6", + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.8", + "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", - "mute-stream": "^4.0.0", + "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -818,17 +818,17 @@ } }, "node_modules/@inquirer/editor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.0.tgz", - "integrity": "sha512-/m+sgRmzSdK6HDtVnl3PmI6MnZC4O+LLezedoJcrX7mINhTjjb0hlC7aEDGZXkFTB4b5uQ0q59AhYTah88KbNg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.0.tgz", + "integrity": "sha512-nnsP/IdJ8s83q7ZuObmgn12QM+uLCkab9E0Oordojbn62WUg1c+v9Ou/F/057pgh0ppX0W+Hj5bO/Dp5hsxQtA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/external-editor": "^3.0.1", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/external-editor": "^3.0.4", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -840,16 +840,16 @@ } }, "node_modules/@inquirer/expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.0.tgz", - "integrity": "sha512-fR7g4BVnIcs+4NApF6C5byflNM/EULxSxsv/2Jvg+gmop0R6eBIPvZqE6RYnTy1tQTFnf9wyHkwNoQSZbofaGA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.2.tgz", + "integrity": "sha512-OWIH1IyyWqEKIyqC9Xy+Bnga7NkGMovFdo4atYZMUOTRqf6rO2WCv9E/1MyzvOErDBCxs+9UFliRUDc50xs/jw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -861,16 +861,16 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.1.tgz", - "integrity": "sha512-tam+Gwjsxg2sx3iUVPkAnhKT/yrk2rd2NAa7XJU/J8OYpU0ifXsnp12xlvzp/DCpWBXVv+vLQsqnpAWwUcWD5Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.4.tgz", + "integrity": "sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==", "license": "MIT", "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -882,25 +882,25 @@ } }, "node_modules/@inquirer/figures": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.6.tgz", - "integrity": "sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.8.tgz", + "integrity": "sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/input": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.0.tgz", - "integrity": "sha512-sVZCz6P6e8tW5g2bSFel1oLpa6jK/u7BexFfrgTqR8syIdnHqy+iopnlSbYBZMsCK52chLjhGNBxt0eRqhsghw==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.3.tgz", + "integrity": "sha512-F/BZHtyEzP+HO+IGVd4AjBRgvX/ywm42bx8S0+dENk2YclzE9tJ3X/15THwtT6ehApmKvdYDMsVTuyyDod0gOQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -912,16 +912,16 @@ } }, "node_modules/@inquirer/number": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.0.tgz", - "integrity": "sha512-VMXB/XejCbaSTf9Xucl7dqjzzsaGsrs6XwSYXPbGZ2QbSuq/Gz8XamhSi9ClRubNXZlGry9xVg1tKkJdTDgCtQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.0.tgz", + "integrity": "sha512-ew+fSDijsQ/WhD4TV3XLb+if400cDuzTzHfGR8sTNBXkK9CYDWoGE8fhaO8GbT312pNv1AJEOsDxy/z/HVettA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -933,17 +933,17 @@ } }, "node_modules/@inquirer/password": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.0.tgz", - "integrity": "sha512-5tqRuKCDIUxdPxTI/CuLnh914kz+WMPmURHKnZgui9gk43ebudEsdu4EwSn1CPSi5R+17YpBG+ba/YqTnRAcJA==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.2.tgz", + "integrity": "sha512-nSdufycW8xynEVssFkNQEYIzTySilog0UlfOVRwh3pXzPSk4frXUT2jZWjHnKae6RU9PaoF9wfy1pGwewQuqGw==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.6", - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -955,24 +955,24 @@ } }, "node_modules/@inquirer/prompts": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.0.tgz", - "integrity": "sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.6.0.tgz", + "integrity": "sha512-WgBVDRy3IQ4v9XMCpQ1YDGpso2PcMUxYJzZdH4Nt4t0eoXhEPmOCh5iZbXbR4GTbdUB9VPWBbJB12rkjbaGDCw==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^5.2.0", - "@inquirer/confirm": "^6.1.0", - "@inquirer/editor": "^5.2.0", - "@inquirer/expand": "^5.1.0", - "@inquirer/input": "^5.1.0", - "@inquirer/number": "^4.1.0", - "@inquirer/password": "^5.1.0", - "@inquirer/rawlist": "^5.3.0", - "@inquirer/search": "^4.2.0", - "@inquirer/select": "^5.2.0" + "@inquirer/checkbox": "^5.2.2", + "@inquirer/confirm": "^6.2.0", + "@inquirer/editor": "^5.3.0", + "@inquirer/expand": "^5.1.2", + "@inquirer/input": "^5.1.3", + "@inquirer/number": "^4.2.0", + "@inquirer/password": "^5.1.2", + "@inquirer/rawlist": "^5.3.2", + "@inquirer/search": "^4.3.0", + "@inquirer/select": "^5.2.2" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -984,16 +984,16 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.0.tgz", - "integrity": "sha512-p+vAeTAD+cGXjGleP1F5LXrX2ISxNDZm+lqeBpnJausNLSZskZZkcggwhomqP8Igx9oIjnoeOrw98xvdFvdm2w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.2.tgz", + "integrity": "sha512-oPSKrYK1X1bMkjXDzIKHUkJp195LFSfgbnVtXnjSKGFjrCbS6I+wyvfAZTwKE9BSt3HwWgfD7JfsXALBgCogzQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1005,17 +1005,17 @@ } }, "node_modules/@inquirer/search": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.0.tgz", - "integrity": "sha512-ByURoSGIaSl5O5Q0AmYmVmUsXbMUcBGNoA3FRL7TOyiA22IeFHymJKRkuILbOIlJwqnBk7AnPpseodyFUBzg+g==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.0.tgz", + "integrity": "sha512-HFxXE5w727ctSUcAwrDquftJGjMgu36OeV5SHEXMlr2j/ahzmRX9xSEeVolV8tzYnTf45cg6vGkdMMRdm3RPhQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^11.2.0", - "@inquirer/figures": "^2.0.6", - "@inquirer/type": "^4.0.6" + "@inquirer/core": "^12.0.0", + "@inquirer/figures": "^2.0.8", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1027,18 +1027,18 @@ } }, "node_modules/@inquirer/select": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.0.tgz", - "integrity": "sha512-6IzkcmEbEXfgVbxZ2d1UyJFbCBoc6dTofulFmrYuomIp88HXiVqRbqbg4/mbfZhvnNo6xYmnYo2AEmDof6fQkg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.2.tgz", + "integrity": "sha512-RkI8dRHWt+bh04oLixvF1kFzKC7e5rqJoHKkzcqSHATebBXFC6GmrT8ddbVkgSzLV0HnHs2cPFuBINr8otij8Q==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.6", - "@inquirer/core": "^11.2.0", - "@inquirer/figures": "^2.0.6", - "@inquirer/type": "^4.0.6" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^12.0.0", + "@inquirer/figures": "^2.0.8", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1050,12 +1050,12 @@ } }, "node_modules/@inquirer/type": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.6.tgz", - "integrity": "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", "license": "MIT", "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -3042,21 +3042,21 @@ } }, "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz", + "integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", "license": "MIT" }, "node_modules/cli-cursor": { @@ -3274,6 +3274,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3698,26 +3699,26 @@ "license": "MIT" }, "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.1.tgz", + "integrity": "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", "figures": "^6.1.0", - "get-stream": "^9.0.0", + "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", + "pretty-ms": "^9.3.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" + "which-command": "^0.1.0", + "yoctocolors": "^2.1.2" }, "engines": { - "node": "^18.19.0 || >=20.5.0" + "node": ">=22" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" @@ -4075,9 +4076,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -4245,6 +4246,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4967,12 +4969,12 @@ "license": "MIT" }, "node_modules/mute-stream": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-4.0.0.tgz", - "integrity": "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "license": "ISC", "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -5090,9 +5092,9 @@ } }, "node_modules/ora": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", - "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -5111,6 +5113,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/oxc-parser": { "version": "0.135.0", "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.135.0.tgz", @@ -5287,6 +5301,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5546,6 +5561,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5558,6 +5574,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6085,6 +6102,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6096,6 +6114,21 @@ "node": ">= 8" } }, + "node_modules/which-command": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz", + "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==", + "license": "MIT", + "bin": { + "which-command": "cli.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sindresorhus/which-command?sponsor=1" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/package.json b/package.json index 44de61b..0573b11 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,14 @@ "author": "mBukator", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "dependencies": { - "@inquirer/prompts": "^8.5.0", - "chalk": "^5.6.2", + "@inquirer/prompts": "^8.6.0", + "chalk": "^6.0.0", "commander": "^15.0.0", - "execa": "^9.6.1", - "ora": "^9.4.0" + "execa": "^10.0.1", + "ora": "^9.4.1" }, "devDependencies": { "@commitlint/cli": "^21.0.2", From 54d61d86ac26e4db8f3c7ced429a9d082211e7eb Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Wed, 14 Oct 2026 17:36:17 +0200 Subject: [PATCH 12/17] docs(readme): replace static logo with animated banner (#117) Replace the static logo at the top of the README with an animated SVG banner: Merlin on a turning summoning circle, pulsing eye beams, a blink, twinkling hat stars and a sheen across the wordmark. Pure SVG and CSS, so it animates inside GitHub's image sandbox where scripts and external fonts are blocked. It shows a still frame when prefers-reduced-motion is set. Remove merlin-logo-large.svg: the banner replaces it and nothing else in the repo references it. --- .github/assets/merlin-banner.svg | 234 +++++++++++++++++++++++++++ .github/assets/merlin-logo-large.svg | 79 --------- README.md | 2 +- 3 files changed, 235 insertions(+), 80 deletions(-) create mode 100644 .github/assets/merlin-banner.svg delete mode 100644 .github/assets/merlin-logo-large.svg diff --git a/.github/assets/merlin-banner.svg b/.github/assets/merlin-banner.svg new file mode 100644 index 0000000..155a80f --- /dev/null +++ b/.github/assets/merlin-banner.svg @@ -0,0 +1,234 @@ + + merlin-commit + Merlin, the wizard of merlin-commit, stands on a summoning circle among the stars. His eyes glow, the stars on his hat twinkle and a sheen passes over the MERLIN COMMIT wordmark. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/assets/merlin-logo-large.svg b/.github/assets/merlin-logo-large.svg deleted file mode 100644 index 57ff645..0000000 --- a/.github/assets/merlin-logo-large.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index 627ea3a..2c5f090 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # merlin-commit

- merlin-commit + merlin-commit

> Interactive CLI for creating [Conventional Commits](https://www.conventionalcommits.org/) with a wizard-themed interface From f7d70f4da36d6e1abefeec4f3eb9f84194f6af68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:46:23 +0200 Subject: [PATCH 13/17] chore(deps): bump the production-dependencies group across 1 directory with 3 updates (#116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the production-dependencies group with 3 updates in the / directory: [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js), [chalk](https://github.com/chalk/chalk) and [execa](https://github.com/sindresorhus/execa). Updates `@inquirer/prompts` from 8.6.0 to 8.7.3
Release notes

Sourced from @​inquirer/prompts's releases.

@​inquirer/prompts@​8.7.3

What's new

  • search() no longer submits a stale choice when Enter is pressed while an async search is still loading — the prompt waits for the fresh results, the query is preserved, and repeated Enter presses during validation no longer run the async validator more than once (@inquirer/search, #2291, closes #2290).
  • confirm() now always accepts the single keystrokes y/n even with localized keywords, and unrecognized input no longer silently falls back on the default answer (@inquirer/confirm, #2281, closes #2279).
  • expand() and rawlist() now ignore surrounding whitespace in answers (@inquirer/expand, @inquirer/rawlist, #2280).
  • All package READMEs now document the Deno install command (deno add npm:@inquirer/prompts) alongside the npm/yarn/pnpm/bun ones (#2269).

Included

  • @inquirer/checkbox@^5.2.6
  • @inquirer/confirm@^6.3.3
  • @inquirer/editor@^5.3.4
  • @inquirer/expand@^5.1.6
  • @inquirer/input@^5.1.7
  • @inquirer/number@^4.2.4
  • @inquirer/password@^5.2.3
  • @inquirer/rawlist@^5.3.6
  • @inquirer/search@^4.3.4
  • @inquirer/select@^5.2.6

@​inquirer/prompts@​8.7.2

What's new

  • Fixed a race where keystrokes batched in the same tick as the key that settled a prompt could still reach keypress handlers after the prompt was done, cancelled, or aborted (@inquirer/core, #2255, closes #1816).
  • confirm() now trims surrounding whitespace from answers before matching yes/no keywords (@inquirer/confirm, #2254).

Included

  • @inquirer/checkbox@^5.2.5
  • @inquirer/confirm@^6.3.2
  • @inquirer/editor@^5.3.3
  • @inquirer/expand@^5.1.5
  • @inquirer/input@^5.1.6
  • @inquirer/number@^4.2.3
  • @inquirer/password@^5.2.2
  • @inquirer/rawlist@^5.3.5
  • @inquirer/search@^4.3.3
  • @inquirer/select@^5.2.5

@​inquirer/prompts@​8.7.1

What's new

  • All bundled prompts now pin @inquirer/type to an exact version in their published manifests. Since these type definitions leak into consumers' tsc runs, a semver range on the types-only dependency could break downstream TypeScript builds without any change to Inquirer.js itself (#2247, fixes #2244).

Included

... (truncated)

Commits
  • a9436bd chore(release): bump @​inquirer/type range to ^4.2.0
  • 61be5df chore: Publish new release
  • 7dc77e2 fix(search): prevent submissions while the prompt is loading
  • 73b5eab docs: document Deno installation (#2269)
  • 79a092a ci: add Deno compatibility test suite (#2271)
  • 148a96a chore: enable vitest fsModuleCache to persist transforms across runs
  • e928543 chore: bump yarn to 4.18.1 via corepack
  • 2348c7b fix(external-editor): use BSD/GNU-compatible truncate flag in test
  • 3c1fdb3 fix(confirm): always accept y/n and reject unrecognized input (#2281)
  • eae8b3e fix(expand, rawlist): ignore surrounding whitespace in answers (#2280)
  • Additional commits viewable in compare view

Updates `chalk` from 6.0.0 to 6.0.1
Release notes

Sourced from chalk's releases.

v6.0.1

  • Fix inconsistent coercion of two arguments 9c93a04

https://github.com/chalk/chalk/compare/v6.0.0...v6.0.1

Commits

Updates `execa` from 10.0.1 to 10.1.0
Release notes

Sourced from execa's releases.

v10.1.0

Behavior changes

These are bug fixes, but they might affect code that relied on the previous incorrect behavior.

  • extendEnv: false without the env option no longer inherits process.env, per docs 67acf60
  • Windows: Arguments of .cmd and .bat files are now escaped like Rust does, which fixes batch files reading %~1 (#1261) 93bddc4
  • Invalid stdio values, such as negative file descriptors, now throw c6bd387
  • Using the same file as input and output now throws, even when the paths are written differently 587045a
  • Synchronous methods now enforce per-file-descriptor maxBuffer values 9547e51
  • Synchronous methods now throw on the signal option, like asynchronous methods 59de065
  • Synchronous methods now write the raw output bytes to files, like asynchronous methods af6264a

Fixes

  • Fix subprocess.kill() signaling the current process group when the subprocess failed to spawn 5464d43
  • Fix crashes when spawning fails with EMFILE/ENFILE (#1262), or when a file target cannot be opened 15c46cd 1cee524
  • Fix error.stdout and error.stderr being undefined when synchronous methods fail to spawn (#1263) bfe4633
  • Fix the subprocess promise hanging until an output stream is read 004f3b3
  • Fix subprocess.pipe() with the stdio option, and destination subprocesses left running 8d3f0a9 ecda56b
  • Fix result.all when only some file descriptors are buffered or split into lines 758768f
  • Fix the encoding option and line splitting in transforms 6adb3c3
  • Fix input and output transforms with synchronous methods 516a5e2 e0b5dbc
  • Fix template strings with emojis and line continuations b67f199
  • Fix killDescendants signaling unrelated processes on Windows 9c5c71c
  • Fix IPC with serialization: 'json', strict: true and the verbose option 9842f1a 698a1b8 b548d8b
  • Fix the subprocess error being hidden when the current directory was deleted 91d9ded
  • Fix shebang scripts on Windows when the file is small 7c1a631

https://github.com/sindresorhus/execa/compare/v10.0.1...v10.1.0

Commits
  • 63ddae6 10.1.0
  • cfac59f Fix CI
  • 1cee524 Fix an uncaught error when a file target cannot be opened and the subprocess ...
  • 15c46cd Fix execa() crashing when spawning fails with EMFILE or ENFILE
  • 54d7404 Tweaks
  • bfe4633 Fix execaSync() returning undefined output when the subprocess fails to s...
  • 93bddc4 Fix arguments of Windows batch files that read them with %~1
  • af6264a Fix execaSync() writing decoded text instead of the output's bytes to files
  • ecda56b Report null pipe options as a rejection
  • 698a1b8 Tweaks
  • Additional commits viewable in compare view

Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 206 +++++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4509dc8..55bf67c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "vitest": "^4.0.17" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { "@commitlint/config-conventional": ">=19.0.0", @@ -739,24 +739,24 @@ } }, "node_modules/@inquirer/ansi": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", - "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.9.tgz", + "integrity": "sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==", "license": "MIT", "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/checkbox": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.2.tgz", - "integrity": "sha512-Y5/bAScMy5Y+9isCx0SKbyJebMCaXXX5em0kxkj115eZNscgV9srOHrgyfS0e5xAVymIfOh9piYBKDILktsMMg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.6.tgz", + "integrity": "sha512-XkOardciF3tow2NUlY6/Qjk1QqYa+6WpmVLTpxXl97Uv4erczDEZ+xg1j6LqBf0ekcRSUiICQA4nQXHHkVKQ9w==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^12.0.0", - "@inquirer/figures": "^2.0.8", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.9", + "@inquirer/core": "^12.0.4", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -771,13 +771,13 @@ } }, "node_modules/@inquirer/confirm": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.2.0.tgz", - "integrity": "sha512-SKXarWrYhtpqOEctf9XGCGy29QjsvJAM0Aq9ZR9z4Ns94OmpqudOly+aSEfNqUf9SwsQaUgY9+Z8hyzG0xX8fw==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.3.tgz", + "integrity": "sha512-ImOBgpl/sJTFp8naeSkNu4S0Sf8F8pxv3/CUx+LoBAdQvCVqFz6rSB2A/oZYsJge3+yphbEDxEkfaRNYq1OU0A==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -792,14 +792,14 @@ } }, "node_modules/@inquirer/core": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.0.tgz", - "integrity": "sha512-+nnvFEXIB08CZNVXpvW3B+zHW96QXvGUjNKJ8NJIPqAZi5Kd4WhYt2S3C234ReepG1qw2HOlEUbjYVHBowXObA==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.4.tgz", + "integrity": "sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/figures": "^2.0.8", - "@inquirer/type": "^4.0.7", + "@inquirer/ansi": "^2.0.9", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.2.0", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", @@ -818,14 +818,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.0.tgz", - "integrity": "sha512-nnsP/IdJ8s83q7ZuObmgn12QM+uLCkab9E0Oordojbn62WUg1c+v9Ou/F/057pgh0ppX0W+Hj5bO/Dp5hsxQtA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.4.tgz", + "integrity": "sha512-imzxQDfw26NpcJAubMj/dE5RjU9Mp80IcT+FzsD6YoxXmL8L1QYnAFdCNv7ccGWh//73+0YP+1kfhwnExdl2VQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/external-editor": "^3.0.4", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/external-editor": "^3.0.6", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -840,13 +840,13 @@ } }, "node_modules/@inquirer/expand": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.2.tgz", - "integrity": "sha512-OWIH1IyyWqEKIyqC9Xy+Bnga7NkGMovFdo4atYZMUOTRqf6rO2WCv9E/1MyzvOErDBCxs+9UFliRUDc50xs/jw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.6.tgz", + "integrity": "sha512-JfiLKi7qGuHmcQO/DqR10UFEp054Y1FXZaszP5ZEfHhgHeuFoT5nNFQIXFhed/5/p07SbsV2tIJFwG/dg5+NmQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -861,9 +861,9 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.4.tgz", - "integrity": "sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.6.tgz", + "integrity": "sha512-rWmw6WG8JZ7LMn/srXEylYGhL6EXXJeROE7/4x4GREh6NgzqJBDbIgpNGsYoLPfobeb4VZtEYzPGnT5EFNYhiQ==", "license": "MIT", "dependencies": { "chardet": "^2.1.1", @@ -882,22 +882,22 @@ } }, "node_modules/@inquirer/figures": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.8.tgz", - "integrity": "sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", "license": "MIT", "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/input": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.3.tgz", - "integrity": "sha512-F/BZHtyEzP+HO+IGVd4AjBRgvX/ywm42bx8S0+dENk2YclzE9tJ3X/15THwtT6ehApmKvdYDMsVTuyyDod0gOQ==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.7.tgz", + "integrity": "sha512-Icrs9tuMBCxGI+lFYNO71VFOnqGO8IIrLIGqnTGh07X8jZ2la9QLAYxXcikHZQjjVhIZUtU2xyH89DVN/pPhVg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -912,13 +912,13 @@ } }, "node_modules/@inquirer/number": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.0.tgz", - "integrity": "sha512-ew+fSDijsQ/WhD4TV3XLb+if400cDuzTzHfGR8sTNBXkK9CYDWoGE8fhaO8GbT312pNv1AJEOsDxy/z/HVettA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.4.tgz", + "integrity": "sha512-C6g80U4U2+w2Vxa1CB0MN3a2WnQIfzLSUG0lduO3Au9GsirJsKATZkOwZPK+vZ1BlAlz+/Z5ParfQCS9E0svmg==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -933,14 +933,14 @@ } }, "node_modules/@inquirer/password": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.2.tgz", - "integrity": "sha512-nSdufycW8xynEVssFkNQEYIzTySilog0UlfOVRwh3pXzPSk4frXUT2jZWjHnKae6RU9PaoF9wfy1pGwewQuqGw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.3.tgz", + "integrity": "sha512-1Uo2qfI2K3wXbfunMpvwAzoZPIUf5iEETwDGqRVbswoorXGqYQJ/bfIJFeLj+pxjq+e2UFPNOmx+fsIl4Urigw==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.9", + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -955,21 +955,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.6.0.tgz", - "integrity": "sha512-WgBVDRy3IQ4v9XMCpQ1YDGpso2PcMUxYJzZdH4Nt4t0eoXhEPmOCh5iZbXbR4GTbdUB9VPWBbJB12rkjbaGDCw==", + "version": "8.7.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.3.tgz", + "integrity": "sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^5.2.2", - "@inquirer/confirm": "^6.2.0", - "@inquirer/editor": "^5.3.0", - "@inquirer/expand": "^5.1.2", - "@inquirer/input": "^5.1.3", - "@inquirer/number": "^4.2.0", - "@inquirer/password": "^5.1.2", - "@inquirer/rawlist": "^5.3.2", - "@inquirer/search": "^4.3.0", - "@inquirer/select": "^5.2.2" + "@inquirer/checkbox": "^5.2.6", + "@inquirer/confirm": "^6.3.3", + "@inquirer/editor": "^5.3.4", + "@inquirer/expand": "^5.1.6", + "@inquirer/input": "^5.1.7", + "@inquirer/number": "^4.2.4", + "@inquirer/password": "^5.2.3", + "@inquirer/rawlist": "^5.3.6", + "@inquirer/search": "^4.3.4", + "@inquirer/select": "^5.2.6" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -984,13 +984,13 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.2.tgz", - "integrity": "sha512-oPSKrYK1X1bMkjXDzIKHUkJp195LFSfgbnVtXnjSKGFjrCbS6I+wyvfAZTwKE9BSt3HwWgfD7JfsXALBgCogzQ==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.6.tgz", + "integrity": "sha512-2exExhQ5xTBqfN/rRlZkvM1Gm3b0eiLcWe2NqxWxe7qYmnU4M3j/n9TwzfPUb+Q7dAAz9TmBtDWWdF3jbp4ZLA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1005,14 +1005,14 @@ } }, "node_modules/@inquirer/search": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.0.tgz", - "integrity": "sha512-HFxXE5w727ctSUcAwrDquftJGjMgu36OeV5SHEXMlr2j/ahzmRX9xSEeVolV8tzYnTf45cg6vGkdMMRdm3RPhQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.4.tgz", + "integrity": "sha512-xsk0UeFP04jsbEeFMzghjUaH2aHXyt+WykSajQzq/8qyBNHsgGYHea/sFwv5T08kW7Sp+KDX8aF124zPUIs1Ww==", "license": "MIT", "dependencies": { - "@inquirer/core": "^12.0.0", - "@inquirer/figures": "^2.0.8", - "@inquirer/type": "^4.0.7" + "@inquirer/core": "^12.0.4", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1027,15 +1027,15 @@ } }, "node_modules/@inquirer/select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.2.tgz", - "integrity": "sha512-RkI8dRHWt+bh04oLixvF1kFzKC7e5rqJoHKkzcqSHATebBXFC6GmrT8ddbVkgSzLV0HnHs2cPFuBINr8otij8Q==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.6.tgz", + "integrity": "sha512-jFu7CFghlr0vpi/326a4wz6BGkJH9X3H/iIu+ahyV2Ja52ktqQE3fC9BYP47JKeSPQYGUv5xquE2Vp9PbepZ3g==", "license": "MIT", "dependencies": { - "@inquirer/ansi": "^2.0.7", - "@inquirer/core": "^12.0.0", - "@inquirer/figures": "^2.0.8", - "@inquirer/type": "^4.0.7" + "@inquirer/ansi": "^2.0.9", + "@inquirer/core": "^12.0.4", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.2.0" }, "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -1050,9 +1050,9 @@ } }, "node_modules/@inquirer/type": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", - "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.0.tgz", + "integrity": "sha512-2rnHX8RPo4glJYqnFc/v3nQeIhDcjyRRyaT7MGcfL/8xv/8K03SXYoA2tX06WVSKywWOE/ncbG3dtuzLjR5TCQ==", "license": "MIT", "engines": { "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" @@ -3042,9 +3042,9 @@ } }, "node_modules/chalk": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz", - "integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.1.tgz", + "integrity": "sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA==", "license": "MIT", "engines": { "node": ">=22" @@ -3699,9 +3699,9 @@ "license": "MIT" }, "node_modules/execa": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.1.tgz", - "integrity": "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-10.1.0.tgz", + "integrity": "sha512-5HT/600HqXEtmkb9mK/PkTWw+bQSCzPnWiZ4/87wD3Hi0wFzCapOuNT9p4M08PDd+KLHdHvFhKjOWmNx/npRNQ==", "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", @@ -3711,11 +3711,11 @@ "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", - "pretty-ms": "^9.3.0", + "pretty-ms": "^9.3.1", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", - "yoctocolors": "^2.1.2" + "yoctocolors": "^2.2.0" }, "engines": { "node": ">=22" @@ -5407,9 +5407,9 @@ } }, "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" @@ -6278,9 +6278,9 @@ } }, "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", "license": "MIT", "engines": { "node": ">=18" From 3ac053f55e70304769d3f8fe5462854d228dfce5 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Wed, 14 Oct 2026 22:09:24 +0200 Subject: [PATCH 14/17] fix(deps): resolve dev dependency security advisories (#118) - Refresh the lockfile with npm audit fix to patch transitive dev dependencies: fast-uri 3.1.8, js-yaml 4.3.2, postcss 8.5.30, smol-toml 1.9.1, brace-expansion 5.0.12, @humanfs/node 0.16.8 and source-map-js 1.2.2 - Raise vitest and @vitest/coverage-v8 to ^4.1.11 together, since coverage-v8 pins an exact vitest peer and blocks npm audit fix Resolves all 15 open Dependabot alerts (11 high, 4 moderate) plus two newer advisories without alerts yet (GHSA-r4xh-jqrq-34v2, GHSA-68fv-2mgg-jv7q). The alerts close once this reaches main. All advisories affect development tooling only. npm audit --omit=dev was already clean, so users of the published package were never exposed. Production dependency resolution is unchanged. --- package-lock.json | 594 ++++++++++++++++++++++------------------------ package.json | 4 +- 2 files changed, 280 insertions(+), 318 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55bf67c..94110a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@types/node": "^25.9.1", "@typescript-eslint/eslint-plugin": "^8.60.0", "@typescript-eslint/parser": "^8.53.0", - "@vitest/coverage-v8": "^4.1.7", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import-x": "^4.16.2", @@ -37,7 +37,7 @@ "prettier": "^3.8.3", "rimraf": "^6.1.3", "typescript": "^6.0.3", - "vitest": "^4.0.17" + "vitest": "^4.1.11" }, "engines": { "node": ">=22" @@ -652,29 +652,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1480,13 +1494,13 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.138.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", - "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { @@ -1820,10 +1834,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", - "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", "cpu": [ "arm64" ], @@ -1838,9 +1869,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", - "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", "cpu": [ "arm64" ], @@ -1855,9 +1886,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", - "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", "cpu": [ "x64" ], @@ -1872,9 +1903,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", - "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", "cpu": [ "x64" ], @@ -1889,9 +1920,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", - "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", "cpu": [ "arm" ], @@ -1906,9 +1937,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", - "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", "cpu": [ "arm64" ], @@ -1923,9 +1954,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", - "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", "cpu": [ "arm64" ], @@ -1940,9 +1971,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", - "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", "cpu": [ "ppc64" ], @@ -1957,9 +1988,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", - "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", "cpu": [ "s390x" ], @@ -1974,9 +2005,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", - "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", "cpu": [ "x64" ], @@ -1991,9 +2022,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", - "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", "cpu": [ "x64" ], @@ -2008,9 +2039,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", - "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", "cpu": [ "arm64" ], @@ -2024,82 +2055,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", - "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", - "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", "cpu": [ "arm64" ], @@ -2114,9 +2073,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", - "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", "cpu": [ "x64" ], @@ -2754,14 +2713,14 @@ ] }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz", - "integrity": "sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.7", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2775,8 +2734,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.7", - "vitest": "4.1.7" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2785,16 +2744,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", - "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2803,13 +2762,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", - "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.7", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2830,9 +2789,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", - "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -2843,13 +2802,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", - "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.7", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -2857,14 +2816,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", - "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2873,9 +2832,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", - "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -2883,13 +2842,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", - "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.7", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3021,6 +2980,29 @@ "dev": true, "license": "MIT" }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3032,9 +3014,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -3374,9 +3356,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, @@ -3725,9 +3707,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3771,9 +3753,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -4306,9 +4288,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -4443,9 +4425,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -4459,23 +4441,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -4494,9 +4476,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -4515,9 +4497,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -4536,9 +4518,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -4557,9 +4539,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -4578,9 +4560,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -4599,9 +4581,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -4620,9 +4602,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -4641,9 +4623,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -4662,9 +4644,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -4683,9 +4665,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -4928,29 +4910,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimatch/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -4978,9 +4937,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -5048,15 +5007,18 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/onetime": { "version": "7.0.0", @@ -5339,9 +5301,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -5352,9 +5314,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", "dev": true, "funding": [ { @@ -5372,9 +5334,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5505,13 +5467,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", - "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.138.0", + "@oxc-project/types": "=0.154.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -5521,21 +5483,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.4", - "@rolldown/binding-darwin-arm64": "1.1.4", - "@rolldown/binding-darwin-x64": "1.1.4", - "@rolldown/binding-freebsd-x64": "1.1.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", - "@rolldown/binding-linux-arm64-gnu": "1.1.4", - "@rolldown/binding-linux-arm64-musl": "1.1.4", - "@rolldown/binding-linux-ppc64-gnu": "1.1.4", - "@rolldown/binding-linux-s390x-gnu": "1.1.4", - "@rolldown/binding-linux-x64-gnu": "1.1.4", - "@rolldown/binding-linux-x64-musl": "1.1.4", - "@rolldown/binding-openharmony-arm64": "1.1.4", - "@rolldown/binding-wasm32-wasi": "1.1.4", - "@rolldown/binding-win32-arm64-msvc": "1.1.4", - "@rolldown/binding-win32-x64-msvc": "1.1.4" + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" } }, "node_modules/safer-buffer": { @@ -5617,9 +5579,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5630,9 +5592,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5657,9 +5619,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -5789,9 +5751,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -5921,16 +5883,16 @@ } }, "node_modules/vite": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", - "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.16", - "rolldown": "~1.1.3", + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", "tinyglobby": "^0.2.17" }, "bin": { @@ -5947,7 +5909,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -5999,19 +5961,19 @@ } }, "node_modules/vitest": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", - "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.7", - "@vitest/mocker": "4.1.7", - "@vitest/pretty-format": "4.1.7", - "@vitest/runner": "4.1.7", - "@vitest/snapshot": "4.1.7", - "@vitest/spy": "4.1.7", - "@vitest/utils": "4.1.7", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -6039,12 +6001,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.7", - "@vitest/browser-preview": "4.1.7", - "@vitest/browser-webdriverio": "4.1.7", - "@vitest/coverage-istanbul": "4.1.7", - "@vitest/coverage-v8": "4.1.7", - "@vitest/ui": "4.1.7", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index 0573b11..55d72da 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@types/node": "^25.9.1", "@typescript-eslint/eslint-plugin": "^8.60.0", "@typescript-eslint/parser": "^8.53.0", - "@vitest/coverage-v8": "^4.1.7", + "@vitest/coverage-v8": "^4.1.11", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import-x": "^4.16.2", @@ -79,7 +79,7 @@ "prettier": "^3.8.3", "rimraf": "^6.1.3", "typescript": "^6.0.3", - "vitest": "^4.0.17" + "vitest": "^4.1.11" }, "peerDependencies": { "@commitlint/config-conventional": ">=19.0.0", From e0f4c931240c65e5e473eb522ca1ebabbf9f25b2 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Wed, 14 Oct 2026 22:50:48 +0200 Subject: [PATCH 15/17] chore: sync release v1.0.2 to develop (#120) Back-merge the v1.0.2 release metadata into develop so it matches main: - Bump version to 1.0.2 - Tighten engines.node to >=22.13.0 - Add the 1.0.2 CHANGELOG entry - Update the Node.js prerequisite in the docs No functional changes. After this merge, develop's tree is identical to main at the v1.0.2 tag. --- CHANGELOG.md | 26 ++++++++++++++++++++------ docs/development/contributing.md | 2 +- docs/getting-started/installation.md | 10 +++++----- docs/guides/team-setup.md | 2 +- docs/index.md | 2 +- package-lock.json | 6 +++--- package.json | 4 ++-- 7 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7c655..5055b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.2] - 2026-10-14 + ### Changed -- Raise the minimum supported Node.js version to 22 +- Raise the minimum supported Node.js version to 22.13.0 The declared `engines.node` range of `>=18` was already inaccurate: `commander@15` requires Node `>=22.12.0`, so v1.0.1 could not install or run on Node 18 or 20 despite claiming support. This corrects the declared - range to match reality rather than removing working functionality. Node 18 - and 20 are both past end-of-life. + range to match reality rather than removing working functionality. The + floor is 22.13.0 because the `@inquirer/prompts` 8.x packages require + `^22.13.0` on the Node 22 line. Node 18 and 20 are both past end-of-life. -- Update production dependencies: `chalk` 5 to 6, `execa` 9 to 10, - `@inquirer/prompts` to 8.6.0, `ora` to 9.4.1 +- Update production dependencies: `chalk` 5 to 6.0.1, `execa` 9 to 10.1.0, + `@inquirer/prompts` to 8.7.3, `ora` to 9.4.1 `@inquirer/prompts` 8.5.2 hardens temp-file handling in `external-editor`, which backs the external editor flow. No merlin code changes were required: only chalk colour helpers are used, and every `execa` call site uses the `execa(file, argsArray)` form that both majors support. -- Run CI and release workflows on Node 22 so the supported floor is actually exercised +- Run CI and release workflows on Node 22 instead of Node 20, which the + dependencies no longer support + +### Security + +- Update development dependencies to resolve 17 security advisories, 15 of + them reported as Dependabot alerts (fast-uri, js-yaml, postcss, smol-toml, + brace-expansion, vitest, @humanfs/node, source-map-js) + + All advisories affect development tooling only. `npm audit --omit=dev` + reports no vulnerabilities, so users of the published package were not + exposed. ## [1.0.1] - 2026-07-05 diff --git a/docs/development/contributing.md b/docs/development/contributing.md index 2054caf..4d0087e 100644 --- a/docs/development/contributing.md +++ b/docs/development/contributing.md @@ -23,7 +23,7 @@ This project follows the [Contributor Covenant Code of Conduct](../../CODE_OF_CO ### Prerequisites -- Node.js >= 22.0.0 +- Node.js >= 22.13.0 - npm >= 9.0.0 - Git diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 0701b1f..23ceb66 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -4,11 +4,11 @@ ## Requirements -| Requirement | Version | -| ----------- | -------------------------------- | -| Node.js | >= 22.0.0 | -| npm | >= 9.0.0 (bundled with Node 18+) | -| Git | Any recent version | +| Requirement | Version | +| ----------- | ------------------------------- | +| Node.js | >= 22.13.0 | +| npm | >= 9.0.0 (bundled with Node.js) | +| Git | Any recent version | Verify your Node.js version: diff --git a/docs/guides/team-setup.md b/docs/guides/team-setup.md index 051df4f..1f7bada 100644 --- a/docs/guides/team-setup.md +++ b/docs/guides/team-setup.md @@ -6,7 +6,7 @@ This guide walks a team lead through setting up merlin-commit for a repository s ## Prerequisites -- Node.js >= 22.0.0 on all developer machines +- Node.js >= 22.13.0 on all developer machines - A git repository with a `package.json` (any Node.js project) - npm, yarn, or pnpm diff --git a/docs/index.md b/docs/index.md index 230d105..3f56efe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ | -------- | --------------- | | Package | `merlin-commit` | | Binary | `merlin` | -| Node.js | >= 22.0.0 | +| Node.js | >= 22.13.0 | | License | MIT | --- diff --git a/package-lock.json b/package-lock.json index 94110a8..314a893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "merlin-commit", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "merlin-commit", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.6.0", @@ -40,7 +40,7 @@ "vitest": "^4.1.11" }, "engines": { - "node": ">=22" + "node": ">=22.13.0" }, "peerDependencies": { "@commitlint/config-conventional": ">=19.0.0", diff --git a/package.json b/package.json index 55d72da..aa055ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "merlin-commit", - "version": "1.0.1", + "version": "1.0.2", "description": "🧙‍♂️ Magical, interactive CLI for creating conventional commits", "type": "module", "main": "./dist/cli.js", @@ -52,7 +52,7 @@ "author": "mBukator", "license": "MIT", "engines": { - "node": ">=22" + "node": ">=22.13.0" }, "dependencies": { "@inquirer/prompts": "^8.6.0", From a03bf6b27b558c8a1bd5b1e490fa0cb88e3702d1 Mon Sep 17 00:00:00 2001 From: Max Bukator Date: Sun, 18 Oct 2026 22:55:33 +0200 Subject: [PATCH 16/17] feat(themes): add custom theme overrides via customTheme config (#122) Merlin shipped two hardcoded message sets, so changing any prompt text meant forking the package, and teams had no way to give every contributor the same voice in the commit wizard. Setting theme to "custom" now applies a customTheme block from user or project config. The block extends wizard or standard and lists only the messages it changes - mergeThemeMessages() layers them over the base, so any message added in a later release falls back to the base text instead of breaking the theme. resolveBaseTheme() drives decoration too, so a custom theme on wizard keeps its emoji columns in the type picker and its icons in the config menu. The three template messages (projectFieldAction, tooLong and overwrite) are written as strings with {field}, {maxCharacters} and {fileName} placeholders. compileMessageTemplate() turns them into functions using plain string substitution - nothing is evaluated. A missing argument drops the parentheses wrapping its placeholder, so "Overwrite? ({fileName})" reads cleanly without a file name, and an unknown placeholder stays visible so typos are easy to spot. A committed project config is untrusted input for every contributor who clones the repo. validateThemeOverrides() walks the built-in theme shape, dropping unknown keys and non-string values without an allow-list, strips C0 and C1 control characters and bidi overrides, and caps each message at 200 code points. Saving from merlin config preserves the raw customTheme block, so a typo in a hand-written theme is left for the user to fix rather than silently deleted. The theme picker gains a custom option and config --show includes the active customTheme. DEFAULT_CONFIG now carries an empty customTheme, so the next save to ~/.merlinrc.json writes an empty starter block. --- docs/reference/config-schema.md | 66 +++++-- docs/reference/configuration.md | 19 ++ docs/usage/themes.md | 150 ++++++++++++++- src/commands/config.ts | 16 +- src/lib/config-loader.ts | 75 +++++++- src/lib/prompt.ts | 3 +- src/types/config.ts | 16 +- src/types/index.ts | 7 +- src/types/theme.ts | 37 ++++ src/utils/config.ts | 35 +++- src/utils/constants.ts | 18 ++ src/utils/theme.ts | 205 +++++++++++++++++++++ tests/unit/config-command.test.ts | 38 ++++ tests/unit/config-loader.test.ts | 152 ++++++++++++++++ tests/unit/config.test.ts | 44 +++++ tests/unit/constants.test.ts | 24 +++ tests/unit/prompt.test.ts | 38 ++++ tests/unit/theme.test.ts | 293 ++++++++++++++++++++++++++++++ 18 files changed, 1207 insertions(+), 29 deletions(-) create mode 100644 src/utils/theme.ts create mode 100644 tests/unit/theme.test.ts diff --git a/docs/reference/config-schema.md b/docs/reference/config-schema.md index f232d70..a1dfa8c 100644 --- a/docs/reference/config-schema.md +++ b/docs/reference/config-schema.md @@ -13,6 +13,7 @@ Both config files are JSON objects at the top level. Every field is optional - o ```json { "theme": "wizard", + "customTheme": { "extends": "wizard", "messages": {} }, "maxSubjectLength": 72, "maxScopeLength": 20, "editor": "vim", @@ -32,7 +33,7 @@ Invalid values are silently ignored and the built-in default is used instead. Me ### `theme` -**Type:** `"wizard"` | `"standard"` +**Type:** `"wizard"` | `"standard"` | `"custom"` **Default:** `"wizard"` Controls the UI theme used throughout the prompt sequence. @@ -41,6 +42,7 @@ Controls the UI theme used throughout the prompt sequence. | ------------ | --------------------------------------------------------------------------------------- | | `"wizard"` | Magical themed prompts with emoji, color, and thematic vocabulary. | | `"standard"` | Minimal prompts with plain text. Better for low-color terminals or accessibility needs. | +| `"custom"` | Your messages from [`customTheme`](#customtheme), layered over its base theme. | **Example:** @@ -54,6 +56,41 @@ See [Themes](../usage/themes.md) for a full comparison including exact prompt te --- +### `customTheme` + +**Type:** `{ extends: "wizard" | "standard"; messages: object }` +**Default:** `{ "extends": "wizard", "messages": {} }` + +Messages used when `theme` is `"custom"`. Only the overridden messages need to be listed - every other message comes from the `extends` theme, which also decides decoration (emoji columns and menu icons). + +| Field | Type | Required | Description | +| ---------- | -------------------------- | -------- | ----------------------------------------------------------------- | +| `extends` | `"wizard"` \| `"standard"` | Yes | Base theme for fallback messages and decoration | +| `messages` | `object` | No | Nested message overrides. Keys mirror the built-in theme messages | + +Template messages (`config.projectFieldAction`, `errors.commit.tooLong`, `init.overwrite`) are written with `{field}`, `{maxCharacters}` and `{fileName}` placeholders. + +**Example:** + +```json +{ + "theme": "custom", + "customTheme": { + "extends": "standard", + "messages": { + "commit": { "intro": "Ready to ship" }, + "errors": { "commit": { "tooLong": "Max {maxCharacters} characters" } } + } + } +} +``` + +Like every field, `customTheme` is replaced as a whole by a higher-priority tier: a project `customTheme` replaces the user one rather than merging with it. + +See [Custom Themes](../usage/themes.md#custom-themes) for placeholders, limits and the full key reference. + +--- + ### `maxSubjectLength` **Type:** `number` @@ -268,15 +305,16 @@ To extend the built-ins, include all 11 default types in your array plus your ad ## Validation Rules -| Field | Validation | On invalid value | -| ---------------------- | ---------------------------------------- | --------------------------------------------------------------- | -| `theme` | Must be `"wizard"` or `"standard"` | Ignored, falls back to default | -| `maxSubjectLength` | Must be a number greater than 0 | Ignored, falls back to default | -| `maxScopeLength` | Must be a number greater than 0 | Ignored, falls back to default | -| `editor` | Must be a non-empty string | Ignored, falls back to default | -| `autoAdd` | Must be a boolean | Ignored, falls back to default | -| `showCharacterCounter` | Must be a boolean | Ignored, falls back to default | -| `types` | Array, each entry filtered by type shape | Invalid entries filtered out; empty array falls back to default | +| Field | Validation | On invalid value | +| ---------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `theme` | Must be `"wizard"`, `"standard"` or `"custom"` | Ignored, falls back to default | +| `customTheme` | Object with `extends` of `"wizard"` or `"standard"`; messages must be strings at known keys | Whole object ignored if `extends` is invalid; unknown keys and non-strings dropped; control and bidi-override characters stripped; messages cut to 200 characters | +| `maxSubjectLength` | Must be a number greater than 0 | Ignored, falls back to default | +| `maxScopeLength` | Must be a number greater than 0 | Ignored, falls back to default | +| `editor` | Must be a non-empty string | Ignored, falls back to default | +| `autoAdd` | Must be a boolean | Ignored, falls back to default | +| `showCharacterCounter` | Must be a boolean | Ignored, falls back to default | +| `types` | Array, each entry filtered by type shape | Invalid entries filtered out; empty array falls back to default | Unknown fields are silently ignored. @@ -286,9 +324,15 @@ Unknown fields are silently ignored. ```json { - // UI theme: "wizard" (default) or "standard" + // UI theme: "wizard" (default), "standard" or "custom" "theme": "wizard", + // Messages for theme "custom", layered over the "extends" theme + "customTheme": { + "extends": "wizard", + "messages": { "commit": { "intro": "🚀 Ready to launch your commit!" } } + }, + // Subject line character limit (10-200, default 72) "maxSubjectLength": 72, diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index f96116e..05cfae5 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -130,6 +130,25 @@ Or edit `~/.merlinrc.json`: See [Themes](../usage/themes.md) for a comparison of both themes. +### Create a custom theme + +Set `theme` to `"custom"` and list only the messages you want to change. Everything else comes from the theme named in `extends`: + +```json +{ + "theme": "custom", + "customTheme": { + "extends": "wizard", + "messages": { + "commit": { "intro": "🚀 Ready to launch your commit!" }, + "prompts": { "type": "🎯 Select mission type:" } + } + } +} +``` + +Put it in the project `.merlinrc.json` to share it with your team. See [Custom Themes](../usage/themes.md#custom-themes) for placeholders, limits and every message key. + ### Enable auto-add When `autoAdd` is true, running `merlin` when no files are staged opens an interactive file picker instead of exiting with an error. diff --git a/docs/usage/themes.md b/docs/usage/themes.md index a11b37e..4b8411e 100644 --- a/docs/usage/themes.md +++ b/docs/usage/themes.md @@ -1,6 +1,6 @@ # Themes -Merlin Commit ships with two UI themes. Both produce identical commit messages - the theme only affects the prompt text and visual decoration. +Merlin Commit ships with two UI themes, and you can build a custom theme on top of either one. Every theme produces identical commit messages - the theme only affects the prompt text and visual decoration. --- @@ -124,6 +124,153 @@ Edit `~/.merlinrc.json` (user, applies everywhere) or `.merlinrc.json` (project, The theme takes effect immediately - no restart needed. +--- + +## Custom Themes + +A custom theme takes one of the built-in themes as its base and replaces only the messages you list. Everything you leave out comes from the base theme, so new messages added in later Merlin versions never break your theme. + +```json +{ + "theme": "custom", + "customTheme": { + "extends": "wizard", + "messages": { + "commit": { "intro": "🚀 Ready to launch your commit!" }, + "prompts": { + "type": "🎯 Select mission type:", + "confirm": "🛰️ Launch this commit?" + }, + "errors": { + "commit": { + "tooLong": "Payload too heavy - max {maxCharacters} characters" + } + } + } + } +} +``` + +| Field | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------------- | +| `theme` | Set to `"custom"` to use `customTheme`. Other values ignore `customTheme`. | +| `customTheme.extends` | `"wizard"` or `"standard"`. Supplies every message you don't override, and decides decoration (see below). | +| `customTheme.messages` | Nested object of messages to replace. Keys mirror the reference table below. | + +You can also pick the theme from the menu: `merlin config` → Theme → `custom`. The messages themselves are edited in `.merlinrc.json`. + +### Decoration follows the base + +`extends` decides more than the fallback text. With `"wizard"` as the base, the type picker shows emoji columns and the config menu shows emoji icons. With `"standard"`, both use plain labels. Your own messages can contain any emoji either way. + +### Template messages + +Three messages take a value at runtime. Write them as strings with `{placeholder}` tokens: + +| Key | Placeholder | Example | +| --------------------------- | ----------------- | ------------------------------------------- | +| `config.projectFieldAction` | `{field}` | `"What should happen to {field}?"` | +| `errors.commit.tooLong` | `{maxCharacters}` | `"Too long - max {maxCharacters}"` | +| `init.overwrite` | `{fileName}` | `"Already exists. Overwrite? ({fileName})"` | + +- `init.overwrite` is sometimes shown without a file name. The placeholder then renders as empty, and parentheses left empty are removed, so the example above becomes `Already exists. Overwrite?`. +- A placeholder Merlin doesn't know (for example a typo like `{maxChars}`) is shown exactly as typed, so the mistake is easy to spot. +- Placeholders are plain text substitution. Nothing in a theme is ever run as code. + +### Validation and limits + +A custom theme is checked when the config loads. Invalid parts are dropped silently, the same way as other config fields: + +- Keys that don't exist in the reference table are ignored. +- Values must be strings. Numbers, arrays and objects in place of a message are ignored. +- Control characters (including the escape characters used for terminal colour and cursor codes) and right-to-left override characters are stripped, so a shared config can't send commands to your terminal or disguise prompt text. +- Messages are cut to 200 characters (counted so an emoji is never split in half). +- If the `customTheme` in a file is invalid (for example `extends` isn't `"wizard"` or `"standard"`), that file's `customTheme` is ignored. Merlin then uses the `customTheme` from the next config level down - project, then user - or, if none is valid, the plain wizard theme. +- Saving settings from `merlin config` never rewrites your `customTheme` block, so a typo in it is left for you to fix rather than silently dropped from the file. + +### Sharing a theme with your team + +Put `theme` and `customTheme` in the project `.merlinrc.json` and commit it. Project config replaces user config field by field, so a project `customTheme` replaces your personal one as a whole - the two are not merged message by message. + +### Message key reference + +Every key a custom theme can override, with the standard theme's text. Template placeholders are shown in braces. + +| Key | Standard text | +| --------------------------------- | ------------------------------------------------------------ | +| `commit.intro` | Ready to create a commit | +| `commit.exit` | Commit complete | +| `commit.dryRunExit` | Dry run complete - no commit was created. | +| `commit.previewHeader` | Commit Preview: | +| `config.intro` | Configuration settings | +| `config.exit` | Configuration closed | +| `config.scopeSelector` | Edit configuration scope: | +| `config.scopeUser` | User config (applies to all repos - ~/.merlinrc.json) | +| `config.scopeProject` | Project config (applies to this repo - .merlinrc.json) | +| `config.noProjectConfig` | No project config found. | +| `config.createProjectConfig` | Create .merlinrc.json for this repo? | +| `config.projectConfigCreated` | Created .merlinrc.json | +| `config.projectFieldAction` | Action for {field}: | +| `config.back` | Returning to scope selection... | +| `checking.repo` | Checking git repository | +| `checking.staged` | Checking staged changes | +| `checking.unstaged` | Checking unstaged files | +| `checking.autoAdd` | Staging selected files... | +| `prompts.type` | Select the type of change: | +| `prompts.scope` | Scope (optional): | +| `prompts.subject` | Short description: | +| `prompts.body` | Add a detailed description? | +| `prompts.breaking` | Are there any breaking changes? | +| `prompts.issues` | Reference any issues? (e.g., fixes #123) | +| `prompts.confirm` | Create this commit? | +| `prompts.selectFiles` | Select files to stage: | +| `success.commit.created` | Commit created successfully | +| `success.commit.amended` | Commit amended successfully | +| `success.commit.dryRun` | Preview mode - no commit created | +| `success.config.saved` | Configuration updated | +| `success.init.completed` | Setup complete | +| `errors.notRepo` | Not a git repository | +| `errors.commit.noStaged` | No staged changes found | +| `errors.commit.required` | This field is required | +| `errors.commit.tooLong` | Text exceeds maximum length (max {maxCharacters} characters) | +| `errors.commit.malformed` | Invalid format | +| `errors.commit.commitFailed` | Failed to create commit | +| `errors.commit.editorFailed` | Editor exited with error | +| `errors.init.noPackageJson` | No package.json found | +| `errors.init.installFailed` | Failed to install dependencies | +| `errors.init.huskyFailed` | Failed to initialize husky | +| `errors.init.hookFailed` | Failed to create commit-msg hook | +| `errors.init.configFailed` | Failed to create commitlint config | +| `errors.init.aliasFailed` | Failed to create git alias | +| `warnings.cancel` | Commit cancelled | +| `warnings.commit.noVerify` | Skipping git hooks (--no-verify) | +| `warnings.config.resetConfig` | This will reset all configuration to defaults | +| `warnings.config.noProjectConfig` | No git repository detected - project config is not available | +| `warnings.init.existingSetup` | Existing setup detected: | +| `warnings.init.aliasExists` | Git alias 'merlin' already exists | +| `tips.commit.gitAdd` | Use "git add " to stage changes | +| `tips.init.runGitInit` | Run "git init" to initialize a repository | +| `tips.init.runNpmInit` | Run "npm init" to create a package.json | +| `tips.init.manualInstall` | Install manually: npm install -D | +| `tips.init.nextSteps` | Setup complete! Next steps: | +| `init.intro` | Setting up conventional commits for your repository | +| `init.checkingPackageJson` | Checking for package.json | +| `init.installDeps` | Install husky and commitlint dependencies? | +| `init.installingDeps` | Installing dependencies... | +| `init.initializingHusky` | Initializing husky... | +| `init.creatingHook` | Creating commit-msg hook... | +| `init.creatingCommitlint` | Creating commitlint config... | +| `init.setupAlias` | Setup 'git merlin' alias? | +| `init.aliasScope` | Select alias scope: | +| `init.creatingAlias` | Creating git alias... | +| `init.skipExisting` | Skipping existing file: | +| `init.overwrite` | File already exists. Overwrite? ({fileName}) | +| `init.aliasScopeGlobal` | Global (all repositories) | +| `init.aliasScopeLocal` | Local (this repository only) | +| `init.createProjectConfig` | Create project config (.merlinrc.json) for team sharing? | +| `init.creatingProjectConfig` | Creating project config... | +| `init.exit` | Repository setup complete. | +

switching themes

@@ -179,5 +326,6 @@ Color output requires a terminal that supports ANSI escape codes. Colors are not ## Related - [Glossary](../GLOSSARY.md) - Complete wizard-theme vocabulary reference +- [Config Schema](../reference/config-schema.md#customtheme) - `customTheme` field reference - [Configuration](../reference/configuration.md) - Setting a default theme and other options - [CLI Reference](../reference/cli-reference.md) - The `merlin config` command diff --git a/src/commands/config.ts b/src/commands/config.ts index ffe2525..164dd97 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -27,6 +27,7 @@ import type { ThemeMessages, } from "../types/index.js"; import { colors, DEFAULT_CONFIG } from "../utils/constants.js"; +import { resolveBaseTheme } from "../utils/theme.js"; import { createNonEmptyValidator, createRangeValidator } from "../utils/validators.js"; type SaveFn = (config: Partial) => void; @@ -81,6 +82,7 @@ const CONFIGURE_PROMPTS = { theme: "Select theme:", themeWizardLabel: "wizard - Magical experience with themed messages", themeStandardLabel: "standard - Minimalist, professional interface", + themeCustomLabel: "custom - Your messages from customTheme in .merlinrc.json", maxSubjectLength: "Maximum subject line length (10-200):", maxScopeLength: "Maximum scope length (5-50):", editor: "External editor command:", @@ -177,7 +179,7 @@ function formatFieldChoice( currentValue: string | number | boolean, config: Required ): MenuChoice { - const isWizardTheme = config.theme === "wizard"; + const isWizardTheme = resolveBaseTheme(config) === "wizard"; const prefix = isWizardTheme ? FIELD_EMOJI_PREFIXES.wizard[field] : FIELD_EMOJI_PREFIXES.standard[field]; @@ -198,7 +200,7 @@ function formatFieldChoiceNoValue( field: ConfigurableField, config: Required ): MenuChoice { - const isWizardTheme = config.theme === "wizard"; + const isWizardTheme = resolveBaseTheme(config) === "wizard"; const prefix = isWizardTheme ? FIELD_EMOJI_PREFIXES.wizard[field] : FIELD_EMOJI_PREFIXES.standard[field]; @@ -218,7 +220,7 @@ function formatActionChoice( action: "show" | "reset" | "syncCommitlint" | "back" | "exit", config: Required ): MenuChoice { - const isWizardTheme = config.theme === "wizard"; + const isWizardTheme = resolveBaseTheme(config) === "wizard"; const prefixMap = isWizardTheme ? FIELD_EMOJI_PREFIXES.wizard : FIELD_EMOJI_PREFIXES.standard; @@ -275,7 +277,7 @@ function buildUserMenuChoices( } /** - * Configure the UI theme (wizard or standard). + * Configure the UI theme (wizard, standard, or custom). */ async function configureTheme( config: Required, @@ -294,6 +296,11 @@ async function configureTheme( name: CONFIGURE_PROMPTS.themeStandardLabel, short: "standard", }, + { + value: "custom" as const, + name: CONFIGURE_PROMPTS.themeCustomLabel, + short: "custom", + }, ], default: config.theme, }); @@ -400,6 +407,7 @@ async function showConfig(): Promise { const displayConfig = { theme: config.theme, + ...(config.theme === "custom" && { customTheme: config.customTheme }), maxSubjectLength: config.maxSubjectLength, maxScopeLength: config.maxScopeLength, editor: config.editor, diff --git a/src/lib/config-loader.ts b/src/lib/config-loader.ts index 1938698..2e7fcfe 100644 --- a/src/lib/config-loader.ts +++ b/src/lib/config-loader.ts @@ -9,6 +9,7 @@ import { STANDARD_MESSAGES, WIZARD_MESSAGES, } from "../utils/constants.js"; +import { mergeThemeMessages, resolveBaseTheme } from "../utils/theme.js"; import { normalizeEmojiSpacing } from "./terminal.js"; const USER_CONFIG_PATH = join(homedir(), ".merlinrc.json"); @@ -101,10 +102,54 @@ export function loadConfig(repoRoot?: string | null): Required { */ export function saveConfig(config: Partial): void { const currentConfig = loadUserConfig(); - const newConfig = { ...DEFAULT_CONFIG, ...currentConfig, ...config }; + const newConfig = preserveRawCustomTheme( + { ...DEFAULT_CONFIG, ...currentConfig, ...config }, + config, + USER_CONFIG_PATH + ); writeFileSync(USER_CONFIG_PATH, `${JSON.stringify(newConfig, null, 4)}\n`); } +/** + * Reads the unvalidated `customTheme` value from a config file. + * Returns undefined when the file is missing, unparseable, or has no customTheme. + * Parse errors are already reported by loadUserConfig/loadProjectConfig. + */ +function readRawCustomTheme(configPath: string): unknown { + if (!existsSync(configPath)) { + return undefined; + } + try { + const rawConfig: unknown = JSON.parse(readFileSync(configPath, "utf-8")); + return typeof rawConfig === "object" && rawConfig !== null + ? (rawConfig as Record).customTheme + : undefined; + } catch { + return undefined; + } +} + +/** + * Keeps the hand-written `customTheme` from disk when a save does not change it. + * + * Saves write back the validated config, which would replace a custom theme with + * its sanitized form - dropping a whole block over one typo. Other fields are + * single values, so only customTheme needs this. + */ +function preserveRawCustomTheme( + newConfig: Partial, + changes: Partial, + configPath: string +): Record { + if (changes.customTheme !== undefined) { + return newConfig; + } + const rawCustomTheme = readRawCustomTheme(configPath); + return rawCustomTheme === undefined + ? newConfig + : { ...newConfig, customTheme: rawCustomTheme }; +} + /** * Resets user configuration to default settings. * @@ -129,7 +174,11 @@ export function resetConfig(): void { export function saveProjectConfig(config: Partial, repoRoot: string): void { const projectConfigPath = join(repoRoot, ".merlinrc.json"); const currentProjectConfig = loadProjectConfig(repoRoot); - const newConfig = { ...currentProjectConfig, ...config }; + const newConfig = preserveRawCustomTheme( + { ...currentProjectConfig, ...config }, + config, + projectConfigPath + ); writeFileSync(projectConfigPath, `${JSON.stringify(newConfig, null, 4)}\n`); } @@ -149,10 +198,11 @@ export function removeProjectConfigField( const currentProjectConfig = loadProjectConfig(repoRoot); delete (currentProjectConfig as Record)[field]; - writeFileSync( - projectConfigPath, - `${JSON.stringify(currentProjectConfig, null, 4)}\n` - ); + const newConfig = + field === "customTheme" + ? currentProjectConfig + : preserveRawCustomTheme(currentProjectConfig, {}, projectConfigPath); + writeFileSync(projectConfigPath, `${JSON.stringify(newConfig, null, 4)}\n`); } /** @@ -200,13 +250,24 @@ function normalizeThemeStrings(object: T): T { * * For the wizard theme, VS16 emoji spacing is normalized at runtime based on * terminal detection. Standard theme has no VS16 emojis and skips normalization. + * A custom theme is layered over its base and always normalized, since user + * strings may contain VS16 emojis. `customTheme` merges like every other field, + * so a project-level custom theme replaces the user-level one as a whole. * * @param repoRoot - Repository root path for project-level config, or null/undefined to skip * @returns Message object containing all UI text for prompts, errors, and tips */ export function getMessages(repoRoot?: string | null): ThemeMessages { const config = loadConfig(repoRoot); - if (config.theme !== "wizard") { + const baseTheme = resolveBaseTheme(config); + const baseMessages = baseTheme === "wizard" ? WIZARD_MESSAGES : STANDARD_MESSAGES; + + if (config.theme === "custom") { + return normalizeThemeStrings( + mergeThemeMessages(baseMessages, config.customTheme.messages) + ); + } + if (baseTheme !== "wizard") { return STANDARD_MESSAGES; } return normalizeThemeStrings(WIZARD_MESSAGES); diff --git a/src/lib/prompt.ts b/src/lib/prompt.ts index c462053..eb7a590 100644 --- a/src/lib/prompt.ts +++ b/src/lib/prompt.ts @@ -8,6 +8,7 @@ import { VALUE_COLUMN_WIDTH, VARIATION_SELECTOR, } from "../utils/constants.js"; +import { resolveBaseTheme } from "../utils/theme.js"; import type { CommitEditorContext } from "./editor-wrapper.js"; import type { StagedFile, StagingCandidate } from "./git.js"; import { isWideEmojiTerminal } from "./terminal.js"; @@ -61,7 +62,7 @@ export async function promptUser( }; // Prompt for commit type - const isWizardTheme = config.theme === "wizard"; + const isWizardTheme = resolveBaseTheme(config) === "wizard"; answers.type = await select({ message: messages.prompts.type, choices: config.types.map((type) => { diff --git a/src/types/config.ts b/src/types/config.ts index fdc8b22..ccc9de9 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,4 +1,5 @@ import type { CommitType } from "./commit.js"; +import type { CustomThemeConfig, ThemeName } from "./theme.js"; /** * Configuration options for customizing Merlin's behavior. @@ -45,9 +46,19 @@ export type MerlinConfig = { * Visual theme for the commit interface. * - "wizard": Guided experience with themed messages and tips * - "standard": Minimalist interface with simple prompts + * - "custom": Messages from `customTheme` layered over its base theme * @default "wizard" */ - theme?: "wizard" | "standard"; + theme?: ThemeName; + + /** + * User-defined messages used when `theme` is "custom". + * Only the overridden messages need to be listed - the rest come from `extends`. + * @default { extends: "wizard", messages: {} } + * @example { extends: "wizard", messages: { commit: { intro: "🚀 Ready to launch" } } } + */ + customTheme?: CustomThemeConfig; + /** * Show real-time character counter on subject and scope inputs. * @default true @@ -87,7 +98,8 @@ export type ConfigMenuAction = /** * Configurable fields that can appear in the project config menu. - * Excludes 'types' since custom commit types are not yet user-configurable. + * Excludes 'types' since custom commit types are not yet user-configurable, + * and 'customTheme' since theme messages are edited in .merlinrc.json directly. */ export type ConfigurableField = | "theme" diff --git a/src/types/index.ts b/src/types/index.ts index d392b60..1f9faee 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,4 +7,9 @@ export type { MerlinConfig, } from "./config.js"; export type { InitOptions } from "./init.js"; -export type { ThemeMessages } from "./theme.js"; +export type { + BaseThemeName, + CustomThemeConfig, + ThemeMessageOverrides, + ThemeMessages, +} from "./theme.js"; diff --git a/src/types/theme.ts b/src/types/theme.ts index 6086a26..eba65bd 100644 --- a/src/types/theme.ts +++ b/src/types/theme.ts @@ -352,3 +352,40 @@ export type ThemeMessages = { creatingProjectConfig: string; }; }; + +/** + * Built-in themes that a custom theme can extend. + * The base also decides decoration (emoji layout in pickers and menus). + */ +export type BaseThemeName = "wizard" | "standard"; + +/** + * Every value accepted by the `theme` config field. + */ +export type ThemeName = BaseThemeName | "custom"; + +/** + * Partial, JSON-friendly version of ThemeMessages used by custom themes. + * Every key is optional, and template functions become strings with + * `{placeholder}` tokens (e.g. "Too long - max {maxCharacters}"). + */ +export type ThemeMessageOverrides = { + [Key in keyof ThemeMessages]?: MessageOverrideNode; +}; + +type MessageOverrideNode = Node extends string + ? string + : Node extends (...parameters: never[]) => string + ? string + : { [Key in keyof Node]?: MessageOverrideNode }; + +/** + * User-defined theme: a base theme plus the messages that replace its text. + */ +export type CustomThemeConfig = { + /** Built-in theme that supplies every message not overridden. */ + extends: BaseThemeName; + + /** Messages that replace the base theme's text. */ + messages: ThemeMessageOverrides; +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index 513a8dd..2e71504 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,29 @@ -import type { CommitType, MerlinConfig } from "../types/index.js"; +import type { + BaseThemeName, + CommitType, + CustomThemeConfig, + MerlinConfig, +} from "../types/index.js"; +import { STANDARD_MESSAGES } from "./constants.js"; +import { validateThemeOverrides } from "./theme.js"; + +function isBaseThemeName(value: unknown): value is BaseThemeName { + return value === "wizard" || value === "standard"; +} + +/** + * Validates a custom theme object. Requires a valid base theme in `extends`; + * messages are filtered down to known, sanitized strings. + */ +function validateCustomTheme(customTheme: unknown): CustomThemeConfig | undefined { + if (typeof customTheme !== "object" || customTheme === null) return undefined; + const rawTheme = customTheme as Record; + if (!isBaseThemeName(rawTheme.extends)) return undefined; + return { + extends: rawTheme.extends, + messages: validateThemeOverrides(rawTheme.messages, STANDARD_MESSAGES), + }; +} /** * Validates a single commit type object. @@ -60,9 +85,15 @@ export function validateConfig(userConfig: unknown): Partial { } // Validate theme - if (config.theme === "wizard" || config.theme === "standard") { + if (isBaseThemeName(config.theme) || config.theme === "custom") { validated.theme = config.theme; } + // Validate customTheme + const customTheme = validateCustomTheme(config.customTheme); + if (customTheme) { + validated.customTheme = customTheme; + } + return validated; } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9f241f1..e5aa8f2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -338,4 +338,22 @@ export const DEFAULT_CONFIG: Required = { autoAdd: false, showCharacterCounter: true, theme: "wizard", + customTheme: { extends: "wizard", messages: {} }, }; + +/** + * Parameter names for every template message in ThemeMessages, keyed by dotted path. + * Custom themes write these messages as strings with `{name}` placeholders, + * and the names listed here map each positional argument to its placeholder. + */ +export const TEMPLATE_PARAMETER_NAMES: Readonly> = { + "config.projectFieldAction": ["field"], + "errors.commit.tooLong": ["maxCharacters"], + "init.overwrite": ["fileName"], +}; + +/** + * Longest custom theme message accepted from config, in characters. + * Longer strings are truncated so a shared config cannot flood the terminal. + */ +export const MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS = 200; diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..489a707 --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,205 @@ +import type { + BaseThemeName, + MerlinConfig, + ThemeMessageOverrides, + ThemeMessages, +} from "../types/index.js"; +import { + MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS, + TEMPLATE_PARAMETER_NAMES, +} from "./constants.js"; + +/** + * A compiled custom template message, called the same way as the built-in + * template functions (e.g. `tooLong(72)`). + */ +export type MessageTemplate = (...templateArguments: unknown[]) => string; + +type MessageTree = { [key: string]: unknown }; + +/** + * Characters stripped from custom messages so a shared .merlinrc.json cannot + * drive or spoof the terminal: C0 controls (including ESC, which starts ANSI + * escape sequences), DEL, C1 controls (including the 8-bit CSI U+009B), and + * bidirectional embedding/override/isolate controls. + */ +// eslint-disable-next-line no-control-regex +const UNSAFE_CHARACTER_REGEX = /[\u0000-\u001F\u007F-\u009F‪-‮⁦-⁩]/g; + +/** + * Matches a `{name}` placeholder, together with parentheses wrapping it directly + * (and the whitespace before them), so a missing argument can drop them too. + */ +const PLACEHOLDER_REGEX = /(\s*\()?\{(\w+)\}(\))?/g; + +function isMessageTree(value: unknown): value is MessageTree { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function joinPath(pathPrefix: string, key: string): string { + return pathPrefix ? `${pathPrefix}.${key}` : key; +} + +/** + * Removes unsafe characters and truncates a custom message to the allowed length. + * Truncation counts code points, so an emoji is never cut into a broken half. + */ +function sanitizeMessage(message: string): string { + const safeCharacters = Array.from(message.replace(UNSAFE_CHARACTER_REGEX, "")); + return safeCharacters.slice(0, MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS).join(""); +} + +/** + * Resolves the built-in theme that supplies messages and decoration. + * A custom theme resolves to the theme it extends. + * + * @param config - Config holding the theme selection + * @returns "wizard" or "standard" + */ +export function resolveBaseTheme( + config: Pick, "theme" | "customTheme"> +): BaseThemeName { + return config.theme === "custom" ? config.customTheme.extends : config.theme; +} + +function validateOverrideNode(rawNode: unknown, referenceNode: MessageTree): MessageTree { + const validated: MessageTree = {}; + if (!isMessageTree(rawNode)) { + return validated; + } + + for (const [key, referenceValue] of Object.entries(referenceNode)) { + const rawValue = rawNode[key]; + const isMessageLeaf = + typeof referenceValue === "string" || typeof referenceValue === "function"; + + if (isMessageLeaf) { + if (typeof rawValue === "string") { + validated[key] = sanitizeMessage(rawValue); + } + continue; + } + + const validatedChild = validateOverrideNode( + rawValue, + referenceValue as MessageTree + ); + if (Object.keys(validatedChild).length > 0) { + validated[key] = validatedChild; + } + } + return validated; +} + +/** + * Validates custom theme messages against the shape of a built-in theme. + * + * Keeps only string values at paths that exist in the reference (including + * template paths, which accept `{placeholder}` strings). Unknown keys and + * non-string values are dropped. Kept strings are sanitized. + * + * @param rawOverrides - Untrusted `customTheme.messages` value from config + * @param reference - Built-in theme whose shape defines the valid paths + * @returns Overrides containing only valid, sanitized messages + */ +export function validateThemeOverrides( + rawOverrides: unknown, + reference: ThemeMessages +): ThemeMessageOverrides { + return validateOverrideNode(rawOverrides, reference); +} + +/** + * Compiles a custom template string into a message function. + * Placeholders are replaced with plain string substitution - no code is evaluated. + * + * - A placeholder whose argument is missing renders as an empty string. When the + * placeholder is wrapped in its own parentheses, those (and the space before them) + * are removed too, so "Overwrite? ({fileName})" becomes "Overwrite?". + * - A placeholder not in `parameterNames` is left as typed, so typos stay visible. + * + * @param template - Message with `{name}` placeholders, e.g. "Too long - max {maxCharacters}" + * @param parameterNames - Placeholder name for each positional argument, in order + * @returns Function that renders the template with the given arguments + * @example + * ```typescript + * const tooLong = compileMessageTemplate("Max {maxCharacters}", ["maxCharacters"]); + * tooLong(72); // "Max 72" + * ``` + */ +export function compileMessageTemplate( + template: string, + parameterNames: readonly string[] +): MessageTemplate { + return (...templateArguments: unknown[]) => + template.replace( + PLACEHOLDER_REGEX, + ( + placeholder: string, + openingParenthesis: string | undefined, + name: string, + closingParenthesis: string | undefined + ) => { + const argumentIndex = parameterNames.indexOf(name); + if (argumentIndex === -1) { + return placeholder; + } + const templateArgument = templateArguments[argumentIndex]; + const isMissing = + templateArgument === undefined || templateArgument === null; + if (isMissing && openingParenthesis && closingParenthesis) { + return ""; + } + const value = isMissing ? "" : String(templateArgument); + return `${openingParenthesis ?? ""}${value}${closingParenthesis ?? ""}`; + } + ); +} + +function mergeMessageNode( + baseNode: MessageTree, + overrideNode: unknown, + pathPrefix: string +): MessageTree { + const overrides = isMessageTree(overrideNode) ? overrideNode : {}; + const merged: MessageTree = {}; + + for (const [key, baseValue] of Object.entries(baseNode)) { + const overrideValue = overrides[key]; + const path = joinPath(pathPrefix, key); + + if (typeof baseValue === "function") { + merged[key] = + typeof overrideValue === "string" + ? compileMessageTemplate( + overrideValue, + TEMPLATE_PARAMETER_NAMES[path] ?? [] + ) + : baseValue; + } else if (typeof baseValue === "string") { + merged[key] = typeof overrideValue === "string" ? overrideValue : baseValue; + } else { + // ThemeMessages leaves are strings or functions, so anything else is a group + merged[key] = mergeMessageNode(baseValue as MessageTree, overrideValue, path); + } + } + return merged; +} + +/** + * Layers custom messages over a built-in theme. + * + * Every message missing from the overrides falls back to the base, so the + * result is always a complete ThemeMessages. Override strings at template + * paths are compiled into functions. Keys not in the base are ignored. + * + * @param base - Built-in theme supplying every message not overridden + * @param overrides - Validated custom messages + * @returns Complete message set + */ +export function mergeThemeMessages( + base: ThemeMessages, + overrides: ThemeMessageOverrides +): ThemeMessages { + return mergeMessageNode(base, overrides, "") as ThemeMessages; +} diff --git a/tests/unit/config-command.test.ts b/tests/unit/config-command.test.ts index 65937af..4b94649 100644 --- a/tests/unit/config-command.test.ts +++ b/tests/unit/config-command.test.ts @@ -341,6 +341,44 @@ describe("configCommand", () => { expect(choices).toContainEqual( expect.objectContaining({ value: "standard" }) ); + expect(choices).toContainEqual(expect.objectContaining({ value: "custom" })); + }); + + it("keeps emoji menu prefixes for a custom theme on the wizard base", async () => { + vi.mocked(loadConfig).mockReturnValue({ + ...DEFAULT_CONFIG, + theme: "custom", + customTheme: { extends: "wizard", messages: {} }, + }); + vi.mocked(select).mockResolvedValueOnce("user").mockResolvedValueOnce("exit"); + + await configCommand({}); + + // calls[0] = scope, calls[1] = menu + const menuChoices = vi.mocked(select).mock.calls[1][0].choices as Array<{ + value?: string; + name?: string; + }>; + const themeChoice = menuChoices.find((choice) => choice.value === "theme"); + expect(themeChoice?.name).toContain("🎨"); + }); + + it("uses plain menu prefixes for a custom theme on the standard base", async () => { + vi.mocked(loadConfig).mockReturnValue({ + ...DEFAULT_CONFIG, + theme: "custom", + customTheme: { extends: "standard", messages: {} }, + }); + vi.mocked(select).mockResolvedValueOnce("user").mockResolvedValueOnce("exit"); + + await configCommand({}); + + const menuChoices = vi.mocked(select).mock.calls[1][0].choices as Array<{ + value?: string; + name?: string; + }>; + const themeChoice = menuChoices.find((choice) => choice.value === "theme"); + expect(themeChoice?.name?.startsWith("•")).toBe(true); }); }); diff --git a/tests/unit/config-loader.test.ts b/tests/unit/config-loader.test.ts index 9e617c3..866933a 100644 --- a/tests/unit/config-loader.test.ts +++ b/tests/unit/config-loader.test.ts @@ -115,6 +115,35 @@ describe("config-loader", () => { expect(parsed.theme).toBe("standard"); expect(parsed.maxSubjectLength).toBe(50); }); + + it("keeps a hand-written customTheme that fails validation", () => { + const rawCustomTheme = { + extends: "Wizard", + messages: { commit: { intro: "Launch", typo: "kept" } }, + }; + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ theme: "standard", customTheme: rawCustomTheme }) + ); + + saveConfig({ theme: "custom" }); + + const writtenContent = vi.mocked(writeFileSync).mock.calls[0][1] as string; + expect(JSON.parse(writtenContent).customTheme).toEqual(rawCustomTheme); + }); + + it("writes customTheme when the save changes it", () => { + const newCustomTheme = { extends: "standard" as const, messages: {} }; + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ customTheme: { extends: "wizard", messages: {} } }) + ); + + saveConfig({ customTheme: newCustomTheme }); + + const writtenContent = vi.mocked(writeFileSync).mock.calls[0][1] as string; + expect(JSON.parse(writtenContent).customTheme).toEqual(newCustomTheme); + }); }); describe("resetConfig", () => { @@ -362,6 +391,19 @@ describe("config-loader", () => { expect(parsed.maxSubjectLength).toBe(50); }); + it("keeps the raw project customTheme when saving another field", () => { + const rawCustomTheme = { extends: "standard", messages: { bogus: "kept" } }; + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ customTheme: rawCustomTheme }) + ); + + saveProjectConfig({ maxSubjectLength: 50 }, repoRoot); + + const writtenContent = vi.mocked(writeFileSync).mock.calls[0][1] as string; + expect(JSON.parse(writtenContent).customTheme).toEqual(rawCustomTheme); + }); + it("writes to the correct path with trailing newline", () => { vi.mocked(existsSync).mockReturnValue(false); @@ -412,6 +454,34 @@ describe("config-loader", () => { expect(parsed.maxSubjectLength).toBeUndefined(); }); + it("keeps the raw customTheme when removing another field", () => { + const rawCustomTheme = { extends: "Wizard", messages: {} }; + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ theme: "custom", customTheme: rawCustomTheme }) + ); + + removeProjectConfigField("theme", repoRoot); + + const writtenContent = vi.mocked(writeFileSync).mock.calls[0][1] as string; + expect(JSON.parse(writtenContent)).toEqual({ customTheme: rawCustomTheme }); + }); + + it("removes customTheme when it is the field being removed", () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ + theme: "custom", + customTheme: { extends: "wizard", messages: {} }, + }) + ); + + removeProjectConfigField("customTheme", repoRoot); + + const writtenContent = vi.mocked(writeFileSync).mock.calls[0][1] as string; + expect(JSON.parse(writtenContent)).toEqual({ theme: "custom" }); + }); + it("writes to the correct path with trailing newline", () => { vi.mocked(existsSync).mockReturnValue(true); vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ theme: "wizard" })); @@ -517,5 +587,87 @@ describe("config-loader", () => { expect(messages).toBe(STANDARD_MESSAGES); }); + + describe("when theme is custom", () => { + it.each([ + ["wizard", WIZARD_MESSAGES], + ["standard", STANDARD_MESSAGES], + ] as const)( + "layers overrides over the %s base", + (baseTheme, baseMessages) => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ + theme: "custom", + customTheme: { + extends: baseTheme, + messages: { commit: { intro: "Launch" } }, + }, + }) + ); + + const messages = getMessages(); + + expect(messages.commit.intro).toBe("Launch"); + expect(messages.commit.exit).toBe(baseMessages.commit.exit); + expect(messages.prompts).toEqual(baseMessages.prompts); + } + ); + + it("renders custom template messages", () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ + theme: "custom", + customTheme: { + extends: "standard", + messages: { + errors: { commit: { tooLong: "Max {maxCharacters}" } }, + }, + }, + }) + ); + + const messages = getMessages(); + + expect(messages.errors.commit.tooLong(50)).toBe("Max 50"); + }); + + it("falls back to the wizard base when customTheme is missing", () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockReturnValue( + JSON.stringify({ theme: "custom" }) + ); + + const messages = getMessages(); + + expect(messages).toEqual(WIZARD_MESSAGES); + }); + + it("uses the project customTheme instead of the user one", () => { + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(readFileSync).mockImplementation((filePath) => + filePath === USER_CONFIG_PATH + ? JSON.stringify({ + theme: "custom", + customTheme: { + extends: "wizard", + messages: { commit: { intro: "User", exit: "Bye" } }, + }, + }) + : JSON.stringify({ + customTheme: { + extends: "standard", + messages: { commit: { intro: "Project" } }, + }, + }) + ); + + const messages = getMessages("/mock/repo"); + + expect(messages.commit.intro).toBe("Project"); + expect(messages.commit.exit).toBe(STANDARD_MESSAGES.commit.exit); + }); + }); }); }); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 9d577af..c02d098 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -39,6 +39,50 @@ describe("validateConfig", () => { expect(result.theme).toBeUndefined(); }); + it("accepts the custom theme value", () => { + expect(validateConfig({ theme: "custom" }).theme).toBe("custom"); + }); + + describe("customTheme", () => { + it("keeps a valid custom theme with validated messages", () => { + const result = validateConfig({ + customTheme: { + extends: "standard", + messages: { commit: { intro: "Launch", unknown: "x" } }, + }, + }); + + expect(result.customTheme).toEqual({ + extends: "standard", + messages: { commit: { intro: "Launch" } }, + }); + }); + + it("defaults messages to empty overrides when missing", () => { + const result = validateConfig({ customTheme: { extends: "wizard" } }); + + expect(result.customTheme).toEqual({ extends: "wizard", messages: {} }); + }); + + it.each([["dark"], ["custom"], [undefined], [42]])( + "rejects a custom theme extending %s", + (baseTheme) => { + const result = validateConfig({ + customTheme: { extends: baseTheme, messages: {} }, + }); + + expect(result.customTheme).toBeUndefined(); + } + ); + + it.each([["string"], [null], [42]])( + "rejects a non-object custom theme (%s)", + (customTheme) => { + expect(validateConfig({ customTheme }).customTheme).toBeUndefined(); + } + ); + }); + it("rejects zero and negative maxSubjectLength", () => { expect(validateConfig({ maxSubjectLength: 0 })).toEqual({}); expect(validateConfig({ maxSubjectLength: -5 })).toEqual({}); diff --git a/tests/unit/constants.test.ts b/tests/unit/constants.test.ts index 286a217..7c67f2a 100644 --- a/tests/unit/constants.test.ts +++ b/tests/unit/constants.test.ts @@ -6,6 +6,7 @@ import { COMMIT_TYPES, DEFAULT_CONFIG, STANDARD_MESSAGES, + TEMPLATE_PARAMETER_NAMES, WIZARD_MESSAGES, } from "../../src/utils/constants.js"; @@ -181,5 +182,28 @@ describe("DEFAULT_CONFIG", () => { expect(DEFAULT_CONFIG.autoAdd).toBe(false); expect(DEFAULT_CONFIG.theme).toBe("wizard"); expect(DEFAULT_CONFIG.types).toBe(COMMIT_TYPES); + expect(DEFAULT_CONFIG.customTheme).toEqual({ extends: "wizard", messages: {} }); + }); +}); + +describe("TEMPLATE_PARAMETER_NAMES", () => { + function collectFunctionPaths(messageTree: object, pathPrefix = ""): string[] { + return Object.entries(messageTree).flatMap(([key, value]) => { + const path = pathPrefix ? `${pathPrefix}.${key}` : key; + if (typeof value === "function") return [path]; + if (typeof value === "object" && value !== null) { + return collectFunctionPaths(value, path); + } + return []; + }); + } + + it.each([ + ["WIZARD_MESSAGES", WIZARD_MESSAGES], + ["STANDARD_MESSAGES", STANDARD_MESSAGES], + ])("lists exactly the template paths in %s", (_, messages) => { + const templatePaths = collectFunctionPaths(messages).sort(); + + expect(Object.keys(TEMPLATE_PARAMETER_NAMES).sort()).toEqual(templatePaths); }); }); diff --git a/tests/unit/prompt.test.ts b/tests/unit/prompt.test.ts index 1d66773..24f35bf 100644 --- a/tests/unit/prompt.test.ts +++ b/tests/unit/prompt.test.ts @@ -91,6 +91,44 @@ describe("promptUser", () => { ); }); + it("keeps emoji decoration for a custom theme on the wizard base", async () => { + setupMinimalFlow(); + const dependencies = createMockDependencies({ + config: { + ...DEFAULT_CONFIG, + theme: "custom", + customTheme: { extends: "wizard", messages: {} }, + }, + }); + + await promptUser(dependencies); + + const choices = vi.mocked(select).mock.calls[0][0].choices as Array<{ + name: string; + }>; + expect(choices[0].name.startsWith(DEFAULT_CONFIG.types[0].emoji)).toBe(true); + }); + + it("uses plain labels for a custom theme on the standard base", async () => { + setupMinimalFlow(); + const dependencies = createMockDependencies({ + config: { + ...DEFAULT_CONFIG, + theme: "custom", + customTheme: { extends: "standard", messages: {} }, + }, + }); + + await promptUser(dependencies); + + const choices = vi.mocked(select).mock.calls[0][0].choices as Array<{ + name: string; + }>; + expect(choices[0].name.startsWith(`${DEFAULT_CONFIG.types[0].value}:`)).toBe( + true + ); + }); + it("applies character counter transformer to scope", async () => { setupMinimalFlow(); const dependencies = createMockDependencies(); diff --git a/tests/unit/theme.test.ts b/tests/unit/theme.test.ts new file mode 100644 index 0000000..5b69b9f --- /dev/null +++ b/tests/unit/theme.test.ts @@ -0,0 +1,293 @@ +import { describe, expect, it } from "vitest"; + +import { + DEFAULT_CONFIG, + MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS, + STANDARD_MESSAGES, + WIZARD_MESSAGES, +} from "../../src/utils/constants.js"; +import { + compileMessageTemplate, + mergeThemeMessages, + resolveBaseTheme, + validateThemeOverrides, +} from "../../src/utils/theme.js"; + +describe("resolveBaseTheme", () => { + it.each([ + ["wizard", "wizard"], + ["standard", "standard"], + ] as const)("returns %s for the built-in %s theme", (theme, expectedBase) => { + const config = { theme, customTheme: DEFAULT_CONFIG.customTheme }; + + expect(resolveBaseTheme(config)).toBe(expectedBase); + }); + + it("returns the extended theme for a custom theme", () => { + const config = { + theme: "custom" as const, + customTheme: { extends: "standard" as const, messages: {} }, + }; + + expect(resolveBaseTheme(config)).toBe("standard"); + }); +}); + +describe("validateThemeOverrides", () => { + describe("when input is not an object", () => { + it.each([null, undefined, "intro", 42, ["commit"]])( + "returns empty overrides for %s", + (rawOverrides) => { + expect(validateThemeOverrides(rawOverrides, STANDARD_MESSAGES)).toEqual( + {} + ); + } + ); + }); + + describe("when input matches the reference shape", () => { + it("keeps nested string messages", () => { + const rawOverrides = { + commit: { intro: "Launch" }, + errors: { commit: { noStaged: "Nothing staged" } }, + }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides).toEqual(rawOverrides); + }); + + it("keeps strings at template function paths", () => { + const rawOverrides = { + errors: { commit: { tooLong: "Max {maxCharacters}" } }, + }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides).toEqual(rawOverrides); + }); + }); + + describe("when input contains invalid entries", () => { + it("drops keys that are not in the reference", () => { + const rawOverrides = { commit: { intro: "Launch", unknown: "x" }, extra: {} }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides).toEqual({ commit: { intro: "Launch" } }); + }); + + it("drops non-string message values", () => { + const rawOverrides = { commit: { intro: 42, exit: null, dryRunExit: ["a"] } }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides).toEqual({}); + }); + + it("drops a string where the reference expects a group", () => { + const overrides = validateThemeOverrides( + { commit: "Launch" }, + STANDARD_MESSAGES + ); + + expect(overrides).toEqual({}); + }); + + it("drops a group where the reference expects a message", () => { + const overrides = validateThemeOverrides( + { commit: { intro: { text: "Launch" } } }, + STANDARD_MESSAGES + ); + + expect(overrides).toEqual({}); + }); + }); + + describe("when strings need sanitizing", () => { + it("strips ANSI escape and other control characters", () => { + const rawOverrides = { commit: { intro: "\u001b[2JLaunch\u0007\n\u007f!" } }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides.commit?.intro).toBe("[2JLaunch!"); + }); + + it("strips C1 controls and bidirectional overrides", () => { + const rawOverrides = { commit: { intro: "\u009b2JLaunch‮!⁦" } }; + + const overrides = validateThemeOverrides(rawOverrides, STANDARD_MESSAGES); + + expect(overrides.commit?.intro).toBe("2JLaunch!"); + }); + + it("keeps emoji variation selectors and joiners", () => { + const emojiMessage = "⚠️ \u{1f9d9}‍♂️ Launch"; + + const overrides = validateThemeOverrides( + { commit: { intro: emojiMessage } }, + STANDARD_MESSAGES + ); + + expect(overrides.commit?.intro).toBe(emojiMessage); + }); + + it("does not split an emoji at the length limit", () => { + const message = `${"a".repeat(MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS - 1)}\u{1f680}\u{1f680}`; + + const overrides = validateThemeOverrides( + { commit: { intro: message } }, + STANDARD_MESSAGES + ); + + expect(overrides.commit?.intro?.endsWith("a\u{1f680}")).toBe(true); + }); + + it("truncates messages longer than the maximum length", () => { + const longMessage = "a".repeat(MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS + 50); + + const overrides = validateThemeOverrides( + { commit: { intro: longMessage } }, + STANDARD_MESSAGES + ); + + expect(overrides.commit?.intro).toHaveLength( + MAX_CUSTOM_MESSAGE_LENGTH_CHARACTERS + ); + }); + }); +}); + +describe("compileMessageTemplate", () => { + it("replaces a placeholder with its argument", () => { + const tooLong = compileMessageTemplate("Max {maxCharacters}", ["maxCharacters"]); + + expect(tooLong(72)).toBe("Max 72"); + }); + + it("keeps parentheses around a present argument", () => { + const overwrite = compileMessageTemplate("Overwrite ({fileName})", ["fileName"]); + + expect(overwrite("a.json")).toBe("Overwrite (a.json)"); + }); + + it("replaces repeated placeholders", () => { + const render = compileMessageTemplate("{field} - set {field}", ["field"]); + + expect(render("theme")).toBe("theme - set theme"); + }); + + it("maps multiple placeholders by argument position", () => { + const render = compileMessageTemplate("{second} then {first}", [ + "first", + "second", + ]); + + expect(render("a", "b")).toBe("b then a"); + }); + + it("returns the template unchanged when it has no placeholders", () => { + const render = compileMessageTemplate("Plain text", ["field"]); + + expect(render("ignored")).toBe("Plain text"); + }); + + it("leaves unknown placeholders as typed", () => { + const render = compileMessageTemplate("Max {maxChars}", ["maxCharacters"]); + + expect(render(72)).toBe("Max {maxChars}"); + }); + + describe("when an argument is missing", () => { + it("removes parentheses left empty", () => { + const overwrite = compileMessageTemplate("Overwrite? ({fileName})", [ + "fileName", + ]); + + expect(overwrite()).toBe("Overwrite?"); + expect(overwrite(".husky/")).toBe("Overwrite? (.husky/)"); + }); + + it("keeps parentheses that do not wrap the placeholder", () => { + const overwrite = compileMessageTemplate("Overwrite? (keep ()) {fileName}", [ + "fileName", + ]); + + expect(overwrite()).toBe("Overwrite? (keep ()) "); + }); + + it("renders the placeholder as an empty string", () => { + const overwrite = compileMessageTemplate("Overwrite {fileName}?", [ + "fileName", + ]); + + expect(overwrite(undefined)).toBe("Overwrite ?"); + }); + }); + + it("does not evaluate placeholder content as code", () => { + const render = compileMessageTemplate("{field}", ["field"]); + + expect(render("${process.exit()}")).toBe("${process.exit()}"); + }); +}); + +describe("mergeThemeMessages", () => { + it("returns messages equal to the base when there are no overrides", () => { + const merged = mergeThemeMessages(WIZARD_MESSAGES, {}); + + expect(merged).toEqual(WIZARD_MESSAGES); + }); + + it("replaces only the overridden messages", () => { + const merged = mergeThemeMessages(STANDARD_MESSAGES, { + commit: { intro: "Launch" }, + prompts: { type: "Mission type:" }, + }); + + expect(merged.commit.intro).toBe("Launch"); + expect(merged.prompts.type).toBe("Mission type:"); + expect(merged.commit.exit).toBe(STANDARD_MESSAGES.commit.exit); + expect(merged.errors).toEqual(STANDARD_MESSAGES.errors); + }); + + it("keeps base template functions that are not overridden", () => { + const merged = mergeThemeMessages(STANDARD_MESSAGES, {}); + + expect(merged.errors.commit.tooLong).toBe( + STANDARD_MESSAGES.errors.commit.tooLong + ); + }); + + it("compiles overridden templates into functions", () => { + const merged = mergeThemeMessages(STANDARD_MESSAGES, { + errors: { commit: { tooLong: "Max {maxCharacters}" } }, + }); + + expect(merged.errors.commit.tooLong(50)).toBe("Max 50"); + }); + + it("compiles the projectFieldAction template with its field parameter", () => { + const merged = mergeThemeMessages(STANDARD_MESSAGES, { + config: { projectFieldAction: "Edit {field}" }, + }); + + expect(merged.config.projectFieldAction("theme")).toBe("Edit theme"); + }); + + it("compiles the overwrite template with its fileName parameter", () => { + const merged = mergeThemeMessages(STANDARD_MESSAGES, { + init: { overwrite: "Replace {fileName}?" }, + }); + + expect(merged.init.overwrite("a.json")).toBe("Replace a.json?"); + }); + + it("does not mutate the base theme", () => { + const originalIntro = STANDARD_MESSAGES.commit.intro; + + mergeThemeMessages(STANDARD_MESSAGES, { commit: { intro: "Launch" } }); + + expect(STANDARD_MESSAGES.commit.intro).toBe(originalIntro); + }); +}); From 4f066f2fb4c9ac7fa3dc663b63086cf0f266dc49 Mon Sep 17 00:00:00 2001 From: mBukator Date: Sun, 18 Oct 2026 23:02:28 +0200 Subject: [PATCH 17/17] chore(release): bump version to 1.1.0 - Bump version 1.0.2 -> 1.1.0 - Add the 1.1.0 CHANGELOG entry for custom themes --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5055b7a..316fa7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.0] - 2026-10-18 + +### Added + +- Custom themes via `"theme": "custom"` and a `customTheme` block in the user + or project `.merlinrc.json` + + `customTheme` takes `extends` (`"wizard"` or `"standard"`) and a + `messages` object. Only the listed messages are overridden; everything + else falls back to the base theme, which also decides decoration such as + emoji columns and menu icons. The three template messages accept + `{field}`, `{maxCharacters}` and `{fileName}` placeholders, filled by + plain string substitution and never evaluated. + +- Validation and sanitization for custom theme messages + + Overrides are checked against the built-in theme shape, so unknown keys + are dropped. C0 and C1 control characters and bidirectional override + characters are stripped, and each message is capped at 200 code points. + +- `custom` option in the `merlin config` theme picker, and the active + `customTheme` in `merlin config --show` output + +- Custom theme documentation: a Custom Themes section and a full message key + reference in `docs/usage/themes.md`, plus `customTheme` entries in + `docs/reference/config-schema.md` and `docs/reference/configuration.md` + +### Changed + +- `DEFAULT_CONFIG` now includes an empty `customTheme` block + + The next time `merlin config` saves `~/.merlinrc.json`, it writes a + starter `"customTheme": { "extends": "wizard", "messages": {} }` entry. + The block has no effect unless `theme` is set to `"custom"`. A + hand-written `customTheme` is kept exactly as written when saving from + `merlin config`. + ## [1.0.2] - 2026-10-14 ### Changed diff --git a/package-lock.json b/package-lock.json index 314a893..dbb9ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "merlin-commit", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "merlin-commit", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.6.0", diff --git a/package.json b/package.json index aa055ca..ca0edb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "merlin-commit", - "version": "1.0.2", + "version": "1.1.0", "description": "🧙‍♂️ Magical, interactive CLI for creating conventional commits", "type": "module", "main": "./dist/cli.js",