aboutsummaryrefslogtreecommitdiffstats
path: root/apps/cli
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-04-02 15:12:40 +0100
committerMohamedBassem <me@mbassem.com>2024-04-02 15:12:40 +0100
commitd4406729df2d285729ab9f2ad3301e0d726ef164 (patch)
treebfba411c289c0c0e97d760b83844de0d5dc18333 /apps/cli
parent79321f83293bc37d37af4b0a0b2bd324f5bafe1a (diff)
downloadkarakeep-d4406729df2d285729ab9f2ad3301e0d726ef164.tar.zst
featuer: Introduce a new CLI for mass manipulation of bookmarks
Diffstat (limited to 'apps/cli')
-rw-r--r--apps/cli/commands/bookmarks.ts112
-rw-r--r--apps/cli/commands/whoami.ts10
-rw-r--r--apps/cli/index.ts29
-rw-r--r--apps/cli/lib/globals.ts17
-rw-r--r--apps/cli/lib/trpc.ts23
-rw-r--r--apps/cli/package.json35
-rw-r--r--apps/cli/tsconfig.json11
7 files changed, 237 insertions, 0 deletions
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<T>(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) + "... <CROPPED>";
+ }
+ }
+ return ret;
+}
+
+bookmarkCmd
+ .command("add")
+ .description("Creates a new bookmark")
+ .option(
+ "--link <link>",
+ "the link to add. Specify multiple times to add multiple links",
+ collect<string>,
+ [],
+ )
+ .option(
+ "--note <note>",
+ "the note text to add. Specify multiple times to add multiple notes",
+ collect<string>,
+ [],
+ )
+ .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("<id>", "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("<id>", "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 <key>", "The API key to interact with the API")
+ .makeOptionMandatory(true)
+ .env("HOARDER_API_KEY"),
+ )
+ .addOption(
+ new Option(
+ "--server-addr <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<AppRouter>({
+ 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,
+ }
+}