diff options
Diffstat (limited to '')
| -rw-r--r-- | apps/cli/package.json | 6 | ||||
| -rw-r--r-- | apps/cli/src/commands/admin.ts | 89 | ||||
| -rw-r--r-- | apps/cli/src/commands/bookmarks.ts | 89 | ||||
| -rw-r--r-- | apps/cli/src/commands/lists.ts | 11 | ||||
| -rw-r--r-- | apps/cli/src/commands/migrate.ts | 1 | ||||
| -rw-r--r-- | apps/cli/src/index.ts | 2 |
6 files changed, 183 insertions, 15 deletions
diff --git a/apps/cli/package.json b/apps/cli/package.json index e0e9b188..04c72f81 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@karakeep/cli", - "version": "0.29.0", + "version": "0.30.0", "description": "Command Line Interface (CLI) for Karakeep", "license": "GNU Affero General Public License version 3", "type": "module", @@ -20,8 +20,8 @@ "@karakeep/shared": "workspace:^0.1.0", "@karakeep/trpc": "workspace:^0.1.0", "@karakeep/tsconfig": "workspace:^0.1.0", - "@trpc/client": "^11.4.3", - "@trpc/server": "^11.4.3", + "@trpc/client": "^11.9.0", + "@trpc/server": "^11.9.0", "@tsconfig/node22": "^22.0.0", "chalk": "^5.3.0", "commander": "^12.0.0", diff --git a/apps/cli/src/commands/admin.ts b/apps/cli/src/commands/admin.ts new file mode 100644 index 00000000..181126f0 --- /dev/null +++ b/apps/cli/src/commands/admin.ts @@ -0,0 +1,89 @@ +import { getGlobalOptions } from "@/lib/globals"; +import { printErrorMessageWithReason, printObject } from "@/lib/output"; +import { getAPIClient } from "@/lib/trpc"; +import { Command } from "@commander-js/extra-typings"; +import { getBorderCharacters, table } from "table"; + +export const adminCmd = new Command() + .name("admin") + .description("admin commands"); + +function toHumanReadableSize(size: number): string { + 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]; +} + +const usersCmd = new Command() + .name("users") + .description("user management commands"); + +usersCmd + .command("list") + .description("list all users") + .action(async () => { + const api = getAPIClient(); + + try { + const [usersResp, userStats] = await Promise.all([ + api.users.list.query(), + api.admin.userStats.query(), + ]); + + if (getGlobalOptions().json) { + printObject({ + users: usersResp.users.map((u) => ({ + ...u, + numBookmarks: userStats[u.id]?.numBookmarks ?? 0, + assetSizes: userStats[u.id]?.assetSizes ?? 0, + })), + }); + } else { + const data: string[][] = [ + [ + "Name", + "Email", + "Num Bookmarks", + "Asset Sizes", + "Role", + "Local User", + ], + ]; + + usersResp.users.forEach((user) => { + const stats = userStats[user.id] ?? { + numBookmarks: 0, + assetSizes: 0, + }; + + const numBookmarksDisplay = `${stats.numBookmarks} / ${user.bookmarkQuota?.toString() ?? "Unlimited"}`; + const assetSizesDisplay = `${toHumanReadableSize(stats.assetSizes)} / ${user.storageQuota ? toHumanReadableSize(user.storageQuota) : "Unlimited"}`; + + data.push([ + user.name, + user.email, + numBookmarksDisplay, + assetSizesDisplay, + user.role ?? "", + user.localUser ? "✓" : "✗", + ]); + }); + + console.log( + table(data, { + border: getBorderCharacters("ramac"), + drawHorizontalLine: (lineIndex, rowCount) => { + return ( + lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount + ); + }, + }), + ); + } + } catch (error) { + printErrorMessageWithReason("Failed to list all users", error as object); + } + }); + +adminCmd.addCommand(usersCmd); diff --git a/apps/cli/src/commands/bookmarks.ts b/apps/cli/src/commands/bookmarks.ts index 021e344f..e2e8efb6 100644 --- a/apps/cli/src/commands/bookmarks.ts +++ b/apps/cli/src/commands/bookmarks.ts @@ -29,18 +29,10 @@ type Bookmark = Omit<ZBookmark, "tags"> & { }; function normalizeBookmark(bookmark: ZBookmark): Bookmark { - const ret = { + return { ...bookmark, tags: bookmark.tags.map((t) => t.name), }; - - if (ret.content.type == BookmarkTypes.LINK && ret.content.htmlContent) { - if (ret.content.htmlContent.length > 10) { - ret.content.htmlContent = - ret.content.htmlContent.substring(0, 10) + "... <CROPPED>"; - } - } - return ret; } function printBookmark(bookmark: ZBookmark) { @@ -151,10 +143,15 @@ bookmarkCmd .command("get") .description("fetch information about a bookmark") .argument("<id>", "The id of the bookmark to get") - .action(async (id) => { + .option( + "--include-content", + "include full bookmark content in results", + false, + ) + .action(async (id, opts) => { const api = getAPIClient(); await api.bookmarks.getBookmark - .query({ bookmarkId: id }) + .query({ bookmarkId: id, includeContent: opts.includeContent }) .then(printBookmark) .catch(printError(`Failed to get the bookmark with id "${id}"`)); }); @@ -254,6 +251,11 @@ bookmarkCmd false, ) .option("--list-id <id>", "if set, only items from that list will be fetched") + .option( + "--include-content", + "include full bookmark content in results", + false, + ) .action(async (opts) => { const api = getAPIClient(); @@ -262,6 +264,7 @@ bookmarkCmd listId: opts.listId, limit: MAX_NUM_BOOKMARKS_PER_PAGE, useCursorV2: true, + includeContent: opts.includeContent, }; try { @@ -282,6 +285,70 @@ bookmarkCmd }); bookmarkCmd + .command("search") + .description("search bookmarks using query matchers") + .argument( + "<query>", + "the search query (supports matchers like tag:name, is:fav, etc.)", + ) + .option( + "--limit <limit>", + "number of results per page", + (val) => parseInt(val, 10), + 50, + ) + .option( + "--sort-order <order>", + "sort order for results", + (val) => { + if (val !== "relevance" && val !== "asc" && val !== "desc") { + throw new Error("sort-order must be one of: relevance, asc, desc"); + } + return val; + }, + "relevance", + ) + .option( + "--include-content", + "include full bookmark content in results", + false, + ) + .option("--all", "fetch all results (paginate through all pages)", false) + .action(async (query, opts) => { + const api = getAPIClient(); + + const request = { + text: query, + limit: opts.limit, + sortOrder: opts.sortOrder as "relevance" | "asc" | "desc", + includeContent: opts.includeContent, + }; + + try { + let resp = await api.bookmarks.searchBookmarks.query(request); + let results: ZBookmark[] = resp.bookmarks; + + // If --all flag is set, fetch all pages + if (opts.all) { + while (resp.nextCursor) { + resp = await api.bookmarks.searchBookmarks.query({ + ...request, + cursor: resp.nextCursor, + }); + results = [...results, ...resp.bookmarks]; + } + } + + printObject(results.map(normalizeBookmark), { maxArrayLength: null }); + } catch (error) { + printStatusMessage(false, "Failed to search bookmarks"); + if (error instanceof Error) { + printStatusMessage(false, error.message); + } + } + }); + +bookmarkCmd .command("delete") .description("delete a bookmark") .argument("<id>", "the id of the bookmark to delete") diff --git a/apps/cli/src/commands/lists.ts b/apps/cli/src/commands/lists.ts index 864fa790..1d9341d7 100644 --- a/apps/cli/src/commands/lists.ts +++ b/apps/cli/src/commands/lists.ts @@ -86,15 +86,24 @@ listsCmd .command("get") .description("gets all the ids of the bookmarks assigned to the list") .requiredOption("--list <id>", "the id of the list") + .option( + "--include-content", + "include full bookmark content in results", + false, + ) .action(async (opts) => { const api = getAPIClient(); try { - let resp = await api.bookmarks.getBookmarks.query({ listId: opts.list }); + let resp = await api.bookmarks.getBookmarks.query({ + listId: opts.list, + includeContent: opts.includeContent, + }); let results: string[] = resp.bookmarks.map((b) => b.id); while (resp.nextCursor) { resp = await api.bookmarks.getBookmarks.query({ listId: opts.list, cursor: resp.nextCursor, + includeContent: opts.includeContent, }); results = [...results, ...resp.bookmarks.map((b) => b.id)]; } diff --git a/apps/cli/src/commands/migrate.ts b/apps/cli/src/commands/migrate.ts index ee0d85c8..6527be23 100644 --- a/apps/cli/src/commands/migrate.ts +++ b/apps/cli/src/commands/migrate.ts @@ -695,6 +695,7 @@ async function migrateBookmarks( summary: b.summary ?? undefined, createdAt: b.createdAt, crawlPriority: "low" as const, + source: b.source === null ? undefined : b.source, }; let createdId: string | null = null; switch (b.content.type) { diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index df7d9512..8158c0b8 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -1,3 +1,4 @@ +import { adminCmd } from "@/commands/admin"; import { bookmarkCmd } from "@/commands/bookmarks"; import { dumpCmd } from "@/commands/dump"; import { listsCmd } from "@/commands/lists"; @@ -31,6 +32,7 @@ const program = new Command() : "0.0.0", ); +program.addCommand(adminCmd); program.addCommand(bookmarkCmd); program.addCommand(listsCmd); program.addCommand(tagsCmd); |
