From d4406729df2d285729ab9f2ad3301e0d726ef164 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Tue, 2 Apr 2024 15:12:40 +0100 Subject: featuer: Introduce a new CLI for mass manipulation of bookmarks --- apps/cli/commands/bookmarks.ts | 112 +++++++++++++++++++++++++++++++++++++++++ apps/cli/commands/whoami.ts | 10 ++++ apps/cli/index.ts | 29 +++++++++++ apps/cli/lib/globals.ts | 17 +++++++ apps/cli/lib/trpc.ts | 23 +++++++++ apps/cli/package.json | 35 +++++++++++++ apps/cli/tsconfig.json | 11 ++++ 7 files changed, 237 insertions(+) create mode 100644 apps/cli/commands/bookmarks.ts create mode 100644 apps/cli/commands/whoami.ts create mode 100644 apps/cli/index.ts create mode 100644 apps/cli/lib/globals.ts create mode 100644 apps/cli/lib/trpc.ts create mode 100644 apps/cli/package.json create mode 100644 apps/cli/tsconfig.json (limited to 'apps/cli') diff --git a/apps/cli/commands/bookmarks.ts b/apps/cli/commands/bookmarks.ts new file mode 100644 index 00000000..1727db22 --- /dev/null +++ b/apps/cli/commands/bookmarks.ts @@ -0,0 +1,112 @@ +import * as fs from "node:fs"; +import { Command } from "@commander-js/extra-typings"; +import chalk from "chalk"; +import { getAPIClient } from "lib/trpc"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +export const bookmarkCmd = new Command() + .name("bookmarks") + .description("Manipulating bookmarks"); + +function collect(val: T, acc: T[]) { + acc.push(val); + return acc; +} + +function normalizeBookmark(bookmark: ZBookmark) { + const ret = { + ...bookmark, + tags: bookmark.tags.map((t) => t.name), + }; + + if (ret.content.type == "link" && ret.content.htmlContent) { + if (ret.content.htmlContent.length > 10) { + ret.content.htmlContent = + ret.content.htmlContent.substring(0, 10) + "... "; + } + } + return ret; +} + +bookmarkCmd + .command("add") + .description("Creates a new bookmark") + .option( + "--link ", + "the link to add. Specify multiple times to add multiple links", + collect, + [], + ) + .option( + "--note ", + "the note text to add. Specify multiple times to add multiple notes", + collect, + [], + ) + .option("--stdin", "reads the data from stdin and store it as a note") + .action(async (opts) => { + const api = getAPIClient(); + + const promises = [ + ...opts.link.map((url) => + api.bookmarks.createBookmark.mutate({ type: "link", url }), + ), + ...opts.note.map((text) => + api.bookmarks.createBookmark.mutate({ type: "text", text }), + ), + ]; + + if (opts.stdin) { + const text = fs.readFileSync(0, "utf-8"); + promises.push( + api.bookmarks.createBookmark.mutate({ type: "text", text }), + ); + } + + const results = await Promise.allSettled(promises); + + for (const res of results) { + if (res.status == "fulfilled") { + console.log(normalizeBookmark(res.value)); + } else { + console.log(chalk.red(`Error: ${res.reason}`)); + } + } + }); + +bookmarkCmd + .command("get") + .description("fetch information about a bookmark") + .argument("", "The id of the bookmark to get") + .action(async (id) => { + const api = getAPIClient(); + const resp = await api.bookmarks.getBookmark.query({ bookmarkId: id }); + console.log(normalizeBookmark(resp)); + }); + +bookmarkCmd + .command("list") + .description("list all bookmarks") + .option( + "--include-archived", + "If set, archived bookmarks will be fetched as well", + false, + ) + .action(async (opts) => { + const api = getAPIClient(); + const resp = await api.bookmarks.getBookmarks.query({ + archived: opts.includeArchived ? undefined : false, + }); + console.log(resp.bookmarks.map(normalizeBookmark)); + }); + +bookmarkCmd + .command("delete") + .description("delete a bookmark") + .argument("", "The id of the bookmark to delete") + .action(async (id) => { + const api = getAPIClient(); + await api.bookmarks.deleteBookmark.mutate({ bookmarkId: id }); + console.log(`Bookmark ${id} got deleted`); + }); diff --git a/apps/cli/commands/whoami.ts b/apps/cli/commands/whoami.ts new file mode 100644 index 00000000..2b32f2f0 --- /dev/null +++ b/apps/cli/commands/whoami.ts @@ -0,0 +1,10 @@ +import { Command } from "@commander-js/extra-typings"; +import { getAPIClient } from "lib/trpc"; + +export const whoamiCmd = new Command() + .name("whoami") + .description("returns info about the owner of this API key") + .action(async () => { + const resp = await getAPIClient().users.whoami.query(); + console.log(resp); + }); diff --git a/apps/cli/index.ts b/apps/cli/index.ts new file mode 100644 index 00000000..03699e55 --- /dev/null +++ b/apps/cli/index.ts @@ -0,0 +1,29 @@ +import { Command, Option } from "@commander-js/extra-typings"; +import { bookmarkCmd } from "commands/bookmarks"; +import { whoamiCmd } from "commands/whoami"; +import { setGlobalOptions } from "lib/globals"; + +const program = new Command() + .name("hoarder-cli") + .description("A CLI interface to interact with the hoarder api") + .addOption( + new Option("--api-key ", "The API key to interact with the API") + .makeOptionMandatory(true) + .env("HOARDER_API_KEY"), + ) + .addOption( + new Option( + "--server-addr ", + "The address of the server to connect to", + ) + .makeOptionMandatory(true) + .env("HOARDER_SERVER_ADDR"), + ) + .version("0.1.0"); + +program.addCommand(bookmarkCmd); +program.addCommand(whoamiCmd); + +setGlobalOptions(program.opts()); + +program.parse(); diff --git a/apps/cli/lib/globals.ts b/apps/cli/lib/globals.ts new file mode 100644 index 00000000..771136da --- /dev/null +++ b/apps/cli/lib/globals.ts @@ -0,0 +1,17 @@ +export interface GlobalOptions { + apiKey: string; + serverAddr: string; +} + +export let globalOpts: GlobalOptions | undefined = undefined; + +export function setGlobalOptions(opts: GlobalOptions) { + globalOpts = opts; +} + +export function getGlobalOptions() { + if (!globalOpts) { + throw new Error("Global options are not initalized yet"); + } + return globalOpts; +} diff --git a/apps/cli/lib/trpc.ts b/apps/cli/lib/trpc.ts new file mode 100644 index 00000000..6f0dccfe --- /dev/null +++ b/apps/cli/lib/trpc.ts @@ -0,0 +1,23 @@ +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import superjson from "superjson"; + +import type { AppRouter } from "@hoarder/trpc/routers/_app"; + +import { getGlobalOptions } from "./globals"; + +export function getAPIClient() { + const globals = getGlobalOptions(); + return createTRPCClient({ + links: [ + httpBatchLink({ + url: `${globals.serverAddr}/api/trpc`, + transformer: superjson, + headers() { + return { + authorization: `Bearer ${globals.apiKey}`, + }; + }, + }), + ], + }); +} diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 00000000..44f7e451 --- /dev/null +++ b/apps/cli/package.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@hoarder/cli", + "version": "0.1.0", + "private": true, + "dependencies": { + "@commander-js/extra-typings": "^12.0.1", + "@hoarder/trpc": "workspace:^0.1.0", + "@hoarder/tsconfig": "workspace:^0.1.0", + "@tsconfig/node21": "^21.0.1", + "tsx": "^4.7.1", + "@trpc/client": "11.0.0-next-beta.308", + "@trpc/server": "11.0.0-next-beta.308", + "chalk": "^5.3.0", + "commander": "^12.0.0", + "superjson": "^2.2.1" + }, + "devDependencies": { + "@hoarder/eslint-config": "workspace:^0.2.0", + "@hoarder/prettier-config": "workspace:^0.1.0" + }, + "scripts": { + "run": "tsx index.ts", + "lint": "eslint .", + "format": "prettier . --ignore-path ../../.prettierignore", + "typecheck": "tsc --noEmit" + }, + "eslintConfig": { + "root": true, + "extends": [ + "@hoarder/eslint-config/base" + ] + }, + "prettier": "@hoarder/prettier-config" +} diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 00000000..dc71844c --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@hoarder/tsconfig/node.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"], + "compilerOptions": { + "baseUrl": ".", + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", + "strictNullChecks": true, + } +} -- cgit v1.2.3-70-g09d2