From 3f9c7d23b80839c3808b5a105531462f46d0171d Mon Sep 17 00:00:00 2001 From: Rolf Koenders Date: Mon, 20 Apr 2026 19:14:05 +0200 Subject: [PATCH] feat: add --force flag and duplicate detection 409 handling - 409 response throws descriptive error: 'This URL is already saved as "Title". Use --force to save anyway.' - --force flag on add command bypasses the backend duplicate check - Add force?: boolean to CreateBookmarkPayload type Implements Keeply-link/docs#5 --- src/api.ts | 11 +++++++++++ src/commands/add.ts | 47 +++++++++++++++++++++++++++++---------------- src/types.ts | 1 + 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/api.ts b/src/api.ts index 2e03e01..50b4ea2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -33,6 +33,17 @@ export class KeeplyApi { headers: { ...this.headers, ...init?.headers }, }); + if (res.status === 409) { + const body = (await res.json().catch(() => ({}))) as { + message?: string; + existingTitle?: string; + }; + const title = body.existingTitle ? ` as "${body.existingTitle}"` : ""; + throw new Error( + `This URL is already saved${title}. Use --force to save anyway.`, + ); + } + if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { message?: string }; const msg = Array.isArray(body.message) diff --git a/src/commands/add.ts b/src/commands/add.ts index fcb958c..4959c10 100644 --- a/src/commands/add.ts +++ b/src/commands/add.ts @@ -1,26 +1,28 @@ -import { Command } from 'commander'; -import chalk from 'chalk'; -import { requireConfig } from '../config.js'; -import { KeeplyApi } from '../api.js'; -import { printError, printSuccess } from '../format.js'; +import { Command } from "commander"; +import chalk from "chalk"; +import { requireConfig } from "../config.js"; +import { KeeplyApi } from "../api.js"; +import { printError, printSuccess } from "../format.js"; interface AddOptions { title?: string; note?: string; folder?: string; tags?: string; + force?: boolean; json: boolean; } export function registerAddCommand(program: Command): void { program - .command('add ') - .description('Save a new bookmark') - .option('-t, --title ', 'Bookmark title') - .option('-n, --note <note>', 'Personal note') - .option('-f, --folder <name|id>', 'Folder name or ID') - .option('--tags <tags>', 'Comma-separated tag names (must already exist)') - .option('--json', 'Output created bookmark as JSON') + .command("add <url>") + .description("Save a new bookmark") + .option("-t, --title <title>", "Bookmark title") + .option("-n, --note <note>", "Personal note") + .option("-f, --folder <name|id>", "Folder name or ID") + .option("--tags <tags>", "Comma-separated tag names (must already exist)") + .option("--force", "Save even if this URL is already bookmarked") + .option("--json", "Output created bookmark as JSON") .action(async (url: string, opts: AddOptions) => { const { apiKey, apiUrl } = requireConfig(); const api = new KeeplyApi(apiKey, apiUrl); @@ -32,23 +34,31 @@ export function registerAddCommand(program: Command): void { if (opts.folder) { const folders = await api.listFolders(); const match = folders.find( - (f) => f.name.toLowerCase() === opts.folder!.toLowerCase() || f.id === opts.folder, + (f) => + f.name.toLowerCase() === opts.folder!.toLowerCase() || + f.id === opts.folder, ); if (!match) { - printError(`Folder "${opts.folder}" not found. Create it in the web app first.`); + printError( + `Folder "${opts.folder}" not found. Create it in the web app first.`, + ); process.exit(1); } folderId = match.id; } if (opts.tags) { - const tagNames = opts.tags.split(',').map((t) => t.trim().toLowerCase()); + const tagNames = opts.tags + .split(",") + .map((t) => t.trim().toLowerCase()); const allTags = await api.listTags(); tagIds = []; for (const name of tagNames) { const found = allTags.find((t) => t.name === name); if (!found) { - printError(`Tag "${name}" not found. Create it in the web app first.`); + printError( + `Tag "${name}" not found. Create it in the web app first.`, + ); process.exit(1); } tagIds.push(found.id); @@ -61,6 +71,7 @@ export function registerAddCommand(program: Command): void { note: opts.note, folderId, tagIds, + force: opts.force, }); if (opts.json) { @@ -71,7 +82,9 @@ export function registerAddCommand(program: Command): void { printSuccess(`Saved ${chalk.bold(bookmark.title ?? url)}`); console.log(chalk.dim(`id: ${bookmark.id}`)); } catch (err) { - printError(err instanceof Error ? err.message : 'Failed to save bookmark'); + printError( + err instanceof Error ? err.message : "Failed to save bookmark", + ); process.exit(1); } }); diff --git a/src/types.ts b/src/types.ts index bcf2e1c..ba2b3d6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,6 +33,7 @@ export interface CreateBookmarkPayload { note?: string; folderId?: string; tagIds?: string[]; + force?: boolean; } export interface UpdateBookmarkPayload {