From 82afbdb3bfd32b5c285c4b32a87e64829e167650 Mon Sep 17 00:00:00 2001 From: Marcus Schwarz Date: Tue, 23 Jun 2026 08:31:08 +0200 Subject: [PATCH 1/3] COPSPA-526: Migrate OpenCode config plugin onto lib-ts-config-sync (Phase 3) - Depend on @techdivision/lib-ts-config-sync (file: link, local). - Rewrite src/config.ts as a thin wrapper: adapts the OpenCode client to the library Logger, reads plugin version, runs OpenCodeAdapter on session.created (completes the previously-missing remote sync). - Remove duplicated logic now owned by the library: src/services/* and src/types/PluginConfig.ts plus their unit/acceptance tests. - Fix logger to use the real client.app.log surface; drop unused helpers. - Remove broken docs symlink; drop non-existent skills/ from package files. - Update scaffolding acceptance test to the new lib-based structure. --- docs | 1 - package-lock.json | 1860 +++++++++++++++++ package.json | 5 +- src/config.ts | 62 +- src/services/ConfigLoader.ts | 139 -- src/services/ConfigMerger.ts | 48 - src/services/interfaces/IConfigLoader.ts | 30 - src/services/interfaces/IConfigMerger.ts | 22 - src/types/PluginConfig.ts | 27 - src/utils/logger.ts | 74 +- .../steps/epic-cfg-01/us-cfg-007.test.ts | 21 +- .../steps/epic-cfg-01/us-cfg-030.test.ts | 147 -- tests/unit/services/ConfigLoader.test.ts | 379 ---- tests/unit/services/ConfigMerger.test.ts | 149 -- tests/unit/types/PluginConfig.test.ts | 62 - 15 files changed, 1943 insertions(+), 1083 deletions(-) delete mode 120000 docs create mode 100644 package-lock.json delete mode 100644 src/services/ConfigLoader.ts delete mode 100644 src/services/ConfigMerger.ts delete mode 100644 src/services/interfaces/IConfigLoader.ts delete mode 100644 src/services/interfaces/IConfigMerger.ts delete mode 100644 src/types/PluginConfig.ts delete mode 100644 tests/acceptance/steps/epic-cfg-01/us-cfg-030.test.ts delete mode 100644 tests/unit/services/ConfigLoader.test.ts delete mode 100644 tests/unit/services/ConfigMerger.test.ts delete mode 100644 tests/unit/types/PluginConfig.test.ts diff --git a/docs b/docs deleted file mode 120000 index 26f37bb..0000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -/Users/wagnert/workspace/claude/npm/opencode-plugins/docs \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..42a65a4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1860 @@ +{ + "name": "@techdivision/opencode-plugin-config", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@techdivision/opencode-plugin-config", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@opencode-ai/plugin": "1.2.5", + "@techdivision/lib-ts-config-sync": "file:../lib-ts-config-sync" + }, + "devDependencies": { + "@amiceli/vitest-cucumber": "^6.0.0", + "@types/bun": "latest", + "@types/node": "^22.15.0", + "typescript": "^5.8.3", + "vitest": "^3.1.1" + }, + "peerDependencies": { + "bun": ">=1.0.0" + } + }, + "../lib-ts-config-sync": { + "name": "@techdivision/lib-ts-config-sync", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "deepmerge": "^4.3.1" + }, + "devDependencies": { + "@types/node": "^22.15.0", + "@vitest/coverage-v8": "^3.1.1", + "typescript": "^5.8.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@amiceli/vitest-cucumber": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@amiceli/vitest-cucumber/-/vitest-cucumber-6.5.0.tgz", + "integrity": "sha512-1883MTz+PsfTcR5C2wYlHh8rQzOEYOogdDhAikGWsx5UfTzIusP4HLH32kpcAPOBvHUpguYcA8Zmw5JxEydbzg==", + "dev": true, + "license": "ISC", + "dependencies": { + "callsites": "4.2.0", + "minimist": "1.2.8", + "parsecurrency": "1.1.1", + "ts-morph": "28.0.0" + }, + "bin": { + "vitest-cucumber": "dist/cli-generate.js" + }, + "peerDependencies": { + "vitest": "^4.0.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.2.5.tgz", + "integrity": "sha512-mZACBKyMMhH87K6HtiigbauLKjK7ooZPVaZTnRs492ldAXhiSfqk28Z8hGYNzhpwpHjkU6Ws8LOJRwOemDVQYg==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.2.5", + "zod": "4.1.8" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.5.tgz", + "integrity": "sha512-oFw08QNupBWuszpLa4HMRf/T+fDVqu8lbYSiqevdFTSH8Zih2cw/vPqLhjHgE6rPOSXyhvg46WivfSO7eNIpPw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@techdivision/lib-ts-config-sync": { + "resolved": "../lib-ts-config-sync", + "link": true + }, + "node_modules/@ts-morph/common": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz", + "integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, + "node_modules/@types/bun": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.14.tgz", + "integrity": "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.3.14" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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.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/bun-types": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.14.tgz", + "integrity": "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", + "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parsecurrency": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parsecurrency/-/parsecurrency-1.1.1.tgz", + "integrity": "sha512-IAw/8PSFgiko70KfZGv63rbEXhmVu+zpb42PvEtgHAm83Mze3eQJHWV1ZoOhPnrYeOyufvv0GS6hZDuQOdBH4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "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": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-morph": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", + "integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.29.0", + "code-block-writer": "^13.0.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index 534dad8..e34437c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "types": "src/config.ts", "files": [ "src/", - "skills/", "schemas/", "plugin.json" ], @@ -25,9 +24,7 @@ }, "dependencies": { "@opencode-ai/plugin": "1.2.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "deepmerge": "^4.3.1" + "@techdivision/lib-ts-config-sync": "file:../lib-ts-config-sync" }, "devDependencies": { "@types/bun": "latest", diff --git a/src/config.ts b/src/config.ts index db9893e..665b803 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,29 +1,61 @@ /** * OpenCode Config Plugin - Entry Point * - * Provides configuration cascade with deep-merge, env-var resolution - * and schema validation for OpenCode projects. + * Thin wrapper around @techdivision/lib-ts-config-sync. The shared library + * owns the entire sync pipeline (local cascade, env-var resolution, n8n + * webhook, per-section schema validation, deep-merge). This wrapper only: + * - adapts the OpenCode SDK client to the library Logger + * - reads the plugin version from plugin.json + * - triggers the OpenCodeAdapter on session start + * + * The merged config is exposed in-memory via + * process.env.OPENCODE_PROJECT_CONFIG for downstream consumer plugins. */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' import type { Plugin } from '@opencode-ai/plugin' +import { OpenCodeAdapter, type Logger } from '@techdivision/lib-ts-config-sync' import { createPluginLogger } from './utils/logger.js' -import { ConfigLoader } from './services/ConfigLoader.js' -import { ConfigMerger } from './services/ConfigMerger.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +/** + * Read this plugin's version from plugin.json. Falls back to '0.0.0'. + */ +function readPluginVersion(): string { + try { + const pluginJsonPath = resolve(__dirname, '..', 'plugin.json') + const parsed = JSON.parse(readFileSync(pluginJsonPath, 'utf-8')) as { version?: string } + return parsed.version ?? '0.0.0' + } catch { + return '0.0.0' + } +} export const ConfigPlugin: Plugin = async (input) => { - const logger = createPluginLogger(input.client, 'config') + const pluginLogger = createPluginLogger(input.client, 'config') - const merger = new ConfigMerger() - const loader = new ConfigLoader(merger, logger) + // Adapt the OpenCode SDK logger to the library's Logger interface. + const logger: Logger = { + debug: (message, context) => pluginLogger.debug(message, context), + info: (message, context) => pluginLogger.info(message, context), + warn: (message, context) => pluginLogger.warn(message, context), + error: (message, context) => pluginLogger.error(message, context), + } - const safeLoadConfig = logger.withLogging( - logger.withErrorHandling( - loader.loadLocalConfig.bind(loader), - {} - ), - 'loadLocalConfig' - ) + const adapter = new OpenCodeAdapter({ + pluginVersion: readPluginVersion(), + logger, + }) - return {} + return { + event: async ({ event }) => { + if (event.type === 'session.created') { + await adapter.run() + } + }, + } } export default ConfigPlugin diff --git a/src/services/ConfigLoader.ts b/src/services/ConfigLoader.ts deleted file mode 100644 index d511934..0000000 --- a/src/services/ConfigLoader.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * ConfigLoader - Reads and merges configuration files - * - * Implements the 2-layer local config cascade: - * Layer 1 (Base): ~/.config/opencode/opencode-project.json - * Layer 2 (Override): /.opencode/opencode-project.json - * - * Uses ConfigMerger for deep-merge with project-precedence semantics. - */ -import fs from 'node:fs' -import path from 'node:path' -import os from 'node:os' -import type { IConfigLoader } from './interfaces/IConfigLoader.js' -import type { IConfigMerger } from './interfaces/IConfigMerger.js' -import type { PluginLogger } from '../utils/logger.js' -import { GLOBAL_CONFIG_PATH, PROJECT_CONFIG_PATH, ENV_VAR_PATTERN } from '../types/PluginConfig.js' - -export class ConfigLoader implements IConfigLoader { - constructor( - private readonly merger: IConfigMerger, - private readonly logger?: PluginLogger - ) {} - - /** - * Read the global config from ~/.config/opencode/opencode-project.json. - * Returns {} if file or directory does not exist. - */ - public readGlobalConfig(): Record { - const globalConfigPath = path.join(os.homedir(), GLOBAL_CONFIG_PATH) - return this.readJsonFile(globalConfigPath) - } - - /** - * Read the project config from /.opencode/opencode-project.json. - * Returns {} if file or directory does not exist. - */ - public readProjectConfig(projectDir: string): Record { - const projectConfigPath = path.join(projectDir, PROJECT_CONFIG_PATH) - return this.readJsonFile(projectConfigPath) - } - - /** - * Resolve {env:VAR} placeholders in config string values via process.env. - * Non-string values are not affected. Unresolvable placeholders are preserved. - * Resolution is NOT recursive — only simple replacement. - */ - public resolveEnvVars(config: Record): Record { - return this.walkAndResolve(config) as Record - } - - /** - * Facade method: orchestrates the full local config pipeline. - * readGlobalConfig → readProjectConfig → merge → resolveEnvVars - */ - public loadLocalConfig(projectDir: string): Record { - const globalConfig = this.readGlobalConfig() - const projectConfig = this.readProjectConfig(projectDir) - const merged = this.merger.merge(globalConfig, projectConfig) - return this.resolveEnvVars(merged) - } - - /** - * Read and parse a JSON file. Returns {} on any error. - * Logs warnings for invalid JSON, non-object content, or permission errors. - */ - private readJsonFile(filePath: string): Record { - try { - const content = fs.readFileSync(filePath, 'utf-8') - - if (content.trim() === '') { - return {} - } - - const parsed = JSON.parse(content) - - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - this.logger?.warn(`Config file is not a JSON object: ${filePath}`, { filePath }) - return {} - } - - return parsed as Record - } catch (error) { - if (error instanceof SyntaxError) { - this.logger?.warn(`Config file contains invalid JSON: ${filePath}`, { - filePath, - error: String(error) - }) - } else if (this.isFileNotFoundError(error)) { - // File or directory does not exist — silent fallback - } else if (this.isPermissionError(error)) { - this.logger?.warn(`Config file has no read permission or access denied: ${filePath}`, { - filePath, - error: String(error) - }) - } else { - this.logger?.warn(`Failed to read config file: ${filePath}`, { - filePath, - error: String(error) - }) - } - return {} - } - } - - /** - * Recursively walk the config object and resolve {env:VAR} placeholders in string values. - */ - private walkAndResolve(value: unknown): unknown { - if (typeof value === 'string') { - return value.replace(ENV_VAR_PATTERN, (match, varName: string) => { - const envValue = process.env[varName] - return envValue !== undefined ? envValue : match - }) - } - - if (Array.isArray(value)) { - return value.map((item) => this.walkAndResolve(item)) - } - - if (typeof value === 'object' && value !== null) { - const result: Record = {} - for (const [key, val] of Object.entries(value)) { - result[key] = this.walkAndResolve(val) - } - return result - } - - return value - } - - private isFileNotFoundError(error: unknown): boolean { - return (error as NodeJS.ErrnoException)?.code === 'ENOENT' - } - - private isPermissionError(error: unknown): boolean { - const code = (error as NodeJS.ErrnoException)?.code - return code === 'EACCES' || code === 'EPERM' - } -} diff --git a/src/services/ConfigMerger.ts b/src/services/ConfigMerger.ts deleted file mode 100644 index 1b50a68..0000000 --- a/src/services/ConfigMerger.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * ConfigMerger - Central merge service - * - * Encapsulates deepmerge library with local-precedence semantics. - * Used by ConfigLoader for local cascade (Global+Project) and - * by the entry point for final merge (Remote+Local). - */ -import deepmerge from 'deepmerge' -import type { IConfigMerger } from './interfaces/IConfigMerger.js' - -/** - * Custom array merge strategy: override array replaces base array completely. - * No concatenation, no deduplication — local array wins. - */ -function overwriteArrays(_target: unknown[], source: unknown[]): unknown[] { - return source -} - -export class ConfigMerger implements IConfigMerger { - /** - * Deep-merge base and override configs. - * Override values take precedence at all nesting levels. - * Arrays in override replace arrays in base completely. - */ - public merge( - base: Record, - override: Record - ): Record { - return deepmerge(base, override, { arrayMerge: overwriteArrays }) - } - - /** - * Deep-merge with protected field handling. - * Removes protected fields from base before merging, - * so override's protected fields are preserved unchanged. - */ - public mergeWithProtectedFields( - base: Record, - override: Record, - protectedFields: readonly string[] - ): Record { - const sanitizedBase = { ...base } - for (const field of protectedFields) { - delete sanitizedBase[field] - } - return this.merge(sanitizedBase, override) - } -} diff --git a/src/services/interfaces/IConfigLoader.ts b/src/services/interfaces/IConfigLoader.ts deleted file mode 100644 index e9a9376..0000000 --- a/src/services/interfaces/IConfigLoader.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Interface for the ConfigLoader service. - * - * Reads and merges configuration from global and project sources. - */ -export interface IConfigLoader { - /** - * Read the global config from ~/.config/opencode/opencode-project.json. - * Returns {} if file or directory does not exist. - */ - readGlobalConfig(): Record - - /** - * Read the project config from /.opencode/opencode-project.json. - * Returns {} if file or directory does not exist. - */ - readProjectConfig(projectDir: string): Record - - /** - * Resolve {env:VAR} placeholders in config string values via process.env. - * Non-string values are not affected. Unresolvable placeholders are preserved. - */ - resolveEnvVars(config: Record): Record - - /** - * Facade method: orchestrates readGlobalConfig → readProjectConfig → merge → resolveEnvVars. - * Returns the fully resolved local config. - */ - loadLocalConfig(projectDir: string): Record -} diff --git a/src/services/interfaces/IConfigMerger.ts b/src/services/interfaces/IConfigMerger.ts deleted file mode 100644 index 1470610..0000000 --- a/src/services/interfaces/IConfigMerger.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Interface for the ConfigMerger service. - * - * Provides generic deep-merge with local-precedence semantics. - */ -export interface IConfigMerger { - /** - * Deep-merge base and override configs. Override values take precedence. - * Arrays in override replace arrays in base completely (no concatenation). - */ - merge(base: Record, override: Record): Record - - /** - * Deep-merge with protected field handling. - * Removes protected fields from base before merging, so override's protected fields are preserved. - */ - mergeWithProtectedFields( - base: Record, - override: Record, - protectedFields: readonly string[] - ): Record -} diff --git a/src/types/PluginConfig.ts b/src/types/PluginConfig.ts deleted file mode 100644 index b98a0da..0000000 --- a/src/types/PluginConfig.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Plugin Configuration Types - * - * Type definitions and constants for the config cascade. - */ - -/** - * Protected top-level fields that are never overwritten by remote config. - * These fields preserve local config integrity (schema reference, version tracking). - */ -export const PROTECTED_FIELDS: readonly string[] = ['$schema', 'version'] as const - -/** - * Global config file path relative to user home directory. - */ -export const GLOBAL_CONFIG_PATH = '.config/opencode/opencode-project.json' - -/** - * Project config file path relative to project root. - */ -export const PROJECT_CONFIG_PATH = '.opencode/opencode-project.json' - -/** - * Regex pattern for environment variable placeholders. - * Matches {env:VARIABLE_NAME} patterns in string values. - */ -export const ENV_VAR_PATTERN = /\{env:([^}]+)\}/g diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 43e577d..000fdd6 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,8 +1,9 @@ /** * Plugin Logger Factory * - * Creates a logger that uses the OpenCode SDK client for logging. - * NEVER use console.log/console.error - they interfere with the OpenCode TUI. + * Creates a logger that writes through the OpenCode SDK client + * (client.app.log). NEVER use console.log/console.error — they interfere + * with the OpenCode TUI. */ export interface PluginLogger { @@ -10,53 +11,42 @@ export interface PluginLogger { info(msg: string, extra?: Record): void warn(msg: string, extra?: Record): void error(msg: string, extra?: Record): void - withLogging any>(fn: T, name: string): T - withErrorHandling any>(fn: T, fallback: any): T } -interface LogClient { - log(params: { body: Record }): void +/** + * Minimal structural view of the OpenCode client's logging surface. + * The real client exposes `client.app.log({ body: {...} })`. + */ +export interface LogClient { + app: { + log(options: { + body: { + service: string + level: 'debug' | 'info' | 'warn' | 'error' + message: string + extra?: Record + } + }): unknown + } } export function createPluginLogger(client: LogClient, service: string): PluginLogger { - function log(level: 'debug' | 'info' | 'warn' | 'error', message: string, extra?: Record): void { - client.log({ body: { service, level, message, extra } }) + function log( + level: 'debug' | 'info' | 'warn' | 'error', + message: string, + extra?: Record + ): void { + try { + client.app.log({ body: { service, level, message, extra } }) + } catch { + // Never let logging break the plugin. + } } return { - debug: (msg: string, extra?: Record) => log('debug', msg, extra), - info: (msg: string, extra?: Record) => log('info', msg, extra), - warn: (msg: string, extra?: Record) => log('warn', msg, extra), - error: (msg: string, extra?: Record) => log('error', msg, extra), - - withLogging any>(fn: T, name: string): T { - return ((...args: any[]) => { - log('debug', `${name} started`) - const start = Date.now() - const result = fn(...args) - if (result instanceof Promise) { - return result - .then((r: any) => { log('info', `${name} completed`, { duration: Date.now() - start }); return r }) - .catch((e: any) => { log('error', `${name} failed`, { error: String(e), duration: Date.now() - start }); throw e }) - } - log('info', `${name} completed`, { duration: Date.now() - start }) - return result - }) as T - }, - - withErrorHandling any>(fn: T, fallback: any): T { - return ((...args: any[]) => { - try { - const result = fn(...args) - if (result instanceof Promise) { - return result.catch((e: any) => { log('warn', 'Graceful degradation', { error: String(e) }); return fallback }) - } - return result - } catch (e) { - log('warn', 'Graceful degradation', { error: String(e) }) - return fallback - } - }) as T - } + debug: (msg, extra) => log('debug', msg, extra), + info: (msg, extra) => log('info', msg, extra), + warn: (msg, extra) => log('warn', msg, extra), + error: (msg, extra) => log('error', msg, extra), } } diff --git a/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts b/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts index 539cc3b..6de534c 100644 --- a/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts +++ b/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts @@ -48,16 +48,8 @@ describe('Feature: Repository Initialization and Project Scaffolding', () => { expect(json.dependencies).toHaveProperty('@opencode-ai/plugin') }) - it('should have deepmerge dependency', () => { - expect(json.dependencies).toHaveProperty('deepmerge') - }) - - it('should have ajv dependency', () => { - expect(json.dependencies).toHaveProperty('ajv') - }) - - it('should have ajv-formats dependency', () => { - expect(json.dependencies).toHaveProperty('ajv-formats') + it('should depend on the shared config-sync library', () => { + expect(json.dependencies).toHaveProperty('@techdivision/lib-ts-config-sync') }) it('should have @types/bun devDependency', () => { @@ -221,17 +213,10 @@ describe('Feature: Repository Initialization and Project Scaffolding', () => { describe('Scenario: Directory structure is complete', () => { const dirs = [ - 'src/services', - 'src/services/interfaces', - 'src/types', 'src/utils', - 'tests/unit', - 'tests/unit/services', - 'tests/integration', 'tests/acceptance', 'tests/acceptance/gherkin', - 'schemas', - 'skills' + 'schemas' ] for (const dir of dirs) { diff --git a/tests/acceptance/steps/epic-cfg-01/us-cfg-030.test.ts b/tests/acceptance/steps/epic-cfg-01/us-cfg-030.test.ts deleted file mode 100644 index 0edeef1..0000000 --- a/tests/acceptance/steps/epic-cfg-01/us-cfg-030.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Acceptance Tests for US-CFG-030: ConfigMerger Deep-Merge - * - * Uses direct vitest describe/it because vitest-cucumber has issues - * with special characters in step text (dots, slashes, dollar signs). - */ -import { describe, it, expect, beforeEach } from 'vitest' -import { ConfigMerger } from '../../../../src/services/ConfigMerger.js' - -function getNestedField(obj: Record, fieldPath: string): unknown { - const parts = fieldPath.split('.') - let current: unknown = obj - for (const part of parts) { - if (typeof current !== 'object' || current === null) return undefined - current = (current as Record)[part] - } - return current -} - -describe('Feature: ConfigMerger Deep-Merge [US-CFG-030]', () => { - let merger: ConfigMerger - - beforeEach(() => { - merger = new ConfigMerger() - }) - - describe('Scenario: Local scalar values override remote scalar values', () => { - it('should override remote scalars with local scalars and preserve remote-only nested values', () => { - const remote = { - jira: { - project: 'REMOTE-PROJ', - base_url: 'https://remote.atlassian.net', - workflow: { status: { open: { name: 'Open', id: '1' } } } - } - } - const local = { - jira: { - project: 'LOCAL-PROJ', - base_url: 'https://local.atlassian.net' - } - } - - const result = merger.merge(remote, local) - - expect(getNestedField(result, 'jira.project')).toBe('LOCAL-PROJ') - expect(getNestedField(result, 'jira.base_url')).toBe('https://local.atlassian.net') - expect(getNestedField(result, 'jira.workflow.status.open.name')).toBe('Open') - }) - }) - - describe('Scenario: Remote-only values are adopted as new defaults', () => { - it('should adopt remote-only values while preserving local values', () => { - const remote = { - jira: { - workflow: { status: { open: { name: 'Open', id: '1' } } }, - tempo: { account_field_id: 'customfield_10039' } - } - } - const local = { jira: { project: 'COPSPA' } } - - const result = merger.merge(remote, local) - - expect(getNestedField(result, 'jira.project')).toBe('COPSPA') - expect(getNestedField(result, 'jira.workflow.status.open.id')).toBe('1') - expect(getNestedField(result, 'jira.tempo.account_field_id')).toBe('customfield_10039') - }) - }) - - describe('Scenario: Local arrays replace remote arrays completely', () => { - it('should replace remote array with local array (no concatenation)', () => { - const remote = { time_tracking: { valid_projects: ['PROJ-A', 'PROJ-B', 'PROJ-C'] } } - const local = { time_tracking: { valid_projects: ['COPSPA'] } } - - const result = merger.merge(remote, local) - - const arr = getNestedField(result, 'time_tracking.valid_projects') as unknown[] - expect(arr).toHaveLength(1) - expect(arr).toContain('COPSPA') - expect(arr).not.toContain('PROJ-A') - }) - }) - - describe('Scenario: Protected fields are removed from remote config before merge', () => { - it('should preserve local protected fields by removing them from remote', () => { - const remote = { - $schema: 'https://remote-schema-url', - version: '0.1.0', - jira: { project: 'COPSPA' } - } - const local = { - $schema: 'https://local-schema-url', - version: '1.0.0', - jira: { base_url: 'https://local.atlassian.net' } - } - - const result = merger.mergeWithProtectedFields(remote, local, ['$schema', 'version']) - - expect(result.$schema).toBe('https://local-schema-url') - expect(result.version).toBe('1.0.0') - expect(getNestedField(result, 'jira.project')).toBe('COPSPA') - expect(getNestedField(result, 'jira.base_url')).toBe('https://local.atlassian.net') - }) - }) - - describe('Scenario: Deep nested objects are merged recursively', () => { - it('should recursively merge nested objects with local precedence', () => { - const remote = { - time_tracking: { - pricing: { - ratio: { input: 0.8, output: 0.2 }, - default: { input: 3, output: 15 }, - periods: [{ from: '2025-11-01', models: {} }] - } - } - } - const local = { - time_tracking: { - pricing: { - ratio: { input: 0.9, output: 0.1 } - } - } - } - - const result = merger.merge(remote, local) - - expect(getNestedField(result, 'time_tracking.pricing.ratio.input')).toBe(0.9) - expect(getNestedField(result, 'time_tracking.pricing.ratio.output')).toBe(0.1) - expect(getNestedField(result, 'time_tracking.pricing.default.input')).toBe(3) - const periods = getNestedField(result, 'time_tracking.pricing.periods') as unknown[] - expect(periods).toHaveLength(1) - }) - }) - - describe('Scenario: Empty remote config results in local config unchanged', () => { - it('should return local config unchanged when remote is empty', () => { - const remote = {} - const local = { - jira: { project: 'COPSPA' }, - time_tracking: { csv_file: '.opencode/tt.csv' } - } - - const result = merger.merge(remote, local) - - expect(result).toEqual(local) - }) - }) -}) diff --git a/tests/unit/services/ConfigLoader.test.ts b/tests/unit/services/ConfigLoader.test.ts deleted file mode 100644 index c9ca6a6..0000000 --- a/tests/unit/services/ConfigLoader.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Unit Tests for ConfigLoader (US-CFG-001, 002, 003, 004, 005, 006) - * - * Tests reading global/project configs, deep-merge, env-var resolution, - * protected fields, and error handling. - */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import { ConfigLoader } from '../../../src/services/ConfigLoader.js' -import type { IConfigMerger } from '../../../src/services/interfaces/IConfigMerger.js' -import type { PluginLogger } from '../../../src/utils/logger.js' - -// Mock fs module -vi.mock('node:fs') - -function createMockMerger(): IConfigMerger { - return { - merge: vi.fn((base, override) => ({ ...base, ...override })), - mergeWithProtectedFields: vi.fn((base, override) => ({ ...base, ...override })) - } -} - -function createMockLogger(): PluginLogger { - return { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - withLogging: vi.fn((fn) => fn), - withErrorHandling: vi.fn((fn) => fn) - } -} - -describe('ConfigLoader', () => { - let mockMerger: IConfigMerger - let mockLogger: PluginLogger - let loader: ConfigLoader - - beforeEach(() => { - mockMerger = createMockMerger() - mockLogger = createMockLogger() - loader = new ConfigLoader(mockMerger, mockLogger) - vi.clearAllMocks() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - // ============================================================ - // US-CFG-001: Read Global Config - // ============================================================ - describe('readGlobalConfig() [US-CFG-001]', () => { - it('should read and parse existing global config file', () => { - const configData = { - config: { sync_url: 'https://n8n.example.com/webhook/oc-config-sync' }, - time_tracking: { pricing: { ratio: { input: 0.8, output: 0.2 } } } - } - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(configData)) - - const result = loader.readGlobalConfig() - - expect(result).toEqual(configData) - const expectedPath = path.join(os.homedir(), '.config/opencode/opencode-project.json') - expect(fs.readFileSync).toHaveBeenCalledWith(expectedPath, 'utf-8') - }) - - it('should return empty object when config file does not exist', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - }) - - it('should return empty object when config directory does not exist', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - }) - }) - - // ============================================================ - // US-CFG-002: Read Project Config - // ============================================================ - describe('readProjectConfig() [US-CFG-002]', () => { - it('should read and parse existing project config file', () => { - const configData = { - jira: { project: 'COPSPA', base_url: 'https://techdivision.atlassian.net' }, - time_tracking: { csv_file: '.opencode/time_tracking/time-tracking.csv' } - } - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(configData)) - - const result = loader.readProjectConfig('/my/project') - - expect(result).toEqual(configData) - const expectedPath = path.join('/my/project', '.opencode/opencode-project.json') - expect(fs.readFileSync).toHaveBeenCalledWith(expectedPath, 'utf-8') - }) - - it('should return empty object when project config file does not exist', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - - const result = loader.readProjectConfig('/my/project') - - expect(result).toEqual({}) - }) - - it('should return empty object when .opencode directory does not exist', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - - const result = loader.readProjectConfig('/my/project') - - expect(result).toEqual({}) - }) - }) - - // ============================================================ - // US-CFG-003: Deep-Merge + loadLocalConfig facade - // ============================================================ - describe('loadLocalConfig() [US-CFG-003]', () => { - it('should orchestrate readGlobal → readProject → merge → resolveEnvVars', () => { - const globalConfig = { time_tracking: { csv_file: 'global.csv' } } - const projectConfig = { jira: { project: 'COPSPA' } } - const mergedConfig = { time_tracking: { csv_file: 'global.csv' }, jira: { project: 'COPSPA' } } - - // First call = global, second call = project - vi.mocked(fs.readFileSync) - .mockReturnValueOnce(JSON.stringify(globalConfig)) - .mockReturnValueOnce(JSON.stringify(projectConfig)) - - vi.mocked(mockMerger.merge).mockReturnValue(mergedConfig) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith(globalConfig, projectConfig) - expect(result).toEqual(mergedConfig) - }) - - it('should return empty object when both configs are empty', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - vi.mocked(mockMerger.merge).mockReturnValue({}) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith({}, {}) - expect(result).toEqual({}) - }) - - it('should use only global config when project config is missing', () => { - const globalConfig = { config: { sync_url: 'https://n8n.example.com' } } - - vi.mocked(fs.readFileSync) - .mockReturnValueOnce(JSON.stringify(globalConfig)) - .mockImplementationOnce(() => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - throw error - }) - - vi.mocked(mockMerger.merge).mockReturnValue(globalConfig) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith(globalConfig, {}) - }) - - it('should use only project config when global config is missing', () => { - const projectConfig = { jira: { project: 'COPSPA' } } - - vi.mocked(fs.readFileSync) - .mockImplementationOnce(() => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - throw error - }) - .mockReturnValueOnce(JSON.stringify(projectConfig)) - - vi.mocked(mockMerger.merge).mockReturnValue(projectConfig) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith({}, projectConfig) - }) - }) - - // ============================================================ - // US-CFG-004: Environment Variable Resolution - // ============================================================ - describe('resolveEnvVars() [US-CFG-004]', () => { - it('should resolve a single {env:VAR} placeholder', () => { - process.env.OC_CONFIG_SYNC_TOKEN = 'my-secret-token' - - const config = { config: { sync_token: '{env:OC_CONFIG_SYNC_TOKEN}' } } - const result = loader.resolveEnvVars(config) as any - - expect(result.config.sync_token).toBe('my-secret-token') - - delete process.env.OC_CONFIG_SYNC_TOKEN - }) - - it('should resolve multiple {env:VAR} placeholders in different values', () => { - process.env.OC_CONFIG_SYNC_URL = 'https://n8n.example.com/webhook/oc-config-sync' - process.env.OC_CONFIG_SYNC_TOKEN = 'my-secret-token' - - const config = { - config: { - sync_url: '{env:OC_CONFIG_SYNC_URL}', - sync_token: '{env:OC_CONFIG_SYNC_TOKEN}' - } - } - const result = loader.resolveEnvVars(config) as any - - expect(result.config.sync_url).toBe('https://n8n.example.com/webhook/oc-config-sync') - expect(result.config.sync_token).toBe('my-secret-token') - - delete process.env.OC_CONFIG_SYNC_URL - delete process.env.OC_CONFIG_SYNC_TOKEN - }) - - it('should resolve {env:VAR} in deeply nested config values', () => { - process.env.TT_TEMPO_API_TOKEN = 'tempo-api-key-123' - - const config = { - time_tracking: { - sync: { - tempo: { - api_token: '{env:TT_TEMPO_API_TOKEN}' - } - } - } - } - const result = loader.resolveEnvVars(config) as any - - expect(result.time_tracking.sync.tempo.api_token).toBe('tempo-api-key-123') - - delete process.env.TT_TEMPO_API_TOKEN - }) - - it('should preserve original placeholder when env var is not set', () => { - delete process.env.NONEXISTENT_VAR - - const config = { config: { sync_token: '{env:NONEXISTENT_VAR}' } } - const result = loader.resolveEnvVars(config) as any - - expect(result.config.sync_token).toBe('{env:NONEXISTENT_VAR}') - }) - - it('should not affect non-string values', () => { - const config = { - time_tracking: { - pricing: { ratio: { input: 0.8, output: 0.2 } }, - valid_projects: ['COPSPA'] - } - } - const result = loader.resolveEnvVars(config) as any - - expect(result.time_tracking.pricing.ratio.input).toBe(0.8) - expect(result.time_tracking.valid_projects).toEqual(['COPSPA']) - }) - - it('should NOT resolve recursively - only simple replacement', () => { - process.env.REDIRECT_VAR = '{env:ANOTHER_VAR}' - process.env.ANOTHER_VAR = 'final-value' - - const config = { config: { sync_url: '{env:REDIRECT_VAR}' } } - const result = loader.resolveEnvVars(config) as any - - expect(result.config.sync_url).toBe('{env:ANOTHER_VAR}') - - delete process.env.REDIRECT_VAR - delete process.env.ANOTHER_VAR - }) - }) - - // ============================================================ - // US-CFG-006: Error Handling - // ============================================================ - describe('Error Handling [US-CFG-006]', () => { - it('should return empty object and log warning for invalid JSON', () => { - vi.mocked(fs.readFileSync).mockReturnValue('{ invalid json content, missing quotes') - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('invalid JSON'), - expect.any(Object) - ) - }) - - it('should return empty object for completely empty file', () => { - vi.mocked(fs.readFileSync).mockReturnValue('') - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - }) - - it('should return empty object and log warning for valid JSON that is not an object', () => { - vi.mocked(fs.readFileSync).mockReturnValue('["this", "is", "an", "array"]') - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('not a JSON object'), - expect.any(Object) - ) - }) - - it('should return empty object and log warning for permission errors', () => { - const error = new Error('EACCES') as NodeJS.ErrnoException - error.code = 'EACCES' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - - const result = loader.readGlobalConfig() - - expect(result).toEqual({}) - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('permission'), - expect.any(Object) - ) - }) - - it('should handle both configs missing without error', () => { - const error = new Error('ENOENT') as NodeJS.ErrnoException - error.code = 'ENOENT' - vi.mocked(fs.readFileSync).mockImplementation(() => { throw error }) - vi.mocked(mockMerger.merge).mockReturnValue({}) - - const result = loader.loadLocalConfig('/my/project') - - expect(result).toEqual({}) - }) - - it('should use project config alone when global config is invalid', () => { - const projectConfig = { jira: { project: 'COPSPA' } } - - vi.mocked(fs.readFileSync) - .mockReturnValueOnce('{ invalid json') - .mockReturnValueOnce(JSON.stringify(projectConfig)) - - vi.mocked(mockMerger.merge).mockReturnValue(projectConfig) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith({}, projectConfig) - }) - - it('should use global config alone when project config is invalid', () => { - const globalConfig = { time_tracking: { pricing: { default: { input: 3, output: 15 } } } } - - vi.mocked(fs.readFileSync) - .mockReturnValueOnce(JSON.stringify(globalConfig)) - .mockReturnValueOnce('{ invalid json') - - vi.mocked(mockMerger.merge).mockReturnValue(globalConfig) - - const result = loader.loadLocalConfig('/my/project') - - expect(mockMerger.merge).toHaveBeenCalledWith(globalConfig, {}) - }) - }) -}) diff --git a/tests/unit/services/ConfigMerger.test.ts b/tests/unit/services/ConfigMerger.test.ts deleted file mode 100644 index 19eb68f..0000000 --- a/tests/unit/services/ConfigMerger.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Unit Tests for ConfigMerger (US-CFG-030) - * - * Tests the central deep-merge service with local-precedence semantics. - */ -import { describe, it, expect } from 'vitest' -import { ConfigMerger } from '../../../src/services/ConfigMerger.js' - -describe('ConfigMerger', () => { - const merger = new ConfigMerger() - - describe('merge()', () => { - it('should override base scalar values with override scalar values', () => { - const base = { jira: { project: 'REMOTE-PROJ', base_url: 'https://remote.atlassian.net' } } - const override = { jira: { project: 'LOCAL-PROJ', base_url: 'https://local.atlassian.net' } } - - const result = merger.merge(base, override) - - expect(result.jira).toEqual({ - project: 'LOCAL-PROJ', - base_url: 'https://local.atlassian.net' - }) - }) - - it('should adopt remote-only values as new defaults', () => { - const base = { - jira: { - workflow: { status: { open: { name: 'Open', id: '1' } } }, - tempo: { account_field_id: 'customfield_10039' } - } - } - const override = { jira: { project: 'COPSPA' } } - - const result = merger.merge(base, override) as any - - expect(result.jira.project).toBe('COPSPA') - expect(result.jira.workflow.status.open.id).toBe('1') - expect(result.jira.tempo.account_field_id).toBe('customfield_10039') - }) - - it('should replace base arrays with override arrays completely (no concatenation)', () => { - const base = { time_tracking: { valid_projects: ['PROJ-A', 'PROJ-B', 'PROJ-C'] } } - const override = { time_tracking: { valid_projects: ['COPSPA'] } } - - const result = merger.merge(base, override) as any - - expect(result.time_tracking.valid_projects).toEqual(['COPSPA']) - expect(result.time_tracking.valid_projects).not.toContain('PROJ-A') - }) - - it('should deep-merge nested objects recursively', () => { - const base = { - time_tracking: { - pricing: { - ratio: { input: 0.8, output: 0.2 }, - default: { input: 3, output: 15 }, - periods: [{ from: '2025-11-01', models: {} }] - } - } - } - const override = { - time_tracking: { - pricing: { - ratio: { input: 0.9, output: 0.1 } - } - } - } - - const result = merger.merge(base, override) as any - - expect(result.time_tracking.pricing.ratio.input).toBe(0.9) - expect(result.time_tracking.pricing.ratio.output).toBe(0.1) - expect(result.time_tracking.pricing.default.input).toBe(3) - expect(result.time_tracking.pricing.periods).toHaveLength(1) - }) - - it('should return override unchanged when base is empty', () => { - const base = {} - const override = { - jira: { project: 'COPSPA' }, - time_tracking: { csv_file: '.opencode/tt.csv' } - } - - const result = merger.merge(base, override) - - expect(result).toEqual(override) - }) - - it('should return base unchanged when override is empty', () => { - const base = { - jira: { project: 'COPSPA' }, - time_tracking: { csv_file: '.opencode/tt.csv' } - } - const override = {} - - const result = merger.merge(base, override) - - expect(result).toEqual(base) - }) - - it('should return empty object when both are empty', () => { - const result = merger.merge({}, {}) - expect(result).toEqual({}) - }) - }) - - describe('mergeWithProtectedFields()', () => { - it('should remove protected fields from base before merge', () => { - const base = { - $schema: 'https://remote-schema-url', - version: '0.1.0', - jira: { project: 'COPSPA' } - } - const override = { - $schema: 'https://local-schema-url', - version: '1.0.0', - jira: { base_url: 'https://local.atlassian.net' } - } - - const result = merger.mergeWithProtectedFields(base, override, ['$schema', 'version']) - - expect(result.$schema).toBe('https://local-schema-url') - expect(result.version).toBe('1.0.0') - expect((result.jira as any).project).toBe('COPSPA') - expect((result.jira as any).base_url).toBe('https://local.atlassian.net') - }) - - it('should preserve override protected fields when base has different values', () => { - const base = { $schema: 'remote-schema', version: '0.5.0', data: 'remote' } - const override = { $schema: 'local-schema', version: '1.0.0', data: 'local' } - - const result = merger.mergeWithProtectedFields(base, override, ['$schema', 'version']) - - expect(result.$schema).toBe('local-schema') - expect(result.version).toBe('1.0.0') - expect(result.data).toBe('local') - }) - - it('should not modify the original base object', () => { - const base = { $schema: 'remote', version: '1.0', key: 'value' } - const override = { $schema: 'local', version: '2.0' } - - merger.mergeWithProtectedFields(base, override, ['$schema', 'version']) - - expect(base.$schema).toBe('remote') - expect(base.version).toBe('1.0') - }) - }) -}) diff --git a/tests/unit/types/PluginConfig.test.ts b/tests/unit/types/PluginConfig.test.ts deleted file mode 100644 index 6be3062..0000000 --- a/tests/unit/types/PluginConfig.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Unit Tests for PluginConfig Types (US-CFG-005) - * - * Tests protected fields constant and config path definitions. - */ -import { describe, it, expect } from 'vitest' -import { - PROTECTED_FIELDS, - GLOBAL_CONFIG_PATH, - PROJECT_CONFIG_PATH, - ENV_VAR_PATTERN -} from '../../../src/types/PluginConfig.js' - -describe('PluginConfig Types [US-CFG-005]', () => { - describe('PROTECTED_FIELDS', () => { - it('should contain "$schema"', () => { - expect(PROTECTED_FIELDS).toContain('$schema') - }) - - it('should contain "version"', () => { - expect(PROTECTED_FIELDS).toContain('version') - }) - - it('should contain exactly 2 fields', () => { - expect(PROTECTED_FIELDS).toHaveLength(2) - }) - - it('should be a readonly array', () => { - // TypeScript enforces readonly at compile time - // At runtime, we verify it's an array - expect(Array.isArray(PROTECTED_FIELDS)).toBe(true) - }) - }) - - describe('Config Paths', () => { - it('should define global config path', () => { - expect(GLOBAL_CONFIG_PATH).toBe('.config/opencode/opencode-project.json') - }) - - it('should define project config path', () => { - expect(PROJECT_CONFIG_PATH).toBe('.opencode/opencode-project.json') - }) - }) - - describe('ENV_VAR_PATTERN', () => { - it('should match {env:VAR} pattern', () => { - const match = '{env:MY_VAR}'.match(ENV_VAR_PATTERN) - expect(match).not.toBeNull() - }) - - it('should capture the variable name', () => { - const regex = new RegExp(ENV_VAR_PATTERN.source, ENV_VAR_PATTERN.flags) - const match = regex.exec('{env:MY_VAR}') - expect(match?.[1]).toBe('MY_VAR') - }) - - it('should not match strings without env prefix', () => { - const match = '{MY_VAR}'.match(ENV_VAR_PATTERN) - expect(match).toBeNull() - }) - }) -}) From 3f0f4135003cc43f583de8e41367cfe6bc353f7d Mon Sep 17 00:00:00 2001 From: Marcus Schwarz Date: Wed, 15 Jul 2026 11:44:07 +0200 Subject: [PATCH 2/3] COPSPA-526: Align env vars with real Config-Builder contract (testing fix) - .env.example: use CONFIG_SYNC_URL / CONFIG_SYNC_BEARER_TOKEN / CONFIG_SYNC_CONFIG_KEY (+ optional CONFIG_SYNC_PLUGINS) and keep OPENCODE_USER_EMAIL (sent as the 'user' field). - Update scaffolding acceptance test to assert the new env-var names. --- .env.example | 10 +++++++--- .../acceptance/steps/epic-cfg-01/us-cfg-007.test.ts | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 6a4e89f..c5cc183 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ # Config Sync Webhook -OC_CONFIG_SYNC_URL=https://n8n.example.com/webhook/oc-config-sync -OC_CONFIG_SYNC_TOKEN=your-webhook-token-here +CONFIG_SYNC_URL=https://n8n.example.com/webhook/config-builder +CONFIG_SYNC_BEARER_TOKEN=your-webhook-token-here +CONFIG_SYNC_CONFIG_KEY=YOURPROJECT -# User identification +# Optional: comma-separated plugin sections (overrides auto-discovery) +# CONFIG_SYNC_PLUGINS=jira,time-tracking + +# User identification (sent as the "user" field in the webhook payload) OPENCODE_USER_EMAIL=your.email@example.com diff --git a/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts b/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts index 6de534c..8826410 100644 --- a/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts +++ b/tests/acceptance/steps/epic-cfg-01/us-cfg-007.test.ts @@ -176,12 +176,16 @@ describe('Feature: Repository Initialization and Project Scaffolding', () => { expect(fs.existsSync(filePath)).toBe(true) }) - it('should contain OC_CONFIG_SYNC_URL', () => { - expect(content).toContain('OC_CONFIG_SYNC_URL') + it('should contain CONFIG_SYNC_URL', () => { + expect(content).toContain('CONFIG_SYNC_URL') }) - it('should contain OC_CONFIG_SYNC_TOKEN', () => { - expect(content).toContain('OC_CONFIG_SYNC_TOKEN') + it('should contain CONFIG_SYNC_BEARER_TOKEN', () => { + expect(content).toContain('CONFIG_SYNC_BEARER_TOKEN') + }) + + it('should contain CONFIG_SYNC_CONFIG_KEY', () => { + expect(content).toContain('CONFIG_SYNC_CONFIG_KEY') }) it('should contain OPENCODE_USER_EMAIL', () => { From 1a39b54e156af48714936eb5d72ca81142dab945 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 16 Jul 2026 10:47:32 +0200 Subject: [PATCH 3/3] COPSPA-526: Refactoring, Logging, Toasting --- package-lock.json | 416 ++++++++++++++++++++++++++++++++++- package.json | 8 +- src/{config.ts => Plugin.ts} | 54 +++-- src/utils/logger.ts | 72 +++--- 4 files changed, 483 insertions(+), 67 deletions(-) rename src/{config.ts => Plugin.ts} (50%) diff --git a/package-lock.json b/package-lock.json index 42a65a4..ac14781 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@opencode-ai/plugin": "1.2.5", "@techdivision/lib-ts-config-sync": "file:../lib-ts-config-sync" }, "devDependencies": { "@amiceli/vitest-cucumber": "^6.0.0", + "@opencode-ai/plugin": "^1.17.0", "@types/bun": "latest", "@types/node": "^22.15.0", "typescript": "^5.8.3", "vitest": "^3.1.1" }, "peerDependencies": { - "bun": ">=1.0.0" + "@opencode-ai/plugin": ">=1.0.0" } }, "../lib-ts-config-sync": { @@ -42,6 +42,19 @@ "node": ">=20" } }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@amiceli/vitest-cucumber": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@amiceli/vitest-cucumber/-/vitest-cucumber-6.5.0.tgz", @@ -510,21 +523,128 @@ "dev": true, "license": "MIT" }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@opencode-ai/plugin": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.2.5.tgz", - "integrity": "sha512-mZACBKyMMhH87K6HtiigbauLKjK7ooZPVaZTnRs492ldAXhiSfqk28Z8hGYNzhpwpHjkU6Ws8LOJRwOemDVQYg==", + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.2.tgz", + "integrity": "sha512-h0N1mShfSfaI0pMHXCAdEevF5fVZ+u+F8B4/PDEurpFIk7n/mSjYkgjVvekIR3Z4SJkApkVlwekctIPO7JhLvg==", + "dev": true, "license": "MIT", "dependencies": { - "@opencode-ai/sdk": "1.2.5", + "@ai-sdk/provider": "3.0.8", + "@opencode-ai/sdk": "1.18.2", + "effect": "4.0.0-beta.83", "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } } }, "node_modules/@opencode-ai/sdk": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.5.tgz", - "integrity": "sha512-oFw08QNupBWuszpLa4HMRf/T+fDVqu8lbYSiqevdFTSH8Zih2cw/vPqLhjHgE6rPOSXyhvg46WivfSO7eNIpPw==", - "license": "MIT" + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.2.tgz", + "integrity": "sha512-40DIMMrl2W0TMFKtTnrYKed3ElXhqEkP0oLahQEd6Mm9mTdu/B2Bc9A19++IkKKFIxbxKFaUhik+vBUiNBrFtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", @@ -915,6 +1035,13 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@techdivision/lib-ts-config-sync": { "resolved": "../lib-ts-config-sync", "link": true @@ -1191,6 +1318,21 @@ "dev": true, "license": "MIT" }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1219,6 +1361,36 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.83", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz", + "integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -1288,6 +1460,29 @@ "node": ">=12.0.0" } }, + "node_modules/fast-check": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", + "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1306,6 +1501,13 @@ } } }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "dev": true, + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1321,6 +1523,23 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -1328,6 +1547,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -1378,6 +1611,46 @@ "dev": true, "license": "MIT" }, + "node_modules/msgpackr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multipasta": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", + "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", @@ -1397,6 +1670,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/parsecurrency": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/parsecurrency/-/parsecurrency-1.1.1.tgz", @@ -1411,6 +1700,16 @@ "dev": true, "license": "MIT" }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -1477,6 +1776,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -1522,6 +1838,29 @@ "fsevents": "~2.3.2" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1627,6 +1966,16 @@ "node": ">=14.0.0" } }, + "node_modules/toml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", + "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/ts-morph": { "version": "28.0.0", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", @@ -1659,6 +2008,20 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", @@ -1830,6 +2193,22 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/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", @@ -1847,10 +2226,27 @@ "node": ">=8" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index e34437c..0599b3f 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "description": "OpenCode Plugin - Configuration cascade with deep-merge, env-var resolution and schema validation", "type": "module", - "main": "src/config.ts", - "types": "src/config.ts", + "main": "src/Plugin.ts", + "types": "src/Plugin.ts", "files": [ "src/", "schemas/", @@ -23,10 +23,10 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "@opencode-ai/plugin": "1.2.5", "@techdivision/lib-ts-config-sync": "file:../lib-ts-config-sync" }, "devDependencies": { + "@opencode-ai/plugin": "^1.17.0", "@types/bun": "latest", "@types/node": "^22.15.0", "typescript": "^5.8.3", @@ -34,7 +34,7 @@ "@amiceli/vitest-cucumber": "^6.0.0" }, "peerDependencies": { - "bun": ">=1.0.0" + "@opencode-ai/plugin": ">=1.0.0" }, "opencode": { "plugin": true, diff --git a/src/config.ts b/src/Plugin.ts similarity index 50% rename from src/config.ts rename to src/Plugin.ts index 665b803..7200e64 100644 --- a/src/config.ts +++ b/src/Plugin.ts @@ -4,9 +4,10 @@ * Thin wrapper around @techdivision/lib-ts-config-sync. The shared library * owns the entire sync pipeline (local cascade, env-var resolution, n8n * webhook, per-section schema validation, deep-merge). This wrapper only: - * - adapts the OpenCode SDK client to the library Logger + * - runs the sync immediately at plugin load (every opencode startup) + * - shows an info toast once the UI is ready (plugin.added primary, + * session.created fallback — same pattern as opencode-cli) * - reads the plugin version from plugin.json - * - triggers the OpenCodeAdapter on session start * * The merged config is exposed in-memory via * process.env.OPENCODE_PROJECT_CONFIG for downstream consumer plugins. @@ -14,15 +15,12 @@ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, resolve } from 'node:path' -import type { Plugin } from '@opencode-ai/plugin' -import { OpenCodeAdapter, type Logger } from '@techdivision/lib-ts-config-sync' +import type { Plugin, PluginInput } from '@opencode-ai/plugin' +import { OpenCodeAdapter, type Logger, type SyncResult } from '@techdivision/lib-ts-config-sync' import { createPluginLogger } from './utils/logger.js' const __dirname = dirname(fileURLToPath(import.meta.url)) -/** - * Read this plugin's version from plugin.json. Falls back to '0.0.0'. - */ function readPluginVersion(): string { try { const pluginJsonPath = resolve(__dirname, '..', 'plugin.json') @@ -33,10 +31,22 @@ function readPluginVersion(): string { } } -export const ConfigPlugin: Plugin = async (input) => { - const pluginLogger = createPluginLogger(input.client, 'config') +async function showSyncToast(client: PluginInput['client'], result: SyncResult): Promise { + const message = result.remoteApplied + ? `Config synced (${result.message.replace('Config synced (', '').replace(')', '')})` + : 'Config: using local settings' + try { + await client.tui.showToast({ + body: { message, variant: 'info', duration: 8000 }, + }) + } catch { + // Toast API unavailable — not critical. + } +} + +export const plugin: Plugin = async ({ client }: PluginInput) => { + const pluginLogger = createPluginLogger() - // Adapt the OpenCode SDK logger to the library's Logger interface. const logger: Logger = { debug: (message, context) => pluginLogger.debug(message, context), info: (message, context) => pluginLogger.info(message, context), @@ -49,13 +59,23 @@ export const ConfigPlugin: Plugin = async (input) => { logger, }) - return { - event: async ({ event }) => { - if (event.type === 'session.created') { - await adapter.run() + // Run immediately at plugin load — before any session event. + let syncResult: SyncResult | null = null + let toastShown = false + + adapter.run().then((result) => { + syncResult = result + setTimeout(() => { + if (!toastShown) { + toastShown = true + showSyncToast(client, result) } - }, - } + }, 3000) + }).catch((err) => { + pluginLogger.error('Config sync failed during init', { error: String(err) }) + }) + + return {} } -export default ConfigPlugin +export default plugin diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 000fdd6..a38b03a 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -2,10 +2,39 @@ * Plugin Logger Factory * * Creates a logger that writes through the OpenCode SDK client - * (client.app.log). NEVER use console.log/console.error — they interfere - * with the OpenCode TUI. + * (client.app.log) AND to a dedicated file log at + * ~/.local/share/opencode/log/config.log — same directory and format as + * opencode.log, auto-linker.log, and safety-net.log. + * + * NEVER use console.log/console.error — they interfere with the OpenCode TUI. */ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +const LOG_DIR = path.join(os.homedir(), '.local', 'share', 'opencode', 'log') +const LOG_FILE = path.join(LOG_DIR, 'config.log') + +function writeToFile( + level: string, + message: string, + extra?: Record +): void { + try { + fs.mkdirSync(LOG_DIR, { recursive: true }) + const fields = extra + ? Object.entries(extra) + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join(' ') + : '' + const line = `timestamp=${new Date().toISOString()} level=${level} service=opencode-plugin/config message=${JSON.stringify(message)}${fields ? ' ' + fields : ''}\n` + fs.appendFileSync(LOG_FILE, line) + } catch { + // Logging must never crash the plugin. + } +} + export interface PluginLogger { debug(msg: string, extra?: Record): void info(msg: string, extra?: Record): void @@ -13,40 +42,11 @@ export interface PluginLogger { error(msg: string, extra?: Record): void } -/** - * Minimal structural view of the OpenCode client's logging surface. - * The real client exposes `client.app.log({ body: {...} })`. - */ -export interface LogClient { - app: { - log(options: { - body: { - service: string - level: 'debug' | 'info' | 'warn' | 'error' - message: string - extra?: Record - } - }): unknown - } -} - -export function createPluginLogger(client: LogClient, service: string): PluginLogger { - function log( - level: 'debug' | 'info' | 'warn' | 'error', - message: string, - extra?: Record - ): void { - try { - client.app.log({ body: { service, level, message, extra } }) - } catch { - // Never let logging break the plugin. - } - } - +export function createPluginLogger(): PluginLogger { return { - debug: (msg, extra) => log('debug', msg, extra), - info: (msg, extra) => log('info', msg, extra), - warn: (msg, extra) => log('warn', msg, extra), - error: (msg, extra) => log('error', msg, extra), + debug: (msg, extra) => writeToFile('DEBUG', msg, extra), + info: (msg, extra) => writeToFile('INFO', msg, extra), + warn: (msg, extra) => writeToFile('WARN', msg, extra), + error: (msg, extra) => writeToFile('ERROR', msg, extra), } }