diff --git a/apps/api/package.json b/apps/api/package.json index 0eb7467..8a1dd3c 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -36,10 +36,14 @@ "@nestjs/websockets": "^11.1.12", "@prisma/client": "^6.19.2", "@types/marked": "^6.0.0", + "adm-zip": "^0.5.16", + "archiver": "^7.0.1", "better-auth": "^1.4.17", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", + "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", + "ioredis": "^5.9.2", "marked": "^17.0.1", "marked-gfm-heading-id": "^4.1.3", "marked-highlight": "^2.2.3", @@ -57,9 +61,11 @@ "@nestjs/schematics": "^11.0.1", "@nestjs/testing": "^11.1.12", "@swc/core": "^1.10.18", + "@types/adm-zip": "^0.5.7", + "@types/archiver": "^7.0.0", "@types/express": "^5.0.1", "@types/highlight.js": "^10.1.0", - "@types/ioredis": "^5.0.0", + "@types/multer": "^2.0.0", "@types/node": "^22.13.4", "@types/sanitize-html": "^2.16.0", "@vitest/coverage-v8": "^4.0.18", diff --git a/apps/api/src/knowledge/dto/import-export.dto.ts b/apps/api/src/knowledge/dto/import-export.dto.ts new file mode 100644 index 0000000..9bb1503 --- /dev/null +++ b/apps/api/src/knowledge/dto/import-export.dto.ts @@ -0,0 +1,51 @@ +import { + IsString, + IsOptional, + IsEnum, + IsArray, +} from "class-validator"; + +/** + * Export format enum + */ +export enum ExportFormat { + MARKDOWN = "markdown", + JSON = "json", +} + +/** + * DTO for export query parameters + */ +export class ExportQueryDto { + @IsOptional() + @IsEnum(ExportFormat, { message: "format must be either 'markdown' or 'json'" }) + format?: ExportFormat = ExportFormat.MARKDOWN; + + @IsOptional() + @IsArray({ message: "entryIds must be an array" }) + @IsString({ each: true, message: "each entryId must be a string" }) + entryIds?: string[]; +} + +/** + * Import result for a single entry + */ +export interface ImportResult { + filename: string; + success: boolean; + entryId?: string; + slug?: string; + title?: string; + error?: string; +} + +/** + * Response DTO for import operation + */ +export interface ImportResponseDto { + success: boolean; + totalFiles: number; + imported: number; + failed: number; + results: ImportResult[]; +} diff --git a/apps/api/src/knowledge/dto/index.ts b/apps/api/src/knowledge/dto/index.ts index f33dd5a..abf3f20 100644 --- a/apps/api/src/knowledge/dto/index.ts +++ b/apps/api/src/knowledge/dto/index.ts @@ -10,3 +10,5 @@ export { RecentEntriesDto, } from "./search-query.dto"; export { GraphQueryDto } from "./graph-query.dto"; +export { ExportQueryDto, ExportFormat } from "./import-export.dto"; +export type { ImportResult, ImportResponseDto } from "./import-export.dto"; diff --git a/apps/api/src/knowledge/import-export.controller.ts b/apps/api/src/knowledge/import-export.controller.ts new file mode 100644 index 0000000..b0176c7 --- /dev/null +++ b/apps/api/src/knowledge/import-export.controller.ts @@ -0,0 +1,126 @@ +import { + Controller, + Post, + Get, + Query, + UseGuards, + UseInterceptors, + UploadedFile, + Res, + BadRequestException, +} from "@nestjs/common"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { Response } from "express"; +import { ImportExportService } from "./services/import-export.service"; +import { ExportQueryDto, ExportFormat, ImportResponseDto } from "./dto"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import { WorkspaceGuard, PermissionGuard } from "../common/guards"; +import { Workspace, Permission, RequirePermission } from "../common/decorators"; +import { CurrentUser } from "../auth/decorators/current-user.decorator"; +import type { AuthUser } from "../auth/types/better-auth-request.interface"; + +/** + * Controller for knowledge import/export endpoints + * All endpoints require authentication and workspace context + */ +@Controller("knowledge") +@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard) +export class ImportExportController { + constructor(private readonly importExportService: ImportExportService) {} + + /** + * POST /api/knowledge/import + * Import knowledge entries from uploaded file (.md or .zip) + * Requires: MEMBER role or higher + */ + @Post("import") + @RequirePermission(Permission.WORKSPACE_MEMBER) + @UseInterceptors( + FileInterceptor("file", { + limits: { + fileSize: 50 * 1024 * 1024, // 50MB max file size + }, + fileFilter: (_req, file, callback) => { + // Only accept .md and .zip files + const allowedMimeTypes = [ + "text/markdown", + "application/zip", + "application/x-zip-compressed", + ]; + const allowedExtensions = [".md", ".zip"]; + const fileExtension = file.originalname.toLowerCase().slice( + file.originalname.lastIndexOf(".") + ); + + if ( + allowedMimeTypes.includes(file.mimetype) || + allowedExtensions.includes(fileExtension) + ) { + callback(null, true); + } else { + callback( + new BadRequestException( + "Invalid file type. Only .md and .zip files are accepted." + ), + false + ); + } + }, + }) + ) + async importEntries( + @Workspace() workspaceId: string, + @CurrentUser() user: AuthUser, + @UploadedFile() file: Express.Multer.File + ): Promise { + if (!file) { + throw new BadRequestException("No file uploaded"); + } + + const result = await this.importExportService.importEntries( + workspaceId, + user.id, + file + ); + + return { + success: result.failed === 0, + totalFiles: result.totalFiles, + imported: result.imported, + failed: result.failed, + results: result.results, + }; + } + + /** + * GET /api/knowledge/export + * Export knowledge entries as a zip file + * Query params: + * - format: 'markdown' (default) or 'json' + * - entryIds: optional array of entry IDs to export (exports all if not provided) + * Requires: Any workspace member + */ + @Get("export") + @RequirePermission(Permission.WORKSPACE_ANY) + async exportEntries( + @Workspace() workspaceId: string, + @Query() query: ExportQueryDto, + @Res() res: Response + ): Promise { + const format = query.format || ExportFormat.MARKDOWN; + const entryIds = query.entryIds; + + const { stream, filename } = await this.importExportService.exportEntries( + workspaceId, + format, + entryIds + ); + + // Set response headers + res.setHeader("Content-Type", "application/zip"); + res.setHeader("Content-Disposition", `attachment; filename="${filename}"`); + + // Pipe the archive stream to response + stream.pipe(res); + } +} diff --git a/apps/api/src/knowledge/knowledge.module.ts b/apps/api/src/knowledge/knowledge.module.ts index 92cfa69..06924f9 100644 --- a/apps/api/src/knowledge/knowledge.module.ts +++ b/apps/api/src/knowledge/knowledge.module.ts @@ -5,17 +5,24 @@ import { KnowledgeService } from "./knowledge.service"; import { KnowledgeController } from "./knowledge.controller"; import { SearchController } from "./search.controller"; import { KnowledgeStatsController } from "./stats.controller"; +import { ImportExportController } from "./import-export.controller"; import { LinkResolutionService, SearchService, LinkSyncService, GraphService, StatsService, + ImportExportService, } from "./services"; @Module({ imports: [PrismaModule, AuthModule], - controllers: [KnowledgeController, SearchController, KnowledgeStatsController], + controllers: [ + KnowledgeController, + SearchController, + KnowledgeStatsController, + ImportExportController, + ], providers: [ KnowledgeService, LinkResolutionService, @@ -23,6 +30,7 @@ import { LinkSyncService, GraphService, StatsService, + ImportExportService, ], exports: [KnowledgeService, LinkResolutionService, SearchService], }) diff --git a/apps/api/src/knowledge/services/import-export.service.spec.ts b/apps/api/src/knowledge/services/import-export.service.spec.ts new file mode 100644 index 0000000..c05de87 --- /dev/null +++ b/apps/api/src/knowledge/services/import-export.service.spec.ts @@ -0,0 +1,297 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { Test, TestingModule } from "@nestjs/testing"; +import { BadRequestException } from "@nestjs/common"; +import { ImportExportService } from "./import-export.service"; +import { KnowledgeService } from "../knowledge.service"; +import { PrismaService } from "../../prisma/prisma.service"; +import { ExportFormat } from "../dto"; +import { EntryStatus, Visibility } from "@prisma/client"; + +describe("ImportExportService", () => { + let service: ImportExportService; + let knowledgeService: KnowledgeService; + let prisma: PrismaService; + + const workspaceId = "workspace-123"; + const userId = "user-123"; + + const mockEntry = { + id: "entry-123", + workspaceId, + slug: "test-entry", + title: "Test Entry", + content: "Test content", + summary: "Test summary", + status: EntryStatus.PUBLISHED, + visibility: Visibility.WORKSPACE, + createdAt: new Date(), + updatedAt: new Date(), + tags: [ + { + tag: { + id: "tag-1", + name: "TypeScript", + slug: "typescript", + color: "#3178c6", + }, + }, + ], + }; + + const mockKnowledgeService = { + create: vi.fn(), + findAll: vi.fn(), + }; + + const mockPrismaService = { + knowledgeEntry: { + findMany: vi.fn(), + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ImportExportService, + { + provide: KnowledgeService, + useValue: mockKnowledgeService, + }, + { + provide: PrismaService, + useValue: mockPrismaService, + }, + ], + }).compile(); + + service = module.get(ImportExportService); + knowledgeService = module.get(KnowledgeService); + prisma = module.get(PrismaService); + + vi.clearAllMocks(); + }); + + describe("importEntries", () => { + it("should import a single markdown file successfully", async () => { + const markdown = `--- +title: Test Entry +status: PUBLISHED +tags: + - TypeScript + - Testing +--- + +This is the content of the entry.`; + + const file: Express.Multer.File = { + fieldname: "file", + originalname: "test.md", + encoding: "utf-8", + mimetype: "text/markdown", + size: markdown.length, + buffer: Buffer.from(markdown), + stream: null as any, + destination: "", + filename: "", + path: "", + }; + + mockKnowledgeService.create.mockResolvedValue({ + id: "entry-123", + slug: "test-entry", + title: "Test Entry", + }); + + const result = await service.importEntries(workspaceId, userId, file); + + expect(result.totalFiles).toBe(1); + expect(result.imported).toBe(1); + expect(result.failed).toBe(0); + expect(result.results[0].success).toBe(true); + expect(result.results[0].title).toBe("Test Entry"); + expect(mockKnowledgeService.create).toHaveBeenCalledWith( + workspaceId, + userId, + expect.objectContaining({ + title: "Test Entry", + content: "This is the content of the entry.", + status: EntryStatus.PUBLISHED, + tags: ["TypeScript", "Testing"], + }) + ); + }); + + it("should use filename as title if frontmatter title is missing", async () => { + const markdown = `This is content without frontmatter.`; + + const file: Express.Multer.File = { + fieldname: "file", + originalname: "my-entry.md", + encoding: "utf-8", + mimetype: "text/markdown", + size: markdown.length, + buffer: Buffer.from(markdown), + stream: null as any, + destination: "", + filename: "", + path: "", + }; + + mockKnowledgeService.create.mockResolvedValue({ + id: "entry-123", + slug: "my-entry", + title: "my-entry", + }); + + const result = await service.importEntries(workspaceId, userId, file); + + expect(result.imported).toBe(1); + expect(mockKnowledgeService.create).toHaveBeenCalledWith( + workspaceId, + userId, + expect.objectContaining({ + title: "my-entry", + content: "This is content without frontmatter.", + }) + ); + }); + + it("should reject invalid file types", async () => { + const file: Express.Multer.File = { + fieldname: "file", + originalname: "test.txt", + encoding: "utf-8", + mimetype: "text/plain", + size: 100, + buffer: Buffer.from("test"), + stream: null as any, + destination: "", + filename: "", + path: "", + }; + + await expect( + service.importEntries(workspaceId, userId, file) + ).rejects.toThrow(BadRequestException); + }); + + it("should handle import errors gracefully", async () => { + const markdown = `--- +title: Test Entry +--- + +Content`; + + const file: Express.Multer.File = { + fieldname: "file", + originalname: "test.md", + encoding: "utf-8", + mimetype: "text/markdown", + size: markdown.length, + buffer: Buffer.from(markdown), + stream: null as any, + destination: "", + filename: "", + path: "", + }; + + mockKnowledgeService.create.mockRejectedValue( + new Error("Database error") + ); + + const result = await service.importEntries(workspaceId, userId, file); + + expect(result.totalFiles).toBe(1); + expect(result.imported).toBe(0); + expect(result.failed).toBe(1); + expect(result.results[0].success).toBe(false); + expect(result.results[0].error).toBe("Database error"); + }); + + it("should reject empty markdown content", async () => { + const markdown = `--- +title: Empty Entry +--- + +`; + + const file: Express.Multer.File = { + fieldname: "file", + originalname: "empty.md", + encoding: "utf-8", + mimetype: "text/markdown", + size: markdown.length, + buffer: Buffer.from(markdown), + stream: null as any, + destination: "", + filename: "", + path: "", + }; + + const result = await service.importEntries(workspaceId, userId, file); + + expect(result.imported).toBe(0); + expect(result.failed).toBe(1); + expect(result.results[0].error).toBe("Empty content"); + }); + }); + + describe("exportEntries", () => { + it("should export entries as markdown format", async () => { + mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([mockEntry]); + + const result = await service.exportEntries( + workspaceId, + ExportFormat.MARKDOWN + ); + + expect(result.filename).toMatch(/knowledge-export-\d{4}-\d{2}-\d{2}\.zip/); + expect(result.stream).toBeDefined(); + expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith({ + where: { workspaceId }, + include: { + tags: { + include: { + tag: true, + }, + }, + }, + orderBy: { + title: "asc", + }, + }); + }); + + it("should export only specified entries", async () => { + const entryIds = ["entry-123", "entry-456"]; + mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([mockEntry]); + + await service.exportEntries(workspaceId, ExportFormat.JSON, entryIds); + + expect(mockPrismaService.knowledgeEntry.findMany).toHaveBeenCalledWith({ + where: { + workspaceId, + id: { in: entryIds }, + }, + include: { + tags: { + include: { + tag: true, + }, + }, + }, + orderBy: { + title: "asc", + }, + }); + }); + + it("should throw error when no entries found", async () => { + mockPrismaService.knowledgeEntry.findMany.mockResolvedValue([]); + + await expect( + service.exportEntries(workspaceId, ExportFormat.MARKDOWN) + ).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/api/src/knowledge/services/import-export.service.ts b/apps/api/src/knowledge/services/import-export.service.ts new file mode 100644 index 0000000..e31e6dc --- /dev/null +++ b/apps/api/src/knowledge/services/import-export.service.ts @@ -0,0 +1,372 @@ +import { Injectable, BadRequestException } from "@nestjs/common"; +import { EntryStatus, Visibility } from "@prisma/client"; +import archiver from "archiver"; +import AdmZip from "adm-zip"; +import matter from "gray-matter"; +import { Readable } from "stream"; +import { PrismaService } from "../../prisma/prisma.service"; +import { KnowledgeService } from "../knowledge.service"; +import type { ExportFormat, ImportResult } from "../dto"; +import type { CreateEntryDto } from "../dto/create-entry.dto"; + +interface ExportEntry { + id: string; + slug: string; + title: string; + content: string; + summary: string | null; + status: EntryStatus; + visibility: Visibility; + tags: string[]; + createdAt: Date; + updatedAt: Date; +} + +/** + * Service for handling knowledge entry import/export operations + */ +@Injectable() +export class ImportExportService { + constructor( + private readonly prisma: PrismaService, + private readonly knowledgeService: KnowledgeService + ) {} + + /** + * Import entries from uploaded file(s) + * Accepts single .md file or .zip containing multiple .md files + */ + async importEntries( + workspaceId: string, + userId: string, + file: Express.Multer.File + ): Promise<{ results: ImportResult[]; totalFiles: number; imported: number; failed: number }> { + const results: ImportResult[] = []; + + try { + if (file.mimetype === "text/markdown" || file.originalname.endsWith(".md")) { + // Single markdown file + const result = await this.importSingleMarkdown( + workspaceId, + userId, + file.originalname, + file.buffer.toString("utf-8") + ); + results.push(result); + } else if ( + file.mimetype === "application/zip" || + file.mimetype === "application/x-zip-compressed" || + file.originalname.endsWith(".zip") + ) { + // Zip file containing multiple markdown files + const zipResults = await this.importZipFile(workspaceId, userId, file.buffer); + results.push(...zipResults); + } else { + throw new BadRequestException( + "Invalid file type. Only .md and .zip files are accepted." + ); + } + } catch (error) { + throw new BadRequestException( + `Failed to import file: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + + const imported = results.filter((r) => r.success).length; + const failed = results.filter((r) => !r.success).length; + + return { + results, + totalFiles: results.length, + imported, + failed, + }; + } + + /** + * Import a single markdown file + */ + private async importSingleMarkdown( + workspaceId: string, + userId: string, + filename: string, + content: string + ): Promise { + try { + // Parse frontmatter + const parsed = matter(content); + const frontmatter = parsed.data; + const markdownContent = parsed.content.trim(); + + if (!markdownContent) { + return { + filename, + success: false, + error: "Empty content", + }; + } + + // Build CreateEntryDto from frontmatter and content + const parsedStatus = this.parseStatus(frontmatter.status); + const parsedVisibility = this.parseVisibility(frontmatter.visibility); + const parsedTags = Array.isArray(frontmatter.tags) ? frontmatter.tags : undefined; + + const createDto: CreateEntryDto = { + title: frontmatter.title || filename.replace(/\.md$/, ""), + content: markdownContent, + changeNote: "Imported from markdown file", + ...(frontmatter.summary && { summary: frontmatter.summary }), + ...(parsedStatus && { status: parsedStatus }), + ...(parsedVisibility && { visibility: parsedVisibility }), + ...(parsedTags && { tags: parsedTags }), + }; + + // Create the entry + const entry = await this.knowledgeService.create( + workspaceId, + userId, + createDto + ); + + return { + filename, + success: true, + entryId: entry.id, + slug: entry.slug, + title: entry.title, + }; + } catch (error) { + return { + filename, + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + } + + /** + * Import entries from a zip file + */ + private async importZipFile( + workspaceId: string, + userId: string, + buffer: Buffer + ): Promise { + const results: ImportResult[] = []; + const MAX_FILES = 1000; // Prevent zip bomb attacks + const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100MB total uncompressed + + try { + const zip = new AdmZip(buffer); + const zipEntries = zip.getEntries(); + + // Security: Check for zip bombs + let totalUncompressedSize = 0; + let fileCount = 0; + + for (const entry of zipEntries) { + if (!entry.isDirectory) { + fileCount++; + totalUncompressedSize += entry.header.size; + } + } + + if (fileCount > MAX_FILES) { + throw new BadRequestException( + `Zip file contains too many files (${fileCount}). Maximum allowed: ${MAX_FILES}` + ); + } + + if (totalUncompressedSize > MAX_TOTAL_SIZE) { + throw new BadRequestException( + `Zip file is too large when uncompressed (${Math.round(totalUncompressedSize / 1024 / 1024)}MB). Maximum allowed: ${Math.round(MAX_TOTAL_SIZE / 1024 / 1024)}MB` + ); + } + + for (const zipEntry of zipEntries) { + // Skip directories and non-markdown files + if (zipEntry.isDirectory || !zipEntry.entryName.endsWith(".md")) { + continue; + } + + // Security: Prevent path traversal attacks + const normalizedPath = zipEntry.entryName.replace(/\\/g, "/"); + if ( + normalizedPath.includes("..") || + normalizedPath.startsWith("/") || + normalizedPath.includes("//") + ) { + results.push({ + filename: zipEntry.entryName, + success: false, + error: "Invalid file path detected (potential path traversal)", + }); + continue; + } + + const content = zipEntry.getData().toString("utf-8"); + const result = await this.importSingleMarkdown( + workspaceId, + userId, + zipEntry.entryName, + content + ); + results.push(result); + } + } catch (error) { + throw new BadRequestException( + `Failed to extract zip file: ${error instanceof Error ? error.message : "Unknown error"}` + ); + } + + return results; + } + + /** + * Export entries as a zip file + */ + async exportEntries( + workspaceId: string, + format: ExportFormat, + entryIds?: string[] + ): Promise<{ stream: Readable; filename: string }> { + // Fetch entries + const entries = await this.fetchEntriesForExport(workspaceId, entryIds); + + if (entries.length === 0) { + throw new BadRequestException("No entries found to export"); + } + + // Create archive + const archive = archiver("zip", { + zlib: { level: 9 }, + }); + + // Add entries to archive + for (const entry of entries) { + if (format === "markdown") { + const markdown = this.entryToMarkdown(entry); + const filename = `${entry.slug}.md`; + archive.append(markdown, { name: filename }); + } else { + // JSON format + const json = JSON.stringify(entry, null, 2); + const filename = `${entry.slug}.json`; + archive.append(json, { name: filename }); + } + } + + // Finalize archive + archive.finalize(); + + // Generate filename + const timestamp = new Date().toISOString().split("T")[0]; + const filename = `knowledge-export-${timestamp}.zip`; + + return { + stream: archive, + filename, + }; + } + + /** + * Fetch entries for export + */ + private async fetchEntriesForExport( + workspaceId: string, + entryIds?: string[] + ): Promise { + const where: Record = { workspaceId }; + + if (entryIds && entryIds.length > 0) { + where.id = { in: entryIds }; + } + + const entries = await this.prisma.knowledgeEntry.findMany({ + where, + include: { + tags: { + include: { + tag: true, + }, + }, + }, + orderBy: { + title: "asc", + }, + }); + + return entries.map((entry) => ({ + id: entry.id, + slug: entry.slug, + title: entry.title, + content: entry.content, + summary: entry.summary, + status: entry.status, + visibility: entry.visibility, + tags: entry.tags.map((et) => et.tag.name), + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + })); + } + + /** + * Convert entry to markdown format with frontmatter + */ + private entryToMarkdown(entry: ExportEntry): string { + const frontmatter: Record = { + title: entry.title, + status: entry.status, + visibility: entry.visibility, + }; + + if (entry.summary) { + frontmatter.summary = entry.summary; + } + + if (entry.tags && entry.tags.length > 0) { + frontmatter.tags = entry.tags; + } + + frontmatter.createdAt = entry.createdAt.toISOString(); + frontmatter.updatedAt = entry.updatedAt.toISOString(); + + // Build frontmatter string + const frontmatterStr = Object.entries(frontmatter) + .map(([key, value]) => { + if (Array.isArray(value)) { + return `${key}:\n - ${value.join("\n - ")}`; + } + return `${key}: ${value}`; + }) + .join("\n"); + + return `---\n${frontmatterStr}\n---\n\n${entry.content}`; + } + + /** + * Parse status from frontmatter + */ + private parseStatus(value: unknown): EntryStatus | undefined { + if (!value) return undefined; + const statusMap: Record = { + DRAFT: EntryStatus.DRAFT, + PUBLISHED: EntryStatus.PUBLISHED, + ARCHIVED: EntryStatus.ARCHIVED, + }; + return statusMap[String(value).toUpperCase()]; + } + + /** + * Parse visibility from frontmatter + */ + private parseVisibility(value: unknown): Visibility | undefined { + if (!value) return undefined; + const visibilityMap: Record = { + PRIVATE: Visibility.PRIVATE, + WORKSPACE: Visibility.WORKSPACE, + PUBLIC: Visibility.PUBLIC, + }; + return visibilityMap[String(value).toUpperCase()]; + } +} diff --git a/apps/api/src/knowledge/services/index.ts b/apps/api/src/knowledge/services/index.ts index fcbde1a..2c3c2e3 100644 --- a/apps/api/src/knowledge/services/index.ts +++ b/apps/api/src/knowledge/services/index.ts @@ -8,3 +8,4 @@ export { LinkSyncService } from "./link-sync.service"; export { SearchService } from "./search.service"; export { GraphService } from "./graph.service"; export { StatsService } from "./stats.service"; +export { ImportExportService } from "./import-export.service"; diff --git a/apps/web/src/app/(authenticated)/knowledge/page.tsx b/apps/web/src/app/(authenticated)/knowledge/page.tsx index 0545a15..f2620cc 100644 --- a/apps/web/src/app/(authenticated)/knowledge/page.tsx +++ b/apps/web/src/app/(authenticated)/knowledge/page.tsx @@ -4,6 +4,7 @@ import { useState, useMemo } from "react"; import { EntryStatus } from "@mosaic/shared"; import { EntryList } from "@/components/knowledge/EntryList"; import { EntryFilters } from "@/components/knowledge/EntryFilters"; +import { ImportExportActions } from "@/components/knowledge"; import { mockEntries, mockTags } from "@/lib/api/knowledge"; import Link from "next/link"; import { Plus } from "lucide-react"; @@ -99,22 +100,35 @@ export default function KnowledgePage() { return (
{/* Header */} -
-
-

Knowledge Base

-

- Documentation, guides, and knowledge entries -

+
+
+
+

Knowledge Base

+

+ Documentation, guides, and knowledge entries +

+
+ + {/* Create button */} + + + Create Entry +
- {/* Create button */} - - - Create Entry - + {/* Import/Export Actions */} +
+ { + // TODO: Refresh the entry list when real API is connected + // For now, this would trigger a refetch of the entries + window.location.reload(); + }} + /> +
{/* Filters */} diff --git a/apps/web/src/components/knowledge/ImportExportActions.tsx b/apps/web/src/components/knowledge/ImportExportActions.tsx new file mode 100644 index 0000000..23cdce7 --- /dev/null +++ b/apps/web/src/components/knowledge/ImportExportActions.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { useState, useRef } from "react"; +import { Upload, Download, Loader2, CheckCircle2, XCircle } from "lucide-react"; + +interface ImportResult { + filename: string; + success: boolean; + entryId?: string; + slug?: string; + title?: string; + error?: string; +} + +interface ImportResponse { + success: boolean; + totalFiles: number; + imported: number; + failed: number; + results: ImportResult[]; +} + +interface ImportExportActionsProps { + selectedEntryIds?: string[]; + onImportComplete?: () => void; +} + +export function ImportExportActions({ + selectedEntryIds = [], + onImportComplete, +}: ImportExportActionsProps) { + const [isImporting, setIsImporting] = useState(false); + const [isExporting, setIsExporting] = useState(false); + const [importResult, setImportResult] = useState(null); + const [showImportDialog, setShowImportDialog] = useState(false); + const fileInputRef = useRef(null); + + /** + * Handle import file selection + */ + const handleImportClick = () => { + fileInputRef.current?.click(); + }; + + /** + * Handle file upload and import + */ + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file type + if (!file.name.endsWith(".md") && !file.name.endsWith(".zip")) { + alert("Please upload a .md or .zip file"); + return; + } + + setIsImporting(true); + setShowImportDialog(true); + setImportResult(null); + + try { + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("/api/knowledge/import", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || "Import failed"); + } + + const result: ImportResponse = await response.json(); + setImportResult(result); + + // Notify parent component + if (result.imported > 0 && onImportComplete) { + onImportComplete(); + } + } catch (error) { + console.error("Import error:", error); + alert(error instanceof Error ? error.message : "Failed to import file"); + setShowImportDialog(false); + } finally { + setIsImporting(false); + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + }; + + /** + * Handle export + */ + const handleExport = async (format: "markdown" | "json" = "markdown") => { + setIsExporting(true); + + try { + // Build query params + const params = new URLSearchParams({ + format, + }); + + // Add selected entry IDs if any + if (selectedEntryIds.length > 0) { + selectedEntryIds.forEach((id) => params.append("entryIds", id)); + } + + const response = await fetch(`/api/knowledge/export?${params.toString()}`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error("Export failed"); + } + + // Get filename from Content-Disposition header + const contentDisposition = response.headers.get("Content-Disposition"); + const filenameMatch = contentDisposition?.match(/filename="(.+)"/); + const filename = filenameMatch?.[1] || `knowledge-export-${format}.zip`; + + // Download file + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error("Export error:", error); + alert("Failed to export entries"); + } finally { + setIsExporting(false); + } + }; + + /** + * Close import dialog + */ + const handleCloseImportDialog = () => { + setShowImportDialog(false); + setImportResult(null); + }; + + return ( + <> + {/* Action Buttons */} +
+ {/* Import Button */} + + + {/* Export Dropdown */} +
+ + + {/* Dropdown Menu */} +
+
+ + + {selectedEntryIds.length > 0 && ( +
+ {selectedEntryIds.length} selected +
+ )} +
+
+
+
+ + {/* Hidden File Input */} + + + {/* Import Result Dialog */} + {showImportDialog && ( +
+
+ {/* Header */} +
+

+ {isImporting ? "Importing..." : "Import Results"} +

+
+ + {/* Content */} +
+ {isImporting && ( +
+ + Processing file... +
+ )} + + {importResult && ( +
+ {/* Summary */} +
+
+
Total Files
+
+ {importResult.totalFiles} +
+
+
+
Imported
+
+ {importResult.imported} +
+
+
+
Failed
+
+ {importResult.failed} +
+
+
+ + {/* Results List */} + {importResult.results.length > 0 && ( +
+

Details

+ {importResult.results.map((result, index) => ( +
+ {result.success ? ( + + ) : ( + + )} +
+
+ {result.title || result.filename} +
+ {result.success ? ( +
+ {result.slug && `Slug: ${result.slug}`} +
+ ) : ( +
{result.error}
+ )} +
+
+ ))} +
+ )} +
+ )} +
+ + {/* Footer */} + {!isImporting && ( +
+ +
+ )} +
+
+ )} + + ); +} diff --git a/apps/web/src/components/knowledge/index.ts b/apps/web/src/components/knowledge/index.ts index 8f0786a..a6a0929 100644 --- a/apps/web/src/components/knowledge/index.ts +++ b/apps/web/src/components/knowledge/index.ts @@ -2,10 +2,8 @@ * Knowledge module components */ -export { BacklinksList } from "./BacklinksList"; export { EntryViewer } from "./EntryViewer"; export { EntryEditor } from "./EntryEditor"; export { EntryMetadata } from "./EntryMetadata"; export { VersionHistory } from "./VersionHistory"; -export { StatsDashboard } from "./StatsDashboard"; -export { EntryGraphViewer } from "./EntryGraphViewer"; +export { ImportExportActions } from "./ImportExportActions"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ab1a4c..e3d0e11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,12 @@ importers: '@types/marked': specifier: ^6.0.0 version: 6.0.0 + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 + archiver: + specifier: ^7.0.1 + version: 7.0.1 better-auth: specifier: ^1.4.17 version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.6.2)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(next@16.1.6(@babel/core@7.28.6)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.17.2)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.0)(tsx@4.21.0)) @@ -77,9 +83,15 @@ importers: class-validator: specifier: ^0.14.3 version: 0.14.3 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 highlight.js: specifier: ^11.11.1 version: 11.11.1 + ioredis: + specifier: ^5.9.2 + version: 5.9.2 marked: specifier: ^17.0.1 version: 17.0.1 @@ -126,15 +138,21 @@ importers: '@swc/core': specifier: ^1.10.18 version: 1.15.11 + '@types/adm-zip': + specifier: ^0.5.7 + version: 0.5.7 + '@types/archiver': + specifier: ^7.0.0 + version: 7.0.0 '@types/express': specifier: ^5.0.1 version: 5.0.6 '@types/highlight.js': specifier: ^10.1.0 version: 10.1.0 - '@types/ioredis': - specifier: ^5.0.0 - version: 5.0.0 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/node': specifier: ^22.13.4 version: 22.19.7 @@ -1695,6 +1713,12 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@types/adm-zip@0.5.7': + resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} + + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1843,10 +1867,6 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/ioredis@5.0.0': - resolution: {integrity: sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g==} - deprecated: This is a stub types definition. ioredis provides its own type definitions, so you do not need this installed. - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1854,6 +1874,9 @@ packages: resolution: {integrity: sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA==} deprecated: This is a stub types definition. marked provides its own type definitions, so you do not need this installed. + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node@22.19.7': resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} @@ -1878,6 +1901,9 @@ packages: '@types/react@19.2.10': resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + '@types/readdir-glob@1.1.5': + resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/sanitize-html@2.16.0': resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} @@ -2094,6 +2120,10 @@ packages: '@xyflow/system@0.0.74': resolution: {integrity: sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -2118,6 +2148,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2185,6 +2219,17 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2205,9 +2250,28 @@ packages: ast-v8-to-istanbul@0.3.10: resolution: {integrity: sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2314,12 +2378,19 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2492,6 +2563,10 @@ packages: resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2550,6 +2625,15 @@ packages: typescript: optional: true + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3105,6 +3189,13 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -3124,6 +3215,10 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} @@ -3137,6 +3232,9 @@ packages: fast-equals@4.0.3: resolution: {integrity: sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3272,6 +3370,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -3373,6 +3475,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3404,6 +3510,10 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3412,6 +3522,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3455,6 +3568,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -3509,6 +3626,10 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -3527,6 +3648,10 @@ packages: layout-base@2.0.1: resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3705,6 +3830,10 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -3800,6 +3929,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} @@ -4040,6 +4173,13 @@ packages: typescript: optional: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -4120,10 +4260,20 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -4202,6 +4352,9 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4226,6 +4379,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4337,6 +4494,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -4354,6 +4514,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4362,6 +4525,9 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -4373,6 +4539,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4442,6 +4612,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -4467,6 +4640,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4944,6 +5120,10 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -6300,6 +6480,14 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@types/adm-zip@0.5.7': + dependencies: + '@types/node': 22.19.7 + + '@types/archiver@7.0.0': + dependencies: + '@types/readdir-glob': 1.1.5 + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -6493,18 +6681,16 @@ snapshots: '@types/http-errors@2.0.5': {} - '@types/ioredis@5.0.0': - dependencies: - ioredis: 5.9.2 - transitivePeerDependencies: - - supports-color - '@types/json-schema@7.0.15': {} '@types/marked@6.0.0': dependencies: marked: 17.0.1 + '@types/multer@2.0.0': + dependencies: + '@types/express': 5.0.6 + '@types/node@22.19.7': dependencies: undici-types: 6.21.0 @@ -6534,6 +6720,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/readdir-glob@1.1.5': + dependencies: + '@types/node': 22.19.7 + '@types/sanitize-html@2.16.0': dependencies: htmlparser2: 8.0.2 @@ -6886,6 +7076,10 @@ snapshots: d3-selection: 3.0.0 d3-zoom: 3.0.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6906,6 +7100,8 @@ snapshots: acorn@8.15.0: {} + adm-zip@0.5.16: {} + agent-base@7.1.4: {} ajv-formats@2.1.1(ajv@8.17.1): @@ -6957,6 +7153,33 @@ snapshots: append-field@1.0.0: {} + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.23 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.0: @@ -6975,8 +7198,14 @@ snapshots: estree-walker: 3.0.3 js-tokens: 9.0.1 + async@3.2.6: {} + + b4a@1.7.3: {} + balanced-match@1.0.2: {} + bare-events@2.8.2: {} + base64-js@1.5.1: {} base64id@2.0.0: {} @@ -7113,6 +7342,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -7120,6 +7351,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -7296,6 +7532,14 @@ snapshots: core-util-is: 1.0.3 esprima: 4.0.1 + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -7345,6 +7589,13 @@ snapshots: optionalDependencies: typescript: 5.9.3 + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7861,6 +8112,14 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} expand-template@2.0.3: {} @@ -7902,6 +8161,10 @@ snapshots: exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 @@ -7912,6 +8175,8 @@ snapshots: fast-equals@4.0.3: {} + fast-fifo@1.3.2: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -8067,6 +8332,13 @@ snapshots: graceful-fs@4.2.11: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + hachure-fill@0.5.2: {} has-flag@4.0.0: {} @@ -8165,6 +8437,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8185,12 +8459,16 @@ snapshots: is-promise@4.0.0: {} + is-stream@2.0.1: {} + is-unicode-supported@0.1.0: {} is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -8236,6 +8514,11 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -8299,6 +8582,8 @@ snapshots: khroma@2.1.0: {} + kind-of@6.0.3: {} + kleur@3.0.3: {} kysely@0.28.10: {} @@ -8315,6 +8600,10 @@ snapshots: layout-base@2.0.1: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -8475,6 +8764,10 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -8562,6 +8855,8 @@ snapshots: node-releases@2.0.27: {} + normalize-path@3.0.0: {} + nwsapi@2.2.23: {} nypm@0.6.4: @@ -8805,6 +9100,10 @@ snapshots: transitivePeerDependencies: - magicast + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -8897,12 +9196,34 @@ snapshots: react@19.2.4: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -9001,6 +9322,8 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} @@ -9033,6 +9356,11 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.3: {} @@ -9208,6 +9536,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + stackback@0.0.2: {} standard-as-callback@2.1.0: {} @@ -9218,6 +9548,15 @@ snapshots: streamsearch@1.1.0: {} + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -9230,6 +9569,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -9242,6 +9585,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-indent@3.0.0: @@ -9302,6 +9647,15 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + terser-webpack-plugin@5.3.16(@swc/core@1.15.11)(webpack@5.104.1(@swc/core@1.15.11)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -9326,6 +9680,12 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -9768,6 +10128,12 @@ snapshots: yoctocolors@2.1.2: {} + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + zod@4.3.6: {} zustand@4.5.7(@types/react@19.2.10)(react@19.2.4):