From 751aa065cdcf8baf05bc5a04b679fd123522b87e Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 28 Dec 2025 18:57:04 -0800 Subject: [PATCH] Add scripts for testing Safari bookmarks saving and API key generation. --- packages/web/tests/bookmarks-save.ts | 197 +++++++++++++++++++++++++ packages/web/tests/generate-api-key.ts | 71 +++++++++ 2 files changed, 268 insertions(+) create mode 100644 packages/web/tests/bookmarks-save.ts create mode 100644 packages/web/tests/generate-api-key.ts diff --git a/packages/web/tests/bookmarks-save.ts b/packages/web/tests/bookmarks-save.ts new file mode 100644 index 00000000..b20a9273 --- /dev/null +++ b/packages/web/tests/bookmarks-save.ts @@ -0,0 +1,197 @@ +/** + * Test script to save Safari tabs as bookmarks to Linsa + * + * Usage: + * pnpm tsx tests/bookmarks-save.ts + * + * Requires: + * - LINSA_API_KEY environment variable (or create one at /settings) + * - Safari running with tabs open + */ + +import { execSync } from "node:child_process" + +const API_URL = process.env.LINSA_API_URL || "http://localhost:5613" +const API_KEY = process.env.LINSA_API_KEY + +if (!API_KEY) { + console.error("Error: LINSA_API_KEY environment variable is required") + console.error("Generate one at /settings or via POST /api/api-keys") + process.exit(1) +} + +interface SafariTab { + title: string + url: string + windowIndex: number +} + +// Get Safari tabs using AppleScript +function getSafariTabs(): SafariTab[] { + const script = ` + tell application "Safari" + set tabList to {} + set windowCount to count of windows + repeat with w from 1 to windowCount + set tabCount to count of tabs of window w + repeat with t from 1 to tabCount + set tabTitle to name of tab t of window w + set tabURL to URL of tab t of window w + set end of tabList to {windowIndex:w, title:tabTitle, url:tabURL} + end repeat + end repeat + return tabList + end tell + ` + + try { + const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { + encoding: "utf-8", + timeout: 10000, + }) + + // Parse AppleScript output: {{windowIndex:1, title:"...", url:"..."}, ...} + const tabs: SafariTab[] = [] + + // AppleScript returns records in format: window index:1, title:..., url:... + const matches = result.matchAll( + /window ?[iI]ndex:(\d+),\s*title:(.*?),\s*url:(.*?)(?=,\s*window|$)/g + ) + + for (const match of matches) { + tabs.push({ + windowIndex: parseInt(match[1]), + title: match[2].trim(), + url: match[3].trim(), + }) + } + + // If regex didn't work, try simpler line-by-line parsing + if (tabs.length === 0) { + // Alternative: get just URLs and titles separately + const urlScript = ` + tell application "Safari" + set urls to {} + repeat with w in windows + repeat with t in tabs of w + set end of urls to URL of t + end repeat + end repeat + return urls + end tell + ` + const titleScript = ` + tell application "Safari" + set titles to {} + repeat with w in windows + repeat with t in tabs of w + set end of titles to name of t + end repeat + end repeat + return titles + end tell + ` + + const urlsRaw = execSync(`osascript -e '${urlScript.replace(/'/g, "'\\''")}'`, { + encoding: "utf-8", + }).trim() + + const titlesRaw = execSync(`osascript -e '${titleScript.replace(/'/g, "'\\''")}'`, { + encoding: "utf-8", + }).trim() + + // Parse comma-separated lists + const urls = urlsRaw.split(", ").filter(Boolean) + const titles = titlesRaw.split(", ").filter(Boolean) + + for (let i = 0; i < urls.length; i++) { + tabs.push({ + windowIndex: 1, + title: titles[i] || "", + url: urls[i], + }) + } + } + + return tabs + } catch (error) { + console.error("Failed to get Safari tabs:", error) + return [] + } +} + +// Save a bookmark to Linsa +async function saveBookmark(tab: SafariTab, sessionTag: string): Promise { + try { + const response = await fetch(`${API_URL}/api/bookmarks`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url: tab.url, + title: tab.title, + tags: `safari,${sessionTag}`, + api_key: API_KEY, + }), + }) + + if (!response.ok) { + const error = await response.json() + console.error(`Failed to save ${tab.url}:`, error) + return false + } + + return true + } catch (error) { + console.error(`Failed to save ${tab.url}:`, error) + return false + } +} + +async function main() { + console.log("Getting Safari tabs...") + const tabs = getSafariTabs() + + if (tabs.length === 0) { + console.log("No Safari tabs found. Make sure Safari is running with tabs open.") + return + } + + console.log(`Found ${tabs.length} tabs`) + + // Create session tag with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19) + const sessionTag = `session-${timestamp}` + + console.log(`Saving to Linsa with tag: ${sessionTag}`) + console.log(`API URL: ${API_URL}`) + console.log("") + + let saved = 0 + let failed = 0 + + for (const tab of tabs) { + // Skip empty URLs or about: pages + if (!tab.url || tab.url.startsWith("about:") || tab.url === "favorites://") { + continue + } + + process.stdout.write(` Saving: ${tab.title.slice(0, 50)}... `) + const success = await saveBookmark(tab, sessionTag) + + if (success) { + console.log("✓") + saved++ + } else { + console.log("✗") + failed++ + } + } + + console.log("") + console.log(`Done! Saved ${saved} bookmarks, ${failed} failed`) + console.log(`Session tag: ${sessionTag}`) +} + +main().catch(console.error) diff --git a/packages/web/tests/generate-api-key.ts b/packages/web/tests/generate-api-key.ts new file mode 100644 index 00000000..0bea20ac --- /dev/null +++ b/packages/web/tests/generate-api-key.ts @@ -0,0 +1,71 @@ +/** + * Generate an API key for a user (for testing) + * + * Usage: + * pnpm tsx tests/generate-api-key.ts + * + * Example: + * pnpm tsx tests/generate-api-key.ts nikiv + */ + +import "dotenv/config" +import { getDb } from "../src/db/connection" +import { api_keys } from "../src/db/schema" + +const databaseUrl = process.env.DATABASE_URL + +if (!databaseUrl) { + console.error("DATABASE_URL is required") + process.exit(1) +} + +const userId = process.argv[2] + +if (!userId) { + console.error("Usage: pnpm tsx tests/generate-api-key.ts ") + process.exit(1) +} + +// Generate a random API key +function generateApiKey(): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + let key = "lk_" + for (let i = 0; i < 32; i++) { + key += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return key +} + +// Hash function +async function hashApiKey(key: string): Promise { + const encoder = new TextEncoder() + const data = encoder.encode(key) + const hashBuffer = await crypto.subtle.digest("SHA-256", data) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("") +} + +async function main() { + const db = getDb(databaseUrl!) + + const plainKey = generateApiKey() + const keyHash = await hashApiKey(plainKey) + + await db.insert(api_keys).values({ + user_id: userId, + key_hash: keyHash, + name: "CLI Generated", + }) + + console.log("") + console.log("API Key generated successfully!") + console.log("") + console.log("Key (save this, it won't be shown again):") + console.log(` ${plainKey}`) + console.log("") + console.log("Usage:") + console.log(` LINSA_API_KEY=${plainKey} pnpm tsx tests/bookmarks-save.ts`) + console.log("") +} + +main().catch(console.error)