diff --git a/AGENTS.md b/AGENTS.md index cd60924..bb14382 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,7 +128,7 @@ test/ # 主项目测试文件(Node.js 内置 test runner) - **入口**: `bin/dt-skill.js`。 - **构建**: `node ./scripts/build.mjs`,输出到 `dist/`。 - **测试**: Vitest,配置在 `vitest.config.ts`(测试 `src/**/*.test.ts`)。 -- **Node 版本要求**: `>=20`(与主项目的 `>=18` 不同)。 +- **Node 版本要求**: `>=18.17`(与主项目 Node 18 对齐;`npx dt-skill` / `test:src` 均可在 18 上运行)。 - **默认 Registry**: 内网部署 `http://172.16.100.225:7001`(无 flag/env 时开箱即用)。 - **本地开发覆盖**: ```bash diff --git a/app/service/skills.js b/app/service/skills.js index 9cfe16b..2b84ded 100644 --- a/app/service/skills.js +++ b/app/service/skills.js @@ -9,7 +9,11 @@ const { resolveSkillIdentifier, createUniqueSkillNames, } = require('../utils/skill-install-key'); -const { normalizeRelativePath: normalizeRelativeFilePath } = require('../utils/skill-utils'); +const { + normalizeRelativePath: normalizeRelativeFilePath, + extractSkillMdDescription, + resolveMarketCardDescription, +} = require('../utils/skill-utils'); const GitHubStarsClient = require('../utils/github-stars'); const CommandRunner = require('../utils/command-runner'); @@ -820,14 +824,6 @@ class SkillsService extends Service { return ''; } - extractDescription(content) { - const stripped = content - .split('\n') - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#') && !line.startsWith('---')); - return stripped[0] || ''; - } - parseFrontmatter(content) { const result = {}; const text = String(content || ''); @@ -1435,13 +1431,11 @@ class SkillsService extends Service { const content = fs.readFileSync(skillFilePath, 'utf8'); const stat = fs.statSync(skillFilePath); const frontmatter = this.parseFrontmatter(content); - const body = frontmatter.__body || content; const name = String(frontmatter.name || path.basename(skillDir)).trim() || path.basename(skillDir); - const description = - String(frontmatter.description || this.extractDescription(body)).trim() || - this.extractDescription(content); + // Same helper as registry publish / CLI default card summary. + const description = extractSkillMdDescription(content); const version = String(frontmatter.version || '').trim(); const allowedTools = this.parseArrayLike( frontmatter['allowed-tools'] || frontmatter.allowedTools || frontmatter.allowed_tools @@ -1917,6 +1911,8 @@ class SkillsService extends Service { transaction, }); + // Same sticky card rules as registry publish (CLI): explicit wins; else keep / backfill. + const hasDescription = Object.prototype.hasOwnProperty.call(params, 'description'); const payload = { name, category, @@ -1926,6 +1922,14 @@ class SkillsService extends Service { if (hasContributor) { payload.contributor = contributor || null; } + if (hasDescription) { + payload.description = resolveMarketCardDescription({ + hasDescription: true, + description: params.description, + currentDescription: itemRow.description, + fromSkillMd: '', + }); + } if (!hasZipUpload) { await itemRow.update(payload, { transaction }); @@ -1967,7 +1971,12 @@ class SkillsService extends Service { await itemRow.update( { ...payload, - description: nextRecord.description, + description: resolveMarketCardDescription({ + hasDescription, + description: params.description, + currentDescription: itemRow.description, + fromSkillMd: nextRecord.description, + }), allowed_tools: JSON.stringify(nextRecord.allowedTools || []), updated_at_remote: nextRecord.updatedAt, source_repo: nextRecord.sourceRepo, @@ -2164,11 +2173,32 @@ class SkillsService extends Service { record.name, usedSlugs ); + const globalExisting = await SkillsItem.findOne({ + where: { slug }, + transaction, + }); + + if ( + globalExisting && + globalExisting.is_delete === 0 && + globalExisting.name !== record.name + ) { + this.ctx.throw(400, 'slug 已存在'); + } + + const targetRow = globalExisting || oldRowMap.get(slug); + // Web zip re-import: sticky market card (same as CLI registry re-publish). + const description = resolveMarketCardDescription({ + hasDescription: false, + description: '', + currentDescription: targetRow ? targetRow.description : '', + fromSkillMd: record.description, + }); const payload = { source_id: sourceId, slug, name: record.name, - description: record.description, + description, category: record.category, version: record.version || '', tags: JSON.stringify(record.tags || []), @@ -2185,20 +2215,6 @@ class SkillsService extends Service { is_package: 0, parent_slug: parentSlug || null, }; - const globalExisting = await SkillsItem.findOne({ - where: { slug }, - transaction, - }); - - if ( - globalExisting && - globalExisting.is_delete === 0 && - globalExisting.name !== record.name - ) { - this.ctx.throw(400, 'slug 已存在'); - } - - const targetRow = globalExisting || oldRowMap.get(slug); let itemRow; if (targetRow) { itemRow = targetRow; diff --git a/app/service/skillsRegistry.js b/app/service/skillsRegistry.js index a2f25f8..98dbf92 100644 --- a/app/service/skillsRegistry.js +++ b/app/service/skillsRegistry.js @@ -402,11 +402,37 @@ class SkillsRegistryService extends Service { }); } - /** Normalize multipart/in-memory uploads into stored-file shape. Requires SKILL.md. */ - normalizePublishFiles(files) { + /** + * Normalize multipart/in-memory uploads into stored-file shape. Requires SKILL.md. + * @param {Array} files multipart or in-memory file objects + * @param {{ filePaths?: string[] }} [options] + * filePaths: optional parallel list of skill-relative paths (same order as files). + * Multipart Content-Disposition filenames often strip directories (RFC 7578), + * so clients should send explicit paths when nesting folders (e.g. agents/openai.yaml). + */ + normalizePublishFiles(files, options = {}) { + // Present but wrong type → hard fail (do not silently flatten nested paths). + if (options.filePaths != null && !Array.isArray(options.filePaths)) { + this.ctx.throw(400, 'filePaths 必须是字符串数组'); + } + const filePaths = Array.isArray(options.filePaths) ? options.filePaths : null; + if (filePaths && filePaths.length > 0 && filePaths.length !== files.length) { + this.ctx.throw( + 400, + `filePaths 数量 (${filePaths.length}) 与上传文件数量 (${files.length}) 不一致` + ); + } + const processedFiles = []; - for (const file of files) { - const originalName = file.filename || path.basename(file.filepath || ''); + for (let i = 0; i < files.length; i += 1) { + const file = files[i]; + // Prefer explicit path map; then multipart filename; then basename of temp filepath. + const declaredPath = + filePaths && filePaths[i] != null && String(filePaths[i]).trim() + ? String(filePaths[i]).trim() + : null; + const originalName = + declaredPath || file.filename || path.basename(file.filepath || ''); const relPath = skillUtils.normalizeRelativePath(originalName); if (!relPath) { this.ctx.throw(400, `非法文件路径: ${originalName}`); @@ -433,18 +459,19 @@ class SkillsRegistryService extends Service { this.ctx.throw(400, `上传文件不存在: ${originalName}`); } processedFiles.push({ - filename: originalName, + filename: path.basename(relPath), relPath, content, isBinary, }); } - const skillMdFile = processedFiles.find( - (f) => f.filename && f.filename.toLowerCase().endsWith('skill.md') - ); + const skillMdFile = processedFiles.find((f) => { + const p = String(f.relPath || f.filename || '').toLowerCase(); + return p === 'skill.md' || p.endsWith('/skill.md'); + }); if (!skillMdFile) { - const uploadedNames = processedFiles.map((f) => f.filename).join(', '); + const uploadedNames = processedFiles.map((f) => f.relPath || f.filename).join(', '); this.ctx.throw(400, `上传内容必须包含 SKILL.md。已上传: ${uploadedNames}`); } @@ -457,12 +484,26 @@ class SkillsRegistryService extends Service { // make the no-op decision against a non-transactional snapshot. const existingFingerprint = await this.computeSkillFingerprint(skill.id, transaction); if (!existingFingerprint || existingFingerprint !== incomingFingerprint) return null; - // Content unchanged: still apply optional metadata (e.g. contributor) without re-storing files. + // Content unchanged: optional metadata only (no file rewrite). + const patch = {}; if (meta.hasContributor) { - await skill.update( - { contributor: meta.contributor || null }, - transaction ? { transaction } : undefined - ); + patch.contributor = meta.contributor || null; + } + const nextDescription = skillUtils.resolveMarketCardDescription({ + hasDescription: Boolean(meta.hasDescription), + description: meta.description, + currentDescription: skill.description, + fromSkillMd: meta.fromSkillMd, + }); + const currentDesc = String(skill.description || '').trim(); + // Always apply explicit override (incl. clear to ""); else only when card changes (e.g. empty backfill). + if (meta.hasDescription || nextDescription !== currentDesc) { + if (meta.hasDescription || nextDescription) { + patch.description = nextDescription; + } + } + if (Object.keys(patch).length > 0) { + await skill.update(patch, transaction ? { transaction } : undefined); } return { ok: true, @@ -509,13 +550,18 @@ class SkillsRegistryService extends Service { const category = this.resolvePublishCategory(payload.category); const hasContributor = Object.prototype.hasOwnProperty.call(payload, 'contributor'); const contributor = hasContributor ? this.validateContributor(payload.contributor) : ''; + // description: present (incl. "") = market override; omit = SKILL.md default / keep card. + const hasDescription = Object.prototype.hasOwnProperty.call(payload, 'description'); if (!SKILL_SLUG_PATTERN.test(String(slug || ''))) { this.ctx.throw(400, 'slug 格式无效'); } const parsedTags = Array.isArray(tags) ? tags : []; - const { processedFiles, skillMdFile } = this.normalizePublishFiles(files); + const { processedFiles, skillMdFile } = this.normalizePublishFiles(files, { + filePaths: payload.filePaths, + }); + const fromSkillMd = skillUtils.extractSkillMdDescription(skillMdFile.content || ''); const incomingFingerprint = this.computeIncomingFingerprint(processedFiles); return await this.app.model.transaction(async (t) => { @@ -529,7 +575,18 @@ class SkillsRegistryService extends Service { transaction: t, }); + // Exact slug first; if missing, resolve installKey / alias so overwrite + // does not create a second skill with a different primary slug. let skill = await SkillsItem.findOne({ where: { slug }, transaction: t }); + if (!skill) { + const aliased = await this._resolveSlug(slug); + if (aliased && aliased.slug && aliased.slug !== slug) { + skill = await SkillsItem.findOne({ + where: { slug: aliased.slug }, + transaction: t, + }); + } + } const noop = await this.tryPublishUnchanged( skill, @@ -538,6 +595,9 @@ class SkillsRegistryService extends Service { { hasContributor, contributor, + hasDescription, + description: payload.description, + fromSkillMd, }, t ); @@ -546,12 +606,17 @@ class SkillsRegistryService extends Service { if (skill) { const updatePayload = { name: displayName, - description: payload.description || '', version, tags: JSON.stringify(parsedTags), skill_md: skillMdFile.content || '', is_delete: 0, source_id: source.id, + description: skillUtils.resolveMarketCardDescription({ + hasDescription, + description: payload.description, + currentDescription: skill.description, + fromSkillMd, + }), }; // Explicit preserve: do not rely on partial-update omitting the field. if (category) { @@ -568,7 +633,12 @@ class SkillsRegistryService extends Service { source_id: source.id, slug, name: displayName, - description: payload.description || '', + description: skillUtils.resolveMarketCardDescription({ + hasDescription, + description: payload.description, + currentDescription: '', + fromSkillMd, + }), version, tags: JSON.stringify(parsedTags), skill_md: skillMdFile.content || '', diff --git a/app/utils/skill-utils.js b/app/utils/skill-utils.js index 0bc46a9..6dd8564 100644 --- a/app/utils/skill-utils.js +++ b/app/utils/skill-utils.js @@ -16,4 +16,100 @@ function normalizeRelativePath(filePath) { return normalized; } -module.exports = { normalizeRelativePath }; +function unquoteYamlScalar(raw) { + const value = String(raw || '').trim(); + if ( + (value.startsWith('"') && value.endsWith('"') && value.length >= 2) || + (value.startsWith("'") && value.endsWith("'") && value.length >= 2) + ) { + return value.slice(1, -1); + } + return value; +} + +/** First non-empty body line that is not a markdown heading. */ +function extractBodySummary(content) { + const stripped = String(content || '') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#') && !line.startsWith('---')); + return stripped[0] || ''; +} + +/** + * Card summary from SKILL.md: frontmatter `description:` then first body line. + * Shared by registry publish and Web zip import. + */ +function extractSkillMdDescription(skillMdContent) { + const text = String(skillMdContent || ''); + const normalized = text.replace(/\r\n/g, '\n'); + let body = normalized; + let frontmatterDescription = ''; + + if (normalized.startsWith('---\n')) { + const endMarkerIndex = normalized.indexOf('\n---\n', 4); + if (endMarkerIndex !== -1) { + const frontmatterText = normalized.slice(4, endMarkerIndex); + body = normalized.slice(endMarkerIndex + 5); + const lines = frontmatterText.split('\n'); + for (let i = 0; i < lines.length; i += 1) { + const keyMatch = lines[i].match(/^description:\s*(.*)$/i); + if (!keyMatch) continue; + const rest = keyMatch[1].trim(); + // Block scalar: description: | / > then indented lines + if (rest === '|' || rest === '>' || rest === '|-' || rest === '>-') { + const collected = []; + for (let j = i + 1; j < lines.length; j += 1) { + const cont = lines[j]; + if (/^\S/.test(cont) && cont.includes(':')) break; + if (/^\s+\S/.test(cont) || cont.trim() === '') { + collected.push(cont.replace(/^\s+/, '')); + } else { + break; + } + } + frontmatterDescription = collected.join(' ').replace(/\s+/g, ' ').trim(); + } else if (rest) { + frontmatterDescription = unquoteYamlScalar(rest); + } else { + // description:\n indented multi-line without |/> + const collected = []; + for (let j = i + 1; j < lines.length; j += 1) { + const cont = lines[j]; + if (/^\S/.test(cont)) break; + if (cont.trim()) collected.push(cont.trim()); + } + frontmatterDescription = collected.join(' ').replace(/\s+/g, ' ').trim(); + } + break; + } + } + } + + const fromFm = String(frontmatterDescription || '').trim(); + if (fromFm) return fromFm; + return extractBodySummary(body) || extractBodySummary(normalized); +} + +/** + * Market card description (CLI registry + Web zip/import/update). + * Explicit override wins; else keep non-empty card; else SKILL.md default. + * + * @param {{ hasDescription?: boolean, description?: string, currentDescription?: string, fromSkillMd?: string }} opts + * @returns {string} + */ +function resolveMarketCardDescription(opts = {}) { + if (opts.hasDescription) { + return String(opts.description || '').trim(); + } + const current = String(opts.currentDescription || '').trim(); + if (current) return current; + return String(opts.fromSkillMd || '').trim(); +} + +module.exports = { + normalizeRelativePath, + extractSkillMdDescription, + extractBodySummary, + resolveMarketCardDescription, +}; diff --git a/app/web/pages/skills/detail/SkillDetailContent.tsx b/app/web/pages/skills/detail/SkillDetailContent.tsx index 7e52f1a..ad78eda 100644 --- a/app/web/pages/skills/detail/SkillDetailContent.tsx +++ b/app/web/pages/skills/detail/SkillDetailContent.tsx @@ -81,9 +81,7 @@ const SkillDetailContent: React.FC = ({ const agentTerminalCommand = isInstallable ? skillInstallCommand : downloadCommand; const heroSummary = useMemo(() => { const rawText = (detail?.description || '').replace(/\s+/g, ' ').trim(); - if (!rawText) return '暂无描述'; - const sentence = rawText.split(/(?<=[.!?。!?])/)[0]?.trim() || rawText; - return sentence; + return rawText || '暂无描述'; }, [detail?.description]); const handleSelectFile = (nextPath: string) => { diff --git a/app/web/pages/skills/detail/style.scss b/app/web/pages/skills/detail/style.scss index 54ef490..0c14043 100644 --- a/app/web/pages/skills/detail/style.scss +++ b/app/web/pages/skills/detail/style.scss @@ -307,15 +307,12 @@ } } .hero-description { - max-width: 760px; margin-bottom: 0; color: #566166; font-size: 14px; line-height: 24px; - display: -webkit-box; - overflow: hidden; - -webkit-line-clamp: 4; - -webkit-box-orient: vertical; + // Use full hero-copy width; do not force early wrap with max-width / line-clamp. + word-break: break-word; } .hero-actions { display: flex; diff --git a/dt-skill/README.md b/dt-skill/README.md index 2188dfa..65cc609 100644 --- a/dt-skill/README.md +++ b/dt-skill/README.md @@ -4,6 +4,8 @@ dt-skill CLI — install, update, search, and publish agent skills plus OpenClaw ## Install +Requires **Node.js >= 18.17** (works on Node 18 used by Doraemon, and Node 20+). + ```bash # Global install npm install -g dt-skill diff --git a/dt-skill/package.json b/dt-skill/package.json index 5d6683d..305a197 100644 --- a/dt-skill/package.json +++ b/dt-skill/package.json @@ -1,6 +1,6 @@ { "name": "dt-skill", - "version": "0.18.5", + "version": "0.18.6", "description": "dt-skill CLI — install, update, search, and publish agent skills.", "homepage": "https://github.com/DTStack/doraemon/tree/master/dt-skill", "bugs": { @@ -36,28 +36,28 @@ "verify:build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@clack/prompts": "1.4.0", + "@clack/prompts": "1.0.0", "adm-zip": "^0.5.17", "arktype": "2.2.0", - "commander": "14.0.3", + "commander": "13.1.0", "fflate": "0.8.2", "ignore": "7.0.5", "json5": "2.2.3", "mime": "4.1.0", - "ora": "9.4.0", - "p-retry": "8.0.0", + "ora": "8.2.0", + "p-retry": "6.2.1", "picocolors": "^1.1.1", "semver": "7.8.0", - "undici": "7.25.0" + "undici": "6.21.3" }, "devDependencies": { "@types/adm-zip": "^0.5.8", "@types/node": "25.7.0", "@types/semver": "^7.7.1", "typescript": "6.0.3", - "vitest": "^4.1.7" + "vitest": "3.2.4" }, "engines": { - "node": ">=20" + "node": ">=18.17" } } diff --git a/dt-skill/pnpm-lock.yaml b/dt-skill/pnpm-lock.yaml index 489b559..6e45d1d 100644 --- a/dt-skill/pnpm-lock.yaml +++ b/dt-skill/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@clack/prompts': - specifier: 1.4.0 - version: 1.4.0 + specifier: 1.0.0 + version: 1.0.0 adm-zip: specifier: ^0.5.17 version: 0.5.17 @@ -18,8 +18,8 @@ importers: specifier: 2.2.0 version: 2.2.0 commander: - specifier: 14.0.3 - version: 14.0.3 + specifier: 13.1.0 + version: 13.1.0 fflate: specifier: 0.8.2 version: 0.8.2 @@ -33,11 +33,11 @@ importers: specifier: 4.1.0 version: 4.1.0 ora: - specifier: 9.4.0 - version: 9.4.0 + specifier: 8.2.0 + version: 8.2.0 p-retry: - specifier: 8.0.0 - version: 8.0.0 + specifier: 6.2.1 + version: 6.2.1 picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -45,8 +45,8 @@ importers: specifier: 7.8.0 version: 7.8.0 undici: - specifier: 7.25.0 - version: 7.25.0 + specifier: 6.21.3 + version: 6.21.3 devDependencies: '@types/adm-zip': specifier: ^0.5.8 @@ -61,8 +61,8 @@ importers: specifier: 6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.7 - version: 4.1.7(@types/node@25.7.0)(vite@8.0.14(@types/node@25.7.0)) + specifier: 3.2.4 + version: 3.2.4(@types/node@25.7.0)(lightningcss@1.32.0) packages: @@ -72,132 +72,295 @@ packages: '@ark/util@0.56.0': resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} - '@clack/core@1.3.1': - resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} - engines: {node: '>= 20.12.0'} + '@clack/core@1.0.0': + resolution: {integrity: sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ==} - '@clack/prompts@1.4.0': - resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} - engines: {node: '>= 20.12.0'} + '@clack/prompts@1.0.0': + resolution: {integrity: sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A==} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@rollup/rollup-android-arm-eabi@4.62.3': + resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} + cpu: [arm] + os: [android] - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-android-arm64@4.62.3': + resolution: {integrity: sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-darwin-arm64@4.62.3': + resolution: {integrity: sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-darwin-x64@4.62.3': + resolution: {integrity: sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-freebsd-arm64@4.62.3': + resolution: {integrity: sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.3': + resolution: {integrity: sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + resolution: {integrity: sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + resolution: {integrity: sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.3': + resolution: {integrity: sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-arm64-musl@4.62.3': + resolution: {integrity: sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-loong64-gnu@4.62.3': + resolution: {integrity: sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.3': + resolution: {integrity: sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + resolution: {integrity: sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==} cpu: [ppc64] os: [linux] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-ppc64-musl@4.62.3': + resolution: {integrity: sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + resolution: {integrity: sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.3': + resolution: {integrity: sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.3': + resolution: {integrity: sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==} cpu: [s390x] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-x64-gnu@4.62.3': + resolution: {integrity: sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-linux-x64-musl@4.62.3': + resolution: {integrity: sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-openbsd-x64@4.62.3': + resolution: {integrity: sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.3': + resolution: {integrity: sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} - engines: {node: ^20.19.0 || >=22.12.0} + '@rollup/rollup-win32-arm64-msvc@4.62.3': + resolution: {integrity: sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] + '@rollup/rollup-win32-ia32-msvc@4.62.3': + resolution: {integrity: sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==} + cpu: [ia32] os: [win32] - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@rollup/rollup-win32-x64-gnu@4.62.3': + resolution: {integrity: sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==} + cpu: [x64] + os: [win32] - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@rollup/rollup-win32-x64-msvc@4.62.3': + resolution: {integrity: sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==} + cpu: [x64] + os: [win32] '@types/adm-zip@0.5.8': resolution: {integrity: sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==} @@ -214,37 +377,43 @@ packages: '@types/node@25.7.0': resolution: {integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==} + '@types/retry@0.12.2': + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@vitest/expect@4.1.7': - resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - '@vitest/mocker@4.1.7': - resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.1.7': - resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - '@vitest/runner@4.1.7': - resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} - '@vitest/snapshot@4.1.7': - resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - '@vitest/spy@4.1.7': - resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - '@vitest/utils@4.1.7': - resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} adm-zip@0.5.17: resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} @@ -264,35 +433,61 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-spinners@3.4.0: - resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} - engines: {node: '>=18.20'} + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -301,15 +496,6 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - - fast-wrap-ansi@0.2.2: - resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -343,10 +529,17 @@ packages: resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} engines: {node: '>=16'} + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -422,10 +615,13 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - log-symbols@7.0.1: - resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -438,29 +634,33 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - ora@9.4.0: - resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} - engines: {node: '>=20'} + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} - p-retry@8.0.0: - resolution: {integrity: sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==} - engines: {node: '>=22'} + p-retry@6.2.1: + resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} + engines: {node: '>=16.17'} pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -476,9 +676,13 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + rollup@4.62.3: + resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true semver@7.8.0: @@ -503,38 +707,45 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - stdin-discarder@0.3.2: - resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} - string-width@8.2.1: - resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} - engines: {node: '>=20'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} strip-ansi@7.2.0: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} - engines: {node: '>=18'} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} @@ -544,20 +755,24 @@ packages: undici-types@7.21.0: resolution: {integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==} - undici@7.25.0: - resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} - engines: {node: '>=20.18.1'} + undici@6.21.3: + resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + engines: {node: '>=18.17'} - vite@8.0.14: - resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.18 - esbuild: ^0.27.0 || ^0.28.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' @@ -568,14 +783,12 @@ packages: peerDependenciesMeta: '@types/node': optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true jiti: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true sass-embedded: @@ -591,39 +804,26 @@ packages: yaml: optional: true - vitest@4.1.7: - resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.7 - '@vitest/browser-preview': 4.1.7 - '@vitest/browser-webdriverio': 4.1.7 - '@vitest/coverage-istanbul': 4.1.7 - '@vitest/coverage-v8': 4.1.7 - '@vitest/ui': 4.1.7 + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true - '@opentelemetry/api': + '@types/debug': optional: true '@types/node': optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': + '@vitest/browser': optional: true '@vitest/ui': optional: true @@ -637,10 +837,6 @@ packages: engines: {node: '>=8'} hasBin: true - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} - snapshots: '@ark/schema@0.56.0': @@ -649,101 +845,170 @@ snapshots: '@ark/util@0.56.0': {} - '@clack/core@1.3.1': + '@clack/core@1.0.0': dependencies: - fast-wrap-ansi: 0.2.2 + picocolors: 1.1.1 sisteransi: 1.0.5 - '@clack/prompts@1.4.0': + '@clack/prompts@1.0.0': dependencies: - '@clack/core': 1.3.1 - fast-string-width: 3.0.2 - fast-wrap-ansi: 0.2.2 + '@clack/core': 1.0.0 + picocolors: 1.1.1 sisteransi: 1.0.5 - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 + '@esbuild/aix-ppc64@0.28.1': optional: true - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 + '@esbuild/android-arm64@0.28.1': optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': optional: true '@jridgewell/sourcemap-codec@1.5.5': {} - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 + '@rollup/rollup-android-arm-eabi@4.62.3': optional: true - '@oxc-project/types@0.132.0': {} + '@rollup/rollup-android-arm64@4.62.3': + optional: true - '@rolldown/binding-android-arm64@1.0.2': + '@rollup/rollup-darwin-arm64@4.62.3': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': + '@rollup/rollup-darwin-x64@4.62.3': optional: true - '@rolldown/binding-darwin-x64@1.0.2': + '@rollup/rollup-freebsd-arm64@4.62.3': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': + '@rollup/rollup-freebsd-x64@4.62.3': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': + '@rollup/rollup-linux-arm-musleabihf@4.62.3': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': + '@rollup/rollup-linux-arm64-gnu@4.62.3': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': + '@rollup/rollup-linux-arm64-musl@4.62.3': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': + '@rollup/rollup-linux-loong64-gnu@4.62.3': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': + '@rollup/rollup-linux-loong64-musl@4.62.3': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': + '@rollup/rollup-linux-ppc64-gnu@4.62.3': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': + '@rollup/rollup-linux-ppc64-musl@4.62.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@rollup/rollup-linux-riscv64-gnu@4.62.3': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': + '@rollup/rollup-linux-riscv64-musl@4.62.3': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': + '@rollup/rollup-linux-s390x-gnu@4.62.3': optional: true - '@rolldown/pluginutils@1.0.1': {} + '@rollup/rollup-linux-x64-gnu@4.62.3': + optional: true - '@standard-schema/spec@1.1.0': {} + '@rollup/rollup-linux-x64-musl@4.62.3': + optional: true - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 + '@rollup/rollup-openbsd-x64@4.62.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true '@types/adm-zip@0.5.8': @@ -763,48 +1028,55 @@ snapshots: dependencies: undici-types: 7.21.0 + '@types/retry@0.12.2': {} + '@types/semver@7.7.1': {} - '@vitest/expect@4.1.7': + '@vitest/expect@3.2.4': dependencies: - '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - chai: 6.2.2 - tinyrainbow: 3.1.0 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 - '@vitest/mocker@4.1.7(vite@8.0.14(@types/node@25.7.0))': + '@vitest/mocker@3.2.4(vite@7.3.6(@types/node@25.7.0)(lightningcss@1.32.0))': dependencies: - '@vitest/spy': 4.1.7 + '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.14(@types/node@25.7.0) + vite: 7.3.6(@types/node@25.7.0)(lightningcss@1.32.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 - '@vitest/pretty-format@4.1.7': + '@vitest/pretty-format@3.2.7': dependencies: - tinyrainbow: 3.1.0 + tinyrainbow: 2.0.0 - '@vitest/runner@4.1.7': + '@vitest/runner@3.2.4': dependencies: - '@vitest/utils': 4.1.7 + '@vitest/utils': 3.2.4 pathe: 2.0.3 + strip-literal: 3.1.0 - '@vitest/snapshot@4.1.7': + '@vitest/snapshot@3.2.4': dependencies: - '@vitest/pretty-format': 4.1.7 - '@vitest/utils': 4.1.7 + '@vitest/pretty-format': 3.2.4 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.7': {} + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 - '@vitest/utils@4.1.7': + '@vitest/utils@3.2.4': dependencies: - '@vitest/pretty-format': 4.1.7 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 adm-zip@0.5.17: {} @@ -822,39 +1094,75 @@ snapshots: assertion-error@2.0.1: {} - chai@6.2.2: {} + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 chalk@5.6.2: {} + check-error@2.1.3: {} + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 - cli-spinners@3.4.0: {} + cli-spinners@2.9.2: {} - commander@14.0.3: {} + commander@13.1.0: {} - convert-source-map@2.0.0: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 - detect-libc@2.1.2: {} + deep-eql@5.0.2: {} - es-module-lexer@2.1.0: {} + detect-libc@2.1.2: + optional: true - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 + emoji-regex@10.6.0: {} - expect-type@1.3.0: {} + es-module-lexer@1.7.0: {} - fast-string-truncated-width@3.0.3: {} + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 - fast-string-width@3.0.2: + estree-walker@3.0.3: dependencies: - fast-string-truncated-width: 3.0.3 + '@types/estree': 1.0.9 - fast-wrap-ansi@0.2.2: - dependencies: - fast-string-width: 3.0.2 + expect-type@1.3.0: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -873,8 +1181,12 @@ snapshots: is-network-error@1.3.2: {} + is-unicode-supported@1.3.0: {} + is-unicode-supported@2.1.0: {} + js-tokens@9.0.1: {} + json5@2.2.3: {} lightningcss-android-arm64@1.32.0: @@ -925,11 +1237,14 @@ snapshots: lightningcss-linux-x64-musl: 1.32.0 lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + optional: true - log-symbols@7.0.1: + log-symbols@6.0.0: dependencies: - is-unicode-supported: 2.1.0 - yoctocolors: 2.1.2 + chalk: 5.6.2 + is-unicode-supported: 1.3.0 + + loupe@3.2.1: {} magic-string@0.30.21: dependencies: @@ -939,31 +1254,36 @@ snapshots: mimic-function@5.0.1: {} - nanoid@3.3.12: {} + ms@2.1.3: {} - obug@2.1.1: {} + nanoid@3.3.12: {} onetime@7.0.0: dependencies: mimic-function: 5.0.1 - ora@9.4.0: + ora@8.2.0: dependencies: chalk: 5.6.2 cli-cursor: 5.0.0 - cli-spinners: 3.4.0 + cli-spinners: 2.9.2 is-interactive: 2.0.0 is-unicode-supported: 2.1.0 - log-symbols: 7.0.1 - stdin-discarder: 0.3.2 - string-width: 8.2.1 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.2.0 - p-retry@8.0.0: + p-retry@6.2.1: dependencies: + '@types/retry': 0.12.2 is-network-error: 1.3.2 + retry: 0.13.1 pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} @@ -979,26 +1299,38 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - rolldown@1.0.2: + retry@0.13.1: {} + + rollup@4.62.3: dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 + '@types/estree': 1.0.9 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 + '@rollup/rollup-android-arm-eabi': 4.62.3 + '@rollup/rollup-android-arm64': 4.62.3 + '@rollup/rollup-darwin-arm64': 4.62.3 + '@rollup/rollup-darwin-x64': 4.62.3 + '@rollup/rollup-freebsd-arm64': 4.62.3 + '@rollup/rollup-freebsd-x64': 4.62.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.3 + '@rollup/rollup-linux-arm-musleabihf': 4.62.3 + '@rollup/rollup-linux-arm64-gnu': 4.62.3 + '@rollup/rollup-linux-arm64-musl': 4.62.3 + '@rollup/rollup-linux-loong64-gnu': 4.62.3 + '@rollup/rollup-linux-loong64-musl': 4.62.3 + '@rollup/rollup-linux-ppc64-gnu': 4.62.3 + '@rollup/rollup-linux-ppc64-musl': 4.62.3 + '@rollup/rollup-linux-riscv64-gnu': 4.62.3 + '@rollup/rollup-linux-riscv64-musl': 4.62.3 + '@rollup/rollup-linux-s390x-gnu': 4.62.3 + '@rollup/rollup-linux-x64-gnu': 4.62.3 + '@rollup/rollup-linux-x64-musl': 4.62.3 + '@rollup/rollup-openbsd-x64': 4.62.3 + '@rollup/rollup-openharmony-arm64': 4.62.3 + '@rollup/rollup-win32-arm64-msvc': 4.62.3 + '@rollup/rollup-win32-ia32-msvc': 4.62.3 + '@rollup/rollup-win32-x64-gnu': 4.62.3 + '@rollup/rollup-win32-x64-msvc': 4.62.3 + fsevents: 2.3.3 semver@7.8.0: {} @@ -1012,12 +1344,13 @@ snapshots: stackback@0.0.2: {} - std-env@4.1.0: {} + std-env@3.10.0: {} - stdin-discarder@0.3.2: {} + stdin-discarder@0.2.2: {} - string-width@8.2.1: + string-width@7.2.0: dependencies: + emoji-regex: 10.6.0 get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 @@ -1025,67 +1358,107 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + tinybench@2.9.0: {} - tinyexec@1.1.2: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinyrainbow@3.1.0: {} + tinypool@1.1.1: {} - tslib@2.8.1: - optional: true + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} typescript@6.0.3: {} undici-types@7.21.0: {} - undici@7.25.0: {} + undici@6.21.3: {} - vite@8.0.14(@types/node@25.7.0): + vite-node@3.2.4(@types/node@25.7.0)(lightningcss@1.32.0): dependencies: - lightningcss: 1.32.0 + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@25.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@25.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.15 - rolldown: 1.0.2 + rollup: 4.62.3 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.7.0 fsevents: 2.3.3 + lightningcss: 1.32.0 - vitest@4.1.7(@types/node@25.7.0)(vite@8.0.14(@types/node@25.7.0)): + vitest@3.2.4(@types/node@25.7.0)(lightningcss@1.32.0): dependencies: - '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@8.0.14(@types/node@25.7.0)) - '@vitest/pretty-format': 4.1.7 - '@vitest/runner': 4.1.7 - '@vitest/snapshot': 4.1.7 - '@vitest/spy': 4.1.7 - '@vitest/utils': 4.1.7 - es-module-lexer: 2.1.0 + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.6(@types/node@25.7.0)(lightningcss@1.32.0)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 expect-type: 1.3.0 magic-string: 0.30.21 - obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 4.1.0 + std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 1.1.2 + tinyexec: 0.3.2 tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 8.0.14(@types/node@25.7.0) + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@25.7.0)(lightningcss@1.32.0) + vite-node: 3.2.4(@types/node@25.7.0)(lightningcss@1.32.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.7.0 transitivePeerDependencies: + - jiti + - less + - lightningcss - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - - yoctocolors@2.1.2: {} diff --git a/dt-skill/src/cli.ts b/dt-skill/src/cli.ts index f6d58b3..b341917 100644 --- a/dt-skill/src/cli.ts +++ b/dt-skill/src/cli.ts @@ -311,6 +311,10 @@ registerCommand(program, ['publish']) .option('--tags ', 'Comma-separated tags', 'latest') .option('--all', 'Batch mode: upload all discovered skills without interactive selection') .option('--category ', 'Category (required on first publish in non-interactive mode)') + .option( + '--description ', + 'Optional market card summary (create defaults from SKILL.md; re-publish keeps card unless set)' + ) .option('--yes', 'Skip overwrite confirmation') .action(async (folder, options) => { const opts = await resolveGlobalOpts(); @@ -375,6 +379,10 @@ registerCommand(skill, ['skill', 'publish']) .option('--clawscan-note ', CLAWSCAN_NOTE_HELP) .option('--tags ', 'Comma-separated tags', 'latest') .option('--category ', 'Category (required on first publish in non-interactive mode)') + .option( + '--description ', + 'Optional market card summary (create defaults from SKILL.md; re-publish keeps card unless set)' + ) .option('--yes', 'Skip overwrite confirmation') .action(async (folder, options) => { const opts = await resolveGlobalOpts(); diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts index 3cae934..9378b26 100644 --- a/dt-skill/src/cli/commands/publish.test.ts +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -258,6 +258,7 @@ describe('cmdPublish', () => { ); expect(payload.acceptLicenseTerms).toBe(true); expect(payload.tags).toEqual(['latest']); + expect(payload.filePaths.sort()).toEqual(['SKILL.md', 'notes.md']); const files = publishForm.getAll('files') as Array; expect(files.map((file) => file.name ?? '').sort()).toEqual(['SKILL.md', 'notes.md']); } finally { @@ -265,6 +266,184 @@ describe('cmdPublish', () => { } }); + it('keeps remote slug when GET resolves installKey alias (no second skill)', async () => { + const workdir = await makeTmpWorkdir(); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + const folder = join(workdir, 'weekly-report'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# weekly\n', 'utf8'); + + httpMocks.apiRequest.mockResolvedValueOnce({ + skill: { + slug: 'upload-weekly-report-weekly-report-7bfcdc00-default-weekly-report', + displayName: 'weekly-report', + summary: 'old desc', + tags: [], + stats: {}, + createdAt: 1, + updatedAt: 1, + category: '其他', + fingerprint: 'remote-fp-different', + }, + latestVersion: null, + owner: null, + moderation: null, + }); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: '1', + versionId: 'v0.0.0', + fingerprint: 'local-fp', + unchanged: false, + }); + + await cmdPublish(makeOpts(workdir), 'weekly-report', { + category: '其他', + yes: true, + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.slug).toBe( + 'upload-weekly-report-weekly-report-7bfcdc00-default-weekly-report' + ); + expect(payload.slug).not.toBe('weekly-report'); + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining('keeping remote slug on overwrite') + ); + } finally { + logSpy.mockRestore(); + await rm(workdir, { recursive: true, force: true }); + } + }); + + it('sends nested filePaths so agents/openai.yaml is not flattened', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'nested-skill'); + await mkdir(join(folder, 'agents'), { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# nested\n', 'utf8'); + await writeFile(join(folder, 'agents', 'openai.yaml'), 'interface: {}\n', 'utf8'); + + httpMocks.apiRequest.mockRejectedValueOnce(new Error('not found')); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_n', + versionId: 'v0.0.0', + fingerprint: 'fpn', + unchanged: false, + }); + + await cmdPublish(makeOpts(workdir), 'nested-skill', { + category: '通用', + yes: true, + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.filePaths.sort()).toEqual(['SKILL.md', 'agents/openai.yaml']); + const files = publishForm.getAll('files') as Array; + // Multipart names are basenames; hierarchy lives in filePaths. + expect(files.map((file) => file.name ?? '').sort()).toEqual([ + 'SKILL.md', + 'openai.yaml', + ]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it('defaults displayName to folder basename (not Title Case)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'weekly-report'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# weekly\n', 'utf8'); + + httpMocks.apiRequest.mockRejectedValueOnce(new Error('not found')); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_wr', + versionId: 'v0.0.0', + fingerprint: 'fp1', + unchanged: false, + }); + + await cmdPublish(makeOpts(workdir), 'weekly-report', { + category: '其他', + yes: true, + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.slug).toBe('weekly-report'); + expect(payload.displayName).toBe('weekly-report'); + expect(payload.displayName).not.toBe('Weekly Report'); + expect(payload).not.toHaveProperty('description'); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it('includes description in payload only when --description is provided', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'with-desc'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# with desc\n', 'utf8'); + + httpMocks.apiRequest.mockRejectedValueOnce(new Error('not found')); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_d', + versionId: 'v0.0.0', + fingerprint: 'fpd', + unchanged: false, + }); + + await cmdPublish(makeOpts(workdir), 'with-desc', { + category: '通用', + yes: true, + description: ' 周报助手卡片描述 ', + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.description).toBe('周报助手卡片描述'); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it('发布时同时上传文本文件和二进制资源', async () => { const workdir = await makeTmpWorkdir(); try { @@ -288,11 +467,12 @@ describe('cmdPublish', () => { const publishCall = httpMocks.apiRequestForm.mock.calls[0]; const publishForm = (publishCall?.[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.filePaths.sort()).toEqual(['SKILL.md', 'assets/logo.png']); const files = publishForm.getAll('files') as Array; - expect(files.map((file) => file.name ?? '').sort()).toEqual([ - 'SKILL.md', - 'assets/logo.png', - ]); + expect(files.map((file) => file.name ?? '').sort()).toEqual(['SKILL.md', 'logo.png']); } finally { await rm(workdir, { recursive: true, force: true }); } diff --git a/dt-skill/src/cli/commands/publish.ts b/dt-skill/src/cli/commands/publish.ts index b5d91b3..2fe5385 100644 --- a/dt-skill/src/cli/commands/publish.ts +++ b/dt-skill/src/cli/commands/publish.ts @@ -16,7 +16,7 @@ import { searchMultiselect } from '../prompts/search-multiselect.js'; import { getRegistry } from '../registry.js'; import { findSkillFolders } from '../scanSkills.js'; import { SKILL_CATEGORY_OPTIONS, SKILL_CATEGORY_SET } from '../skillCategories.js'; -import { sanitizeSlug, titleCase } from '../slug.js'; +import { sanitizeSlug } from '../slug.js'; import type { GlobalOpts } from '../types.js'; import { createSpinner, @@ -64,6 +64,11 @@ export async function cmdPublish( migrateOwner?: boolean; all?: boolean; category?: string; + /** + * Optional market card summary. + * Omit: create uses SKILL.md; re-publish keeps existing card (empty → SKILL.md backfill). + */ + description?: string; yes?: boolean; } ) { @@ -91,8 +96,12 @@ export async function cmdPublish( const registry = await getRegistry(opts, { cache: true }); const contributor = resolveContributorForPublish(); - const slug = options.slug ?? sanitizeSlug(basename(folder)); - const displayName = options.name ?? titleCase(basename(folder)); + // Requested identity from folder / --slug. May be rewritten to remote slug if registry + // already has this skill under another primary key (installKey alias / legacy upload-*). + const requestedSlug = options.slug ?? sanitizeSlug(basename(folder)); + let slug = requestedSlug; + // Display name follows folder name as-is (no Title Case / 大驼峰转换). + const displayName = options.name?.trim() || basename(folder); const ownerHandle = options.owner?.trim().replace(/^@+/, ''); // Version is optional for authors; default is a compatibility placeholder. Change detection uses content hash. const version = options.version?.trim() || DEFAULT_PUBLISH_VERSION; @@ -116,20 +125,33 @@ export async function cmdPublish( if (!slug) fail('--slug required'); if (!displayName) fail('--name required'); + // Optional market override. Omit → server fills from SKILL.md on create / empty card only. + const descriptionOption = options.description; + const hasDescription = typeof descriptionOption === 'string'; + const description = hasDescription ? descriptionOption.trim() : undefined; + // Detect whether slug already exists on registry (first publish needs category). + // GET resolves installKey aliases — if remote slug differs, keep it (do not create a second skill). let existingCategory: string | null = null; let existingFingerprint: string | null = null; let skillExists = false; try { const existing = await apiRequest( registry, - { method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}` }, + { method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(requestedSlug)}` }, ApiV1SkillResponseSchema ); skillExists = Boolean(existing?.skill); if (existing?.skill?.category) existingCategory = String(existing.skill.category); // Canonical: skill.fingerprint only (single-slot current content) existingFingerprint = existing?.skill?.fingerprint ?? null; + const remoteSlug = String(existing?.skill?.slug || '').trim(); + if (skillExists && remoteSlug && remoteSlug !== requestedSlug) { + console.log( + `Remote skill already exists as "${remoteSlug}" (matched "${requestedSlug}"); keeping remote slug on overwrite.` + ); + slug = remoteSlug; + } } catch { skillExists = false; } @@ -185,6 +207,9 @@ export async function cmdPublish( spinner.start(`Publishing ${slug}`); } + // Explicit paths: multipart filename often strips directories (agents/foo → foo). + const filePaths = filesOnDisk.map((file) => file.relPath); + const form = new FormData(); form.set( 'payload', @@ -201,6 +226,8 @@ export async function cmdPublish( ...(category ? { category } : {}), ...(forkOf ? { forkOf } : {}), ...(contributor ? { contributor } : {}), + ...(hasDescription ? { description: description ?? '' } : {}), + filePaths, }) ); @@ -211,7 +238,8 @@ export async function cmdPublish( const blob = new Blob([Buffer.from(file.bytes)], { type: file.contentType ?? 'text/plain', }); - form.append('files', blob, file.relPath); + // Keep basename in multipart filename; real path is in payload.filePaths[i]. + form.append('files', blob, basename(file.relPath)); } spinner.text = `Publishing ${slug}`; diff --git a/dt-skill/src/cli/scanSkills.ts b/dt-skill/src/cli/scanSkills.ts index 471455d..75fa6d3 100644 --- a/dt-skill/src/cli/scanSkills.ts +++ b/dt-skill/src/cli/scanSkills.ts @@ -1,7 +1,7 @@ import { readdir, stat } from 'node:fs/promises'; import { basename, join, resolve } from 'node:path'; -import { sanitizeSlug, titleCase } from './slug.js'; +import { sanitizeSlug } from './slug.js'; export type SkillFolder = { folder: string; @@ -35,7 +35,8 @@ async function isSkillFolder(folder: string): Promise { const base = basename(folder); const slug = sanitizeSlug(base); if (!slug) return null; - const displayName = titleCase(base); + // Match single-skill publish: display name = folder basename, not Title Case. + const displayName = base; return { folder, slug, displayName }; } diff --git a/test/skill-utils-normalize-path.test.js b/test/skill-utils-normalize-path.test.js index eaec4c5..f45480b 100644 --- a/test/skill-utils-normalize-path.test.js +++ b/test/skill-utils-normalize-path.test.js @@ -1,7 +1,11 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const { normalizeRelativePath } = require('../app/utils/skill-utils'); +const { + normalizeRelativePath, + extractSkillMdDescription, + resolveMarketCardDescription, +} = require('../app/utils/skill-utils'); // This is the path-traversal guard that getSkillFileContent, buildSkillZip, // and the upload flow all rely on. If it drifts, all three defenses rot at @@ -27,3 +31,74 @@ test('normalizeRelativePath rejects traversal and accepts clean relative paths', test('normalizeRelativePath currently passes absolute paths through (known gap)', () => { assert.equal(normalizeRelativePath('/etc/passwd'), 'etc/passwd'); }); + +test('extractSkillMdDescription prefers frontmatter description', () => { + const md = `--- +name: zentao-api +description: 通过禅道 HTTP API 获取 Bug 详情。 +--- + +# 禅道 API 工具 + +正文不应优先于 frontmatter。 +`; + assert.equal(extractSkillMdDescription(md), '通过禅道 HTTP API 获取 Bug 详情。'); +}); + +test('extractSkillMdDescription falls back to first body line', () => { + const md = `# Title + +First useful sentence here. + +More body. +`; + assert.equal(extractSkillMdDescription(md), 'First useful sentence here.'); +}); + +test('extractSkillMdDescription handles quoted and block scalars', () => { + assert.equal( + extractSkillMdDescription('---\ndescription: "quoted value"\n---\n\n# x\n'), + 'quoted value' + ); + assert.equal( + extractSkillMdDescription('---\ndescription: |\n line one\n line two\n---\n\n# x\n'), + 'line one line two' + ); +}); + +test('resolveMarketCardDescription sticky keep / backfill / explicit', () => { + assert.equal( + resolveMarketCardDescription({ + hasDescription: false, + currentDescription: 'market card', + fromSkillMd: 'from package', + }), + 'market card' + ); + assert.equal( + resolveMarketCardDescription({ + hasDescription: false, + currentDescription: '', + fromSkillMd: 'from package', + }), + 'from package' + ); + assert.equal( + resolveMarketCardDescription({ + hasDescription: true, + description: ' override ', + currentDescription: 'market card', + fromSkillMd: 'from package', + }), + 'override' + ); + assert.equal( + resolveMarketCardDescription({ + hasDescription: true, + description: '', + currentDescription: 'market card', + fromSkillMd: 'from package', + }), + '' + ); +}); diff --git a/test/skills-registry-contract.test.js b/test/skills-registry-contract.test.js index 2a0b118..627844e 100644 --- a/test/skills-registry-contract.test.js +++ b/test/skills-registry-contract.test.js @@ -740,6 +740,455 @@ test('publishSkill re-publish without category keeps existing category', async ( assert.equal(mainUpdate.category, '安全'); }); +test('publishSkill updates installKey-aliased skill without creating new slug', async () => { + const service = Object.create(SkillsRegistryService.prototype); + const updatePayloads = []; + const createCalls = []; + const longSlug = 'upload-weekly-report-weekly-report-7bfcdc00-default-weekly-report'; + const skillRow = { + id: 91, + slug: longSlug, + name: 'weekly-report', + description: 'old market copy', + version: '0.0.0', + category: '其他', + is_delete: 0, + update: async (data) => { + updatePayloads.push(data); + Object.assign(skillRow, data); + }, + }; + service.app = createMockApp({ + SkillsItem: { + findOne: async (options) => { + const whereSlug = options?.where?.slug; + if (whereSlug === 'weekly-report') return null; + if (whereSlug === longSlug) return skillRow; + return null; + }, + create: async (data) => { + createCalls.push(data); + return { id: 999, ...data, update: async () => {} }; + }, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [ + { + file_path: 'SKILL.md', + content: '# old\n', + is_binary: 0, + }, + ], + create: async () => ({}), + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + service.ctx.service = { + skills: { + ensureSkillCache: async () => ({ + byInstallKey: new Map([ + ['weekly-report', { slug: longSlug, installKey: 'weekly-report' }], + ]), + }), + }, + }; + + const result = await service.publishSkill( + { slug: 'weekly-report', displayName: 'weekly-report' }, + [ + { + filename: 'SKILL.md', + content: '---\nname: weekly-report\ndescription: from frontmatter\n---\n\n# Body\n', + }, + ] + ); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, false); + assert.equal(createCalls.length, 0, 'must not create a second skill row'); + assert.ok(updatePayloads.length > 0, 'must update existing aliased skill'); + assert.equal(skillRow.slug, longSlug, 'remote primary slug must stay unchanged'); + // Content-changing publish without payload.description → keep existing market card + assert.equal(skillRow.description, 'old market copy'); +}); + +test('publishSkill uses payload.filePaths to preserve nested directories', async () => { + const service = Object.create(SkillsRegistryService.prototype); + const createdFiles = []; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => null, + create: async (data) => ({ + id: 90, + ...data, + update: async (fields) => Object.assign(data, fields), + }), + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [], + create: async (row) => { + createdFiles.push(row); + return row; + }, + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + // Multipart often delivers basename-only filenames; filePaths restores nesting. + const result = await service.publishSkill( + { + slug: 'nested-skill', + displayName: 'nested-skill', + filePaths: ['SKILL.md', 'agents/openai.yaml'], + }, + [ + { filename: 'SKILL.md', content: '# nested\n' }, + { filename: 'openai.yaml', content: 'interface: {}\n' }, + ] + ); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, false); + const paths = createdFiles.map((f) => f.file_path).sort(); + assert.deepEqual(paths, ['SKILL.md', 'agents/openai.yaml']); +}); + +test('publishSkill rejects filePaths length mismatch', async () => { + const service = Object.create(SkillsRegistryService.prototype); + service.app = createMockApp({ + SkillsItem: { findOne: async () => null, create: async () => ({ id: 1 }) }, + SkillsSource: { findOrCreate: async () => [{ id: 1 }] }, + SkillsFile: { findAll: async () => [], create: async () => ({}), update: async () => ({}) }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + await assert.rejects( + () => + service.publishSkill( + { + slug: 'bad-paths', + displayName: 'bad-paths', + filePaths: ['SKILL.md'], + }, + [ + { filename: 'SKILL.md', content: '# a\n' }, + { filename: 'extra.md', content: '# b\n' }, + ] + ), + (err) => err.status === 400 && /filePaths 数量/.test(err.message) + ); +}); + +test('publishSkill rejects non-array filePaths', async () => { + const service = Object.create(SkillsRegistryService.prototype); + service.app = createMockApp({ + SkillsItem: { findOne: async () => null, create: async () => ({ id: 1 }) }, + SkillsSource: { findOrCreate: async () => [{ id: 1 }] }, + SkillsFile: { findAll: async () => [], create: async () => ({}), update: async () => ({}) }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + await assert.rejects( + () => + service.publishSkill( + { + slug: 'bad-type', + displayName: 'bad-type', + filePaths: 'SKILL.md', + }, + [{ filename: 'SKILL.md', content: '# a\n' }] + ), + (err) => err.status === 400 && /filePaths 必须是字符串数组/.test(err.message) + ); +}); + +test('publishSkill rejects filePaths with path traversal', async () => { + const service = Object.create(SkillsRegistryService.prototype); + service.app = createMockApp({ + SkillsItem: { findOne: async () => null, create: async () => ({ id: 1 }) }, + SkillsSource: { findOrCreate: async () => [{ id: 1 }] }, + SkillsFile: { findAll: async () => [], create: async () => ({}), update: async () => ({}) }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + await assert.rejects( + () => + service.publishSkill( + { + slug: 'evil-path', + displayName: 'evil-path', + filePaths: ['SKILL.md', '../etc/passwd'], + }, + [ + { filename: 'SKILL.md', content: '# a\n' }, + { filename: 'passwd', content: 'x\n' }, + ] + ), + (err) => err.status === 400 && /非法文件路径/.test(err.message) + ); +}); + +test('publishSkill create without description uses SKILL.md frontmatter', async () => { + const service = Object.create(SkillsRegistryService.prototype); + let created = null; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => null, + create: async (data) => { + created = { + id: 60, + ...data, + update: async () => {}, + }; + return created; + }, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [], + create: async () => ({}), + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + const skillMd = + '---\nname: zentao-api\ndescription: 通过禅道 HTTP API 获取 Bug 详情。\n---\n\n# Body\n'; + const result = await service.publishSkill( + { slug: 'from-fm', displayName: 'from-fm', category: '前端' }, + [{ filename: 'SKILL.md', content: skillMd }] + ); + + assert.equal(result.ok, true); + assert.equal(created.description, '通过禅道 HTTP API 获取 Bug 详情。'); +}); + +test('publishSkill re-publish without description keeps existing market card', async () => { + const service = Object.create(SkillsRegistryService.prototype); + const updatePayloads = []; + const skillRow = { + id: 52, + slug: 'keep-desc', + name: 'Keep Desc', + description: 'original card summary', + version: '0.0.0', + category: '通用', + is_delete: 0, + update: async (data) => { + updatePayloads.push(data); + Object.assign(skillRow, data); + }, + }; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skillRow, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [ + { + file_path: 'SKILL.md', + content: '# old\n', + is_binary: 0, + }, + ], + create: async () => ({}), + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + // CLI-shaped payload: no description field → keep market card (sticky override). + const result = await service.publishSkill({ slug: 'keep-desc', displayName: 'Keep Desc' }, [ + { + filepath: 'SKILL.md', + content: '---\nname: keep-desc\ndescription: refreshed from package\n---\n\n# new\n', + }, + ]); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, false); + const mainUpdate = updatePayloads.find((p) => Object.prototype.hasOwnProperty.call(p, 'name')); + assert.ok(mainUpdate, 'skill.update should run the main re-publish payload'); + assert.equal(mainUpdate.description, 'original card summary'); + assert.equal(skillRow.description, 'original card summary'); +}); + +test('publishSkill re-publish without description backfills empty card from SKILL.md', async () => { + const service = Object.create(SkillsRegistryService.prototype); + const updatePayloads = []; + const skillRow = { + id: 56, + slug: 'empty-desc', + name: 'Empty Desc', + description: '', + version: '0.0.0', + category: '通用', + is_delete: 0, + update: async (data) => { + updatePayloads.push(data); + Object.assign(skillRow, data); + }, + }; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skillRow, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [ + { + file_path: 'SKILL.md', + content: '# old\n', + is_binary: 0, + }, + ], + create: async () => ({}), + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + const result = await service.publishSkill({ slug: 'empty-desc', displayName: 'Empty Desc' }, [ + { + filepath: 'SKILL.md', + content: '---\nname: empty-desc\ndescription: filled from package\n---\n\n# new\n', + }, + ]); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, false); + const mainUpdate = updatePayloads.find((p) => Object.prototype.hasOwnProperty.call(p, 'name')); + assert.ok(mainUpdate); + assert.equal(mainUpdate.description, 'filled from package'); + assert.equal(skillRow.description, 'filled from package'); +}); + +test('publishSkill re-publish with description updates description', async () => { + const service = Object.create(SkillsRegistryService.prototype); + const updatePayloads = []; + const skillRow = { + id: 53, + slug: 'set-desc', + name: 'Set Desc', + description: 'old summary', + version: '0.0.0', + is_delete: 0, + update: async (data) => { + updatePayloads.push(data); + Object.assign(skillRow, data); + }, + }; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skillRow, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => [ + { + file_path: 'SKILL.md', + content: '# old\n', + is_binary: 0, + }, + ], + create: async () => ({}), + update: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + const result = await service.publishSkill( + { slug: 'set-desc', displayName: 'Set Desc', description: ' new summary ' }, + [{ filepath: 'SKILL.md', content: '# new content\n' }] + ); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, false); + const mainUpdate = updatePayloads.find((p) => Object.prototype.hasOwnProperty.call(p, 'name')); + assert.ok(mainUpdate); + assert.equal(mainUpdate.description, 'new summary'); + assert.equal(skillRow.description, 'new summary'); +}); + +test('publishSkill same content backfills empty card from SKILL.md', async () => { + const service = Object.create(SkillsRegistryService.prototype); + let metaUpdate = null; + const skillMd = '---\nname: backfill\ndescription: card should fill\n---\n\n# Title\n'; + const skillRow = { + id: 55, + slug: 'backfill-desc', + name: 'Backfill', + description: '', + version: '0.0.0', + is_delete: 0, + update: async (payload) => { + metaUpdate = payload; + Object.assign(skillRow, payload); + }, + }; + const files = [ + { + file_path: 'SKILL.md', + content: skillMd, + is_binary: 0, + }, + ]; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skillRow, + }, + SkillsSource: { + findOrCreate: async () => [{ id: 1 }], + }, + SkillsFile: { + findAll: async () => files, + create: async () => { + throw new Error('should not create on no-op'); + }, + update: async () => { + throw new Error('should not soft-delete on no-op'); + }, + }, + }); + service.ctx = createMockCtx(); + service.ctx.logger = { warn: () => {}, info: () => {}, error: () => {} }; + + const result = await service.publishSkill({ slug: 'backfill-desc', displayName: 'Backfill' }, [ + { filepath: 'SKILL.md', content: skillMd }, + ]); + + assert.equal(result.ok, true); + assert.equal(result.unchanged, true); + assert.deepEqual(metaUpdate, { description: 'card should fill' }); + assert.equal(skillRow.description, 'card should fill'); +}); + test('publishSkill same content is unchanged no-op', async () => { const service = Object.create(SkillsRegistryService.prototype); const skillRow = {