diff --git a/flow.toml b/flow.toml index 932aff5b..30775cd1 100644 --- a/flow.toml +++ b/flow.toml @@ -2346,6 +2346,68 @@ run().catch(console.error); dependencies = ["node", "pnpm"] shortcuts = ["user"] +[[tasks]] +name = "save-tabs" +description = "Save all Safari tabs to Linsa as bookmarks" +command = ''' +set -euo pipefail + +cd packages/web + +# Load env +if [ -f .env ]; then + set -a + . .env + set +a +fi + +if [ -z "${LINSA_API_KEY:-}" ]; then + echo "❌ LINSA_API_KEY not set" + echo "" + echo "Generate one with: f gen-api-key" + exit 1 +fi + +echo "Saving Safari tabs to Linsa..." +echo "" + +LINSA_API_KEY="$LINSA_API_KEY" LINSA_API_URL="${LINSA_API_URL:-http://localhost:5613}" pnpm tsx tests/bookmarks-save.ts +''' +dependencies = ["node", "pnpm"] +shortcuts = ["tabs", "safari"] + +[[tasks]] +name = "gen-api-key" +interactive = true +description = "Generate a Linsa API key for current user" +command = ''' +set -euo pipefail + +cd packages/web + +# Load env +if [ -f .env ]; then + set -a + . .env + set +a +fi + +if [ -z "${PROD_DATABASE_URL:-}" ] && [ -z "${DATABASE_URL:-}" ]; then + echo "❌ No database URL found" + echo "Set DATABASE_URL or PROD_DATABASE_URL in packages/web/.env" + exit 1 +fi + +DB_URL="${PROD_DATABASE_URL:-$DATABASE_URL}" + +read -p "Enter user ID to generate key for [nikiv]: " USER_ID +USER_ID="${USER_ID:-nikiv}" + +DATABASE_URL="$DB_URL" pnpm tsx tests/generate-api-key.ts "$USER_ID" +''' +dependencies = ["node", "pnpm"] +shortcuts = ["apikey", "genkey"] + [[tasks]] name = "test-jazz-stream" description = "Test Jazz live stream recording flow (API → Jazz FileStream → Timeline)" diff --git a/packages/web/package.json b/packages/web/package.json index a5074ec0..6984817c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -56,6 +56,7 @@ "zod": "^4.1.13" }, "devDependencies": { + "@nikiv/ts-utils": "^0.1.7", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/node": "^24.10.1", diff --git a/packages/web/src/components/Settings-panel.tsx b/packages/web/src/components/Settings-panel.tsx index d6a95665..f63ea73a 100644 --- a/packages/web/src/components/Settings-panel.tsx +++ b/packages/web/src/components/Settings-panel.tsx @@ -1,4 +1,3 @@ -import { useMemo } from "react" import { ArrowLeft, SlidersHorizontal, @@ -6,9 +5,10 @@ import { type LucideIcon, CreditCard, Video, + Key, } from "lucide-react" -type SettingsSection = "preferences" | "profile" | "streaming" | "billing" +type SettingsSection = "preferences" | "profile" | "streaming" | "api" | "billing" interface UserProfile { name?: string | null @@ -33,35 +33,10 @@ const navItems: NavItem[] = [ { id: "preferences", label: "Preferences", icon: SlidersHorizontal }, { id: "profile", label: "Profile", icon: UserRound }, { id: "streaming", label: "Streaming", icon: Video }, + { id: "api", label: "API Keys", icon: Key }, { id: "billing", label: "Manage Billing", icon: CreditCard }, ] -function Avatar({ profile }: { profile?: UserProfile | null }) { - const initial = useMemo(() => { - if (!profile) return "G" - return ( - profile.name?.slice(0, 1) ?? - profile.email?.slice(0, 1)?.toUpperCase() ?? - "G" - ) - }, [profile]) - - if (profile?.image) { - return ( - {profile.name - ) - } - - return ( -
- {initial} -
- ) -} export default function SettingsPanel({ activeSection, diff --git a/packages/web/src/routeTree.gen.ts b/packages/web/src/routeTree.gen.ts index 9fb5eb92..4c017f36 100644 --- a/packages/web/src/routeTree.gen.ts +++ b/packages/web/src/routeTree.gen.ts @@ -45,6 +45,7 @@ import { Route as ApiChatMessagesRouteImport } from './routes/api/chat-messages' import { Route as ApiCanvasRouteImport } from './routes/api/canvas' import { Route as ApiBrowserSessionsRouteImport } from './routes/api/browser-sessions' import { Route as ApiArchivesRouteImport } from './routes/api/archives' +import { Route as ApiApiKeysRouteImport } from './routes/api/api-keys' import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs' import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request' import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names' @@ -260,6 +261,11 @@ const ApiArchivesRoute = ApiArchivesRouteImport.update({ path: '/api/archives', getParentRoute: () => rootRouteImport, } as any) +const ApiApiKeysRoute = ApiApiKeysRouteImport.update({ + id: '/api/api-keys', + path: '/api/api-keys', + getParentRoute: () => rootRouteImport, +} as any) const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({ id: '/demo/start/server-funcs', path: '/demo/start/server-funcs', @@ -454,6 +460,7 @@ export interface FileRoutesByFullPath { '/streams': typeof StreamsRoute '/urls': typeof UrlsRoute '/users': typeof UsersRoute + '/api/api-keys': typeof ApiApiKeysRoute '/api/archives': typeof ApiArchivesRouteWithChildren '/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren '/api/canvas': typeof ApiCanvasRouteWithChildren @@ -525,6 +532,7 @@ export interface FileRoutesByTo { '/streams': typeof StreamsRoute '/urls': typeof UrlsRoute '/users': typeof UsersRoute + '/api/api-keys': typeof ApiApiKeysRoute '/api/archives': typeof ApiArchivesRouteWithChildren '/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren '/api/canvas': typeof ApiCanvasRouteWithChildren @@ -598,6 +606,7 @@ export interface FileRoutesById { '/streams': typeof StreamsRoute '/urls': typeof UrlsRoute '/users': typeof UsersRoute + '/api/api-keys': typeof ApiApiKeysRoute '/api/archives': typeof ApiArchivesRouteWithChildren '/api/browser-sessions': typeof ApiBrowserSessionsRouteWithChildren '/api/canvas': typeof ApiCanvasRouteWithChildren @@ -672,6 +681,7 @@ export interface FileRouteTypes { | '/streams' | '/urls' | '/users' + | '/api/api-keys' | '/api/archives' | '/api/browser-sessions' | '/api/canvas' @@ -743,6 +753,7 @@ export interface FileRouteTypes { | '/streams' | '/urls' | '/users' + | '/api/api-keys' | '/api/archives' | '/api/browser-sessions' | '/api/canvas' @@ -815,6 +826,7 @@ export interface FileRouteTypes { | '/streams' | '/urls' | '/users' + | '/api/api-keys' | '/api/archives' | '/api/browser-sessions' | '/api/canvas' @@ -888,6 +900,7 @@ export interface RootRouteChildren { StreamsRoute: typeof StreamsRoute UrlsRoute: typeof UrlsRoute UsersRoute: typeof UsersRoute + ApiApiKeysRoute: typeof ApiApiKeysRoute ApiArchivesRoute: typeof ApiArchivesRouteWithChildren ApiBrowserSessionsRoute: typeof ApiBrowserSessionsRouteWithChildren ApiCanvasRoute: typeof ApiCanvasRouteWithChildren @@ -1183,6 +1196,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiArchivesRouteImport parentRoute: typeof rootRouteImport } + '/api/api-keys': { + id: '/api/api-keys' + path: '/api/api-keys' + fullPath: '/api/api-keys' + preLoaderRoute: typeof ApiApiKeysRouteImport + parentRoute: typeof rootRouteImport + } '/demo/start/server-funcs': { id: '/demo/start/server-funcs' path: '/demo/start/server-funcs' @@ -1589,6 +1609,7 @@ const rootRouteChildren: RootRouteChildren = { StreamsRoute: StreamsRoute, UrlsRoute: UrlsRoute, UsersRoute: UsersRoute, + ApiApiKeysRoute: ApiApiKeysRoute, ApiArchivesRoute: ApiArchivesRouteWithChildren, ApiBrowserSessionsRoute: ApiBrowserSessionsRouteWithChildren, ApiCanvasRoute: ApiCanvasRouteWithChildren, diff --git a/packages/web/src/routes/api/api-keys.ts b/packages/web/src/routes/api/api-keys.ts index 2bcb6d1b..f09472f2 100644 --- a/packages/web/src/routes/api/api-keys.ts +++ b/packages/web/src/routes/api/api-keys.ts @@ -1,9 +1,14 @@ -import { createAPIFileRoute } from "@tanstack/react-start/api" +import { createFileRoute } from "@tanstack/react-router" import { eq } from "drizzle-orm" import { getDb } from "@/db/connection" import { api_keys } from "@/db/schema" -import { auth } from "@/lib/auth" -import { headers } from "@tanstack/react-start/server" +import { getAuth } from "@/lib/auth" + +const json = (data: unknown, status = 200) => + new Response(JSON.stringify(data), { + status, + headers: { "content-type": "application/json" }, + }) // Generate a random API key function generateApiKey(): string { @@ -24,110 +29,117 @@ async function hashApiKey(key: string): Promise { return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("") } -export const APIRoute = createAPIFileRoute("/api/api-keys")({ - // GET - List user's API keys (without the actual key, just metadata) - GET: async () => { - try { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user?.id) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) - } +export const Route = createFileRoute("/api/api-keys")({ + server: { + handlers: { + // GET - List user's API keys (without the actual key, just metadata) + GET: async ({ request }) => { + try { + const auth = getAuth() + const session = await auth.api.getSession({ headers: request.headers }) + if (!session?.user?.id) { + return json({ error: "Unauthorized" }, 401) + } - const db = getDb(process.env.DATABASE_URL!) + const db = getDb(process.env.DATABASE_URL!) - const keys = await db - .select({ - id: api_keys.id, - name: api_keys.name, - last_used_at: api_keys.last_used_at, - created_at: api_keys.created_at, - }) - .from(api_keys) - .where(eq(api_keys.user_id, session.user.id)) - .orderBy(api_keys.created_at) + const keys = await db + .select({ + id: api_keys.id, + name: api_keys.name, + last_used_at: api_keys.last_used_at, + created_at: api_keys.created_at, + }) + .from(api_keys) + .where(eq(api_keys.user_id, session.user.id)) + .orderBy(api_keys.created_at) - return Response.json({ keys }) - } catch (error) { - console.error("Error fetching API keys:", error) - return Response.json({ error: "Failed to fetch API keys" }, { status: 500 }) - } - }, + return json({ keys }) + } catch (error) { + console.error("Error fetching API keys:", error) + return json({ error: "Failed to fetch API keys" }, 500) + } + }, - // POST - Create a new API key - POST: async ({ request }) => { - try { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user?.id) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) - } + // POST - Create a new API key + POST: async ({ request }) => { + try { + const auth = getAuth() + const session = await auth.api.getSession({ headers: request.headers }) + if (!session?.user?.id) { + return json({ error: "Unauthorized" }, 401) + } - const body = await request.json().catch(() => ({})) - const name = body.name || "Default" + const body = (await request.json().catch(() => ({}))) as { name?: string } + const name = body.name || "Default" - const db = getDb(process.env.DATABASE_URL!) + const db = getDb(process.env.DATABASE_URL!) - // Generate new key - const plainKey = generateApiKey() - const keyHash = await hashApiKey(plainKey) + // Generate new key + const plainKey = generateApiKey() + const keyHash = await hashApiKey(plainKey) - // Insert key record - const [keyRecord] = await db - .insert(api_keys) - .values({ - user_id: session.user.id, - key_hash: keyHash, - name, - }) - .returning({ - id: api_keys.id, - name: api_keys.name, - created_at: api_keys.created_at, - }) + // Insert key record + const [keyRecord] = await db + .insert(api_keys) + .values({ + user_id: session.user.id, + key_hash: keyHash, + name, + }) + .returning({ + id: api_keys.id, + name: api_keys.name, + created_at: api_keys.created_at, + }) - // Return the plain key ONLY on creation (it won't be retrievable later) - return Response.json({ - key: plainKey, - id: keyRecord.id, - name: keyRecord.name, - created_at: keyRecord.created_at, - }) - } catch (error) { - console.error("Error creating API key:", error) - return Response.json({ error: "Failed to create API key" }, { status: 500 }) - } - }, + // Return the plain key ONLY on creation (it won't be retrievable later) + return json({ + key: plainKey, + id: keyRecord.id, + name: keyRecord.name, + created_at: keyRecord.created_at, + }) + } catch (error) { + console.error("Error creating API key:", error) + return json({ error: "Failed to create API key" }, 500) + } + }, - // DELETE - Revoke an API key - DELETE: async ({ request }) => { - try { - const session = await auth.api.getSession({ headers: await headers() }) - if (!session?.user?.id) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) - } + // DELETE - Revoke an API key + DELETE: async ({ request }) => { + try { + const auth = getAuth() + const session = await auth.api.getSession({ headers: request.headers }) + if (!session?.user?.id) { + return json({ error: "Unauthorized" }, 401) + } - const url = new URL(request.url) - const keyId = url.searchParams.get("id") + const url = new URL(request.url) + const keyId = url.searchParams.get("id") - if (!keyId) { - return Response.json({ error: "Key ID is required" }, { status: 400 }) - } + if (!keyId) { + return json({ error: "Key ID is required" }, 400) + } - const db = getDb(process.env.DATABASE_URL!) + const db = getDb(process.env.DATABASE_URL!) - // Delete key (only if it belongs to the user) - const [deleted] = await db - .delete(api_keys) - .where(eq(api_keys.id, keyId)) - .returning() + // Delete key (only if it belongs to the user) + const [deleted] = await db + .delete(api_keys) + .where(eq(api_keys.id, keyId)) + .returning() - if (!deleted) { - return Response.json({ error: "Key not found" }, { status: 404 }) - } + if (!deleted) { + return json({ error: "Key not found" }, 404) + } - return Response.json({ success: true }) - } catch (error) { - console.error("Error deleting API key:", error) - return Response.json({ error: "Failed to delete API key" }, { status: 500 }) - } + return json({ success: true }) + } catch (error) { + console.error("Error deleting API key:", error) + return json({ error: "Failed to delete API key" }, 500) + } + }, + }, }, }) diff --git a/packages/web/src/routes/settings.tsx b/packages/web/src/routes/settings.tsx index e228fabc..dd66319c 100644 --- a/packages/web/src/routes/settings.tsx +++ b/packages/web/src/routes/settings.tsx @@ -12,9 +12,14 @@ import { HelpCircle, Copy, ExternalLink, + Key, + Trash2, + Plus, + Eye, + EyeOff, } from "lucide-react" -type SectionId = "preferences" | "profile" | "streaming" | "billing" +type SectionId = "preferences" | "profile" | "streaming" | "api" | "billing" const PLAN_CARD_NOISE = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.18'/%3E%3C/svg%3E" @@ -842,6 +847,232 @@ function StreamingSection({ username }: { username: string | null | undefined }) ) } +interface ApiKeyData { + id: string + name: string + last_used_at: string | null + created_at: string +} + +function ApiKeysSection() { + const [keys, setKeys] = useState([]) + const [loading, setLoading] = useState(true) + const [creating, setCreating] = useState(false) + const [newKeyName, setNewKeyName] = useState("") + const [newKey, setNewKey] = useState(null) + const [showNewKey, setShowNewKey] = useState(false) + const [copied, setCopied] = useState(false) + const [error, setError] = useState(null) + + const fetchKeys = async () => { + try { + const res = await fetch("/api/api-keys", { credentials: "include" }) + if (res.ok) { + const data = await res.json() + setKeys(data.keys || []) + } + } catch { + // Ignore errors + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchKeys() + }, []) + + const handleCreateKey = async () => { + setCreating(true) + setError(null) + try { + const res = await fetch("/api/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ name: newKeyName || "Default" }), + }) + const data = await res.json() + if (!res.ok) { + setError(data.error || "Failed to create key") + } else { + setNewKey(data.key) + setShowNewKey(true) + setNewKeyName("") + fetchKeys() + } + } catch { + setError("Network error") + } finally { + setCreating(false) + } + } + + const handleDeleteKey = async (id: string) => { + try { + const res = await fetch(`/api/api-keys?id=${id}`, { + method: "DELETE", + credentials: "include", + }) + if (res.ok) { + setKeys(keys.filter((k) => k.id !== id)) + } + } catch { + // Ignore errors + } + } + + const copyKey = () => { + if (newKey) { + navigator.clipboard.writeText(newKey) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + } + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return "Never" + const date = new Date(dateStr) + return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + } + + return ( +
+ +
+ {/* Create new key */} + +
+
+

+ API keys allow you to access Linsa programmatically. Use them to save bookmarks, sync data, and integrate with other tools. +

+
+
+ setNewKeyName(e.target.value)} + placeholder="Key name (optional)" + className="flex-1 bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-teal-500" + /> + +
+ {error &&

{error}

} +
+
+ + {/* New key display */} + {newKey && ( + +
+
+

+ Copy this key now. You won't be able to see it again! +

+
+
+ + {showNewKey ? newKey : "•".repeat(40)} + + + +
+ +
+
+ )} + + {/* Existing keys */} + +
+ {loading ? ( +
+ ) : keys.length === 0 ? ( +

+ No API keys yet. Create one above. +

+ ) : ( +
+ {keys.map((key) => ( +
+
+ +
+

{key.name}

+

+ Created {formatDate(key.created_at)} • Last used {formatDate(key.last_used_at)} +

+
+
+ +
+ ))} +
+ )} +
+ + + {/* Usage example */} + +
+

+ Use your API key to save bookmarks: +

+
+{`curl -X POST https://linsa.io/api/bookmarks \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "url": "https://example.com",
+    "title": "Example",
+    "api_key": "lk_your_key_here"
+  }'`}
+            
+
+
+
+
+ ) +} + function BillingSection() { const [isSubscribed, setIsSubscribed] = useState(false) const [loading, setLoading] = useState(true) @@ -1031,6 +1262,8 @@ function SettingsPage() { /> ) : activeSection === "streaming" ? ( + ) : activeSection === "api" ? ( + ) : activeSection === "billing" && BILLING_ENABLED ? ( ) : null} diff --git a/packages/web/tests/bookmarks-save.ts b/packages/web/tests/bookmarks-save.ts index b20a9273..5313759b 100644 --- a/packages/web/tests/bookmarks-save.ts +++ b/packages/web/tests/bookmarks-save.ts @@ -1,127 +1,57 @@ /** - * Test script to save Safari tabs as bookmarks to Linsa + * Save Safari tabs as bookmarks to Linsa * * Usage: - * pnpm tsx tests/bookmarks-save.ts + * LINSA_API_KEY=lk_xxx pnpm tsx tests/bookmarks-save.ts * - * Requires: - * - LINSA_API_KEY environment variable (or create one at /settings) - * - Safari running with tabs open + * Or via flow: + * f save-tabs */ -import { execSync } from "node:child_process" +import { executeJxa } from "@nikiv/ts-utils" 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") + console.error("Generate one at /settings or via: f gen-api-key") process.exit(1) } -interface SafariTab { +type LocalTab = { + uuid: string title: string url: string - windowIndex: number + window_id: number + index: number + is_local: boolean } -// 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], +async function fetchSafariTabs(): Promise { + return executeJxa(` + const safari = Application("com.apple.Safari"); + const tabs = []; + safari.windows().map(window => { + const windowTabs = window.tabs(); + if (windowTabs) { + return windowTabs.map(tab => { + tabs.push({ + uuid: window.id() + '-' + tab.index(), + title: tab.name(), + url: tab.url() || '', + window_id: window.id(), + index: tab.index(), + is_local: true + }); }) } - } - - return tabs - } catch (error) { - console.error("Failed to get Safari tabs:", error) - return [] - } + }); + return tabs; + `) } -// Save a bookmark to Linsa -async function saveBookmark(tab: SafariTab, sessionTag: string): Promise { +async function saveBookmark(tab: LocalTab, sessionTag: string): Promise { try { const response = await fetch(`${API_URL}/api/bookmarks`, { method: "POST", @@ -138,59 +68,63 @@ async function saveBookmark(tab: SafariTab, sessionTag: string): Promise t.window_id)).size} windows`) // Create session tag with timestamp - const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19) + const date = new Date() + const timestamp = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}-${String(date.getHours()).padStart(2, "0")}${String(date.getMinutes()).padStart(2, "0")}` const sessionTag = `session-${timestamp}` - console.log(`Saving to Linsa with tag: ${sessionTag}`) + console.log(`\nSaving to Linsa with tag: ${sessionTag}`) console.log(`API URL: ${API_URL}`) console.log("") let saved = 0 + let skipped = 0 let failed = 0 for (const tab of tabs) { - // Skip empty URLs or about: pages + // Skip empty URLs, about: pages, favorites if (!tab.url || tab.url.startsWith("about:") || tab.url === "favorites://") { + skipped++ continue } - process.stdout.write(` Saving: ${tab.title.slice(0, 50)}... `) + const shortTitle = tab.title.length > 50 ? tab.title.slice(0, 47) + "..." : tab.title + process.stdout.write(` ${shortTitle} `) + 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(`Done! Saved: ${saved}, Skipped: ${skipped}, Failed: ${failed}`) console.log(`Session tag: ${sessionTag}`) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63a874e7..b7ebfa3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,6 +200,9 @@ importers: specifier: ^4.1.13 version: 4.1.13 devDependencies: + '@nikiv/ts-utils': + specifier: ^0.1.7 + version: 0.1.7 '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -248,6 +251,9 @@ importers: packages/worker: dependencies: + '@1focus/logs': + specifier: file:/Users/nikiv/lang/ts/lib/1focus/packages/logs + version: file:../../../lang/ts/lib/1focus/packages/logs drizzle-orm: specifier: ^0.45.0 version: 0.45.0(@cloudflare/workers-types@4.20251205.0)(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.15.6)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7) @@ -282,6 +288,9 @@ importers: packages: + '@1focus/logs@file:../../../lang/ts/lib/1focus/packages/logs': + resolution: {directory: ../../../lang/ts/lib/1focus/packages/logs, type: directory} + '@acemir/cssom@0.9.28': resolution: {integrity: sha512-LuS6IVEivI75vKN8S04qRD+YySP0RmU/cV8UNukhQZvprxF+76Z43TNo/a08eCodaGhT1Us8etqS1ZRY9/Or0A==} @@ -1874,6 +1883,9 @@ packages: '@neondatabase/serverless@0.10.4': resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==} + '@nikiv/ts-utils@0.1.7': + resolution: {integrity: sha512-hWYla5Xm08xzsac5az32AK++rK76qK6ZndkO0DAAYiIovcv15rhbRtaQPGIXFUCzBM1JgqzZJdyuFJZrxr5cIg==} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -1936,6 +1948,61 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@oven/bun-darwin-aarch64@1.3.5': + resolution: {integrity: sha512-8GvNtMo0NINM7Emk9cNAviCG3teEgr3BUX9be0+GD029zIagx2Sf54jMui1Eu1IpFm7nWHODuLEefGOQNaJ0gQ==} + cpu: [arm64] + os: [darwin] + + '@oven/bun-darwin-x64-baseline@1.3.5': + resolution: {integrity: sha512-p5q3rJk48qhLuLBOFehVc+kqCE03YrswTc6NCxbwsxiwfySXwcAvpF2KWKF/ZZObvvR8hCCvqe1F81b2p5r2dg==} + cpu: [x64] + os: [darwin] + + '@oven/bun-darwin-x64@1.3.5': + resolution: {integrity: sha512-r33eHQOHAwkuiBJIwmkXIyqONQOQMnd1GMTpDzaxx9vf9+svby80LZO9Hcm1ns6KT/TBRFyODC/0loA7FAaffg==} + cpu: [x64] + os: [darwin] + + '@oven/bun-linux-aarch64-musl@1.3.5': + resolution: {integrity: sha512-HKBeUlJdNduRkzJKZ5DXM+pPqntfC50/Hu2X65jVX0Y7hu/6IC8RaUTqpr8FtCZqqmc9wDK0OTL+Mbi9UQIKYQ==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-aarch64@1.3.5': + resolution: {integrity: sha512-zkcHPI23QxJ1TdqafhgkXt1NOEN8o5C460sVeNnrhfJ43LwZgtfcvcQE39x/pBedu67fatY8CU0iY00nOh46ZQ==} + cpu: [arm64] + os: [linux] + + '@oven/bun-linux-x64-baseline@1.3.5': + resolution: {integrity: sha512-FeCQyBU62DMuB0nn01vPnf3McXrKOsrK9p7sHaBFYycw0mmoU8kCq/WkBkGMnLuvQljJSyen8QBTx+fXdNupWg==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl-baseline@1.3.5': + resolution: {integrity: sha512-TJiYC7KCr0XxFTsxgwQOeE7dncrEL/RSyL0EzSL3xRkrxJMWBCvCSjQn7LV1i6T7hFst0+3KoN3VWvD5BinqHA==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64-musl@1.3.5': + resolution: {integrity: sha512-XkCCHkByYn8BIDvoxnny898znju4xnW2kvFE8FT5+0Y62cWdcBGMZ9RdsEUTeRz16k8hHtJpaSfLcEmNTFIwRQ==} + cpu: [x64] + os: [linux] + + '@oven/bun-linux-x64@1.3.5': + resolution: {integrity: sha512-n7zhKTSDZS0yOYg5Rq8easZu5Y/o47sv0c7yGr2ciFdcie9uYV55fZ7QMqhWMGK33ezCSikh5EDkUMCIvfWpjA==} + cpu: [x64] + os: [linux] + + '@oven/bun-windows-x64-baseline@1.3.5': + resolution: {integrity: sha512-rtVQB9/1XK8FWJgFtsOthbPifRMYypgJwxu+pK3NHx8WvFKmq7HcPDqNr8xLzGULjQEO7eAo2aOZfONOwYz+5g==} + cpu: [x64] + os: [win32] + + '@oven/bun-windows-x64@1.3.5': + resolution: {integrity: sha512-T3xkODItb/0ftQPFsZDc7EAX2D6A4TEazQ2YZyofZToO8Q7y8YT8ooWdhd0BQiTCd66uEvgE1DCZetynwg2IoA==} + cpu: [x64] + os: [win32] + '@planetscale/database@1.19.0': resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==} engines: {node: '>=16'} @@ -3030,6 +3097,12 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bun@1.3.5: + resolution: {integrity: sha512-c1YHIGUfgvYPJmLug5QiLzNWlX2Dg7X/67JWu1Va+AmMXNXzC/KQn2lgQ7rD+n1u1UqDpJMowVGGxTNpbPydNw==} + cpu: [arm64, x64] + os: [darwin, linux, win32] + hasBin: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -3125,6 +3198,10 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + clipboardy@4.0.0: + resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} + engines: {node: '>=18'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3696,6 +3773,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -3861,6 +3942,10 @@ packages: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -3995,6 +4080,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -4060,6 +4149,11 @@ packages: engines: {node: '>=8'} hasBin: true + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -4075,6 +4169,11 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4086,10 +4185,22 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + is64bit@2.0.0: + resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} + engines: {node: '>=18'} + isbot@5.1.32: resolution: {integrity: sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ==} engines: {node: '>=18'} @@ -4666,6 +4777,10 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + mimic-response@1.0.1: resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} engines: {node: '>=4'} @@ -4749,6 +4864,10 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -4788,6 +4907,10 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + open@7.4.2: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} @@ -4799,6 +4922,9 @@ packages: orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + osascript-tag@0.1.2: + resolution: {integrity: sha512-aAp0P3/liaNfWGVkwWYlUv8z/kB3+fsdhTHFdsenMOqsS4DayJlUeoc4Rw7Zgqh18jRSAZzQAsODS7Qku3weUg==} + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -4858,6 +4984,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -5341,6 +5471,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + simple-swizzle@0.2.4: resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} @@ -5431,6 +5565,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5477,6 +5615,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + system-architecture@0.1.0: + resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} + engines: {node: '>=18'} + tailwind-merge@3.0.2: resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==} @@ -6067,6 +6209,8 @@ packages: snapshots: + '@1focus/logs@file:../../../lang/ts/lib/1focus/packages/logs': {} + '@acemir/cssom@0.9.28': {} '@ai-sdk/gateway@2.0.18(zod@4.1.13)': @@ -7300,6 +7444,13 @@ snapshots: '@types/pg': 8.11.6 optional: true + '@nikiv/ts-utils@0.1.7': + dependencies: + bun: 1.3.5 + clipboardy: 4.0.0 + osascript-tag: 0.1.2 + ufo: 1.6.1 + '@noble/ciphers@1.3.0': {} '@noble/ciphers@2.1.1': {} @@ -7353,6 +7504,39 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@oven/bun-darwin-aarch64@1.3.5': + optional: true + + '@oven/bun-darwin-x64-baseline@1.3.5': + optional: true + + '@oven/bun-darwin-x64@1.3.5': + optional: true + + '@oven/bun-linux-aarch64-musl@1.3.5': + optional: true + + '@oven/bun-linux-aarch64@1.3.5': + optional: true + + '@oven/bun-linux-x64-baseline@1.3.5': + optional: true + + '@oven/bun-linux-x64-musl-baseline@1.3.5': + optional: true + + '@oven/bun-linux-x64-musl@1.3.5': + optional: true + + '@oven/bun-linux-x64@1.3.5': + optional: true + + '@oven/bun-windows-x64-baseline@1.3.5': + optional: true + + '@oven/bun-windows-x64@1.3.5': + optional: true + '@planetscale/database@1.19.0': optional: true @@ -8776,6 +8960,20 @@ snapshots: buffer-from@1.1.2: {} + bun@1.3.5: + optionalDependencies: + '@oven/bun-darwin-aarch64': 1.3.5 + '@oven/bun-darwin-x64': 1.3.5 + '@oven/bun-darwin-x64-baseline': 1.3.5 + '@oven/bun-linux-aarch64': 1.3.5 + '@oven/bun-linux-aarch64-musl': 1.3.5 + '@oven/bun-linux-x64': 1.3.5 + '@oven/bun-linux-x64-baseline': 1.3.5 + '@oven/bun-linux-x64-musl': 1.3.5 + '@oven/bun-linux-x64-musl-baseline': 1.3.5 + '@oven/bun-windows-x64': 1.3.5 + '@oven/bun-windows-x64-baseline': 1.3.5 + cac@6.7.14: {} cacheable-lookup@5.0.4: {} @@ -8896,6 +9094,12 @@ snapshots: cjs-module-lexer@1.4.3: {} + clipboardy@4.0.0: + dependencies: + execa: 8.0.1 + is-wsl: 3.1.0 + is64bit: 2.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -9524,6 +9728,18 @@ snapshots: eventsource-parser@3.0.6: {} + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + exit-hook@2.2.1: {} expect-type@1.2.2: {} @@ -9679,6 +9895,8 @@ snapshots: dependencies: pump: 3.0.3 + get-stream@8.0.1: {} + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -9848,6 +10066,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@5.0.0: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -9904,6 +10124,8 @@ snapshots: is-docker@2.2.1: {} + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -9914,16 +10136,30 @@ snapshots: is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-number@7.0.0: {} is-plain-obj@4.1.0: {} is-potential-custom-element-name@1.0.1: {} + is-stream@3.0.0: {} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + is64bit@2.0.0: + dependencies: + system-architecture: 0.1.0 + isbot@5.1.32: {} isexe@2.0.0: {} @@ -10860,6 +11096,8 @@ snapshots: mime@3.0.0: {} + mimic-fn@4.0.0: {} + mimic-response@1.0.1: {} mimic-response@3.1.0: {} @@ -10945,6 +11183,10 @@ snapshots: normalize-url@6.1.0: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -10979,6 +11221,10 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + open@7.4.2: dependencies: is-docker: 2.2.1 @@ -10995,6 +11241,8 @@ snapshots: orderedmap@2.1.1: {} + osascript-tag@0.1.2: {} + p-cancelable@2.1.1: {} p-limit@2.3.0: @@ -11054,6 +11302,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -11679,6 +11929,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + simple-swizzle@0.2.4: dependencies: is-arrayish: 0.3.4 @@ -11752,6 +12004,8 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-final-newline@3.0.0: {} + strip-json-comments@3.1.1: {} stripe@20.1.0(@types/node@24.10.1): @@ -11801,6 +12055,8 @@ snapshots: symbol-tree@3.2.4: {} + system-architecture@0.1.0: {} + tailwind-merge@3.0.2: {} tailwindcss@4.1.17: {}