aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/components/dashboard/admin/AdminActions.tsx22
-rw-r--r--apps/web/components/dashboard/admin/ServerStats.tsx6
-rw-r--r--apps/web/components/dashboard/admin/UserList.tsx18
-rw-r--r--apps/workers/index.ts12
-rw-r--r--apps/workers/tidyAssetsWorker.ts107
-rw-r--r--packages/shared/assetdb.ts42
-rw-r--r--packages/shared/package.json1
-rw-r--r--packages/shared/queues.ts16
-rw-r--r--packages/trpc/routers/admin.ts65
-rw-r--r--pnpm-lock.yaml70
10 files changed, 351 insertions, 8 deletions
diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx
index dfdf65eb..e5d6ed69 100644
--- a/apps/web/components/dashboard/admin/AdminActions.tsx
+++ b/apps/web/components/dashboard/admin/AdminActions.tsx
@@ -52,6 +52,21 @@ export default function AdminActions() {
},
});
+ const { mutateAsync: tidyAssets, isPending: isTidyAssetsPending } =
+ api.admin.tidyAssets.useMutation({
+ onSuccess: () => {
+ toast({
+ description: "Tidy assets request has been enqueued!",
+ });
+ },
+ onError: (e) => {
+ toast({
+ variant: "destructive",
+ description: e.message,
+ });
+ },
+ });
+
return (
<div>
<div className="mb-2 mt-8 text-xl font-medium">Actions</div>
@@ -97,6 +112,13 @@ export default function AdminActions() {
>
Reindex All Bookmarks
</ActionButton>
+ <ActionButton
+ variant="destructive"
+ loading={isTidyAssetsPending}
+ onClick={() => tidyAssets()}
+ >
+ Compact Assets
+ </ActionButton>
</div>
</div>
);
diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx
index e95dc437..f45d86c5 100644
--- a/apps/web/components/dashboard/admin/ServerStats.tsx
+++ b/apps/web/components/dashboard/admin/ServerStats.tsx
@@ -122,6 +122,12 @@ export default function ServerStats() {
<TableCell>{serverStats.inferenceStats.pending}</TableCell>
<TableCell>{serverStats.inferenceStats.failed}</TableCell>
</TableRow>
+ <TableRow>
+ <TableCell>Tidy Assets Jobs</TableCell>
+ <TableCell>{serverStats.tidyAssetsStats.queued}</TableCell>
+ <TableCell>-</TableCell>
+ <TableCell>-</TableCell>
+ </TableRow>
</TableBody>
</Table>
</div>
diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx
index 024325a3..65bf4068 100644
--- a/apps/web/components/dashboard/admin/UserList.tsx
+++ b/apps/web/components/dashboard/admin/UserList.tsx
@@ -15,10 +15,18 @@ import { api } from "@/lib/trpc";
import { Trash } from "lucide-react";
import { useSession } from "next-auth/react";
+function toHumanReadableSize(size: number) {
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
+ if (size === 0) return "0 Bytes";
+ const i = Math.floor(Math.log(size) / Math.log(1024));
+ return (size / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
+}
+
export default function UsersSection() {
const { data: session } = useSession();
const invalidateUserList = api.useUtils().users.list.invalidate;
const { data: users } = api.users.list.useQuery();
+ const { data: userStats } = api.admin.userStats.useQuery();
const { mutate: deleteUser, isPending: isDeletionPending } =
api.users.delete.useMutation({
onSuccess: () => {
@@ -35,7 +43,7 @@ export default function UsersSection() {
},
});
- if (!users) {
+ if (!users || !userStats) {
return <LoadingSpinner />;
}
@@ -47,6 +55,8 @@ export default function UsersSection() {
<TableHeader className="bg-gray-200">
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
+ <TableHead>Num Bookmarks</TableHead>
+ <TableHead>Asset Sizes</TableHead>
<TableHead>Role</TableHead>
<TableHead>Action</TableHead>
</TableHeader>
@@ -55,6 +65,12 @@ export default function UsersSection() {
<TableRow key={u.id}>
<TableCell className="py-1">{u.name}</TableCell>
<TableCell className="py-1">{u.email}</TableCell>
+ <TableCell className="py-1">
+ {userStats[u.id].numBookmarks}
+ </TableCell>
+ <TableCell className="py-1">
+ {toHumanReadableSize(userStats[u.id].assetSizes)}
+ </TableCell>
<TableCell className="py-1 capitalize">{u.role}</TableCell>
<TableCell className="py-1">
<ActionButton
diff --git a/apps/workers/index.ts b/apps/workers/index.ts
index e576776a..f9a05e59 100644
--- a/apps/workers/index.ts
+++ b/apps/workers/index.ts
@@ -1,5 +1,7 @@
import "dotenv/config";
+import { TidyAssetsWorker } from "tidyAssetsWorker";
+
import serverConfig from "@hoarder/shared/config";
import logger from "@hoarder/shared/logger";
import { runQueueDBMigrations } from "@hoarder/shared/queues";
@@ -13,21 +15,25 @@ async function main() {
logger.info(`Workers version: ${serverConfig.serverVersion ?? "not set"}`);
runQueueDBMigrations();
- const [crawler, openai, search] = [
+ const [crawler, openai, search, tidyAssets] = [
await CrawlerWorker.build(),
OpenAiWorker.build(),
SearchIndexingWorker.build(),
+ TidyAssetsWorker.build(),
];
await Promise.any([
- Promise.all([crawler.run(), openai.run(), search.run()]),
+ Promise.all([crawler.run(), openai.run(), search.run(), tidyAssets.run()]),
shutdownPromise,
]);
- logger.info("Shutting down crawler, openai and search workers ...");
+ logger.info(
+ "Shutting down crawler, openai, tidyAssets and search workers ...",
+ );
crawler.stop();
openai.stop();
search.stop();
+ tidyAssets.stop();
}
main();
diff --git a/apps/workers/tidyAssetsWorker.ts b/apps/workers/tidyAssetsWorker.ts
new file mode 100644
index 00000000..bc14aab9
--- /dev/null
+++ b/apps/workers/tidyAssetsWorker.ts
@@ -0,0 +1,107 @@
+import { eq } from "drizzle-orm";
+
+import { db } from "@hoarder/db";
+import { assets } from "@hoarder/db/schema";
+import { DequeuedJob, Runner } from "@hoarder/queue";
+import { deleteAsset, getAllAssets } from "@hoarder/shared/assetdb";
+import logger from "@hoarder/shared/logger";
+import {
+ TidyAssetsQueue,
+ ZTidyAssetsRequest,
+ zTidyAssetsRequestSchema,
+} from "@hoarder/shared/queues";
+
+export class TidyAssetsWorker {
+ static build() {
+ logger.info("Starting tidy assets worker ...");
+ const worker = new Runner<ZTidyAssetsRequest>(
+ TidyAssetsQueue,
+ {
+ run: runTidyAssets,
+ onComplete: (job) => {
+ const jobId = job?.id ?? "unknown";
+ logger.info(`[tidyAssets][${jobId}] Completed successfully`);
+ return Promise.resolve();
+ },
+ onError: (job) => {
+ const jobId = job?.id ?? "unknown";
+ logger.error(
+ `[tidyAssets][${jobId}] tidy assets job failed: ${job.error}\n${job.error.stack}`,
+ );
+ return Promise.resolve();
+ },
+ },
+ {
+ concurrency: 1,
+ pollIntervalMs: 1000,
+ timeoutSecs: 30,
+ },
+ );
+
+ return worker;
+ }
+}
+
+async function handleAsset(
+ asset: {
+ assetId: string;
+ userId: string;
+ size: number;
+ contentType: string;
+ fileName?: string | null;
+ },
+ request: ZTidyAssetsRequest,
+ jobId: string,
+) {
+ const dbRow = await db.query.assets.findFirst({
+ where: eq(assets.id, asset.assetId),
+ });
+ if (!dbRow) {
+ if (request.cleanDanglingAssets) {
+ await deleteAsset({ userId: asset.userId, assetId: asset.assetId });
+ logger.info(
+ `[tidyAssets][${jobId}] Asset ${asset.assetId} not found in the database. Deleting it.`,
+ );
+ } else {
+ logger.warn(
+ `[tidyAssets][${jobId}] Asset ${asset.assetId} not found in the database. Not deleting it because cleanDanglingAssets is false.`,
+ );
+ }
+ return;
+ }
+
+ if (request.syncAssetMetadata) {
+ await db
+ .update(assets)
+ .set({
+ contentType: asset.contentType,
+ fileName: asset.fileName,
+ size: asset.size,
+ })
+ .where(eq(assets.id, asset.assetId));
+ logger.info(
+ `[tidyAssets][${jobId}] Updated metadata for asset ${asset.assetId}`,
+ );
+ }
+}
+
+async function runTidyAssets(job: DequeuedJob<ZTidyAssetsRequest>) {
+ const jobId = job.id ?? "unknown";
+
+ const request = zTidyAssetsRequestSchema.safeParse(job.data);
+ if (!request.success) {
+ throw new Error(
+ `[tidyAssets][${jobId}] Got malformed job request: ${request.error.toString()}`,
+ );
+ }
+
+ for await (const asset of getAllAssets()) {
+ try {
+ handleAsset(asset, request.data, jobId);
+ } catch (e) {
+ logger.error(
+ `[tidyAssets][${jobId}] Failed to tidy asset ${asset.assetId}: ${e}`,
+ );
+ }
+ }
+}
diff --git a/packages/shared/assetdb.ts b/packages/shared/assetdb.ts
index 4edfa1ec..64413e9f 100644
--- a/packages/shared/assetdb.ts
+++ b/packages/shared/assetdb.ts
@@ -1,5 +1,6 @@
import * as fs from "fs";
import * as path from "path";
+import { Glob } from "glob";
import { z } from "zod";
import serverConfig from "./config";
@@ -120,6 +121,25 @@ export async function readAsset({
return { asset, metadata };
}
+export async function readAssetMetadata({
+ userId,
+ assetId,
+}: {
+ userId: string;
+ assetId: string;
+}) {
+ const assetDir = getAssetDir(userId, assetId);
+
+ const metadataStr = await fs.promises.readFile(
+ path.join(assetDir, "metadata.json"),
+ {
+ encoding: "utf8",
+ },
+ );
+
+ return zAssetMetadataSchema.parse(JSON.parse(metadataStr));
+}
+
export async function getAssetSize({
userId,
assetId,
@@ -154,3 +174,25 @@ export async function deleteUserAssets({ userId }: { userId: string }) {
}
await fs.promises.rm(userDir, { recursive: true });
}
+
+export async function* getAllAssets() {
+ const g = new Glob(`/**/**/asset.bin`, {
+ maxDepth: 3,
+ root: ROOT_PATH,
+ cwd: ROOT_PATH,
+ absolute: false,
+ });
+ for await (const file of g) {
+ const [userId, assetId] = file.split("/").slice(0, 2);
+ const [size, metadata] = await Promise.all([
+ getAssetSize({ userId, assetId }),
+ readAssetMetadata({ userId, assetId }),
+ ]);
+ yield {
+ userId,
+ assetId,
+ ...metadata,
+ size,
+ };
+ }
+}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 2b1ae973..69d93075 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -6,6 +6,7 @@
"type": "module",
"dependencies": {
"@hoarder/queue": "workspace:^0.1.0",
+ "glob": "^11.0.0",
"meilisearch": "^0.37.0",
"winston": "^3.11.0",
"zod": "^3.22.4"
diff --git a/packages/shared/queues.ts b/packages/shared/queues.ts
index cadeefd0..6b04b988 100644
--- a/packages/shared/queues.ts
+++ b/packages/shared/queues.ts
@@ -65,6 +65,22 @@ export const SearchIndexingQueue = new SqliteQueue<ZSearchIndexingRequest>(
},
);
+// Tidy Assets Worker
+export const zTidyAssetsRequestSchema = z.object({
+ cleanDanglingAssets: z.boolean().optional().default(false),
+ syncAssetMetadata: z.boolean().optional().default(false),
+});
+export type ZTidyAssetsRequest = z.infer<typeof zTidyAssetsRequestSchema>;
+export const TidyAssetsQueue = new SqliteQueue<ZTidyAssetsRequest>(
+ "tidy_assets_queue",
+ queueDB,
+ {
+ defaultJobArgs: {
+ numRetries: 1,
+ },
+ },
+);
+
export async function triggerSearchReindex(bookmarkId: string) {
await SearchIndexingQueue.enqueue({
bookmarkId,
diff --git a/packages/trpc/routers/admin.ts b/packages/trpc/routers/admin.ts
index b3fb2383..ff1249d0 100644
--- a/packages/trpc/routers/admin.ts
+++ b/packages/trpc/routers/admin.ts
@@ -1,11 +1,12 @@
-import { count, eq } from "drizzle-orm";
+import { count, eq, sum } from "drizzle-orm";
import { z } from "zod";
-import { bookmarkLinks, bookmarks, users } from "@hoarder/db/schema";
+import { assets, bookmarkLinks, bookmarks, users } from "@hoarder/db/schema";
import {
LinkCrawlerQueue,
OpenAIQueue,
SearchIndexingQueue,
+ TidyAssetsQueue,
triggerSearchReindex,
} from "@hoarder/shared/queues";
@@ -30,6 +31,9 @@ export const adminAppRouter = router({
indexingStats: z.object({
queued: z.number(),
}),
+ tidyAssetsStats: z.object({
+ queued: z.number(),
+ }),
}),
)
.query(async ({ ctx }) => {
@@ -49,6 +53,9 @@ export const adminAppRouter = router({
queuedInferences,
[{ value: pendingInference }],
[{ value: failedInference }],
+
+ // Tidy Assets
+ queuedTidyAssets,
] = await Promise.all([
ctx.db.select({ value: count() }).from(users),
ctx.db.select({ value: count() }).from(bookmarks),
@@ -77,6 +84,9 @@ export const adminAppRouter = router({
.select({ value: count() })
.from(bookmarks)
.where(eq(bookmarks.taggingStatus, "failure")),
+
+ // Tidy Assets
+ TidyAssetsQueue.stats(),
]);
return {
@@ -95,6 +105,9 @@ export const adminAppRouter = router({
indexingStats: {
queued: queuedIndexing.pending + queuedIndexing.pending_retry,
},
+ tidyAssetsStats: {
+ queued: queuedTidyAssets.pending + queuedTidyAssets.pending_retry,
+ },
};
}),
recrawlLinks: adminProcedure
@@ -143,4 +156,52 @@ export const adminAppRouter = router({
bookmarkIds.map((b) => OpenAIQueue.enqueue({ bookmarkId: b.id })),
);
}),
+ tidyAssets: adminProcedure.mutation(async () => {
+ await TidyAssetsQueue.enqueue({
+ cleanDanglingAssets: true,
+ syncAssetMetadata: true,
+ });
+ }),
+ userStats: adminProcedure
+ .output(
+ z.record(
+ z.string(),
+ z.object({
+ numBookmarks: z.number(),
+ assetSizes: z.number(),
+ }),
+ ),
+ )
+ .query(async ({ ctx }) => {
+ const [userIds, bookmarkStats, assetStats] = await Promise.all([
+ ctx.db.select({ id: users.id }).from(users),
+ ctx.db
+ .select({ id: bookmarks.userId, value: count() })
+ .from(bookmarks)
+ .groupBy(bookmarks.userId),
+ ctx.db
+ .select({ id: assets.userId, value: sum(assets.size) })
+ .from(assets)
+ .groupBy(assets.userId),
+ ]);
+
+ const results: Record<
+ string,
+ { numBookmarks: number; assetSizes: number }
+ > = {};
+ for (const user of userIds) {
+ results[user.id] = {
+ numBookmarks: 0,
+ assetSizes: 0,
+ };
+ }
+ for (const stat of bookmarkStats) {
+ results[stat.id].numBookmarks = stat.value;
+ }
+ for (const stat of assetStats) {
+ results[stat.id].assetSizes = parseInt(stat.value ?? "0");
+ }
+
+ return results;
+ }),
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a6cc45d..cb4e0106 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -906,6 +906,9 @@ importers:
'@hoarder/queue':
specifier: workspace:^0.1.0
version: link:../queue
+ glob:
+ specifier: ^11.0.0
+ version: 11.0.0
meilisearch:
specifier: ^0.37.0
version: 0.37.0
@@ -7260,6 +7263,11 @@ packages:
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
+ glob@11.0.0:
+ resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==}
+ engines: {node: 20 || >=22}
+ hasBin: true
+
glob@6.0.4:
resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
deprecated: Glob versions prior to v9 are no longer supported
@@ -8066,6 +8074,10 @@ packages:
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
engines: {node: '>=14'}
+ jackspeak@4.0.2:
+ resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==}
+ engines: {node: 20 || >=22}
+
jake@10.8.7:
resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==}
engines: {node: '>=10'}
@@ -8521,6 +8533,10 @@ packages:
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
engines: {node: 14 || >=16.14}
+ lru-cache@11.0.1:
+ resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==}
+ engines: {node: 20 || >=22}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -9017,6 +9033,10 @@ packages:
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
+ minimatch@10.0.1:
+ resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
+ engines: {node: 20 || >=22}
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -9075,6 +9095,10 @@ packages:
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
engines: {node: '>=16 || 14 >=14.17'}
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
@@ -9545,6 +9569,9 @@ packages:
resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==}
engines: {node: '>= 14'}
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
package-json@8.1.1:
resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==}
engines: {node: '>=14.16'}
@@ -9642,6 +9669,10 @@ packages:
resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==}
engines: {node: '>=16 || 14 >=14.17'}
+ path-scurry@2.0.0:
+ resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
+ engines: {node: 20 || >=22}
+
path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
@@ -22462,10 +22493,20 @@ snapshots:
dependencies:
foreground-child: 3.1.1
jackspeak: 2.3.6
- minimatch: 9.0.3
+ minimatch: 9.0.4
minipass: 7.0.4
path-scurry: 1.10.1
+ glob@11.0.0:
+ dependencies:
+ foreground-child: 3.1.1
+ jackspeak: 4.0.2
+ minimatch: 10.0.1
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 2.0.0
+ dev: false
+
glob@6.0.4:
dependencies:
inflight: 1.0.6
@@ -23466,6 +23507,11 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
+ jackspeak@4.0.2:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ dev: false
+
jake@10.8.7:
dependencies:
async: 3.2.5
@@ -24044,6 +24090,9 @@ snapshots:
lru-cache@10.2.0: {}
+ lru-cache@11.0.1:
+ dev: false
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -25090,6 +25139,11 @@ snapshots:
minimalistic-assert@1.0.1:
dev: false
+ minimatch@10.0.1:
+ dependencies:
+ brace-expansion: 2.0.1
+ dev: false
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
@@ -25106,11 +25160,11 @@ snapshots:
minimatch@9.0.3:
dependencies:
brace-expansion: 2.0.1
+ dev: true
minimatch@9.0.4:
dependencies:
brace-expansion: 2.0.1
- dev: false
minimist@1.2.8: {}
@@ -25158,6 +25212,9 @@ snapshots:
minipass@7.0.4: {}
+ minipass@7.1.2:
+ dev: false
+
minizlib@2.1.2:
dependencies:
minipass: 3.3.6
@@ -25770,6 +25827,9 @@ snapshots:
netmask: 2.0.2
dev: false
+ package-json-from-dist@1.0.1:
+ dev: false
+
package-json@8.1.1:
dependencies:
got: 12.6.1
@@ -25893,6 +25953,12 @@ snapshots:
lru-cache: 10.2.0
minipass: 7.0.4
+ path-scurry@2.0.0:
+ dependencies:
+ lru-cache: 11.0.1
+ minipass: 7.1.2
+ dev: false
+
path-to-regexp@0.1.7:
dev: false