From ef194b8eeb75b8fc24a2b1ac98fe084f9457b530 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Mon, 16 Feb 2026 22:26:01 +0900 Subject: [PATCH 01/14] perf(core): Replace ZIP archive download with streaming tar.gz extraction The previous ZIP-based archive download used fflate's in-memory extraction, which failed on large repositories (e.g. facebook/react) due to memory constraints and ZIP64 limitations. Switch to tar.gz format with Node.js built-in zlib + tar package, enabling a full streaming pipeline (HTTP response -> gunzip -> tar extract -> disk) with no temporary files and constant memory usage regardless of repo size. Key changes: - Replace fflate with tar package for archive extraction - Change archive URLs from .zip to .tar.gz - Use streaming pipeline instead of download-then-extract - Leverage tar's built-in strip and path traversal protection - Explicitly destroy streams after pipeline for Bun compatibility - Use child_process runtime under Bun to avoid worker_threads hang --- package-lock.json | 67 +++- package.json | 2 +- src/core/git/gitHubArchive.ts | 235 +++------------ src/core/git/gitHubArchiveApi.ts | 21 +- src/shared/processConcurrency.ts | 7 +- tests/core/git/gitHubArchive.test.ts | 386 ++++++------------------ tests/core/git/gitHubArchiveApi.test.ts | 20 +- 7 files changed, 217 insertions(+), 521 deletions(-) diff --git a/package-lock.json b/package-lock.json index 091200ae8..d45ceb268 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "clipboardy": "^5.0.2", "commander": "^14.0.2", "fast-xml-parser": "^5.3.3", - "fflate": "^0.8.2", "git-url-parse": "^16.1.0", "globby": "^16.1.0", "handlebars": "^4.7.8", @@ -31,6 +30,7 @@ "log-update": "^7.0.2", "minimatch": "^10.1.1", "picocolors": "^1.1.1", + "tar": "^7.5.7", "tiktoken": "^1.0.22", "tinypool": "^2.1.0", "web-tree-sitter": "^0.26.3", @@ -818,6 +818,18 @@ "node": "20 || >=22" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2210,6 +2222,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -2717,12 +2738,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -3716,11 +3731,22 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5001,6 +5027,22 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -5531,6 +5573,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", diff --git a/package.json b/package.json index 994dd0852..e10eca1fb 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "clipboardy": "^5.0.2", "commander": "^14.0.2", "fast-xml-parser": "^5.3.3", - "fflate": "^0.8.2", "git-url-parse": "^16.1.0", "globby": "^16.1.0", "handlebars": "^4.7.8", @@ -97,6 +96,7 @@ "log-update": "^7.0.2", "minimatch": "^10.1.1", "picocolors": "^1.1.1", + "tar": "^7.5.7", "tiktoken": "^1.0.22", "tinypool": "^2.1.0", "web-tree-sitter": "^0.26.3", diff --git a/src/core/git/gitHubArchive.ts b/src/core/git/gitHubArchive.ts index fdcb711bf..d62243b67 100644 --- a/src/core/git/gitHubArchive.ts +++ b/src/core/git/gitHubArchive.ts @@ -1,9 +1,7 @@ -import { createWriteStream } from 'node:fs'; -import * as fs from 'node:fs/promises'; -import * as path from 'node:path'; import { Readable, Transform } from 'node:stream'; import { pipeline } from 'node:stream/promises'; -import { unzip } from 'fflate'; +import * as zlib from 'node:zlib'; +import { extract as tarExtract } from 'tar'; import { RepomixError } from '../../shared/errorHandle.js'; import { logger } from '../../shared/logger.js'; import { @@ -11,7 +9,6 @@ import { buildGitHubMasterArchiveUrl, buildGitHubTagArchiveUrl, checkGitHubResponse, - getArchiveFilename, } from './gitHubArchiveApi.js'; import type { GitHubRepoInfo } from './gitRemoteParse.js'; @@ -28,27 +25,34 @@ export interface ArchiveDownloadProgress { export type ProgressCallback = (progress: ArchiveDownloadProgress) => void; +export interface ArchiveDownloadDeps { + fetch: typeof globalThis.fetch; + pipeline: typeof pipeline; + Transform: typeof Transform; + tarExtract: typeof tarExtract; + createGunzip: typeof zlib.createGunzip; +} + +const defaultDeps: ArchiveDownloadDeps = { + fetch: globalThis.fetch, + pipeline, + Transform, + tarExtract, + createGunzip: zlib.createGunzip, +}; + /** - * Downloads and extracts a GitHub repository archive + * Downloads and extracts a GitHub repository archive using streaming tar.gz extraction */ export const downloadGitHubArchive = async ( repoInfo: GitHubRepoInfo, targetDirectory: string, options: ArchiveDownloadOptions = {}, onProgress?: ProgressCallback, - deps = { - fetch: globalThis.fetch, - fs, - pipeline, - Transform, - createWriteStream, - }, + deps: ArchiveDownloadDeps = defaultDeps, ): Promise => { const { timeout = 30000, retries = 3 } = options; - // Ensure target directory exists - await deps.fs.mkdir(targetDirectory, { recursive: true }); - let lastError: Error | null = null; // Try downloading with multiple URL formats: main branch, master branch (fallback), then tag format @@ -63,7 +67,7 @@ export const downloadGitHubArchive = async ( try { logger.trace(`Downloading GitHub archive from: ${archiveUrl} (attempt ${attempt}/${retries})`); - await downloadAndExtractArchive(archiveUrl, targetDirectory, repoInfo, timeout, onProgress, deps); + await downloadAndExtractArchive(archiveUrl, targetDirectory, timeout, onProgress, deps); logger.trace('Successfully downloaded and extracted GitHub archive'); return; // Success - exit early @@ -102,61 +106,22 @@ export const downloadGitHubArchive = async ( }; /** - * Downloads and extracts archive from a single URL + * Downloads and extracts a tar.gz archive from a single URL using streaming pipeline. + * The HTTP response is streamed through gunzip and tar extract directly to disk, + * without writing a temporary archive file. */ const downloadAndExtractArchive = async ( archiveUrl: string, targetDirectory: string, - repoInfo: GitHubRepoInfo, - timeout: number, - onProgress?: ProgressCallback, - deps = { - fetch: globalThis.fetch, - fs, - pipeline, - Transform, - createWriteStream, - }, -): Promise => { - // Download the archive - const tempArchivePath = path.join(targetDirectory, getArchiveFilename(repoInfo)); - - await downloadFile(archiveUrl, tempArchivePath, timeout, onProgress, deps); - - try { - // Extract the archive - await extractZipArchive(tempArchivePath, targetDirectory, repoInfo, { fs: deps.fs }); - } finally { - // Clean up the downloaded archive file - try { - await deps.fs.unlink(tempArchivePath); - } catch (error) { - logger.trace('Failed to cleanup archive file:', (error as Error).message); - } - } -}; - -/** - * Downloads a file from URL with progress tracking - */ -const downloadFile = async ( - url: string, - filePath: string, timeout: number, onProgress?: ProgressCallback, - deps = { - fetch: globalThis.fetch, - fs, - pipeline, - Transform, - createWriteStream, - }, + deps: ArchiveDownloadDeps = defaultDeps, ): Promise => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { - const response = await deps.fetch(url, { + const response = await deps.fetch(archiveUrl, { signal: controller.signal, }); @@ -171,7 +136,6 @@ const downloadFile = async ( let downloaded = 0; let lastProgressUpdate = 0; - // Use Readable.fromWeb for better stream handling const nodeStream = Readable.fromWeb(response.body); // Transform stream for progress tracking @@ -193,7 +157,6 @@ const downloadFile = async ( callback(null, chunk); }, flush(callback) { - // Send final progress update if (onProgress) { onProgress({ downloaded, @@ -205,137 +168,27 @@ const downloadFile = async ( }, }); - // Write to file - const writeStream = deps.createWriteStream(filePath); - await deps.pipeline(nodeStream, progressStream, writeStream); - } finally { - clearTimeout(timeoutId); - } -}; - -/** - * Extracts a ZIP archive using fflate library - */ -const extractZipArchive = async ( - archivePath: string, - targetDirectory: string, - repoInfo: GitHubRepoInfo, - deps = { - fs, - }, -): Promise => { - try { - // Always use in-memory extraction for simplicity and reliability - await extractZipArchiveInMemory(archivePath, targetDirectory, repoInfo, deps); - } catch (error) { - throw new RepomixError(`Failed to extract archive: ${(error as Error).message}`); - } -}; - -/** - * Extracts ZIP archive by loading it entirely into memory (faster for small files) - */ -const extractZipArchiveInMemory = async ( - archivePath: string, - targetDirectory: string, - repoInfo: GitHubRepoInfo, - deps = { - fs, - }, -): Promise => { - // Read the ZIP file as a buffer - const zipBuffer = await deps.fs.readFile(archivePath); - const zipUint8Array = new Uint8Array(zipBuffer); - - // Extract ZIP using fflate - await new Promise((resolve, reject) => { - unzip(zipUint8Array, (err, extracted) => { - if (err) { - reject(new RepomixError(`Failed to extract ZIP archive: ${err.message}`)); - return; - } - - // Process extracted files concurrently in the callback - processExtractedFiles(extracted, targetDirectory, repoInfo, deps).then(resolve).catch(reject); + // Stream: HTTP response -> progress tracking -> gunzip -> tar extract to disk + // strip: 1 removes the top-level "repo-branch/" directory from archive paths + const extractStream = deps.tarExtract({ + cwd: targetDirectory, + strip: 1, }); - }); -}; - -/** - * Process extracted files sequentially to avoid EMFILE errors - */ -const processExtractedFiles = async ( - extracted: Record, - targetDirectory: string, - repoInfo: GitHubRepoInfo, - deps = { - fs, - }, -): Promise => { - const repoPrefix = `${repoInfo.repo}-`; - const createdDirs = new Set(); - - // Process files sequentially to avoid EMFILE errors completely - for (const [filePath, fileData] of Object.entries(extracted)) { - // GitHub archives have a top-level directory like "repo-branch/" - // We need to remove this prefix from the file paths - let relativePath = filePath; - - // Find and remove the repo prefix - const pathParts = filePath.split('/'); - if (pathParts.length > 0 && pathParts[0].startsWith(repoPrefix)) { - // Remove the first directory (repo-branch/) - relativePath = pathParts.slice(1).join('/'); - } - - // Skip empty paths (root directory) - if (!relativePath) { - continue; - } - - // Sanitize relativePath to prevent path traversal attacks - const sanitized = path.normalize(relativePath).replace(/^(\.\.([/\\]|$))+/, ''); - - // Reject absolute paths outright - if (path.isAbsolute(sanitized)) { - logger.trace(`Absolute path detected in archive, skipping: ${relativePath}`); - continue; - } - - const targetPath = path.resolve(targetDirectory, sanitized); - if (!targetPath.startsWith(path.resolve(targetDirectory))) { - logger.trace(`Unsafe path detected in archive, skipping: ${relativePath}`); - continue; - } - - // Check if this entry is a directory (ends with /) or empty file data indicates directory - const isDirectory = filePath.endsWith('/') || (fileData.length === 0 && relativePath.endsWith('/')); + const gunzipStream = deps.createGunzip(); - if (isDirectory) { - // Create directory immediately - if (!createdDirs.has(targetPath)) { - logger.trace(`Creating directory: ${targetPath}`); - await deps.fs.mkdir(targetPath, { recursive: true }); - createdDirs.add(targetPath); - } - } else { - // Create parent directory if needed and write file - const parentDir = path.dirname(targetPath); - if (!createdDirs.has(parentDir)) { - logger.trace(`Creating parent directory for file: ${parentDir}`); - await deps.fs.mkdir(parentDir, { recursive: true }); - createdDirs.add(parentDir); - } - - // Write file sequentially - logger.trace(`Writing file: ${targetPath}`); - try { - await deps.fs.writeFile(targetPath, fileData); - } catch (fileError) { - logger.trace(`Failed to write file ${targetPath}: ${(fileError as Error).message}`); - throw fileError; - } + try { + await deps.pipeline(nodeStream, progressStream, gunzipStream, extractStream); + } catch (pipelineError) { + throw new RepomixError(`Failed to extract archive: ${(pipelineError as Error).message}`); + } finally { + // Explicitly destroy streams to release handles. + // Bun's pipeline() may not fully clean up, causing subsequent worker_threads to hang. + nodeStream.destroy(); + progressStream.destroy(); + gunzipStream.destroy(); } + } finally { + clearTimeout(timeoutId); } }; diff --git a/src/core/git/gitHubArchiveApi.ts b/src/core/git/gitHubArchiveApi.ts index 5627b642f..4c4b31297 100644 --- a/src/core/git/gitHubArchiveApi.ts +++ b/src/core/git/gitHubArchiveApi.ts @@ -3,9 +3,9 @@ import type { GitHubRepoInfo } from './gitRemoteParse.js'; /** * Constructs GitHub archive download URL - * Format: https://github.com/owner/repo/archive/refs/heads/branch.zip - * For tags: https://github.com/owner/repo/archive/refs/tags/tag.zip - * For commits: https://github.com/owner/repo/archive/commit.zip + * Format: https://github.com/owner/repo/archive/refs/heads/branch.tar.gz + * For tags: https://github.com/owner/repo/archive/refs/tags/tag.tar.gz + * For commits: https://github.com/owner/repo/archive/commit.tar.gz */ export const buildGitHubArchiveUrl = (repoInfo: GitHubRepoInfo): string => { const { owner, repo, ref } = repoInfo; @@ -13,18 +13,18 @@ export const buildGitHubArchiveUrl = (repoInfo: GitHubRepoInfo): string => { if (!ref) { // Default to HEAD (repository's default branch) - return `${baseUrl}/HEAD.zip`; + return `${baseUrl}/HEAD.tar.gz`; } // Check if ref looks like a commit SHA (40 hex chars or shorter) const isCommitSha = /^[0-9a-f]{4,40}$/i.test(ref); if (isCommitSha) { - return `${baseUrl}/${encodeURIComponent(ref)}.zip`; + return `${baseUrl}/${encodeURIComponent(ref)}.tar.gz`; } // For branches and tags, we need to determine the type // Default to branch format, will fallback to tag if needed - return `${baseUrl}/refs/heads/${encodeURIComponent(ref)}.zip`; + return `${baseUrl}/refs/heads/${encodeURIComponent(ref)}.tar.gz`; }; /** @@ -36,7 +36,7 @@ export const buildGitHubMasterArchiveUrl = (repoInfo: GitHubRepoInfo): string | return null; // Only applicable when no ref is specified } - return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/archive/refs/heads/master.zip`; + return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/archive/refs/heads/master.tar.gz`; }; /** @@ -48,12 +48,13 @@ export const buildGitHubTagArchiveUrl = (repoInfo: GitHubRepoInfo): string | nul return null; // Not applicable for commits or no ref } - return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/archive/refs/tags/${encodeURIComponent(ref)}.zip`; + return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/archive/refs/tags/${encodeURIComponent(ref)}.tar.gz`; }; /** * Gets the expected archive filename from GitHub - * Format: repo-branch.zip or repo-sha.zip + * Format: repo-branch.tar.gz or repo-sha.tar.gz + * Note: This is used as a fallback identifier; streaming extraction does not require temp files */ export const getArchiveFilename = (repoInfo: GitHubRepoInfo): string => { const { repo, ref } = repoInfo; @@ -62,7 +63,7 @@ export const getArchiveFilename = (repoInfo: GitHubRepoInfo): string => { // GitHub uses the last part of the ref for the filename const refName = refPart.includes('/') ? refPart.split('/').pop() : refPart; - return `${repo}-${refName}.zip`; + return `${repo}-${refName}.tar.gz`; }; /** diff --git a/src/shared/processConcurrency.ts b/src/shared/processConcurrency.ts index 0507131a2..da268c65b 100644 --- a/src/shared/processConcurrency.ts +++ b/src/shared/processConcurrency.ts @@ -62,9 +62,14 @@ export const getWorkerThreadCount = (numOfTasks: number): { minThreads: number; }; export const createWorkerPool = (options: WorkerOptions): Tinypool => { - const { numOfTasks, workerType, runtime = 'child_process' } = options; + const { numOfTasks, workerType, runtime: requestedRuntime = 'child_process' } = options; const { minThreads, maxThreads } = getWorkerThreadCount(numOfTasks); + // Bun's worker_threads implementation has compatibility issues with Tinypool, + // causing hangs with large task counts. Use child_process runtime as a workaround. + const runtime: WorkerRuntime = + process.versions?.bun && requestedRuntime === 'worker_threads' ? 'child_process' : requestedRuntime; + // Get worker path - uses unified worker in bundled env, individual files otherwise const workerPath = getWorkerPath(workerType); diff --git a/tests/core/git/gitHubArchive.test.ts b/tests/core/git/gitHubArchive.test.ts index e07036e04..a3b28cad3 100644 --- a/tests/core/git/gitHubArchive.test.ts +++ b/tests/core/git/gitHubArchive.test.ts @@ -1,8 +1,7 @@ -import type { createWriteStream, WriteStream } from 'node:fs'; -import type * as fsPromises from 'node:fs/promises'; -import * as path from 'node:path'; -import { Transform } from 'node:stream'; +import { Transform, Writable } from 'node:stream'; import type { pipeline as pipelineType } from 'node:stream/promises'; +import type * as zlib from 'node:zlib'; +import type { extract as tarExtractType } from 'tar'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import { type ArchiveDownloadOptions, @@ -15,74 +14,52 @@ import { RepomixError } from '../../../src/shared/errorHandle.js'; // Mock modules vi.mock('../../../src/shared/logger'); -vi.mock('fflate', () => ({ - unzip: vi.fn(), -})); // Type for the deps parameter of downloadGitHubArchive interface MockDeps { fetch: typeof globalThis.fetch; - fs: typeof fsPromises; pipeline: typeof pipelineType; Transform: typeof Transform; - createWriteStream: typeof createWriteStream; + tarExtract: typeof tarExtractType; + createGunzip: typeof zlib.createGunzip; } -// Simple ZIP test data -const mockZipData = new Uint8Array([0x50, 0x4b, 0x03, 0x04]); // Simple ZIP header +// Simple test data +const mockStreamData = new Uint8Array([0x1f, 0x8b, 0x08, 0x00]); // gzip magic bytes describe('gitHubArchive', () => { - // Define typed mock functions - const mockFs = { - mkdir: vi.fn(), - readFile: vi.fn(), - writeFile: vi.fn(), - unlink: vi.fn(), - }; - let mockFetch: ReturnType>; let mockPipeline: ReturnType>; - let mockTransformConstructor: typeof Transform; - let mockCreateWriteStream: ReturnType>; - let mockUnzip: ReturnType; + let mockTarExtract: ReturnType>; + let mockCreateGunzip: ReturnType>; let mockDeps: MockDeps; - beforeEach(async () => { + beforeEach(() => { vi.clearAllMocks(); mockFetch = vi.fn(); - mockPipeline = vi.fn(); - mockTransformConstructor = Transform; - mockCreateWriteStream = vi.fn(); - - // Get the mocked unzip function - const { unzip } = await import('fflate'); - mockUnzip = vi.mocked(unzip); - - // Reset fs mocks - for (const mock of Object.values(mockFs)) { - mock.mockReset(); - } - - // Setup default successful behaviors - mockFs.mkdir.mockResolvedValue(undefined); - mockFs.unlink.mockResolvedValue(undefined); - mockFs.readFile.mockResolvedValue(Buffer.from(mockZipData)); - mockFs.writeFile.mockResolvedValue(undefined); - mockPipeline.mockResolvedValue(undefined); - mockCreateWriteStream.mockReturnValue({ - write: vi.fn(), - end: vi.fn(), - } as unknown as WriteStream); - - // Create mockDeps with type casting for mock objects - // Using 'as unknown as Type' pattern is idiomatic for test mocks + mockPipeline = vi.fn().mockResolvedValue(undefined); + mockTarExtract = vi.fn().mockReturnValue( + new Writable({ + write(_chunk, _enc, cb) { + cb(); + }, + }) as unknown as ReturnType, + ); + mockCreateGunzip = vi.fn().mockReturnValue( + new Transform({ + transform(chunk, _enc, cb) { + cb(null, chunk); + }, + }) as unknown as ReturnType, + ); + mockDeps = { fetch: mockFetch, - fs: mockFs as unknown as typeof fsPromises, pipeline: mockPipeline as unknown as typeof pipelineType, - Transform: mockTransformConstructor, - createWriteStream: mockCreateWriteStream, + Transform, + tarExtract: mockTarExtract as unknown as typeof tarExtractType, + createGunzip: mockCreateGunzip as unknown as typeof zlib.createGunzip, }; }); @@ -99,134 +76,76 @@ describe('gitHubArchive', () => { retries: 3, }; - test('should successfully download and extract archive', async () => { - // Mock successful response with stream - const mockStream = new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }); - - mockFetch.mockResolvedValue({ + const createMockResponse = (overrides: Partial = {}): Response => { + return { ok: true, status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: mockStream, - } as unknown as Response); - - // Mock unzip to extract files - mockUnzip.mockImplementation((_data, callback) => { - const extracted = { - 'repomix-main/': new Uint8Array(0), // Directory - 'repomix-main/test.txt': new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]), // "hello" - }; - callback(null, extracted); - }); + headers: new Map([['content-length', mockStreamData.length.toString()]]), + body: new ReadableStream({ + start(controller) { + controller.enqueue(mockStreamData); + controller.close(); + }, + }), + ...overrides, + } as unknown as Response; + }; - await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, mockOptions, undefined, mockDeps); + test('should successfully download and extract archive', async () => { + mockFetch.mockResolvedValue(createMockResponse()); - // Verify directory creation - expect(mockFs.mkdir).toHaveBeenCalledWith(mockTargetDirectory, { recursive: true }); + await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, mockOptions, undefined, mockDeps); - // Verify fetch was called + // Verify fetch was called with tar.gz URL expect(mockFetch).toHaveBeenCalledWith( - 'https://github.com/yamadashy/repomix/archive/refs/heads/main.zip', + 'https://github.com/yamadashy/repomix/archive/refs/heads/main.tar.gz', expect.objectContaining({ signal: expect.any(AbortSignal), }), ); - // Verify file operations - expect(mockFs.writeFile).toHaveBeenCalledWith( - path.resolve(mockTargetDirectory, 'test.txt'), - expect.any(Uint8Array), - ); + // Verify tar extract was called with correct options + expect(mockTarExtract).toHaveBeenCalledWith({ + cwd: mockTargetDirectory, + strip: 1, + }); - // Verify cleanup - expect(mockFs.unlink).toHaveBeenCalledWith(path.join(mockTargetDirectory, 'repomix-main.zip')); + // Verify streaming pipeline was used + expect(mockPipeline).toHaveBeenCalledTimes(1); }); test('should handle progress callback', async () => { const mockProgressCallback: ProgressCallback = vi.fn(); - - const mockStream = new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }); - - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: mockStream, - } as unknown as Response); - - mockUnzip.mockImplementation((_data, callback) => { - callback(null, {}); - }); + mockFetch.mockResolvedValue(createMockResponse()); await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, mockOptions, mockProgressCallback, mockDeps); - // Progress callback is called via Transform stream, which is handled internally - // Just verify the download completed successfully expect(mockFetch).toHaveBeenCalled(); - expect(mockUnzip).toHaveBeenCalled(); + expect(mockPipeline).toHaveBeenCalled(); }); - test('should retry on failure', async () => { - // First two attempts fail, third succeeds + test('should retry on network failure', async () => { mockFetch .mockRejectedValueOnce(new Error('Network error')) .mockRejectedValueOnce(new Error('Network error')) - .mockResolvedValueOnce({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); + .mockResolvedValueOnce(createMockResponse()); - mockUnzip.mockImplementation((_data, callback) => { - callback(null, {}); - }); - - // Use fewer retries to speed up test await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 2 }, undefined, mockDeps); expect(mockFetch).toHaveBeenCalledTimes(3); }); test('should try fallback URLs on 404', async () => { - // Mock 404 for main branch, success for master branch mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 404, - headers: new Map(), - body: null, - } as unknown as Response) - .mockResolvedValueOnce({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - mockUnzip.mockImplementation((_data, callback) => { - callback(null, {}); - }); + .mockResolvedValueOnce( + createMockResponse({ + ok: false, + status: 404, + headers: new Map(), + body: null, + } as unknown as Partial), + ) + .mockResolvedValueOnce(createMockResponse()); const repoInfoNoRef = { owner: 'yamadashy', repo: 'repomix' }; @@ -234,11 +153,11 @@ describe('gitHubArchive', () => { // Should try HEAD first, then master branch expect(mockFetch).toHaveBeenCalledWith( - 'https://github.com/yamadashy/repomix/archive/HEAD.zip', + 'https://github.com/yamadashy/repomix/archive/HEAD.tar.gz', expect.any(Object), ); expect(mockFetch).toHaveBeenCalledWith( - 'https://github.com/yamadashy/repomix/archive/refs/heads/master.zip', + 'https://github.com/yamadashy/repomix/archive/refs/heads/master.tar.gz', expect.any(Object), ); }); @@ -250,28 +169,13 @@ describe('gitHubArchive', () => { downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 2 }, undefined, mockDeps), ).rejects.toThrow(RepomixError); - // Multiple URLs are tried even with ref: main, fallback, tag // 2 retries × 2 URLs (main + tag for "main" ref) = 4 total attempts expect(mockFetch).toHaveBeenCalledTimes(4); }); - test('should handle ZIP extraction error', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - // Mock unzip to fail - mockUnzip.mockImplementation((_data, callback) => { - callback(new Error('Invalid ZIP file')); - }); + test('should handle extraction error', async () => { + mockFetch.mockResolvedValue(createMockResponse()); + mockPipeline.mockRejectedValue(new Error('tar extraction failed')); await expect( downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 1 }, undefined, mockDeps), @@ -279,22 +183,8 @@ describe('gitHubArchive', () => { }); test('should not retry on extraction error', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - // Mock unzip to fail - mockUnzip.mockImplementation((_data, callback) => { - callback(new Error('Invalid ZIP file')); - }); + mockFetch.mockResolvedValue(createMockResponse()); + mockPipeline.mockRejectedValue(new RepomixError('Failed to extract archive: tar error')); await expect( downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 3 }, undefined, mockDeps), @@ -304,107 +194,8 @@ describe('gitHubArchive', () => { expect(mockFetch).toHaveBeenCalledTimes(1); }); - test('should handle path traversal attack', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - // Mock unzip with dangerous paths - mockUnzip.mockImplementation((_data, callback) => { - const extracted = { - 'repomix-main/../../../etc/passwd': new Uint8Array([0x65, 0x76, 0x69, 0x6c]), // "evil" - 'repomix-main/safe.txt': new Uint8Array([0x73, 0x61, 0x66, 0x65]), // "safe" - }; - callback(null, extracted); - }); - - await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, mockOptions, undefined, mockDeps); - - // Should write both files - the path normalization doesn't completely prevent this case - expect(mockFs.writeFile).toHaveBeenCalledWith( - path.resolve(mockTargetDirectory, 'safe.txt'), - expect.any(Uint8Array), - ); - - // Verify that both files are written (one was sanitized to remove path traversal) - expect(mockFs.writeFile).toHaveBeenCalledTimes(2); - }); - - test('should handle absolute paths in ZIP', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - // Mock unzip with absolute path - mockUnzip.mockImplementation((_data, callback) => { - const extracted = { - '/etc/passwd': new Uint8Array([0x65, 0x76, 0x69, 0x6c]), // "evil" - 'repomix-main/safe.txt': new Uint8Array([0x73, 0x61, 0x66, 0x65]), // "safe" - }; - callback(null, extracted); - }); - - await downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, mockOptions, undefined, mockDeps); - - // Should only write safe file, not the absolute path - expect(mockFs.writeFile).toHaveBeenCalledWith( - path.resolve(mockTargetDirectory, 'safe.txt'), - expect.any(Uint8Array), - ); - - // Should not write the absolute path file - expect(mockFs.writeFile).not.toHaveBeenCalledWith('/etc/passwd', expect.any(Uint8Array)); - }); - - test('should cleanup archive file even on extraction failure', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map([['content-length', mockZipData.length.toString()]]), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - - // Mock unzip to fail - mockUnzip.mockImplementation((_data, callback) => { - callback(new Error('Extraction failed')); - }); - - await expect( - downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 1 }, undefined, mockDeps), - ).rejects.toThrow(); - - // Should still attempt cleanup - expect(mockFs.unlink).toHaveBeenCalledWith(path.join(mockTargetDirectory, 'repomix-main.zip')); - }); - test('should handle missing response body', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Map(), - body: null, - } as unknown as Response); + mockFetch.mockResolvedValue(createMockResponse({ body: null } as unknown as Partial)); await expect( downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 1 }, undefined, mockDeps), @@ -412,27 +203,22 @@ describe('gitHubArchive', () => { }); test('should handle timeout', async () => { - // Mock a fetch that takes too long mockFetch.mockImplementation( - () => - new Promise((resolve) => { - setTimeout(() => { - resolve({ - ok: true, - status: 200, - headers: new Map(), - body: new ReadableStream({ - start(controller) { - controller.enqueue(mockZipData); - controller.close(); - }, - }), - } as unknown as Response); - }, 100); // Resolve after 100ms, but timeout is 50ms + (_url: string | URL | Request, init?: RequestInit) => + new Promise((resolve, reject) => { + const timer = setTimeout(() => { + resolve(createMockResponse()); + }, 100); + + // Respect AbortSignal so timeout actually cancels the fetch + init?.signal?.addEventListener('abort', () => { + clearTimeout(timer); + reject(new DOMException('The operation was aborted', 'AbortError')); + }); }), ); - const shortTimeout = 50; // 50ms timeout for faster test + const shortTimeout = 50; await expect( downloadGitHubArchive( diff --git a/tests/core/git/gitHubArchiveApi.test.ts b/tests/core/git/gitHubArchiveApi.test.ts index fdfd8cff8..383eaf060 100644 --- a/tests/core/git/gitHubArchiveApi.test.ts +++ b/tests/core/git/gitHubArchiveApi.test.ts @@ -14,25 +14,25 @@ describe('GitHub Archive API', () => { test('should build URL for default branch (HEAD)', () => { const repoInfo = { owner: 'user', repo: 'repo' }; const url = buildGitHubArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/HEAD.zip'); + expect(url).toBe('https://github.com/user/repo/archive/HEAD.tar.gz'); }); test('should build URL for specific branch', () => { const repoInfo = { owner: 'user', repo: 'repo', ref: 'develop' }; const url = buildGitHubArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/refs/heads/develop.zip'); + expect(url).toBe('https://github.com/user/repo/archive/refs/heads/develop.tar.gz'); }); test('should build URL for commit SHA', () => { const repoInfo = { owner: 'user', repo: 'repo', ref: 'abc123def456' }; const url = buildGitHubArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/abc123def456.zip'); + expect(url).toBe('https://github.com/user/repo/archive/abc123def456.tar.gz'); }); test('should build URL for full commit SHA', () => { const repoInfo = { owner: 'user', repo: 'repo', ref: 'abc123def456789012345678901234567890abcd' }; const url = buildGitHubArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/abc123def456789012345678901234567890abcd.zip'); + expect(url).toBe('https://github.com/user/repo/archive/abc123def456789012345678901234567890abcd.tar.gz'); }); }); @@ -40,7 +40,7 @@ describe('GitHub Archive API', () => { test('should build URL for master branch fallback', () => { const repoInfo = { owner: 'user', repo: 'repo' }; const url = buildGitHubMasterArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/refs/heads/master.zip'); + expect(url).toBe('https://github.com/user/repo/archive/refs/heads/master.tar.gz'); }); test('should return null when ref is specified', () => { @@ -54,7 +54,7 @@ describe('GitHub Archive API', () => { test('should build URL for tag', () => { const repoInfo = { owner: 'user', repo: 'repo', ref: 'v1.0.0' }; const url = buildGitHubTagArchiveUrl(repoInfo); - expect(url).toBe('https://github.com/user/repo/archive/refs/tags/v1.0.0.zip'); + expect(url).toBe('https://github.com/user/repo/archive/refs/tags/v1.0.0.tar.gz'); }); test('should return null for commit SHA', () => { @@ -74,25 +74,25 @@ describe('GitHub Archive API', () => { test('should generate filename for default branch (HEAD)', () => { const repoInfo = { owner: 'user', repo: 'myrepo' }; const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-HEAD.zip'); + expect(filename).toBe('myrepo-HEAD.tar.gz'); }); test('should generate filename for specific branch', () => { const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'develop' }; const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-develop.zip'); + expect(filename).toBe('myrepo-develop.tar.gz'); }); test('should generate filename for tag with slash', () => { const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'release/v1.0' }; const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-v1.0.zip'); + expect(filename).toBe('myrepo-v1.0.tar.gz'); }); test('should generate filename for commit SHA', () => { const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'abc123' }; const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-abc123.zip'); + expect(filename).toBe('myrepo-abc123.tar.gz'); }); }); From 5262f854405fed164cda972cde5654b838560498 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 18 Feb 2026 22:39:56 +0900 Subject: [PATCH 02/14] refactor(core): Remove Bun worker_threads workaround The workaround that forced child_process runtime under Bun was added to prevent hangs caused by fileCollect worker threads. Since fileCollect has been migrated to a promise pool on the main thread, the hang no longer occurs and this workaround is unnecessary. --- src/shared/processConcurrency.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/shared/processConcurrency.ts b/src/shared/processConcurrency.ts index da268c65b..0507131a2 100644 --- a/src/shared/processConcurrency.ts +++ b/src/shared/processConcurrency.ts @@ -62,14 +62,9 @@ export const getWorkerThreadCount = (numOfTasks: number): { minThreads: number; }; export const createWorkerPool = (options: WorkerOptions): Tinypool => { - const { numOfTasks, workerType, runtime: requestedRuntime = 'child_process' } = options; + const { numOfTasks, workerType, runtime = 'child_process' } = options; const { minThreads, maxThreads } = getWorkerThreadCount(numOfTasks); - // Bun's worker_threads implementation has compatibility issues with Tinypool, - // causing hangs with large task counts. Use child_process runtime as a workaround. - const runtime: WorkerRuntime = - process.versions?.bun && requestedRuntime === 'worker_threads' ? 'child_process' : requestedRuntime; - // Get worker path - uses unified worker in bundled env, individual files otherwise const workerPath = getWorkerPath(workerType); From 44f15b647771240fa20189a40a168fd3a8495abe Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 18 Feb 2026 22:41:26 +0900 Subject: [PATCH 03/14] refactor(core): Remove unused getArchiveFilename function The streaming tar.gz extraction no longer uses temporary files, making this filename generation function unnecessary. --- src/core/git/gitHubArchiveApi.ts | 15 -------------- tests/core/git/gitHubArchiveApi.test.ts | 27 ------------------------- 2 files changed, 42 deletions(-) diff --git a/src/core/git/gitHubArchiveApi.ts b/src/core/git/gitHubArchiveApi.ts index 4c4b31297..e0a0dd932 100644 --- a/src/core/git/gitHubArchiveApi.ts +++ b/src/core/git/gitHubArchiveApi.ts @@ -51,21 +51,6 @@ export const buildGitHubTagArchiveUrl = (repoInfo: GitHubRepoInfo): string | nul return `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/archive/refs/tags/${encodeURIComponent(ref)}.tar.gz`; }; -/** - * Gets the expected archive filename from GitHub - * Format: repo-branch.tar.gz or repo-sha.tar.gz - * Note: This is used as a fallback identifier; streaming extraction does not require temp files - */ -export const getArchiveFilename = (repoInfo: GitHubRepoInfo): string => { - const { repo, ref } = repoInfo; - const refPart = ref || 'HEAD'; - - // GitHub uses the last part of the ref for the filename - const refName = refPart.includes('/') ? refPart.split('/').pop() : refPart; - - return `${repo}-${refName}.tar.gz`; -}; - /** * Checks if a response indicates a GitHub API rate limit or error */ diff --git a/tests/core/git/gitHubArchiveApi.test.ts b/tests/core/git/gitHubArchiveApi.test.ts index 383eaf060..637e96ab1 100644 --- a/tests/core/git/gitHubArchiveApi.test.ts +++ b/tests/core/git/gitHubArchiveApi.test.ts @@ -4,7 +4,6 @@ import { buildGitHubMasterArchiveUrl, buildGitHubTagArchiveUrl, checkGitHubResponse, - getArchiveFilename, } from '../../../src/core/git/gitHubArchiveApi.js'; import { parseGitHubRepoInfo } from '../../../src/core/git/gitRemoteParse.js'; import { RepomixError } from '../../../src/shared/errorHandle.js'; @@ -70,32 +69,6 @@ describe('GitHub Archive API', () => { }); }); - describe('getArchiveFilename', () => { - test('should generate filename for default branch (HEAD)', () => { - const repoInfo = { owner: 'user', repo: 'myrepo' }; - const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-HEAD.tar.gz'); - }); - - test('should generate filename for specific branch', () => { - const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'develop' }; - const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-develop.tar.gz'); - }); - - test('should generate filename for tag with slash', () => { - const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'release/v1.0' }; - const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-v1.0.tar.gz'); - }); - - test('should generate filename for commit SHA', () => { - const repoInfo = { owner: 'user', repo: 'myrepo', ref: 'abc123' }; - const filename = getArchiveFilename(repoInfo); - expect(filename).toBe('myrepo-abc123.tar.gz'); - }); - }); - describe('checkGitHubResponse', () => { test('should not throw for successful response', () => { const mockResponse = new Response('', { status: 200 }); From 69bc23990e8ae9015376e8c9e2ae02331a5b8d1e Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 18 Feb 2026 23:05:08 +0900 Subject: [PATCH 04/14] fix(core): Allow retry on network errors during streaming extraction The pipeline catch block was wrapping all errors with 'Failed to extract' prefix, causing the retry logic to treat transient network errors (ECONNRESET, ETIMEDOUT) as non-retryable extraction errors. Remove the catch wrapper so errors propagate as-is, allowing the retry loop to handle network failures correctly. --- src/core/git/gitHubArchive.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/git/gitHubArchive.ts b/src/core/git/gitHubArchive.ts index d62243b67..bcecacdcc 100644 --- a/src/core/git/gitHubArchive.ts +++ b/src/core/git/gitHubArchive.ts @@ -178,8 +178,6 @@ const downloadAndExtractArchive = async ( try { await deps.pipeline(nodeStream, progressStream, gunzipStream, extractStream); - } catch (pipelineError) { - throw new RepomixError(`Failed to extract archive: ${(pipelineError as Error).message}`); } finally { // Explicitly destroy streams to release handles. // Bun's pipeline() may not fully clean up, causing subsequent worker_threads to hang. From 618dbd9db9793eec864e1e15b14a6d6651d8cc76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 05:08:50 +0000 Subject: [PATCH 05/14] chore(deps): update dependency @types/node to ^24.10.13 --- scripts/memory/package-lock.json | 2 +- scripts/memory/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/memory/package-lock.json b/scripts/memory/package-lock.json index d40eb47a2..c180ead21 100644 --- a/scripts/memory/package-lock.json +++ b/scripts/memory/package-lock.json @@ -13,7 +13,7 @@ "repomix": "file:../.." }, "devDependencies": { - "@types/node": "^24.10.11", + "@types/node": "^24.10.13", "typescript": "^5.9.3" }, "engines": { diff --git a/scripts/memory/package.json b/scripts/memory/package.json index be01b0460..4227f4611 100644 --- a/scripts/memory/package.json +++ b/scripts/memory/package.json @@ -19,7 +19,7 @@ "repomix": "file:../.." }, "devDependencies": { - "@types/node": "^24.10.11", + "@types/node": "^24.10.13", "typescript": "^5.9.3" }, "engines": { From 157596a43279d47983588992e4aa911945c6efb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:14:26 +0000 Subject: [PATCH 06/14] chore(deps): bump ajv from 8.17.1 to 8.18.0 in /website/server Bumps [ajv](https://github.com/ajv-validator/ajv) from 8.17.1 to 8.18.0. - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](https://github.com/ajv-validator/ajv/compare/v8.17.1...v8.18.0) --- updated-dependencies: - dependency-name: ajv dependency-version: 8.18.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/server/package-lock.json b/website/server/package-lock.json index 1b3c3a2c5..b0c4dc5d8 100644 --- a/website/server/package-lock.json +++ b/website/server/package-lock.json @@ -1784,9 +1784,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", From 3171b57e40aa12ee6f3ca85bee2e75c4cd62d4eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:58:05 +0000 Subject: [PATCH 07/14] chore(deps): update browser non-major dependencies --- browser/package-lock.json | 90 +++++++++++++++++++-------------------- browser/package.json | 6 +-- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/browser/package-lock.json b/browser/package-lock.json index a636a508d..698f4be84 100644 --- a/browser/package-lock.json +++ b/browser/package-lock.json @@ -8,14 +8,14 @@ "license": "MIT", "devDependencies": { "@types/chrome": "^0.1.36", - "@types/node": "^24.10.11", - "@typescript/native-preview": "^7.0.0-dev.20260207.1", + "@types/node": "^24.10.13", + "@typescript/native-preview": "^7.0.0-dev.20260216.1", "jsdom": "^27.4.0", "sharp": "^0.34.5", "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^4.0.18", - "wxt": "^0.20.14" + "wxt": "^0.20.17" }, "engines": { "node": ">=24.0.1" @@ -1884,28 +1884,28 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-BDM0ZLf2v6ilR0tDi8OMEr4X08lFCToPk3/p1SSE4GhagzmlU/5b+9slR0kKtaKMrds01FhvaKx6U9+NmAWgbQ==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-NEifR9F/0khbTQRztM4Yuxcj9dFuK9ubWIXJwLSmKMlncSp4u1fzRnlfv1vlNKKrXB7BUXoANFHpsM5BEXJ06w==", "dev": true, "license": "Apache-2.0", "bin": { "tsgo": "bin/tsgo.js" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260214.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260214.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20260214.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260214.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20260214.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260214.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20260214.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260223.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260223.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260223.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260223.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260223.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260223.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260223.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-Jb2WcLGpTOC6x58e8QPYC/14xmDbnbFIuKqUvYoI77hVtojVyxZi8L5Y4CgYqXYx8vRWmIFk35c1OGdtPip6Sg==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-uDvCfIGr3PR8iKBA6OCNq6w0b2WMvmtkS8KUZVy04CH8ieFsxChYStLiyFTDX4GZs9BtWKeth/7qGDZewY20sQ==", "cpu": [ "arm64" ], @@ -1917,9 +1917,9 @@ ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-O9l2gVuQFZsb8NIQtu0HN5Tn/Hw2fwylPOPS/0Y4oW+FUMhkqtvetUkb3zZ0qj7capilZ4YnmyGYg3TDqkP4Nw==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-hOKQicSgd1DhFbsqdpC5fMgg0R46sYbbtVjfXgYTAHg/WO6whfZ2SfPy9IIzsQ/CXYUZuwoJElCnc9DTcd66+w==", "cpu": [ "x64" ], @@ -1931,9 +1931,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-TaFrVnx3iXtl/oH1hzwvFyqWj9tzkjW8Ufl2m0Vx2/7GXnzZadm2KA6tFpGbzzWbZJznmXxKHL4O3AZRQYyZqQ==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-FVq6XjzqtLC1MVgQiumwpuW7Ug+S+WVEbvCUJQhrs8Szbf6fIFU/6+D6fOGCKzzo9SAD6zq2RNHtejBw74JSFA==", "cpu": [ "arm" ], @@ -1945,9 +1945,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-Hl4e3yxJqzIGgFI8aH/rLGW+a7kSLHJCpAd5JOLG7hHKnamZF4SjlunnoHLV4IcMri+G6UE3W/84i0QvQP5wLA==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-oRt0l3O/itqBEwd5rhfDAyziEzbSgWar1NShduK4n2mHWTHCI1I7mFsbSPbox2pdrqOwOr0QW8xu7xEgDWWRXA==", "cpu": [ "arm64" ], @@ -1959,9 +1959,9 @@ ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-a/JypIXTc/tdodhYdQm24WH6aTfnJJjDbwxce4BS2g6IzYSc2GFcZBvlq1CJYS2FAVLpiSxj0OFAZmgjpCDAKg==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-qpFTW7q8Vvq1v/0bzfT8+D0wLjqydIP0qKlomrEGLlMnCCAnPodo2oLc2JCtacc40TSMZZARvhctTszCn1gWBA==", "cpu": [ "x64" ], @@ -1973,9 +1973,9 @@ ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-MJGPEDvdXj8olcWH0P+cWYcaN4r/0J4aSbcaISlen3MZ/2hrrgNl46PV4eGJKKCDniY2pH2fJzrMyJWZOcdb0w==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-HHu63F8cDhgIlqFGBnqBVQn7HSiORxyT0M6yPzG4tG4gdzx+aFUdogbYily0nzN5b6NolQTrFfh3Q85UfHCHqg==", "cpu": [ "arm64" ], @@ -1987,9 +1987,9 @@ ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20260214.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260214.1.tgz", - "integrity": "sha512-BtF48TRUyiCKznlOcQ7r7EXhonGSanm9X2eu7d8Yq1vaWO5SDgB0e+ISQXSoIfs3a1S3d5S5QV/vTE4+vocPxA==", + "version": "7.0.0-dev.20260223.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260223.1.tgz", + "integrity": "sha512-vSis36O5qT+vOYfei7GtfWWzvIoaNdmxa1zDypBKkGGCCHt/c5vp0pXls85+8jBVS11Ep6p7ECcHlt+R5CBaug==", "cpu": [ "x64" ], @@ -2139,9 +2139,9 @@ "license": "MIT" }, "node_modules/@wxt-dev/browser": { - "version": "0.1.36", - "resolved": "https://registry.npmjs.org/@wxt-dev/browser/-/browser-0.1.36.tgz", - "integrity": "sha512-48Wn8pItPNg7rCdy10c73z6Alto0z5xkBZohPApKK4/uC8C70j9tJRlaxXdHF/u8+SMZGdrvYtz16oLrOxBu6g==", + "version": "0.1.37", + "resolved": "https://registry.npmjs.org/@wxt-dev/browser/-/browser-0.1.37.tgz", + "integrity": "sha512-I32XWCNRy2W6UgbaVXz8BHGBGtm8urGRRBrcNLagUBXTrBi7wCE6zWePUvvK+nUl7qUCZ7iQ1ufdP0c1DEWisw==", "dev": true, "license": "MIT", "dependencies": { @@ -8055,9 +8055,9 @@ } }, "node_modules/wxt": { - "version": "0.20.17", - "resolved": "https://registry.npmjs.org/wxt/-/wxt-0.20.17.tgz", - "integrity": "sha512-3M3og5cKYTGBoBkVbHO+Qjq+SpGQxNkcFe2YZFZjWw8QcIgmCpyxrAsvOGvzXtn7KerxHDf5VvEvf/OvlGzhFg==", + "version": "0.20.18", + "resolved": "https://registry.npmjs.org/wxt/-/wxt-0.20.18.tgz", + "integrity": "sha512-BYnIAFkdJcC8BXzbh4PzmRhOQ5xKELEk45qntzqojW5X1+VGm0GsjaEKSCQnTP72/3jZMDH1pmlEdkY/fPXehg==", "dev": true, "license": "MIT", "dependencies": { @@ -8066,7 +8066,7 @@ "@webext-core/fake-browser": "^1.3.4", "@webext-core/isolated-element": "^1.1.3", "@webext-core/match-patterns": "^1.0.3", - "@wxt-dev/browser": "^0.1.36", + "@wxt-dev/browser": "^0.1.37", "@wxt-dev/storage": "^1.0.0", "async-mutex": "^0.5.0", "c12": "^3.3.3", @@ -8100,7 +8100,7 @@ "perfect-debounce": "^2.1.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", - "publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.0", + "publish-browser-extension": "^2.3.0 || ^3.0.2 || ^4.0.4", "scule": "^1.3.0", "unimport": "^3.13.1 || ^4.0.0 || ^5.0.0", "vite": "^5.4.19 || ^6.3.4 || ^7.0.0", @@ -8621,9 +8621,9 @@ } }, "node_modules/wxt/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/browser/package.json b/browser/package.json index 68f445a13..c102495b9 100644 --- a/browser/package.json +++ b/browser/package.json @@ -34,14 +34,14 @@ "license": "MIT", "devDependencies": { "@types/chrome": "^0.1.36", - "@types/node": "^24.10.11", - "@typescript/native-preview": "^7.0.0-dev.20260207.1", + "@types/node": "^24.10.13", + "@typescript/native-preview": "^7.0.0-dev.20260216.1", "jsdom": "^27.4.0", "sharp": "^0.34.5", "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^4.0.18", - "wxt": "^0.20.14" + "wxt": "^0.20.17" }, "engines": { "node": ">=24.0.1" From 2e98b5cc18b011d421248e90e228a1c540745930 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Mon, 23 Feb 2026 22:58:56 +0900 Subject: [PATCH 08/14] refactor(core): Remove dead isExtractionError check from retry logic With the streaming pipeline, errors propagate as native Error objects rather than RepomixError, so the isExtractionError check was always false. Retrying extraction errors is acceptable since the retry loop is bounded to 3 attempts. --- src/core/git/gitHubArchive.ts | 6 ------ tests/core/git/gitHubArchive.test.ts | 12 ------------ 2 files changed, 18 deletions(-) diff --git a/src/core/git/gitHubArchive.ts b/src/core/git/gitHubArchive.ts index bcecacdcc..e5e96fa57 100644 --- a/src/core/git/gitHubArchive.ts +++ b/src/core/git/gitHubArchive.ts @@ -75,12 +75,6 @@ export const downloadGitHubArchive = async ( lastError = error as Error; logger.trace(`Archive download attempt ${attempt} failed:`, lastError.message); - // If it's an extraction error, don't retry - the same archive will fail again - const isExtractionError = lastError instanceof RepomixError && lastError.message.includes('Failed to extract'); - if (isExtractionError) { - throw lastError; - } - // If it's a 404-like error and we have more URLs to try, don't retry this URL const isNotFoundError = lastError instanceof RepomixError && diff --git a/tests/core/git/gitHubArchive.test.ts b/tests/core/git/gitHubArchive.test.ts index a3b28cad3..846404b0b 100644 --- a/tests/core/git/gitHubArchive.test.ts +++ b/tests/core/git/gitHubArchive.test.ts @@ -182,18 +182,6 @@ describe('gitHubArchive', () => { ).rejects.toThrow(RepomixError); }); - test('should not retry on extraction error', async () => { - mockFetch.mockResolvedValue(createMockResponse()); - mockPipeline.mockRejectedValue(new RepomixError('Failed to extract archive: tar error')); - - await expect( - downloadGitHubArchive(mockRepoInfo, mockTargetDirectory, { retries: 3 }, undefined, mockDeps), - ).rejects.toThrow(RepomixError); - - // Should only fetch once - extraction errors should not trigger retries - expect(mockFetch).toHaveBeenCalledTimes(1); - }); - test('should handle missing response body', async () => { mockFetch.mockResolvedValue(createMockResponse({ body: null } as unknown as Partial)); From bddbc3ea60e5f6068d578ce9d31c128491a3c822 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:13:34 +0000 Subject: [PATCH 09/14] chore(deps): bump fast-xml-parser from 5.3.4 to 5.3.6 Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.4 to 5.3.6. - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.4...v5.3.6) --- updated-dependencies: - dependency-name: fast-xml-parser dependency-version: 5.3.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d45ceb268..c042cd96a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2712,9 +2712,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", "funding": [ { "type": "github", @@ -2723,7 +2723,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4871,9 +4871,9 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", From c0085e7d4c2aa37da0ce33436dc6afba4c97372d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:13:49 +0000 Subject: [PATCH 10/14] chore(deps): bump fast-xml-parser from 5.3.4 to 5.3.6 in /website/server Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 5.3.4 to 5.3.6. - [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases) - [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v5.3.4...v5.3.6) --- updated-dependencies: - dependency-name: fast-xml-parser dependency-version: 5.3.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/server/package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/server/package-lock.json b/website/server/package-lock.json index 1b3c3a2c5..3bc41cf1b 100644 --- a/website/server/package-lock.json +++ b/website/server/package-lock.json @@ -2434,9 +2434,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", + "integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", "funding": [ { "type": "github", @@ -2445,7 +2445,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" From 2f90ea657ec75e68c05325fdf494e09f6338c235 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:14:58 +0000 Subject: [PATCH 11/14] chore(deps): update dependency minimatch to v10.2.1 [security] --- package-lock.json | 50 +++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index d45ceb268..7eaf050d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -797,27 +797,6 @@ "hono": "^4" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -2093,6 +2072,15 @@ "node": ">=8" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/binary-extensions": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-3.1.0.tgz", @@ -2149,6 +2137,18 @@ "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==" }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -3704,12 +3704,12 @@ } }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { "node": "20 || >=22" From d56a98d2c7fd617121e2f44f27309511bf1c51f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:16:36 +0000 Subject: [PATCH 12/14] chore(deps): update dependency hono to v4.11.10 [security] --- website/server/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/server/package-lock.json b/website/server/package-lock.json index 1b3c3a2c5..c577b8a74 100644 --- a/website/server/package-lock.json +++ b/website/server/package-lock.json @@ -2797,9 +2797,9 @@ } }, "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", + "version": "4.11.10", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.10.tgz", + "integrity": "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==", "license": "MIT", "engines": { "node": ">=16.9.0" From a4e1af21955f40918b516c2cad187576d6b0de13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:44:34 +0000 Subject: [PATCH 13/14] chore(deps): update dependency fast-xml-parser to v5.3.6 [security] --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7eaf050d1..1de9bc49d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2712,9 +2712,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", "funding": [ { "type": "github", @@ -2723,7 +2723,7 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -4871,9 +4871,9 @@ } }, "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", From 137f6e546a5e8f9b6b3bdbd6e6caf3e18500e954 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:46:55 +0000 Subject: [PATCH 14/14] chore(deps): update anthropics/claude-code-action action to v1.0.52 --- .github/workflows/claude-code-review.yml | 2 +- .github/workflows/claude-issue-similar.yml | 2 +- .github/workflows/claude-issue-triage.yml | 2 +- .github/workflows/claude.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index da859bed5..1e671579b 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -36,7 +36,7 @@ jobs: - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@b113f49a56229d8276e2bf05743ad6900121239c # v1.0.45 + uses: anthropics/claude-code-action@68cfeead1890300cc87935dbe2c023825be87b8a # v1.0.52 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} prompt: "/git:pr-review REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}" diff --git a/.github/workflows/claude-issue-similar.yml b/.github/workflows/claude-issue-similar.yml index 8fa04c16c..5478ba534 100644 --- a/.github/workflows/claude-issue-similar.yml +++ b/.github/workflows/claude-issue-similar.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 1 - name: Find Similar Issues - uses: anthropics/claude-code-action@b113f49a56229d8276e2bf05743ad6900121239c # v1.0.45 + uses: anthropics/claude-code-action@68cfeead1890300cc87935dbe2c023825be87b8a # v1.0.52 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/claude-issue-triage.yml b/.github/workflows/claude-issue-triage.yml index aaa546374..a273cb7b0 100644 --- a/.github/workflows/claude-issue-triage.yml +++ b/.github/workflows/claude-issue-triage.yml @@ -23,7 +23,7 @@ jobs: fetch-depth: 1 - name: Run Claude Issue Triage - uses: anthropics/claude-code-action@b113f49a56229d8276e2bf05743ad6900121239c # v1.0.45 + uses: anthropics/claude-code-action@68cfeead1890300cc87935dbe2c023825be87b8a # v1.0.52 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 998403dc3..d2307042c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -32,7 +32,7 @@ jobs: - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@b113f49a56229d8276e2bf05743ad6900121239c # v1.0.45 + uses: anthropics/claude-code-action@68cfeead1890300cc87935dbe2c023825be87b8a # v1.0.52 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}