From 9b3f3db59f6e5ed6ad2824608257d1f35ae07f64 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Thu, 10 Sep 2026 21:23:01 -0700 Subject: [PATCH] feat: verify registered skills in package archives --- .changeset/verify-maintainer-package.md | 5 + packages/intent/package.json | 1 + packages/intent/src/cli.ts | 5 +- packages/intent/src/commands/maintainer.ts | 27 +- packages/intent/src/core/markdown.ts | 29 ++- .../intent/src/maintainer/verify-package.ts | 237 ++++++++++++++++++ packages/intent/src/shared/utils.ts | 6 + .../tests/integration/packed-release.test.ts | 13 +- packages/intent/tests/verify-package.test.ts | 230 +++++++++++++++++ pnpm-lock.yaml | 8 + 10 files changed, 550 insertions(+), 11 deletions(-) create mode 100644 .changeset/verify-maintainer-package.md create mode 100644 packages/intent/src/maintainer/verify-package.ts create mode 100644 packages/intent/tests/verify-package.test.ts diff --git a/.changeset/verify-maintainer-package.md b/.changeset/verify-maintainer-package.md new file mode 100644 index 00000000..b0b7d60a --- /dev/null +++ b/.changeset/verify-maintainer-package.md @@ -0,0 +1,5 @@ +--- +'@tanstack/intent': minor +--- + +Add read-only verification of supplied npm package archives against registered skills, package identity, bundled resources, and inline Markdown references. Support structured CI failures without extraction, publishing, or script execution, using a small dependency-free tar parser. diff --git a/packages/intent/package.json b/packages/intent/package.json index 6b241c51..8cd19b0f 100644 --- a/packages/intent/package.json +++ b/packages/intent/package.json @@ -30,6 +30,7 @@ ], "dependencies": { "@clack/prompts": "1.7.0", + "@remix-run/tar-parser": "0.7.1", "cac": "^7.0.0", "jsonc-parser": "^3.3.1", "semver": "^7.8.4", diff --git a/packages/intent/src/cli.ts b/packages/intent/src/cli.ts index b5f85aed..0090766b 100644 --- a/packages/intent/src/cli.ts +++ b/packages/intent/src/cli.ts @@ -197,7 +197,7 @@ function createCli( 'Set up, author, synchronize, and check library skills', ) .usage( - 'maintainer [name] [options]', + 'maintainer [name] [options]', ) .option( '--artifacts ', @@ -260,6 +260,9 @@ function createCli( .example('maintainer review --json') .example('maintainer review --interactive') .example('maintainer check --base origin/main') + .example( + 'maintainer verify-package library.tgz --package packages/client --json', + ) .action( async ( action: string, diff --git a/packages/intent/src/commands/maintainer.ts b/packages/intent/src/commands/maintainer.ts index 5e5e5b30..ee3d68e9 100644 --- a/packages/intent/src/commands/maintainer.ts +++ b/packages/intent/src/commands/maintainer.ts @@ -71,12 +71,13 @@ export async function runMaintainerCommand( sync: ['artifacts'], review: ['base', 'json', 'record', 'interactive'], check: ['artifacts', 'base'], + 'verify-package': ['artifacts', 'package', 'json'], } if (!allowed[action]) fail( - `Unknown maintainer action: ${action}. Expected setup, adopt, add, status, sync, review, or check.`, + `Unknown maintainer action: ${action}. Expected setup, adopt, add, status, sync, review, check, or verify-package.`, ) - if (name !== undefined && action !== 'add') + if (name !== undefined && !['add', 'verify-package'].includes(action)) fail(`maintainer ${action} does not take a skill name.`) for (const key of Object.keys(options)) { if (key !== '--' && !allowed[action].includes(key)) @@ -104,6 +105,28 @@ export async function runMaintainerCommand( return } const project = resolveMaintainerProject(process.cwd(), options.artifacts) + if (action === 'verify-package') { + if (!name) + fail('Pass the package archive: maintainer verify-package .') + const { verifyPackageArchive } = + await import('../maintainer/verify-package.js') + const report = await verifyPackageArchive(project, name, options.package) + if (options.json) console.log(JSON.stringify(report, null, 2)) + else { + console.log( + `${report.package.name}: ${report.skills.length} registered skill(s), ${report.problems.length} package problem(s).`, + ) + for (const problem of report.problems) + console.log( + ` ${JSON.stringify(problem.file)}${problem.target ? ` -> ${JSON.stringify(problem.target)}` : ''}: ${problem.message}`, + ) + } + if (!report.valid) + fail( + 'Package verification failed. Fix the package contents and rebuild the archive.', + ) + return + } if (action === 'adopt') { let input: unknown if (options.apply) { diff --git a/packages/intent/src/core/markdown.ts b/packages/intent/src/core/markdown.ts index c6d15f11..7f85466f 100644 --- a/packages/intent/src/core/markdown.ts +++ b/packages/intent/src/core/markdown.ts @@ -197,11 +197,11 @@ function rewriteMarkdownDestination({ } function rewriteMarkdownLineDestinations({ - context, line, + rewrite, }: { - context: MarkdownDestinationRewriteContext line: string + rewrite: (destination: string) => string }): string { if (!line.includes('[')) return line @@ -259,10 +259,7 @@ function rewriteMarkdownLineDestinations({ continue } - const rewritten = rewriteMarkdownDestination({ - context, - destination: destination.destination, - }) + const rewritten = rewrite(destination.destination) output += line.slice(linkStart, destination.destinationStart) + rewritten + @@ -290,6 +287,24 @@ export function rewriteLoadedSkillMarkdownDestinations({ skillDir: dirname(skillFilePath), rewrittenDestinations: new Map(), } + return mapMarkdownDestinations(content, (destination) => + rewriteMarkdownDestination({ context, destination }), + ) +} + +export function collectMarkdownDestinations(content: string): Array { + const destinations = new Set() + mapMarkdownDestinations(content, (destination) => { + destinations.add(destination) + return destination + }) + return [...destinations] +} + +function mapMarkdownDestinations( + content: string, + rewrite: (destination: string) => string, +): string { let inFence: '`' | '~' | null = null const parts = content.split(/(\r?\n)/) let output = '' @@ -313,8 +328,8 @@ export function rewriteLoadedSkillMarkdownDestinations({ output += rewriteMarkdownLineDestinations({ - context, line, + rewrite, }) + newline } diff --git a/packages/intent/src/maintainer/verify-package.ts b/packages/intent/src/maintainer/verify-package.ts new file mode 100644 index 00000000..bd63771f --- /dev/null +++ b/packages/intent/src/maintainer/verify-package.ts @@ -0,0 +1,237 @@ +import { execFileSync } from 'node:child_process' +import { createReadStream, readFileSync, statSync } from 'node:fs' +import { posix, resolve } from 'node:path' +import { Readable } from 'node:stream' +import { parseTar } from '@remix-run/tar-parser' +import { collectMarkdownDestinations } from '../core/markdown.js' +import { parseFrontmatterText } from '../shared/utils.js' +import { isObject, projectPath, skillEntries } from './project.js' +import type { MaintainerProject } from './project.js' + +interface PackageProblem { + file: string + target?: string + message: string +} + +async function readArchive(archive: string) { + if (statSync(archive).size > 128 * 1024 * 1024) + throw new Error('Compressed archive exceeds the 128 MiB inspection limit.') + const input = createReadStream(archive) + const files = new Set() + const documents = new Map() + let expanded = 0 + let textBytes = 0 + let entries = 0 + const stream = (Readable.toWeb(input) as ReadableStream) + .pipeThrough(new DecompressionStream('gzip')) + .pipeThrough( + new TransformStream({ + transform(chunk, controller) { + expanded += chunk.byteLength + if (expanded > 512 * 1024 * 1024) + throw new Error( + 'Expanded archive exceeds the 512 MiB inspection limit.', + ) + controller.enqueue(chunk) + }, + }), + ) + try { + await parseTar(stream, { allowUnknownFormat: false }, (entry) => { + if (++entries > 50_000) + throw new Error('Archive exceeds the 50,000-entry inspection limit.') + const name = entry.name.replace(/^(\.\/)+/, '').replace(/\/$/, '') + if ( + !name || + /[\\:]/.test(name) || + [...name].some( + (character) => + character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127, + ) || + name + .split('/') + .some((part) => !part || part === '.' || part === '..') || + (name !== 'package' && !name.startsWith('package/')) + ) + throw new Error( + `Unsafe package archive path: ${JSON.stringify(entry.name)}`, + ) + if (!['file', 'directory'].includes(entry.header.type)) + throw new Error( + `Unsupported archive entry ${entry.header.type}: ${name}`, + ) + if (!Number.isSafeInteger(entry.size) || entry.size < 0) + throw new Error(`Invalid archive entry size: ${name}`) + if (entry.header.type === 'directory') return + const path = name.slice('package/'.length) + if (!path || files.has(path)) + throw new Error(`Duplicate or invalid package file: ${name}`) + files.add(path) + if (path !== 'package.json' && !path.endsWith('.md')) + return entry.body.pipeTo(new WritableStream()) + textBytes += entry.size + if (entry.size > 4 * 1024 * 1024 || textBytes > 32 * 1024 * 1024) + throw new Error( + 'Package text exceeds the 4 MiB file or 32 MiB total inspection limit.', + ) + return entry.text().then((content) => { + documents.set(path, content) + }) + }) + } finally { + input.destroy() + } + return { files, documents } +} + +export async function verifyPackageArchive( + project: MaintainerProject, + archive: string, + packageDirectory = '', +) { + if (packageDirectory === '.') packageDirectory = '' + const manifestPath = projectPath( + project.root, + packageDirectory ? `${packageDirectory}/package.json` : 'package.json', + ) + const manifest: unknown = JSON.parse(readFileSync(manifestPath, 'utf8')) + if (!isObject(manifest) || typeof manifest.name !== 'string') + throw new Error('The selected package needs a valid package.json name.') + const registered = skillEntries(project).filter( + (entry) => + (entry.package ?? '') === packageDirectory && + !['planned', 'retired'].includes(String(entry.status)), + ) + const problems: Array = [] + const skills = registered.map((entry) => entry.path) + if (!skills.length) + problems.push({ + file: 'skill_tree.yaml', + message: 'No active skills are registered for the selected package.', + }) + const archivePath = resolve(archive) + try { + const { files, documents } = await readArchive(archivePath) + const packedManifest: unknown = JSON.parse( + documents.get('package.json') ?? 'null', + ) + if ( + !isObject(packedManifest) || + packedManifest.name !== manifest.name || + packedManifest.version !== manifest.version + ) + problems.push({ + file: 'package.json', + message: + 'Archive name and version must match the selected source package.', + }) + const sourceFiles = skills.length + ? execFileSync( + 'git', + [ + '-c', + 'core.fsmonitor=false', + '--literal-pathspecs', + 'ls-files', + '--cached', + '--others', + '--exclude-standard', + '-z', + '--', + ...skills.map((skill) => + posix.join(packageDirectory, posix.dirname(skill)), + ), + ], + { cwd: project.root, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }, + ) + .split('\0') + .filter(Boolean) + : [] + const resources = sourceFiles.map((file) => + packageDirectory ? file.slice(packageDirectory.length + 1) : file, + ) + for (const resource of resources) { + if (files.has(resource) || skills.includes(resource)) continue + problems.push({ + file: skills.find((skill) => + resource.startsWith(`${posix.dirname(skill)}/`), + )!, + target: resource, + message: 'Skill-folder resource is missing from the archive.', + }) + } + const pending = [ + ...skills, + ...resources.filter((file) => file.endsWith('.md')), + ] + const visited = new Set() + for (const entry of registered) { + const content = documents.get(entry.path) + const frontmatter = + content === undefined ? null : parseFrontmatterText(content) + if (!frontmatter || frontmatter.name !== (entry.slug ?? entry.name)) + problems.push({ + file: entry.path, + message: + 'Registered skill is missing or has a different identity in the archive.', + }) + } + for (const file of pending) { + if (visited.has(file)) continue + visited.add(file) + const content = documents.get(file) + if (content === undefined) continue + const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, '') + for (const destination of collectMarkdownDestinations(body)) { + if ( + !destination || + /^[#?]/.test(destination) || + destination.startsWith('//') + ) + continue + if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(destination)) continue + const path = decodeURIComponent(destination.split(/[?#]/)[0]!).replace( + /\\([!"#$%&'()*+,\-./:;<=>?@[\]^_`{|}~\\])/g, + '$1', + ) + const target = posix.normalize(posix.join(posix.dirname(file), path)) + if ( + path.startsWith('/') || + target === '..' || + target.startsWith('../') || + target.includes('\\') + ) { + problems.push({ + file, + target: destination, + message: 'Reference leaves the installed package.', + }) + continue + } + if (!files.has(target)) { + problems.push({ + file, + target, + message: 'Required reference is missing from the archive.', + }) + continue + } + if (target.endsWith('.md')) pending.push(target) + } + } + } catch (error) { + problems.push({ + file: archivePath, + message: error instanceof Error ? error.message : String(error), + }) + } + return { + schemaVersion: 1, + archive: archivePath, + package: { name: manifest.name, version: manifest.version }, + skills, + problems, + valid: problems.length === 0, + } +} diff --git a/packages/intent/src/shared/utils.ts b/packages/intent/src/shared/utils.ts index 1d7ac0d7..d1056ac5 100644 --- a/packages/intent/src/shared/utils.ts +++ b/packages/intent/src/shared/utils.ts @@ -403,6 +403,12 @@ export function parseFrontmatter( ): Record | null { const content = readFrontmatterRegion(filePath, fs) if (content === null) return null + return parseFrontmatterText(content) +} + +export function parseFrontmatterText( + content: string, +): Record | null { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) if (!match?.[1]) return null try { diff --git a/packages/intent/tests/integration/packed-release.test.ts b/packages/intent/tests/integration/packed-release.test.ts index 99e804f1..5aa06ba9 100644 --- a/packages/intent/tests/integration/packed-release.test.ts +++ b/packages/intent/tests/integration/packed-release.test.ts @@ -255,7 +255,7 @@ describe('packed release', () => { expect(synced.status, synced.stderr).toBe(0) expect(run(['maintainer', 'check']).status).toBe(1) const packed = JSON.parse( - execFileSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json'], { + execFileSync('npm', ['pack', '--ignore-scripts', '--json'], { cwd, encoding: 'utf8', timeout, @@ -268,6 +268,17 @@ describe('packed release', () => { expect( packed[0].files.map((file: { path: string }) => file.path), ).not.toContain('skills/_artifacts/skill_spec.md') + const verified = run([ + 'maintainer', + 'verify-package', + join(cwd, packed[0].filename), + '--json', + ]) + expect(verified.status, verified.stderr).toBe(0) + expect(JSON.parse(verified.stdout)).toMatchObject({ + valid: true, + skills: ['skills/query/SKILL.md'], + }) }) it('validates a manually authored skill without maintainer setup', () => { diff --git a/packages/intent/tests/verify-package.test.ts b/packages/intent/tests/verify-package.test.ts new file mode 100644 index 00000000..f4ad5a83 --- /dev/null +++ b/packages/intent/tests/verify-package.test.ts @@ -0,0 +1,230 @@ +import { execFileSync } from 'node:child_process' +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { main } from '../src/cli.js' + +let root: string +let previousCwd: string + +function write(path: string, content: string) { + mkdirSync(dirname(join(root, path)), { recursive: true }) + writeFileSync(join(root, path), content) +} + +function pack(files: Array, packageDirectory = ''): string { + const staging = join(root, 'archive/package') + mkdirSync(staging, { recursive: true }) + for (const file of files) { + mkdirSync(dirname(join(staging, file)), { recursive: true }) + cpSync(join(root, packageDirectory, file), join(staging, file), { + recursive: true, + }) + } + const archive = join(root, 'library.tgz') + execFileSync('tar', ['-czf', archive, '-C', dirname(staging), 'package'], { + env: { ...process.env, COPYFILE_DISABLE: '1' }, + }) + return archive +} + +beforeEach(() => { + previousCwd = process.cwd() + root = realpathSync(mkdtempSync(join(tmpdir(), 'intent-verify-package-'))) + process.chdir(root) + vi.spyOn(console, 'log').mockImplementation(() => {}) + vi.spyOn(console, 'error').mockImplementation(() => {}) + execFileSync('git', ['-c', 'core.fsmonitor=false', 'init', '-q']) + write('package.json', '{"name":"library","version":"1.0.0"}\n') + write( + 'skills/_artifacts/skill_tree.yaml', + 'skills:\n - name: core\n path: skills/core/SKILL.md\n', + ) + write( + 'skills/core/SKILL.md', + '---\nname: core\ndescription: Core task\nsources: [src/private.ts]\n---\nRead [the example](references/example.md#usage) and run [the helper](scripts/check.mjs).\n\n```md\n[Illustration](not-a-resource.md)\n```\n', + ) + write('skills/core/references/example.md', '# Usage\n\nRead the value.\n') + write('skills/core/scripts/check.mjs', 'throw new Error("Do not execute")\n') +}) + +afterEach(() => { + process.chdir(previousCwd) + vi.restoreAllMocks() + rmSync(root, { recursive: true, force: true }) +}) + +it('verifies packaged skills and resources in CI without extracting or running them', async () => { + const archive = pack(['package.json', 'skills/core']) + const original = readFileSync(archive) + expect( + await main(['maintainer', 'verify-package', archive, '--json'], { + isTTY: true, + isCI: true, + }), + ).toBe(0) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report).toMatchObject({ + valid: true, + package: { name: 'library', version: '1.0.0' }, + skills: ['skills/core/SKILL.md'], + problems: [], + }) + expect(readFileSync(archive)).toEqual(original) + expect(existsSync(join(root, '.intent'))).toBe(false) + expect(existsSync(join(root, 'package'))).toBe(false) +}) + +it('reports a missing packaged reference and fails the CI check', async () => { + const archive = pack([ + 'package.json', + 'skills/core/SKILL.md', + 'skills/core/scripts/check.mjs', + ]) + expect(await main(['maintainer', 'verify-package', archive, '--json'])).toBe( + 1, + ) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.valid).toBe(false) + expect(report.problems).toContainEqual( + expect.objectContaining({ + file: 'skills/core/SKILL.md', + target: 'skills/core/references/example.md', + }), + ) +}) + +it('requires skill-folder resources even when instructions do not link them', async () => { + write('skills/core/scripts/extra.mjs', 'export const required = true\n') + const archive = pack([ + 'package.json', + 'skills/core/SKILL.md', + 'skills/core/references', + 'skills/core/scripts/check.mjs', + ]) + expect(await main(['maintainer', 'verify-package', archive, '--json'])).toBe( + 1, + ) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.problems).toContainEqual( + expect.objectContaining({ target: 'skills/core/scripts/extra.mjs' }), + ) +}) + +it('checks nested references without looping and rejects package escapes', async () => { + write( + 'skills/core/references/example.md', + '[Back](../SKILL.md)\n[Missing](missing.md)\n[Outside](../../../../outside.md)\n', + ) + const archive = pack(['package.json', 'skills/core']) + expect(await main(['maintainer', 'verify-package', archive, '--json'])).toBe( + 1, + ) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.problems).toEqual( + expect.arrayContaining([ + expect.objectContaining({ target: 'skills/core/references/missing.md' }), + expect.objectContaining({ target: '../../../../outside.md' }), + ]), + ) +}) + +it.each(['missing skill', 'wrong package', 'wrong version', 'corrupt archive'])( + 'fails for %s with structured output', + async (failure) => { + const archive = pack( + failure === 'missing skill' + ? ['package.json'] + : ['package.json', 'skills/core'], + ) + if (failure === 'wrong package') + write('package.json', '{"name":"another","version":"1.0.0"}\n') + if (failure === 'wrong version') + write('package.json', '{"name":"library","version":"2.0.0"}\n') + if (failure === 'corrupt archive') writeFileSync(archive, 'not gzip or tar') + expect( + await main(['maintainer', 'verify-package', archive, '--json']), + ).toBe(1) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.valid).toBe(false) + expect(report.problems.length).toBeGreaterThan(0) + expect(existsSync(join(root, '.intent'))).toBe(false) + }, +) + +it('rejects archive symbolic links without following them', async () => { + symlinkSync(join(root, 'package.json'), join(root, 'skills/core/linked.json')) + const archive = pack(['package.json', 'skills/core']) + expect(await main(['maintainer', 'verify-package', archive, '--json'])).toBe( + 1, + ) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.problems[0].message).toContain('Unsupported archive entry') +}) + +it('verifies a real npm archive without requiring provenance files', async () => { + write( + 'package.json', + JSON.stringify({ + name: 'library', + version: '1.0.0', + files: ['skills/core'], + scripts: { prepack: 'node -e "process.exit(1)"' }, + }), + ) + const packed = JSON.parse( + execFileSync('npm', ['pack', '--ignore-scripts', '--json'], { + cwd: root, + encoding: 'utf8', + timeout: 30_000, + env: { ...process.env, npm_config_cache: join(root, 'npm-cache') }, + }), + ) + expect( + await main(['maintainer', 'verify-package', packed[0].filename, '--json']), + ).toBe(0) + expect(existsSync(join(root, 'src/private.ts'))).toBe(false) + expect(existsSync(join(root, '.intent'))).toBe(false) +}, 30_000) + +it('checks only the selected workspace package and its active registrations', async () => { + write('pnpm-workspace.yaml', 'packages: [packages/*]\n') + write( + 'packages/client/package.json', + '{"name":"@library/client","version":"2.0.0"}\n', + ) + write( + 'packages/client/skills/query/SKILL.md', + '---\nname: query\ndescription: Query\n---\nRead [the reference](references/query.md).\n', + ) + write('packages/client/skills/query/references/query.md', 'Query example.\n') + write( + 'skills/_artifacts/skill_tree.yaml', + 'skills:\n - name: query\n package: packages/client\n path: skills/query/SKILL.md\n - name: future\n package: packages/client\n path: skills/future/SKILL.md\n status: planned\n - name: old\n package: packages/client\n path: skills/old/SKILL.md\n status: retired\n - name: other\n package: packages/other\n path: skills/other/SKILL.md\n', + ) + const archive = pack(['package.json', 'skills/query'], 'packages/client') + expect( + await main([ + 'maintainer', + 'verify-package', + archive, + '--package', + 'packages/client', + '--json', + ]), + ).toBe(0) + const report = JSON.parse(String(vi.mocked(console.log).mock.calls[0]![0])) + expect(report.skills).toEqual(['skills/query/SKILL.md']) + expect(report.package.name).toBe('@library/client') +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a6bf0d3..b7cbfded 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: '@clack/prompts': specifier: 1.7.0 version: 1.7.0 + '@remix-run/tar-parser': + specifier: 0.7.1 + version: 0.7.1 cac: specifier: ^7.0.0 version: 7.0.0 @@ -793,6 +796,9 @@ packages: '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@remix-run/tar-parser@0.7.1': + resolution: {integrity: sha512-NZKTuA66rj0zqpljWAb6v147cNu5BtRCiv8FY5kn64ZPvLmoI62Ehm2hoUh0g0wJHeCNmgS5QZg1xhw6FX67SA==} + '@rolldown/binding-android-arm-eabi@1.2.6': resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4621,6 +4627,8 @@ snapshots: dependencies: quansync: 1.0.0 + '@remix-run/tar-parser@0.7.1': {} + '@rolldown/binding-android-arm-eabi@1.2.6': optional: true