From baa1b3aa544c18b04ca498a2480d543f475306c5 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Fri, 15 Aug 2025 15:18:49 +0000 Subject: [PATCH 01/21] refactor: restructured mcp components into `vault-core` package --- packages/vault-core/package.json | 3 +- .../src/adapters/reddit/src/drizzle.config.ts | 14 +- .../src/adapters/reddit/src/index.ts | 341 +++++++++++++++++- packages/vault-core/src/index.ts | 2 +- packages/vault-core/src/types.ts | 194 ++++++++++ 5 files changed, 523 insertions(+), 31 deletions(-) create mode 100644 packages/vault-core/src/types.ts diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index 9ba0e63d53..8c2ca69e71 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -1,11 +1,12 @@ { "name": "@repo/vault-core", + "private": true, "version": "0.0.0", "description": "Core package for adapter & MCP functionality", "type": "module", "exports": { ".": "./src/index.ts", - "./adapters/*": "./src/adapters/index.ts" + "./adapters": "./src/adapters/index.ts" }, "devDependencies": { "typescript": "catalog:" diff --git a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts b/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts index 417e465508..2d0e441a15 100644 --- a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts +++ b/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts @@ -1,21 +1,15 @@ -import { fileURLToPath } from 'node:url'; import { defineConfig } from 'drizzle-kit'; -// Resolve paths relative to this module so they work regardless of process CWD -// Migrations live at the adapter root (../migrations), not inside src/ -// TODO custom migration format/process that doesn't rely on node:fs (??) -const out = fileURLToPath(new URL('../migrations', import.meta.url)); -const schema = fileURLToPath(new URL('./schema.ts', import.meta.url)) as string; - export default defineConfig({ // Using sqlite dialect; schema is in this package dialect: 'sqlite', casing: 'snake_case', strict: true, - out, + out: './migrations', - // Use absolute schema path for CLI compatibility as well - schema, + // Schema can stay undefined, since it will be passed at runtime + // For the sake of migrations, we'll use the actual path + schema: './src/schema.ts', // Every adapter *must* have a unique migrations table name, in order for everything to play nicely with other adapters migrations: { diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index d25bd8e917..808705dbcc 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -1,15 +1,325 @@ -import { type Adapter, defineAdapter } from '@repo/vault-core'; +import { defineAdapter } from '@repo/vault-core'; +import { type } from 'arktype'; import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; -import type { RedditAdapterConfig } from './config'; import drizzleConfig from './drizzle.config'; import { metadata } from './metadata'; import { parseRedditExport } from './parse'; import * as tables from './schema'; import { upsertRedditData } from './upsert'; -import { parseSchema } from './validation'; // Expose all tables from schema module (runtime values only; TS types are erased) -export const schema = tables; +export const schema = tables as unknown as Record; + +// ArkType parse schema +// Tight validation for posts and comments (parsed today), +// explicit object-array schemas for all other datasets to avoid 'unknown'. +export const parseSchema = type({ + // Core content + posts: [ + { + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + title: 'string | undefined', + url: 'string | undefined', + body: 'string | undefined', + }, + ], + post_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + url: 'string | undefined', + }, + ], + comments: [ + { + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + link: 'string', + post_id: 'string | undefined', + parent: 'string | undefined', + body: 'string | undefined', + media: 'string | undefined', + }, + ], + comment_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + link: 'string | undefined', + parent: 'string | undefined', + }, + ], + + // Votes / visibility / saves + post_votes: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }, + ], + comment_votes: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }, + ], + saved_posts: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], + saved_comments: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], + hidden_posts: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], + + // Messaging + message_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }, + ], + messages: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }, + ], + messages_archive_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }, + ], + messages_archive: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }, + ], + + // Chat + chat_history: [ + { + message_id: 'string | undefined', + created_at: 'Date | undefined', + updated_at: 'Date | undefined', + username: 'string | undefined', + message: 'string | undefined', + thread_parent_message_id: 'string | undefined', + channel_url: 'string | undefined', + subreddit: 'string | undefined', + channel_name: 'string | undefined', + conversation_type: 'string | undefined', + }, + ], + + // Account and preferences + account_gender: [{ account_gender: 'string | undefined' }], + sensitive_ads_preferences: [ + { type: 'string | undefined', preference: 'string | undefined' }, + ], + birthdate: [ + { + birthdate: 'Date | undefined', + verified_birthdate: 'Date | undefined', + verification_state: 'string | undefined', + verification_method: 'string | undefined', + }, + ], + user_preferences: [ + { preference: 'string | undefined', value: 'string | undefined' }, + ], + linked_identities: [ + { issuer_id: 'string | undefined', subject_id: 'string | undefined' }, + ], + linked_phone_number: [{ phone_number: 'string | undefined' }], + twitter: [{ username: 'string | undefined' }], + + // Moderation / subscriptions / subreddits + approved_submitter_subreddits: [{ subreddit: 'string | undefined' }], + moderated_subreddits: [{ subreddit: 'string | undefined' }], + subscribed_subreddits: [{ subreddit: 'string | undefined' }], + multireddits: [ + { + id: 'string | undefined', + display_name: 'string | undefined', + date: 'Date | undefined', + description: 'string | undefined', + privacy: 'string | undefined', + subreddits: 'string | undefined', + image_url: 'string | undefined', + is_owner: 'string | undefined', + favorited: 'string | undefined', + followers: 'string | undefined', + }, + ], + + // Commerce and payouts + purchases: [ + { + processor: 'string | undefined', + transaction_id: 'string | undefined', + product: 'string | undefined', + date: 'Date | undefined', + cost: 'string | undefined', + currency: 'string | undefined', + status: 'string | undefined', + }, + ], + subscriptions: [ + { + processor: 'string | undefined', + subscription_id: 'string | undefined', + product: 'string | undefined', + product_id: 'string | undefined', + product_name: 'string | undefined', + status: 'string | undefined', + start_date: 'Date | undefined', + end_date: 'Date | undefined', + }, + ], + payouts: [ + { + payout_amount_usd: 'string | undefined', + date: 'Date | undefined', + payout_id: 'string | undefined', + }, + ], + stripe: [{ stripe_account_id: 'string | undefined' }], + + // Misc + announcements: [ + { + announcement_id: 'string | undefined', + sent_at: 'Date | undefined', + read_at: 'Date | undefined', + from_id: 'string | undefined', + from_username: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + }, + ], + drafts: [ + { + id: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + kind: 'string | undefined', + created: 'Date | undefined', + spoiler: 'string | undefined', + nsfw: 'string | undefined', + original_content: 'string | undefined', + content_category: 'string | undefined', + flair_id: 'string | undefined', + flair_text: 'string | undefined', + send_replies: 'string | undefined', + subreddit: 'string | undefined', + is_public_link: 'string | undefined', + }, + ], + friends: [{ username: 'string | undefined', note: 'string | undefined' }], + gilded_content: [ + { + content_link: 'string | undefined', + award: 'string | undefined', + amount: 'string | undefined', + date: 'Date | undefined', + }, + ], + gold_received: [ + { + content_link: 'string | undefined', + gold_received: 'string | undefined', + gilder_username: 'string | undefined', + date: 'Date | undefined', + }, + ], + ip_logs: [{ date: 'Date | undefined', ip: 'string | undefined' }], + persona: [{ persona_inquiry_id: 'string | undefined' }], + poll_votes: [ + { + post_id: 'string | undefined', + user_selection: 'string | undefined', + text: 'string | undefined', + image_url: 'string | undefined', + is_prediction: 'string | undefined', + stake_amount: 'string | undefined', + }, + ], + scheduled_posts: [ + { + scheduled_post_id: 'string | undefined', + subreddit: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + submission_time: 'Date | undefined', + recurrence: 'string | undefined', + }, + ], + statistics: [ + { statistic: 'string | undefined', value: 'string | undefined' }, + ], + checkfile: [{ filename: 'string | undefined', sha256: 'string | undefined' }], +}); + // ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. // Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. type Arrayify = T extends readonly [infer E] ? E[] : T; @@ -21,19 +331,12 @@ export type ParsedRedditExport = { export type ParseResult = ParsedRedditExport; // Adapter export -export const redditAdapter = defineAdapter((args: RedditAdapterConfig) => { - args; // TODO - - const adapter = { - id: 'reddit', - name: 'Reddit Adapter', - schema, - metadata, - validator: parseSchema, - drizzleConfig, - parse: parseRedditExport, - upsert: upsertRedditData, - }; - - return adapter; +export const redditAdapter = defineAdapter({ + name: 'Reddit Adapter', + schema, + metadata, + parseSchema, + drizzleConfig, + parse: parseRedditExport, + upsert: upsertRedditData, }); diff --git a/packages/vault-core/src/index.ts b/packages/vault-core/src/index.ts index 4b0e041376..fcb073fefc 100644 --- a/packages/vault-core/src/index.ts +++ b/packages/vault-core/src/index.ts @@ -1 +1 @@ -export * from './core'; +export * from './types'; diff --git a/packages/vault-core/src/types.ts b/packages/vault-core/src/types.ts new file mode 100644 index 0000000000..5f19a1bb6b --- /dev/null +++ b/packages/vault-core/src/types.ts @@ -0,0 +1,194 @@ +import { type Type, type } from 'arktype'; +import type { defineConfig } from 'drizzle-kit'; +import type { ColumnsSelection } from 'drizzle-orm'; +import type { LibSQLDatabase } from 'drizzle-orm/libsql'; +import { + type BaseSQLiteDatabase, + integer, + type SQLiteTable, + type SubqueryWithSelection, + sqliteTable, + text, +} from 'drizzle-orm/sqlite-core'; + +type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> + ? R + : never; +type ResultSet = ExtractedResult; + +// Bootstrapped type to represent compatible Drizzle database types across the codebase +export type CompatibleDB> = + BaseSQLiteDatabase<'sync' | 'async', TSchema | ResultSet>; + +type DrizzleConfig = ReturnType; + +export type ColumnDescriptions> = { + [K in keyof T]: { + [C in keyof T[K]['_']['columns']]: string; + }; +}; + +type View< + T extends string, + TSelection extends ColumnsSelection, + TSchema extends Record, + TDatabase extends CompatibleDB, +> = { + name: T; + definition: (db: TDatabase) => SubqueryWithSelection; +}; + +export interface Adapter< + TID extends string = string, + TSchema extends Record = Record, + TDatabase extends CompatibleDB = CompatibleDB, + TParserShape extends Type = Type, + TParsed = TParserShape['infer'], +> { + /** + * Unique identifier for the adapter + * + * Should be lowercase, no spaces, alpha-numeric. + * @example "twitter" + */ + id: TID; + + /** + * User-facing name + * @example "Reddit Adapter" + */ + name: string; + + /** Database schema */ + schema: TSchema; + + /** Column descriptions for every table/column */ + metadata: ColumnDescriptions; + + /** + * ArkType schema for parsing/validation + * + * This will be used by the MCP server to validate data returned from the `parse` method. + */ + validator: TParserShape; + + /** + * Predefined views/CTEs + * + * Should be used for common queries that a user will want to query for. This is especially helpful if the data storage format is complex/unintuitive. + * + * @example + * "recently_played": { + * description: "Recently played songs", + * definition: (db) => db.select().from(songs).where(...) + * } + */ + views?: { + [Alias in string]: View; + }; + + /** + * Drizzle config + * + * @example + * defineConfig({ + * dialect: 'sqlite', + * schema: './src/schema.ts', + * out: './migrations', + * migrations: { + * table: 'test_migrations', + * }, + * }) + */ + drizzleConfig: DrizzleConfig; + + // Lifecycle hooks + + /** + * Parse a blob into a parsed representation + * @example + * const text = await b.text(); + * return JSON.parse(text); + */ + parse: (file: Blob) => Promise; + + /** Upsert data into the database */ + upsert: (db: TDatabase, data: TParsed) => Promise; +} + +// Note: If a generic only appears in a function parameter position, TS won't infer it and will fall back to the constraint (e.g. `object`). +// These overloads infer the full function type `F` instead, preserving the args type. +export function defineAdapter< + // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories + F extends () => Adapter, +>(adapter: F): F; +export function defineAdapter< + // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories + F extends (args: any) => Adapter, +>(adapter: F): F; +// Implementation signature can be broad; overloads provide strong typing to callers +export function defineAdapter unknown>( + adapter: F, +): F { + return adapter; +} + +// Example +// TODO remove + +const songs = sqliteTable('songs', { + id: integer('id').primaryKey(), + title: text('title'), + artist: text('artist'), + album: text('album'), + year: integer('year'), +}); + +const testAdapter = defineAdapter(() => ({ + id: 'test', + name: 'Test Adapter', + validator: type({ + id: 'number', + title: 'string', + artist: 'string', + album: 'string', + year: 'number', + }), + schema: { + songs, + }, + drizzleConfig: { + dialect: 'sqlite', + schema: './src/schema.ts', + casing: 'snake_case', + strict: true, + out: './migrations', + migrations: { + table: 'test_migrations', + }, + }, + parse: (file) => file.text().then(JSON.parse), + upsert: (db, data) => + db + .insert(songs) + .values(data) + .onConflictDoUpdate({ + target: songs.id, + set: { + title: data.title, + artist: data.artist, + album: data.album, + year: data.year, + }, + }) + .then(() => undefined), + metadata: { + songs: { + id: 'Unique identifier for the song', + title: 'Title of the song', + artist: 'Artist of the song', + album: 'Album of the song', + year: 'Year of release', + }, + }, +})); From 1293a5e81121732b9b685152af25be252e0ad04f Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Fri, 15 Aug 2025 15:22:50 +0000 Subject: [PATCH 02/21] refactor(reddit): move validation into another --- .../src/adapters/reddit/src/index.ts | 311 +--------- .../src/adapters/reddit/src/validation.ts | 540 ++++++++++-------- 2 files changed, 290 insertions(+), 561 deletions(-) diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index 808705dbcc..c74cd97d0f 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -6,320 +6,11 @@ import { metadata } from './metadata'; import { parseRedditExport } from './parse'; import * as tables from './schema'; import { upsertRedditData } from './upsert'; +import { parseSchema } from './validation'; // Expose all tables from schema module (runtime values only; TS types are erased) export const schema = tables as unknown as Record; -// ArkType parse schema -// Tight validation for posts and comments (parsed today), -// explicit object-array schemas for all other datasets to avoid 'unknown'. -export const parseSchema = type({ - // Core content - posts: [ - { - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - title: 'string | undefined', - url: 'string | undefined', - body: 'string | undefined', - }, - ], - post_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - url: 'string | undefined', - }, - ], - comments: [ - { - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - link: 'string', - post_id: 'string | undefined', - parent: 'string | undefined', - body: 'string | undefined', - media: 'string | undefined', - }, - ], - comment_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - link: 'string | undefined', - parent: 'string | undefined', - }, - ], - - // Votes / visibility / saves - post_votes: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }, - ], - comment_votes: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }, - ], - saved_posts: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], - saved_comments: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], - hidden_posts: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], - - // Messaging - message_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }, - ], - messages: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }, - ], - messages_archive_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }, - ], - messages_archive: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }, - ], - - // Chat - chat_history: [ - { - message_id: 'string | undefined', - created_at: 'Date | undefined', - updated_at: 'Date | undefined', - username: 'string | undefined', - message: 'string | undefined', - thread_parent_message_id: 'string | undefined', - channel_url: 'string | undefined', - subreddit: 'string | undefined', - channel_name: 'string | undefined', - conversation_type: 'string | undefined', - }, - ], - - // Account and preferences - account_gender: [{ account_gender: 'string | undefined' }], - sensitive_ads_preferences: [ - { type: 'string | undefined', preference: 'string | undefined' }, - ], - birthdate: [ - { - birthdate: 'Date | undefined', - verified_birthdate: 'Date | undefined', - verification_state: 'string | undefined', - verification_method: 'string | undefined', - }, - ], - user_preferences: [ - { preference: 'string | undefined', value: 'string | undefined' }, - ], - linked_identities: [ - { issuer_id: 'string | undefined', subject_id: 'string | undefined' }, - ], - linked_phone_number: [{ phone_number: 'string | undefined' }], - twitter: [{ username: 'string | undefined' }], - - // Moderation / subscriptions / subreddits - approved_submitter_subreddits: [{ subreddit: 'string | undefined' }], - moderated_subreddits: [{ subreddit: 'string | undefined' }], - subscribed_subreddits: [{ subreddit: 'string | undefined' }], - multireddits: [ - { - id: 'string | undefined', - display_name: 'string | undefined', - date: 'Date | undefined', - description: 'string | undefined', - privacy: 'string | undefined', - subreddits: 'string | undefined', - image_url: 'string | undefined', - is_owner: 'string | undefined', - favorited: 'string | undefined', - followers: 'string | undefined', - }, - ], - - // Commerce and payouts - purchases: [ - { - processor: 'string | undefined', - transaction_id: 'string | undefined', - product: 'string | undefined', - date: 'Date | undefined', - cost: 'string | undefined', - currency: 'string | undefined', - status: 'string | undefined', - }, - ], - subscriptions: [ - { - processor: 'string | undefined', - subscription_id: 'string | undefined', - product: 'string | undefined', - product_id: 'string | undefined', - product_name: 'string | undefined', - status: 'string | undefined', - start_date: 'Date | undefined', - end_date: 'Date | undefined', - }, - ], - payouts: [ - { - payout_amount_usd: 'string | undefined', - date: 'Date | undefined', - payout_id: 'string | undefined', - }, - ], - stripe: [{ stripe_account_id: 'string | undefined' }], - - // Misc - announcements: [ - { - announcement_id: 'string | undefined', - sent_at: 'Date | undefined', - read_at: 'Date | undefined', - from_id: 'string | undefined', - from_username: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - }, - ], - drafts: [ - { - id: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - kind: 'string | undefined', - created: 'Date | undefined', - spoiler: 'string | undefined', - nsfw: 'string | undefined', - original_content: 'string | undefined', - content_category: 'string | undefined', - flair_id: 'string | undefined', - flair_text: 'string | undefined', - send_replies: 'string | undefined', - subreddit: 'string | undefined', - is_public_link: 'string | undefined', - }, - ], - friends: [{ username: 'string | undefined', note: 'string | undefined' }], - gilded_content: [ - { - content_link: 'string | undefined', - award: 'string | undefined', - amount: 'string | undefined', - date: 'Date | undefined', - }, - ], - gold_received: [ - { - content_link: 'string | undefined', - gold_received: 'string | undefined', - gilder_username: 'string | undefined', - date: 'Date | undefined', - }, - ], - ip_logs: [{ date: 'Date | undefined', ip: 'string | undefined' }], - persona: [{ persona_inquiry_id: 'string | undefined' }], - poll_votes: [ - { - post_id: 'string | undefined', - user_selection: 'string | undefined', - text: 'string | undefined', - image_url: 'string | undefined', - is_prediction: 'string | undefined', - stake_amount: 'string | undefined', - }, - ], - scheduled_posts: [ - { - scheduled_post_id: 'string | undefined', - subreddit: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - submission_time: 'Date | undefined', - recurrence: 'string | undefined', - }, - ], - statistics: [ - { statistic: 'string | undefined', value: 'string | undefined' }, - ], - checkfile: [{ filename: 'string | undefined', sha256: 'string | undefined' }], -}); - // ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. // Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. type Arrayify = T extends readonly [infer E] ? E[] : T; diff --git a/packages/vault-core/src/adapters/reddit/src/validation.ts b/packages/vault-core/src/adapters/reddit/src/validation.ts index 2e215c28f0..d5d4c783f3 100644 --- a/packages/vault-core/src/adapters/reddit/src/validation.ts +++ b/packages/vault-core/src/adapters/reddit/src/validation.ts @@ -4,269 +4,307 @@ import { type } from 'arktype'; // explicit object-array schemas for all other datasets to avoid 'unknown'. export const parseSchema = type({ // Core content - posts: type({ - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - title: 'string | undefined', - url: 'string | undefined', - body: 'string | undefined', - }).array(), - post_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - url: 'string | undefined', - }).array(), - comments: type({ - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - link: 'string', - post_id: 'string | undefined', - parent: 'string | undefined', - body: 'string | undefined', - media: 'string | undefined', - }).array(), - comment_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - link: 'string | undefined', - parent: 'string | undefined', - }).array(), + posts: [ + { + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + title: 'string | undefined', + url: 'string | undefined', + body: 'string | undefined', + }, + ], + post_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + url: 'string | undefined', + }, + ], + comments: [ + { + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + link: 'string', + post_id: 'string | undefined', + parent: 'string | undefined', + body: 'string | undefined', + media: 'string | undefined', + }, + ], + comment_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + link: 'string | undefined', + parent: 'string | undefined', + }, + ], // Votes / visibility / saves - post_votes: type({ - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }).array(), - comment_votes: type({ - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }).array(), - saved_posts: type({ - id: 'string | undefined', - permalink: 'string | undefined', - }).array(), - saved_comments: type({ - id: 'string | undefined', - permalink: 'string | undefined', - }).array(), - hidden_posts: type({ - id: 'string | undefined', - permalink: 'string | undefined', - }).array(), + post_votes: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }, + ], + comment_votes: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }, + ], + saved_posts: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], + saved_comments: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], + hidden_posts: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + }, + ], // Messaging - message_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }).array(), - messages: type({ - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }).array(), - messages_archive_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }).array(), - messages_archive: type({ - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }).array(), + message_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }, + ], + messages: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }, + ], + messages_archive_headers: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }, + ], + messages_archive: [ + { + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }, + ], // Chat - chat_history: type({ - message_id: 'string | undefined', - created_at: 'Date | undefined', - updated_at: 'Date | undefined', - username: 'string | undefined', - message: 'string | undefined', - thread_parent_message_id: 'string | undefined', - channel_url: 'string | undefined', - subreddit: 'string | undefined', - channel_name: 'string | undefined', - conversation_type: 'string | undefined', - }).array(), + chat_history: [ + { + message_id: 'string | undefined', + created_at: 'Date | undefined', + updated_at: 'Date | undefined', + username: 'string | undefined', + message: 'string | undefined', + thread_parent_message_id: 'string | undefined', + channel_url: 'string | undefined', + subreddit: 'string | undefined', + channel_name: 'string | undefined', + conversation_type: 'string | undefined', + }, + ], // Account and preferences - account_gender: type({ account_gender: 'string | undefined' }).array(), - sensitive_ads_preferences: type({ - type: 'string | undefined', - preference: 'string | undefined', - }).array(), - birthdate: type({ - birthdate: 'Date | undefined', - verified_birthdate: 'Date | undefined', - verification_state: 'string | undefined', - verification_method: 'string | undefined', - }).array(), - user_preferences: type({ - preference: 'string | undefined', - value: 'string | undefined', - }).array(), - linked_identities: type({ - issuer_id: 'string | undefined', - subject_id: 'string | undefined', - }).array(), - linked_phone_number: type({ phone_number: 'string | undefined' }).array(), - twitter: type({ username: 'string | undefined' }).array(), + account_gender: [{ account_gender: 'string | undefined' }], + sensitive_ads_preferences: [ + { type: 'string | undefined', preference: 'string | undefined' }, + ], + birthdate: [ + { + birthdate: 'Date | undefined', + verified_birthdate: 'Date | undefined', + verification_state: 'string | undefined', + verification_method: 'string | undefined', + }, + ], + user_preferences: [ + { preference: 'string | undefined', value: 'string | undefined' }, + ], + linked_identities: [ + { issuer_id: 'string | undefined', subject_id: 'string | undefined' }, + ], + linked_phone_number: [{ phone_number: 'string | undefined' }], + twitter: [{ username: 'string | undefined' }], // Moderation / subscriptions / subreddits - approved_submitter_subreddits: type({ - subreddit: 'string | undefined', - }).array(), - moderated_subreddits: type({ subreddit: 'string | undefined' }).array(), - subscribed_subreddits: type({ subreddit: 'string | undefined' }).array(), - multireddits: type({ - id: 'string | undefined', - display_name: 'string | undefined', - date: 'Date | undefined', - description: 'string | undefined', - privacy: 'string | undefined', - subreddits: 'string | undefined', - image_url: 'string | undefined', - is_owner: 'string | undefined', - favorited: 'string | undefined', - followers: 'string | undefined', - }).array(), + approved_submitter_subreddits: [{ subreddit: 'string | undefined' }], + moderated_subreddits: [{ subreddit: 'string | undefined' }], + subscribed_subreddits: [{ subreddit: 'string | undefined' }], + multireddits: [ + { + id: 'string | undefined', + display_name: 'string | undefined', + date: 'Date | undefined', + description: 'string | undefined', + privacy: 'string | undefined', + subreddits: 'string | undefined', + image_url: 'string | undefined', + is_owner: 'string | undefined', + favorited: 'string | undefined', + followers: 'string | undefined', + }, + ], // Commerce and payouts - purchases: type({ - processor: 'string | undefined', - transaction_id: 'string | undefined', - product: 'string | undefined', - date: 'Date | undefined', - cost: 'string | undefined', - currency: 'string | undefined', - status: 'string | undefined', - }).array(), - subscriptions: type({ - processor: 'string | undefined', - subscription_id: 'string | undefined', - product: 'string | undefined', - product_id: 'string | undefined', - product_name: 'string | undefined', - status: 'string | undefined', - start_date: 'Date | undefined', - end_date: 'Date | undefined', - }).array(), - payouts: type({ - payout_amount_usd: 'string | undefined', - date: 'Date | undefined', - payout_id: 'string | undefined', - }).array(), - stripe: type({ stripe_account_id: 'string | undefined' }).array(), + purchases: [ + { + processor: 'string | undefined', + transaction_id: 'string | undefined', + product: 'string | undefined', + date: 'Date | undefined', + cost: 'string | undefined', + currency: 'string | undefined', + status: 'string | undefined', + }, + ], + subscriptions: [ + { + processor: 'string | undefined', + subscription_id: 'string | undefined', + product: 'string | undefined', + product_id: 'string | undefined', + product_name: 'string | undefined', + status: 'string | undefined', + start_date: 'Date | undefined', + end_date: 'Date | undefined', + }, + ], + payouts: [ + { + payout_amount_usd: 'string | undefined', + date: 'Date | undefined', + payout_id: 'string | undefined', + }, + ], + stripe: [{ stripe_account_id: 'string | undefined' }], // Misc - announcements: type({ - announcement_id: 'string | undefined', - sent_at: 'Date | undefined', - read_at: 'Date | undefined', - from_id: 'string | undefined', - from_username: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - }).array(), - drafts: type({ - id: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - kind: 'string | undefined', - created: 'Date | undefined', - spoiler: 'string | undefined', - nsfw: 'string | undefined', - original_content: 'string | undefined', - content_category: 'string | undefined', - flair_id: 'string | undefined', - flair_text: 'string | undefined', - send_replies: 'string | undefined', - subreddit: 'string | undefined', - is_public_link: 'string | undefined', - }).array(), - friends: type({ - username: 'string | undefined', - note: 'string | undefined', - }).array(), - gilded_content: type({ - content_link: 'string | undefined', - award: 'string | undefined', - amount: 'string | undefined', - date: 'Date | undefined', - }).array(), - gold_received: type({ - content_link: 'string | undefined', - gold_received: 'string | undefined', - gilder_username: 'string | undefined', - date: 'Date | undefined', - }).array(), - ip_logs: type({ date: 'Date | undefined', ip: 'string | undefined' }).array(), - persona: type({ persona_inquiry_id: 'string | undefined' }).array(), - poll_votes: type({ - post_id: 'string | undefined', - user_selection: 'string | undefined', - text: 'string | undefined', - image_url: 'string | undefined', - is_prediction: 'string | undefined', - stake_amount: 'string | undefined', - }).array(), - scheduled_posts: type({ - scheduled_post_id: 'string | undefined', - subreddit: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - submission_time: 'Date | undefined', - recurrence: 'string | undefined', - }).array(), - statistics: type({ - statistic: 'string | undefined', - value: 'string | undefined', - }).array(), - checkfile: type({ - filename: 'string | undefined', - sha256: 'string | undefined', - }).array(), + announcements: [ + { + announcement_id: 'string | undefined', + sent_at: 'Date | undefined', + read_at: 'Date | undefined', + from_id: 'string | undefined', + from_username: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + }, + ], + drafts: [ + { + id: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + kind: 'string | undefined', + created: 'Date | undefined', + spoiler: 'string | undefined', + nsfw: 'string | undefined', + original_content: 'string | undefined', + content_category: 'string | undefined', + flair_id: 'string | undefined', + flair_text: 'string | undefined', + send_replies: 'string | undefined', + subreddit: 'string | undefined', + is_public_link: 'string | undefined', + }, + ], + friends: [{ username: 'string | undefined', note: 'string | undefined' }], + gilded_content: [ + { + content_link: 'string | undefined', + award: 'string | undefined', + amount: 'string | undefined', + date: 'Date | undefined', + }, + ], + gold_received: [ + { + content_link: 'string | undefined', + gold_received: 'string | undefined', + gilder_username: 'string | undefined', + date: 'Date | undefined', + }, + ], + ip_logs: [{ date: 'Date | undefined', ip: 'string | undefined' }], + persona: [{ persona_inquiry_id: 'string | undefined' }], + poll_votes: [ + { + post_id: 'string | undefined', + user_selection: 'string | undefined', + text: 'string | undefined', + image_url: 'string | undefined', + is_prediction: 'string | undefined', + stake_amount: 'string | undefined', + }, + ], + scheduled_posts: [ + { + scheduled_post_id: 'string | undefined', + subreddit: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + submission_time: 'Date | undefined', + recurrence: 'string | undefined', + }, + ], + statistics: [ + { statistic: 'string | undefined', value: 'string | undefined' }, + ], + checkfile: [{ filename: 'string | undefined', sha256: 'string | undefined' }], }); From b599eb737683986775730c65fbf7864f4d5c7b8e Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Fri, 15 Aug 2025 20:52:21 +0000 Subject: [PATCH 03/21] feat: added `vault-core` functionality --- packages/vault-core/package.json | 3 +- .../src/adapters/reddit/src/drizzle.config.ts | 14 +- .../src/adapters/reddit/src/index.ts | 29 +- .../src/adapters/reddit/src/validation.ts | 540 ++++++++---------- packages/vault-core/src/index.ts | 2 +- packages/vault-core/src/types.ts | 194 ------- 6 files changed, 280 insertions(+), 502 deletions(-) delete mode 100644 packages/vault-core/src/types.ts diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index 8c2ca69e71..9ba0e63d53 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -1,12 +1,11 @@ { "name": "@repo/vault-core", - "private": true, "version": "0.0.0", "description": "Core package for adapter & MCP functionality", "type": "module", "exports": { ".": "./src/index.ts", - "./adapters": "./src/adapters/index.ts" + "./adapters/*": "./src/adapters/index.ts" }, "devDependencies": { "typescript": "catalog:" diff --git a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts b/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts index 2d0e441a15..fe2fcd3a6d 100644 --- a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts +++ b/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts @@ -1,15 +1,21 @@ import { defineConfig } from 'drizzle-kit'; +import { fileURLToPath } from 'node:url'; + +// Resolve paths relative to this module so they work regardless of process CWD +// Migrations live at the adapter root (../migrations), not inside src/ +// TODO custom migration format/process that doesn't rely on node:fs (??) +const out = fileURLToPath(new URL('../migrations', import.meta.url)); +const schema = fileURLToPath(new URL('./schema.ts', import.meta.url)) as string; export default defineConfig({ // Using sqlite dialect; schema is in this package dialect: 'sqlite', casing: 'snake_case', strict: true, - out: './migrations', + out, - // Schema can stay undefined, since it will be passed at runtime - // For the sake of migrations, we'll use the actual path - schema: './src/schema.ts', + // Use absolute schema path for CLI compatibility as well + schema, // Every adapter *must* have a unique migrations table name, in order for everything to play nicely with other adapters migrations: { diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index c74cd97d0f..cb7df709dc 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -1,6 +1,5 @@ import { defineAdapter } from '@repo/vault-core'; -import { type } from 'arktype'; -import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; +import type { RedditAdapterConfig } from './config'; import drizzleConfig from './drizzle.config'; import { metadata } from './metadata'; import { parseRedditExport } from './parse'; @@ -9,8 +8,7 @@ import { upsertRedditData } from './upsert'; import { parseSchema } from './validation'; // Expose all tables from schema module (runtime values only; TS types are erased) -export const schema = tables as unknown as Record; - +export const schema = tables; // ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. // Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. type Arrayify = T extends readonly [infer E] ? E[] : T; @@ -22,12 +20,19 @@ export type ParsedRedditExport = { export type ParseResult = ParsedRedditExport; // Adapter export -export const redditAdapter = defineAdapter({ - name: 'Reddit Adapter', - schema, - metadata, - parseSchema, - drizzleConfig, - parse: parseRedditExport, - upsert: upsertRedditData, +export const redditAdapter = defineAdapter((args: RedditAdapterConfig) => { + args; // TODO + + const adapter = { + id: 'reddit', + name: 'Reddit Adapter', + schema, + metadata, + validator: parseSchema, + drizzleConfig, + parse: parseRedditExport, + upsert: upsertRedditData, + }; + + return adapter; }); diff --git a/packages/vault-core/src/adapters/reddit/src/validation.ts b/packages/vault-core/src/adapters/reddit/src/validation.ts index d5d4c783f3..2e215c28f0 100644 --- a/packages/vault-core/src/adapters/reddit/src/validation.ts +++ b/packages/vault-core/src/adapters/reddit/src/validation.ts @@ -4,307 +4,269 @@ import { type } from 'arktype'; // explicit object-array schemas for all other datasets to avoid 'unknown'. export const parseSchema = type({ // Core content - posts: [ - { - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - title: 'string | undefined', - url: 'string | undefined', - body: 'string | undefined', - }, - ], - post_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - url: 'string | undefined', - }, - ], - comments: [ - { - id: 'string', - permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', - subreddit: 'string', - gildings: 'number | undefined', - link: 'string', - post_id: 'string | undefined', - parent: 'string | undefined', - body: 'string | undefined', - media: 'string | undefined', - }, - ], - comment_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - link: 'string | undefined', - parent: 'string | undefined', - }, - ], + posts: type({ + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + title: 'string | undefined', + url: 'string | undefined', + body: 'string | undefined', + }).array(), + post_headers: type({ + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + url: 'string | undefined', + }).array(), + comments: type({ + id: 'string', + permalink: 'string', + date: 'Date', + created_utc: 'Date', + ip: 'string | undefined', + subreddit: 'string', + gildings: 'number | undefined', + link: 'string', + post_id: 'string | undefined', + parent: 'string | undefined', + body: 'string | undefined', + media: 'string | undefined', + }).array(), + comment_headers: type({ + id: 'string | undefined', + permalink: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + subreddit: 'string | undefined', + gildings: 'number | undefined', + link: 'string | undefined', + parent: 'string | undefined', + }).array(), // Votes / visibility / saves - post_votes: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }, - ], - comment_votes: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', - }, - ], - saved_posts: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], - saved_comments: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], - hidden_posts: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - }, - ], + post_votes: type({ + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }).array(), + comment_votes: type({ + id: 'string | undefined', + permalink: 'string | undefined', + direction: 'string | undefined', + }).array(), + saved_posts: type({ + id: 'string | undefined', + permalink: 'string | undefined', + }).array(), + saved_comments: type({ + id: 'string | undefined', + permalink: 'string | undefined', + }).array(), + hidden_posts: type({ + id: 'string | undefined', + permalink: 'string | undefined', + }).array(), // Messaging - message_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }, - ], - messages: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }, - ], - messages_archive_headers: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - }, - ], - messages_archive: [ - { - id: 'string | undefined', - permalink: 'string | undefined', - thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - from: 'string | undefined', - to: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - }, - ], + message_headers: type({ + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }).array(), + messages: type({ + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }).array(), + messages_archive_headers: type({ + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + }).array(), + messages_archive: type({ + id: 'string | undefined', + permalink: 'string | undefined', + thread_id: 'string | undefined', + date: 'Date | undefined', + ip: 'string | undefined', + from: 'string | undefined', + to: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + }).array(), // Chat - chat_history: [ - { - message_id: 'string | undefined', - created_at: 'Date | undefined', - updated_at: 'Date | undefined', - username: 'string | undefined', - message: 'string | undefined', - thread_parent_message_id: 'string | undefined', - channel_url: 'string | undefined', - subreddit: 'string | undefined', - channel_name: 'string | undefined', - conversation_type: 'string | undefined', - }, - ], + chat_history: type({ + message_id: 'string | undefined', + created_at: 'Date | undefined', + updated_at: 'Date | undefined', + username: 'string | undefined', + message: 'string | undefined', + thread_parent_message_id: 'string | undefined', + channel_url: 'string | undefined', + subreddit: 'string | undefined', + channel_name: 'string | undefined', + conversation_type: 'string | undefined', + }).array(), // Account and preferences - account_gender: [{ account_gender: 'string | undefined' }], - sensitive_ads_preferences: [ - { type: 'string | undefined', preference: 'string | undefined' }, - ], - birthdate: [ - { - birthdate: 'Date | undefined', - verified_birthdate: 'Date | undefined', - verification_state: 'string | undefined', - verification_method: 'string | undefined', - }, - ], - user_preferences: [ - { preference: 'string | undefined', value: 'string | undefined' }, - ], - linked_identities: [ - { issuer_id: 'string | undefined', subject_id: 'string | undefined' }, - ], - linked_phone_number: [{ phone_number: 'string | undefined' }], - twitter: [{ username: 'string | undefined' }], + account_gender: type({ account_gender: 'string | undefined' }).array(), + sensitive_ads_preferences: type({ + type: 'string | undefined', + preference: 'string | undefined', + }).array(), + birthdate: type({ + birthdate: 'Date | undefined', + verified_birthdate: 'Date | undefined', + verification_state: 'string | undefined', + verification_method: 'string | undefined', + }).array(), + user_preferences: type({ + preference: 'string | undefined', + value: 'string | undefined', + }).array(), + linked_identities: type({ + issuer_id: 'string | undefined', + subject_id: 'string | undefined', + }).array(), + linked_phone_number: type({ phone_number: 'string | undefined' }).array(), + twitter: type({ username: 'string | undefined' }).array(), // Moderation / subscriptions / subreddits - approved_submitter_subreddits: [{ subreddit: 'string | undefined' }], - moderated_subreddits: [{ subreddit: 'string | undefined' }], - subscribed_subreddits: [{ subreddit: 'string | undefined' }], - multireddits: [ - { - id: 'string | undefined', - display_name: 'string | undefined', - date: 'Date | undefined', - description: 'string | undefined', - privacy: 'string | undefined', - subreddits: 'string | undefined', - image_url: 'string | undefined', - is_owner: 'string | undefined', - favorited: 'string | undefined', - followers: 'string | undefined', - }, - ], + approved_submitter_subreddits: type({ + subreddit: 'string | undefined', + }).array(), + moderated_subreddits: type({ subreddit: 'string | undefined' }).array(), + subscribed_subreddits: type({ subreddit: 'string | undefined' }).array(), + multireddits: type({ + id: 'string | undefined', + display_name: 'string | undefined', + date: 'Date | undefined', + description: 'string | undefined', + privacy: 'string | undefined', + subreddits: 'string | undefined', + image_url: 'string | undefined', + is_owner: 'string | undefined', + favorited: 'string | undefined', + followers: 'string | undefined', + }).array(), // Commerce and payouts - purchases: [ - { - processor: 'string | undefined', - transaction_id: 'string | undefined', - product: 'string | undefined', - date: 'Date | undefined', - cost: 'string | undefined', - currency: 'string | undefined', - status: 'string | undefined', - }, - ], - subscriptions: [ - { - processor: 'string | undefined', - subscription_id: 'string | undefined', - product: 'string | undefined', - product_id: 'string | undefined', - product_name: 'string | undefined', - status: 'string | undefined', - start_date: 'Date | undefined', - end_date: 'Date | undefined', - }, - ], - payouts: [ - { - payout_amount_usd: 'string | undefined', - date: 'Date | undefined', - payout_id: 'string | undefined', - }, - ], - stripe: [{ stripe_account_id: 'string | undefined' }], + purchases: type({ + processor: 'string | undefined', + transaction_id: 'string | undefined', + product: 'string | undefined', + date: 'Date | undefined', + cost: 'string | undefined', + currency: 'string | undefined', + status: 'string | undefined', + }).array(), + subscriptions: type({ + processor: 'string | undefined', + subscription_id: 'string | undefined', + product: 'string | undefined', + product_id: 'string | undefined', + product_name: 'string | undefined', + status: 'string | undefined', + start_date: 'Date | undefined', + end_date: 'Date | undefined', + }).array(), + payouts: type({ + payout_amount_usd: 'string | undefined', + date: 'Date | undefined', + payout_id: 'string | undefined', + }).array(), + stripe: type({ stripe_account_id: 'string | undefined' }).array(), // Misc - announcements: [ - { - announcement_id: 'string | undefined', - sent_at: 'Date | undefined', - read_at: 'Date | undefined', - from_id: 'string | undefined', - from_username: 'string | undefined', - subject: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - }, - ], - drafts: [ - { - id: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - kind: 'string | undefined', - created: 'Date | undefined', - spoiler: 'string | undefined', - nsfw: 'string | undefined', - original_content: 'string | undefined', - content_category: 'string | undefined', - flair_id: 'string | undefined', - flair_text: 'string | undefined', - send_replies: 'string | undefined', - subreddit: 'string | undefined', - is_public_link: 'string | undefined', - }, - ], - friends: [{ username: 'string | undefined', note: 'string | undefined' }], - gilded_content: [ - { - content_link: 'string | undefined', - award: 'string | undefined', - amount: 'string | undefined', - date: 'Date | undefined', - }, - ], - gold_received: [ - { - content_link: 'string | undefined', - gold_received: 'string | undefined', - gilder_username: 'string | undefined', - date: 'Date | undefined', - }, - ], - ip_logs: [{ date: 'Date | undefined', ip: 'string | undefined' }], - persona: [{ persona_inquiry_id: 'string | undefined' }], - poll_votes: [ - { - post_id: 'string | undefined', - user_selection: 'string | undefined', - text: 'string | undefined', - image_url: 'string | undefined', - is_prediction: 'string | undefined', - stake_amount: 'string | undefined', - }, - ], - scheduled_posts: [ - { - scheduled_post_id: 'string | undefined', - subreddit: 'string | undefined', - title: 'string | undefined', - body: 'string | undefined', - url: 'string | undefined', - submission_time: 'Date | undefined', - recurrence: 'string | undefined', - }, - ], - statistics: [ - { statistic: 'string | undefined', value: 'string | undefined' }, - ], - checkfile: [{ filename: 'string | undefined', sha256: 'string | undefined' }], + announcements: type({ + announcement_id: 'string | undefined', + sent_at: 'Date | undefined', + read_at: 'Date | undefined', + from_id: 'string | undefined', + from_username: 'string | undefined', + subject: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + }).array(), + drafts: type({ + id: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + kind: 'string | undefined', + created: 'Date | undefined', + spoiler: 'string | undefined', + nsfw: 'string | undefined', + original_content: 'string | undefined', + content_category: 'string | undefined', + flair_id: 'string | undefined', + flair_text: 'string | undefined', + send_replies: 'string | undefined', + subreddit: 'string | undefined', + is_public_link: 'string | undefined', + }).array(), + friends: type({ + username: 'string | undefined', + note: 'string | undefined', + }).array(), + gilded_content: type({ + content_link: 'string | undefined', + award: 'string | undefined', + amount: 'string | undefined', + date: 'Date | undefined', + }).array(), + gold_received: type({ + content_link: 'string | undefined', + gold_received: 'string | undefined', + gilder_username: 'string | undefined', + date: 'Date | undefined', + }).array(), + ip_logs: type({ date: 'Date | undefined', ip: 'string | undefined' }).array(), + persona: type({ persona_inquiry_id: 'string | undefined' }).array(), + poll_votes: type({ + post_id: 'string | undefined', + user_selection: 'string | undefined', + text: 'string | undefined', + image_url: 'string | undefined', + is_prediction: 'string | undefined', + stake_amount: 'string | undefined', + }).array(), + scheduled_posts: type({ + scheduled_post_id: 'string | undefined', + subreddit: 'string | undefined', + title: 'string | undefined', + body: 'string | undefined', + url: 'string | undefined', + submission_time: 'Date | undefined', + recurrence: 'string | undefined', + }).array(), + statistics: type({ + statistic: 'string | undefined', + value: 'string | undefined', + }).array(), + checkfile: type({ + filename: 'string | undefined', + sha256: 'string | undefined', + }).array(), }); diff --git a/packages/vault-core/src/index.ts b/packages/vault-core/src/index.ts index fcb073fefc..4b0e041376 100644 --- a/packages/vault-core/src/index.ts +++ b/packages/vault-core/src/index.ts @@ -1 +1 @@ -export * from './types'; +export * from './core'; diff --git a/packages/vault-core/src/types.ts b/packages/vault-core/src/types.ts deleted file mode 100644 index 5f19a1bb6b..0000000000 --- a/packages/vault-core/src/types.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { type Type, type } from 'arktype'; -import type { defineConfig } from 'drizzle-kit'; -import type { ColumnsSelection } from 'drizzle-orm'; -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import { - type BaseSQLiteDatabase, - integer, - type SQLiteTable, - type SubqueryWithSelection, - sqliteTable, - text, -} from 'drizzle-orm/sqlite-core'; - -type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> - ? R - : never; -type ResultSet = ExtractedResult; - -// Bootstrapped type to represent compatible Drizzle database types across the codebase -export type CompatibleDB> = - BaseSQLiteDatabase<'sync' | 'async', TSchema | ResultSet>; - -type DrizzleConfig = ReturnType; - -export type ColumnDescriptions> = { - [K in keyof T]: { - [C in keyof T[K]['_']['columns']]: string; - }; -}; - -type View< - T extends string, - TSelection extends ColumnsSelection, - TSchema extends Record, - TDatabase extends CompatibleDB, -> = { - name: T; - definition: (db: TDatabase) => SubqueryWithSelection; -}; - -export interface Adapter< - TID extends string = string, - TSchema extends Record = Record, - TDatabase extends CompatibleDB = CompatibleDB, - TParserShape extends Type = Type, - TParsed = TParserShape['infer'], -> { - /** - * Unique identifier for the adapter - * - * Should be lowercase, no spaces, alpha-numeric. - * @example "twitter" - */ - id: TID; - - /** - * User-facing name - * @example "Reddit Adapter" - */ - name: string; - - /** Database schema */ - schema: TSchema; - - /** Column descriptions for every table/column */ - metadata: ColumnDescriptions; - - /** - * ArkType schema for parsing/validation - * - * This will be used by the MCP server to validate data returned from the `parse` method. - */ - validator: TParserShape; - - /** - * Predefined views/CTEs - * - * Should be used for common queries that a user will want to query for. This is especially helpful if the data storage format is complex/unintuitive. - * - * @example - * "recently_played": { - * description: "Recently played songs", - * definition: (db) => db.select().from(songs).where(...) - * } - */ - views?: { - [Alias in string]: View; - }; - - /** - * Drizzle config - * - * @example - * defineConfig({ - * dialect: 'sqlite', - * schema: './src/schema.ts', - * out: './migrations', - * migrations: { - * table: 'test_migrations', - * }, - * }) - */ - drizzleConfig: DrizzleConfig; - - // Lifecycle hooks - - /** - * Parse a blob into a parsed representation - * @example - * const text = await b.text(); - * return JSON.parse(text); - */ - parse: (file: Blob) => Promise; - - /** Upsert data into the database */ - upsert: (db: TDatabase, data: TParsed) => Promise; -} - -// Note: If a generic only appears in a function parameter position, TS won't infer it and will fall back to the constraint (e.g. `object`). -// These overloads infer the full function type `F` instead, preserving the args type. -export function defineAdapter< - // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories - F extends () => Adapter, ->(adapter: F): F; -export function defineAdapter< - // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories - F extends (args: any) => Adapter, ->(adapter: F): F; -// Implementation signature can be broad; overloads provide strong typing to callers -export function defineAdapter unknown>( - adapter: F, -): F { - return adapter; -} - -// Example -// TODO remove - -const songs = sqliteTable('songs', { - id: integer('id').primaryKey(), - title: text('title'), - artist: text('artist'), - album: text('album'), - year: integer('year'), -}); - -const testAdapter = defineAdapter(() => ({ - id: 'test', - name: 'Test Adapter', - validator: type({ - id: 'number', - title: 'string', - artist: 'string', - album: 'string', - year: 'number', - }), - schema: { - songs, - }, - drizzleConfig: { - dialect: 'sqlite', - schema: './src/schema.ts', - casing: 'snake_case', - strict: true, - out: './migrations', - migrations: { - table: 'test_migrations', - }, - }, - parse: (file) => file.text().then(JSON.parse), - upsert: (db, data) => - db - .insert(songs) - .values(data) - .onConflictDoUpdate({ - target: songs.id, - set: { - title: data.title, - artist: data.artist, - album: data.album, - year: data.year, - }, - }) - .then(() => undefined), - metadata: { - songs: { - id: 'Unique identifier for the song', - title: 'Title of the song', - artist: 'Artist of the song', - album: 'Album of the song', - year: 'Year of release', - }, - }, -})); From 094b03f360eea18be8ee512053f94c65bbd1a61c Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 19 Aug 2025 19:03:34 +0000 Subject: [PATCH 04/21] feat: new vault system --- apps/demo-mcp/README.md | 46 +++ apps/demo-mcp/package.json | 13 +- apps/demo-mcp/src/cli.ts | 204 +++++++++++-- .../20250819T155643-vault-architecture.md | 84 ++++++ docs/vault-core-diagram.md | 210 +++++++------- packages/vault-core/README.md | 156 ++++++++-- .../vault-core/src/adapters/reddit/index.ts | 2 +- .../src/adapters/reddit/src/adapter.ts | 8 + .../src/adapters/reddit/src/importer.ts | 24 ++ .../src/adapters/reddit/src/index.ts | 40 +-- .../src/adapters/reddit/src/types.ts | 12 + .../src/adapters/reddit/src/upsert.ts | 18 +- packages/vault-core/src/codecs/index.ts | 2 + packages/vault-core/src/codecs/json.ts | 27 ++ packages/vault-core/src/codecs/markdown.ts | 71 +++++ packages/vault-core/src/core/adapter.ts | 172 +---------- packages/vault-core/src/core/codec.ts | 108 +++++++ packages/vault-core/src/core/config.ts | 75 +++-- packages/vault-core/src/core/fs.ts | 69 +++++ packages/vault-core/src/core/importer.ts | 83 ++++++ packages/vault-core/src/core/index.ts | 9 +- packages/vault-core/src/core/migrations.ts | 155 ++++++++++ packages/vault-core/src/core/sync.ts | 18 ++ packages/vault-core/src/core/vault-client.ts | 28 ++ packages/vault-core/src/core/vault-service.ts | 274 ++++++++++++++++++ packages/vault-core/src/core/vault.ts | 144 --------- packages/vault-core/src/fs/index.ts | 1 + .../vault-core/src/fs/local-file-store.ts | 91 ++++++ packages/vault-core/src/sync/git.ts | 39 +++ packages/vault-core/src/sync/index.ts | 1 + 30 files changed, 1644 insertions(+), 540 deletions(-) create mode 100644 docs/specs/20250819T155643-vault-architecture.md create mode 100644 packages/vault-core/src/adapters/reddit/src/adapter.ts create mode 100644 packages/vault-core/src/adapters/reddit/src/importer.ts create mode 100644 packages/vault-core/src/adapters/reddit/src/types.ts create mode 100644 packages/vault-core/src/codecs/index.ts create mode 100644 packages/vault-core/src/codecs/json.ts create mode 100644 packages/vault-core/src/codecs/markdown.ts create mode 100644 packages/vault-core/src/core/codec.ts create mode 100644 packages/vault-core/src/core/fs.ts create mode 100644 packages/vault-core/src/core/importer.ts create mode 100644 packages/vault-core/src/core/migrations.ts create mode 100644 packages/vault-core/src/core/sync.ts create mode 100644 packages/vault-core/src/core/vault-client.ts create mode 100644 packages/vault-core/src/core/vault-service.ts delete mode 100644 packages/vault-core/src/core/vault.ts create mode 100644 packages/vault-core/src/fs/index.ts create mode 100644 packages/vault-core/src/fs/local-file-store.ts create mode 100644 packages/vault-core/src/sync/git.ts create mode 100644 packages/vault-core/src/sync/index.ts diff --git a/apps/demo-mcp/README.md b/apps/demo-mcp/README.md index b4426ec096..a9e53876d4 100644 --- a/apps/demo-mcp/README.md +++ b/apps/demo-mcp/README.md @@ -12,6 +12,52 @@ bun run apps/demo-mcp/src/cli.ts import reddit --file ./export_username_date.zip This creates a SQLite database at `.data/reddit.db` with your Reddit posts, comments, and other data. +## Plaintext export/import (Markdown) + +Export database rows to deterministic plaintext files under `vault//...` using Markdown (with frontmatter): + +```bash +bun run apps/demo-mcp/src/cli.ts export-fs reddit --db ./.data/reddit.db --repo . +``` + +Import files from the repo back into the database: + +```bash +bun run apps/demo-mcp/src/cli.ts import-fs reddit --db ./.data/reddit.db --repo . +``` + +Notes: + +- Files are Markdown only, written under `vault///.md`. + +## Try it + +1) Import your Reddit export into a local DB + +```bash +bun run apps/demo-mcp/src/cli.ts import reddit --file ./export_username_date.zip --db ./.data/reddit.db +``` + +2) Export DB rows to Markdown files in your repo + +```bash +bun run apps/demo-mcp/src/cli.ts export-fs reddit --db ./.data/reddit.db --repo . +``` + +You should see Markdown files under `vault/reddit/
/...`. To re-import from files into the DB: + +```bash +bun run apps/demo-mcp/src/cli.ts import-fs reddit --db ./.data/reddit.db --repo . +``` + +### CLI usage + +```bash +bun run apps/demo-mcp/src/cli.ts --help +# or +bun run apps/demo-mcp/src/cli.ts help +``` + ## MCP Integration with Claude Code Once you have imported your data, you can connect the database to Claude Code for natural language querying. diff --git a/apps/demo-mcp/package.json b/apps/demo-mcp/package.json index 387ce0e3a1..4fe919408d 100644 --- a/apps/demo-mcp/package.json +++ b/apps/demo-mcp/package.json @@ -5,13 +5,16 @@ "type": "module", "description": "Minimal CLI to import Reddit export ZIP into a local LibSQL DB using the Reddit adapter", "scripts": { - "dev": "bun run src/cli.ts import", - "import": "bun run src/cli.ts import", - "serve": "bun run src/cli.ts serve", - "check": "tsc --noEmit" + "dev": "bun run src/cli.ts import reddit", + "import": "bun run src/cli.ts import reddit", + "export-fs": "bun run src/cli.ts export-fs reddit", + "import-fs": "bun run src/cli.ts import-fs reddit", + "sync": "bun run src/cli.ts sync reddit", + "serve": "bun run src/cli.ts serve" }, "dependencies": { "@libsql/client": "^0.11.0", - "drizzle-orm": "catalog:" + "drizzle-orm": "catalog:", + "@repo/vault-core": "workspace:*" } } diff --git a/apps/demo-mcp/src/cli.ts b/apps/demo-mcp/src/cli.ts index 50eafd6cdc..8b0c17642e 100644 --- a/apps/demo-mcp/src/cli.ts +++ b/apps/demo-mcp/src/cli.ts @@ -1,25 +1,34 @@ #!/usr/bin/env bun /** - * Minimal CLI to import a Reddit export ZIP into a local LibSQL database. + * Minimal CLI for the Reddit demo adapter. + * * Commands: - * - import [--file ] [--db ] - * - serve [--db ] (stub) + * - import [--file ] [--db ] + * - export-fs [--db ] [--repo ] (Markdown only) + * - import-fs [--db ] [--repo ] (Markdown only) + * - serve [--db ] (stub) * - * Defaults: - * --file defaults to ./export_rocket_scientist2_20250811.zip (cwd) - * --db defaults to ./.data/reddit.db (cwd) + * Defaults (if not provided): + * --file ./export_rocket_scientist2_20250811.zip (relative to cwd) + * --db ./.data/reddit.db (relative to cwd) + * --repo . (current working directory) * - * DATABASE_URL (optional): - * If set, overrides the db URL entirely (e.g., libsql://..., file:/abs/path.db). + * Environment: + * DATABASE_URL (optional) overrides the db URL entirely (e.g., libsql://..., file:/abs/path.db). */ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createClient } from '@libsql/client'; -import type { Adapter } from '@repo/vault-core'; -import { Vault } from '@repo/vault-core'; +import type { Importer } from '@repo/vault-core'; +import { + defaultConvention, + LocalFileStore, + markdownFormat, + VaultService, +} from '@repo/vault-core'; import { drizzle } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; @@ -28,6 +37,7 @@ type CLIArgs = { _: string[]; // positional file?: string; db?: string; + repo?: string; }; function parseArgs(argv: string[]): CLIArgs { @@ -42,6 +52,10 @@ function parseArgs(argv: string[]): CLIArgs { out.db = argv[++i]; } else if (a.startsWith('--db=')) { out.db = a.slice('--db='.length); + } else if (a === '--repo') { + out.repo = argv[++i]; + } else if (a.startsWith('--repo=')) { + out.repo = a.slice('--repo='.length); } else if (!a.startsWith('-')) { out._.push(a); } @@ -56,6 +70,18 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '../../..'); // apps/demo-mcp/src -> repo root +function getBinPath(): string { + const rel = path.relative(process.cwd(), __filename); + return rel || __filename; +} + +function printHelp(): void { + const bin = getBinPath(); + console.log( + `Usage:\n bun run ${bin} [options]\n\nCommands:\n import Import a Reddit export ZIP into the database\n export-fs Export DB rows to Markdown files under vault//...\n import-fs Import Markdown files from vault//... into the DB\n serve Start stub server (not implemented)\n\nOptions:\n --file Path to Reddit export ZIP (import only)\n --db Path to SQLite DB file (default: ./.data/reddit.db or DATABASE_URL)\n --repo Repo root for plaintext I/O (default: .)\n -h, --help Show this help\n\nNotes:\n - Files are Markdown only, written under vault//
/.md\n - DATABASE_URL, if set, overrides --db entirely.\n`, + ); +} + function resolveZipPath(p?: string): string { const candidate = p ?? './export_rocket_scientist2_20250811.zip'; return path.resolve(process.cwd(), candidate); @@ -66,6 +92,11 @@ function resolveDbFile(p?: string): string { return path.resolve(process.cwd(), candidate); } +function resolveRepoDir(p?: string): string { + const candidate = p ?? '.'; + return path.resolve(process.cwd(), candidate); +} + // Note: migrations folder is resolved per-adapter below // ------------------------------------------------------------- @@ -104,7 +135,7 @@ async function cmdImport(args: CLIArgs, adapterID: string) { const blob = new Blob([new Uint8Array(data)], { type: 'application/zip' }); // Build adapter instances, ensuring migrations path is absolute per adapter package - let adapter: Adapter | undefined; + let importer: Importer | undefined; // This is just patch code, don't look too closely! const keys = await fs.readdir( @@ -121,28 +152,118 @@ async function cmdImport(args: CLIArgs, adapterID: string) { // TODO if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { - adapter = a as Adapter; + importer = a as Importer; } } } - if (!adapter) throw new Error(`Could not find adapter for key ${adapterID}`); + if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); - // Initialize Vault (runs migrations implicitly) - const vault = await Vault.create({ - adapters: [adapter], + // Initialize VaultService (runs migrations implicitly) + const service = await VaultService.create({ + importers: [importer], database: db, migrateFunc: migrate, }); - const summary = await vault.importBlob(blob, adapterID); - for (const r of summary.reports) { - console.log(`\n=== Adapter: ${r.adapter} ===`); - printCounts(r.counts); + const res = await service.importBlob(blob, adapterID); + const counts = countRecords(res.parsed); + console.log(`\n=== Adapter: ${res.importer} ===`); + printCounts(counts); + console.log(`\nImport complete. DB path: ${dbFile}`); +} + +// ------------------------------------------------------------- +// Export DB -> Files (Markdown only) +// ------------------------------------------------------------- +async function cmdExportFs(args: CLIArgs, adapterID: string) { + const dbFile = resolveDbFile(args.db); + const dbUrl = toDbUrl(dbFile); + const repoDir = resolveRepoDir(args.repo); + + await ensureDirExists(dbFile); + const client = createClient({ url: dbUrl }); + const rawDb = drizzle(client); + const db = rawDb; + + let importer: Importer | undefined; + const keys = await fs.readdir( + path.resolve(repoRoot, 'packages/vault-core/src/adapters'), + ); + for (const key of keys) { + const modulePath = import.meta.resolve( + `../../../packages/vault-core/src/adapters/${key}`, + ); + const mod = (await import(modulePath)) as Record; + for (const func of Object.values(mod)) { + if (typeof func !== 'function') continue; + const a = func(); + if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { + importer = a as Importer; + } + } } - console.log(`\nAll adapters complete. DB path: ${dbFile}`); + if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); - vault.getCurrentLayout(); + const service = await VaultService.create({ + importers: [importer], + database: db, + migrateFunc: migrate, + codec: markdownFormat, + conventions: defaultConvention(), + }); + + const store = new LocalFileStore(repoDir); + const result = await service.export(adapterID, store); + const n = Object.keys(result.files).length; + console.log(`Exported ${n} files to ${repoDir}/vault/${adapterID}`); +} + +// ------------------------------------------------------------- +// Import Files -> DB (Markdown only) +// ------------------------------------------------------------- +async function cmdImportFs(args: CLIArgs, adapterID: string) { + const dbFile = resolveDbFile(args.db); + const dbUrl = toDbUrl(dbFile); + const repoDir = resolveRepoDir(args.repo); + + await ensureDirExists(dbFile); + const client = createClient({ url: dbUrl }); + const rawDb = drizzle(client); + const db = rawDb; + + let importer: Importer | undefined; + const keys = await fs.readdir( + path.resolve(repoRoot, 'packages/vault-core/src/adapters'), + ); + for (const key of keys) { + const modulePath = import.meta.resolve( + `../../../packages/vault-core/src/adapters/${key}`, + ); + const mod = (await import(modulePath)) as Record; + for (const func of Object.values(mod)) { + if (typeof func !== 'function') continue; + const a = func(); + if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { + importer = a as Importer; + } + } + } + if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); + + const service = await VaultService.create({ + importers: [importer], + database: db, + migrateFunc: migrate, + codec: markdownFormat, + conventions: defaultConvention(), + }); + + const store = new LocalFileStore(repoDir); + await service.import(adapterID, store); + console.log( + `Imported files from ${repoDir}/vault/${adapterID} into DB ${dbFile}`, + ); } function printCounts(parsedOrCounts: Record) { @@ -158,6 +279,16 @@ function printCounts(parsedOrCounts: Record) { } } +function countRecords(parsed: unknown): Record { + const out: Record = {}; + if (parsed && typeof parsed === 'object') { + for (const [k, v] of Object.entries(parsed as Record)) { + out[k] = Array.isArray(v) ? v.length : 0; + } + } + return out; +} + // ------------------------------------------------------------- // Serve command (stub) // ------------------------------------------------------------- @@ -181,24 +312,39 @@ async function main() { const argv = process.argv.slice(2); const args = parseArgs(argv); - const command = args._.at(0) ?? 'import'; + // Global help + if (argv.includes('--help') || argv.includes('-h') || args._[0] === 'help') { + printHelp(); + return; + } + + const command = args._[0] ?? 'import'; switch (command) { case 'import': { - const adapter = args._[1]; + // Default to 'reddit' for this demo if adapter not provided + const adapter = args._[1] ?? 'reddit'; await cmdImport(args, adapter); } break; + case 'export-fs': + { + const adapter = args._[1] ?? 'reddit'; + await cmdExportFs(args, adapter); + } + break; + case 'import-fs': + { + const adapter = args._[1] ?? 'reddit'; + await cmdImportFs(args, adapter); + } + break; case 'serve': await cmdServe(args); break; default: console.error(`Unknown command: ${command}`); - console.error('Usage:'); - console.error( - ' bun run src/cli.ts import [--file ] [--db ]', - ); - console.error(' bun run src/cli.ts serve [--db ]'); + printHelp(); process.exit(1); } } diff --git a/docs/specs/20250819T155643-vault-architecture.md b/docs/specs/20250819T155643-vault-architecture.md new file mode 100644 index 0000000000..d6289ee4f6 --- /dev/null +++ b/docs/specs/20250819T155643-vault-architecture.md @@ -0,0 +1,84 @@ +# Vault Architecture Overview + +## Discussion + +After a long discussion, we settled on loose requirements for the vault architectures. There was extensive discussion over the user of nano-ids, document-based storage, and having multiple data ingest layers. + +### Key Requirements + +After careful consideration, the following were established as key requirements. + +- **Portability**: Data needs to be serializable/deserializable at the filesystem level. +- **Transparency**: The user can easily inspect the data and its structure. +- **Synchronization**: Data should be syncronizable across devices using a reliable mechanism. + +Some conclusions on the above: + +#### Portability + +Concrete requirements for portability are slightly vague for now. We verbally settled on a "push pull" model, wherein data can be externally reset or pushed to a remote source. See [synchronization](#synchronization) for more details. + +Due to changing requirements, a composable strategy should be adopted, in order to remain flexible and minimize churn in the future. + +#### Transparency + +While SQLite is **not** a proprietary storage format, it was deemed "too opaque" for our needs. We want to ensure that users can easily understand and interact with the data, and synchronize it via a CLI tool. + +#### Synchronization + +One of our maintainers informed us of a few software-based solutions that could be used for synchronization, but expressed their concerns about sustainability. Our primary target for synchronization is Git, while attempting to avoid the possibility of merge conflicts. + +### Strategy + +Main discussion revolved around these two concepts: + +- SQLite-first +- Document-first + +While a document-first approach significantly increases complexity through side effects, it was decided that this would be the target approach moving forward, due to the aforementioned requirements. + +### Considerations + +It is worth noting that our current plan is considered naive. + +- Many problems still unaccounted for. +- Previous preferences/tooling choices may need to be revisited. +- Implementation details are still vague. +- User preferences are not yet fully understood. + +## Architecture + +The `vault-core` package separates responsibilities into: + +- VaultClient: Runs in the app (web/desktop). Holds Adapters for schema/metadata only. Uses RPC to talk to VaultService for operations. +- VaultService: Runs on a server or sidecar process. Holds Importers which implement parse/validate/upsert and own DB/migrations. + +### Key concepts + +- Adapter: Schema-only (drizzle schema and drizzleConfig). Optionally metadata for column descriptions. +- Importer: Encapsulates one data source workflow: id, name, adapter, validator, parse(blob), upsert(db, parsed), and optional views. +- Service DB: The service owns the database connection and migration function. + +### Suggested RPC pattern (to be implemented by host app) + +Define a minimal protocol to connect client and service. The exact transport is undetermined and left to the host app. A basic shape could include: + +- importers.list -> returns available importers (id, name) +- importers.migrate { id } -> triggers service-side migration for a single importer +- import.importBlob { id, file } -> uploads a blob and triggers parse+upsert on the service +- schema.describe { id } -> returns human-readable schema info for an adapter (optional; client can compute this locally from Adapter if bundled) + +### Lifecycle + +- Client constructs VaultClient with a list of Adapters for type-safe UI and schema introspection. +- Service constructs VaultService with Importers and a DB connection. On startup, it runs migrations for all importers (or selectively). +- Client submits a requests via RPC. Service parses with the selected importer, validates, executes, and responds. +- Synchronization needs to occur via an interface, configured by the host. + +### Synchronization + +WIP + +### Others + +Details like transport, auth, streaming, retries, and backpressure are left TBD. diff --git a/docs/vault-core-diagram.md b/docs/vault-core-diagram.md index 8f4066f7fa..fe82430204 100644 --- a/docs/vault-core-diagram.md +++ b/docs/vault-core-diagram.md @@ -1,8 +1,8 @@ -# Vault Core: architecture and adapter relationships +# Vault Core: architecture and adapter relationships (new) -Purpose: explain core functionality, how adapters plug in, and how data flows at runtime. This is a snapshot of the current code to help coworkers review what is exposed and how it connects. +Purpose: explain core functionality, how adapters plug in, and how data flows at runtime. Snapshot of the current VaultService-centric design. -Quick links to top level barrels: +Quick links - [src/index.ts](/packages/vault-core/src/index.ts) - [src/core/index.ts](/packages/vault-core/src/core/index.ts) @@ -10,138 +10,128 @@ Quick links to top level barrels: Core surfaces -- Vault container - - Class: [Vault()](/packages/vault-core/src/core/vault.ts) - - Init and migrations: [migrate()](/packages/vault-core/src/core/vault.ts) - - Import flow: [importBlob()](/packages/vault-core/src/core/vault.ts) - - Current layout exposure: [getCurrentLayout()](/packages/vault-core/src/core/vault.ts) - - Row counter used in summary: [countRecords()](/packages/vault-core/src/core/vault.ts) -- Vault configuration contract - - Interface: [VaultConfig()](/packages/vault-core/src/core/config.ts) - - Caller supplies: - - Database: any Drizzle SQLite database compatible with adapter schemas; see [CompatibleDB()](/packages/vault-core/src/core/adapter.ts) - - Platform migrate function: e.g. drizzle-orm migrator for libsql or better; passed to Vault -- Adapter contract and helper - - Contract: [Adapter()](/packages/vault-core/src/core/adapter.ts) - - Factory helper: [defineAdapter()](/packages/vault-core/src/core/adapter.ts) -- Schema readability utility - - Human readable schema and metadata merge: [readableSchemaInfo()](/packages/vault-core/src/core/strip.ts) +- VaultService orchestrator + - Class: [VaultService](/packages/vault-core/src/core/vault-service.ts) + - Blob import: `importBlob(blob, importerId)` (ArkType validation ON) + - Filesystem export/import: `export(importerId, store)`, `import(importerId, store)` (no ArkType) + - Migration orchestration: `migrateImportMigrate(importerId, store, { targetTag })` + - Optional Git helpers via SyncEngine: `gitPull()`, `gitCommit(msg)`, `gitPush()` +- Vault configuration + - Interface: [VaultServiceConfig](/packages/vault-core/src/core/config.ts) + - Caller supplies: Drizzle DB ([CompatibleDB](/packages/vault-core/src/core/adapter.ts)), `migrateFunc`, optional `syncEngine`, a single Markdown `codec`, and a convention profile +- Conventions & Codec + - Convention is part of [codec.ts](/packages/vault-core/src/core/codec.ts) via `defaultConvention()` + - Codec: [markdown](/packages/vault-core/src/codecs/markdown.ts) +- Sync Engine + - Interface: [SyncEngine](/packages/vault-core/src/core/sync.ts) + - Git implementation: [GitSyncEngine](/packages/vault-core/src/sync/git.ts) + - File store: [LocalFileStore](/packages/vault-core/src/fs/local-file-store.ts) +- Migrations utilities + - Module: [migrations.ts](/packages/vault-core/src/core/migrations.ts) + - Reads drizzle journal, plans SQL, drops adapter tables, applies steps (DB-specific TODOs called out) + Concrete adapter example: Reddit - Adapter factory: [redditAdapter()](/packages/vault-core/src/adapters/reddit/src/index.ts) -- Drizzle schema tables: [adapters/reddit/src/schema.ts](/packages/vault-core/src/adapters/reddit/src/schema.ts) -- Natural language column metadata: [metadata()](/packages/vault-core/src/adapters/reddit/src/metadata.ts) -- ArkType validation schema: [parseSchema()](/packages/vault-core/src/adapters/reddit/src/validation.ts) -- Parser (ZIP containing CSV files): [parseRedditExport()](/packages/vault-core/src/adapters/reddit/src/parse.ts) -- Upsert logic (transactional onConflictDoUpdate): [upsertRedditData()](/packages/vault-core/src/adapters/reddit/src/upsert.ts) -- Drizzle adapter config and migrations path: [adapters/reddit/src/drizzle.config.ts](/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts) -- Config placeholder type: [adapters/reddit/src/config.ts](/packages/vault-core/src/adapters/reddit/src/config.ts) +- Drizzle schema tables: [schema.ts](/packages/vault-core/src/adapters/reddit/src/schema.ts) +- Column metadata: [metadata.ts](/packages/vault-core/src/adapters/reddit/src/metadata.ts) +- ArkType validation: [validation.ts](/packages/vault-core/src/adapters/reddit/src/validation.ts) +- Parser (ZIP of CSVs): [parse.ts](/packages/vault-core/src/adapters/reddit/src/parse.ts) +- Upsert logic: [upsert.ts](/packages/vault-core/src/adapters/reddit/src/upsert.ts) +- Drizzle config & migrations: [drizzle.config.ts](/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts) Mermaid diagram: components and data flow -Arrow convention: arrows point from source to consumer (adapter-owned schema and metadata flow into core utilities; external DB flows into Vault and then into adapter upsert). +Arrows point from source to consumer. Adapters own schema and migrations; VaultService owns orchestration. ```mermaid flowchart LR subgraph Core - C1[Vault class] - C2[Vault config] - C3[Readable schema util] - C4[Adapter contract] - C5[Adapter factory helper] + VS[VaultService] + CFG[VaultConfig] + MIG[Migrations utils] end - subgraph Adapter_Reddit + subgraph Conventions_and_Codecs + CNV[Convention profile] + MDM[Markdown codec] + end + + subgraph Sync + SYN[SyncEngine interface] + GSE[GitSyncEngine] + FSI[FileStore interface] + LFS[LocalFileStore] + end + + subgraph Adapters_and_Importers + I1[Importer] A1[Adapter factory] A2[Drizzle schema] A3[Column metadata] A4[ArkType validator] - A5[ZIP CSV parser] + A5[Parser] A6[Upsert logic] A7[Drizzle config] end subgraph External - E1[Migrate function from caller] - E2[SQLite DB] - E3[Adapters barrel export] + MGR[Migrate function] + DB[SQLite DB] + GIT[Git repo] end - %% External injection - E2 -->|database instance| C1 - - %% Initialization path - C1 -->|init| C1M[Migrate step] - C1M -->|per adapter| A7 - A7 -->|adapter migrations folder| E1 - C1M -->|calls| E1 - E1 -->|uses adapter migrations folder| E2 - - %% Import path - U1[import blob] --> C1 - C1 -->|select adapter by id| A1 - A1 --> A5 - A5 --> A4 - A4 -->|asserts| C1U[valid parsed object] - C1U --> A6 - C1 -.->|db instance| A6 - A2 -->|table objects| A6 - A6 -->|insert or update| E2 - - %% Schema exposure - C1 -->|list layout| C3 - A2 -->|provides schema| C3 - A3 -->|provides descriptions| C3 - - %% Exports - X1[Package root export] -.-> C1 - X2[Adapters index export] -.-> A1 + %% Config and injection + DB -->|database instance| VS + CFG --> VS + VS --> SYN + SYN -.-> GSE + SYN --> FSI + GSE --> FSI + FSI --> LFS + LFS -.-> GIT + + %% Initialization & migrations + VS -->|migrate to head| MGR + MGR -->|uses| A7 + A7 -->|migrations folder| MGR + VS -->|use| MIG + + %% Blob import + U1[importBlob] --> VS + VS -->|select importer| I1 + I1 --> A5 --> A4 --> V1[validated data] + V1 --> A6 -->|insert or update| DB + A2 --> A6 + + %% Filesystem export/import + VS -->|export| MDM + VS -->|read and write| FSI + FSI --> LFS + LFS -->|files| GIT + VS -->|import no ArkType| MDM + MDM -->|parse, de/normalize| VS + VS -->|upsert via importer| A6 + + %% Migrate → Import → Migrate + VS -->|drop tables| MIG + VS -->|plan to tag| MIG + VS -->|apply plan| MIG + VS -->|import vault| LFS + VS -->|migrate to head| MGR %% Styling linkStyle default curve: basis ``` -Legend with exact code references - -- Core - - C1 Vault class → [Vault()](/packages/vault-core/src/core/vault.ts) - - C1M migrate step → [migrate()](/packages/vault-core/src/core/vault.ts) - - C1U validated parsed object produced inside → [importBlob()](/packages/vault-core/src/core/vault.ts) - - C2 Vault config → [VaultConfig()](/packages/vault-core/src/core/config.ts) - - C3 Readable schema util → [readableSchemaInfo()](/packages/vault-core/src/core/strip.ts) - - C4 Adapter contract → [Adapter()](/packages/vault-core/src/core/adapter.ts) - - C5 Adapter factory helper → [defineAdapter()](/packages/vault-core/src/core/adapter.ts) - - X1 Package root export → [src/index.ts](/packages/vault-core/src/index.ts) -- Adapter Reddit - - A1 Adapter factory → [redditAdapter()](/packages/vault-core/src/adapters/reddit/src/index.ts) - - A2 Drizzle schema module → [adapters/reddit/src/schema.ts](/packages/vault-core/src/adapters/reddit/src/schema.ts) - - A3 Column metadata → [metadata()](/packages/vault-core/src/adapters/reddit/src/metadata.ts) - - A4 ArkType validator → [parseSchema()](/packages/vault-core/src/adapters/reddit/src/validation.ts) - - A5 ZIP CSV parser → [parseRedditExport()](/packages/vault-core/src/adapters/reddit/src/parse.ts) - - A6 Upsert logic → [upsertRedditData()](/packages/vault-core/src/adapters/reddit/src/upsert.ts) - - A7 Drizzle config → [adapters/reddit/src/drizzle.config.ts](/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts) - - E3 Adapters barrel export → [adapters/index.ts](/packages/vault-core/src/adapters/index.ts) - -Runtime relationships in prose - -- Ownership and boundaries - - SQLite DB instance: constructed externally by the caller and injected via [VaultConfig()](/packages/vault-core/src/core/config.ts) database. [Vault()](/packages/vault-core/src/core/vault.ts) stores this instance and passes it into the adapter’s upsert per the [Adapter()](/packages/vault-core/src/core/adapter.ts) contract. Core does not create or own the DB connection. - - Drizzle schema: defined entirely inside adapters (for example [adapters/reddit/src/schema.ts](/packages/vault-core/src/adapters/reddit/src/schema.ts)). Core never defines tables; it only reads adapter.schema for layout exposure via [readableSchemaInfo()](/packages/vault-core/src/core/strip.ts) and uses adapter.drizzleConfig for migrations during [migrate()](/packages/vault-core/src/core/vault.ts). - -- Initialization and migrations - - A caller prepares adapters and a migrate function via [VaultConfig()](/packages/vault-core/src/core/config.ts), then constructs or calls [Vault.create()](/packages/vault-core/src/core/vault.ts). - - Vault resolves the adapters barrel and scans for a factory whose id matches the selected adapter; for each match, Vault calls the caller’s migrate function with paths from adapter.drizzleConfig. Migrations folders are adapter-local to each adapter (for Reddit see [adapters/reddit/src/drizzle.config.ts](/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts)), executed during [migrate()](/packages/vault-core/src/core/vault.ts). -- Import flow - - The caller invokes [importBlob()](/packages/vault-core/src/core/vault.ts) with a specific adapter id. - - Vault locates the adapter by id and performs parse, validate, upsert in sequence: - - Parse: adapter.parse; Reddit uses [parseRedditExport()](/packages/vault-core/src/adapters/reddit/src/parse.ts) - - Validate: adapter.validator.assert; Reddit uses [parseSchema()](/packages/vault-core/src/adapters/reddit/src/validation.ts) - - Upsert: adapter.upsert; Reddit uses [upsertRedditData()](/packages/vault-core/src/adapters/reddit/src/upsert.ts) - - Vault returns a summary including counts computed by [countRecords()](/packages/vault-core/src/core/vault.ts). -- Schema and metadata exposure - - Callers use [getCurrentLayout()](/packages/vault-core/src/core/vault.ts) to obtain a human readable view of tables and columns, derived from adapter.schema and adapter.metadata via [readableSchemaInfo()](/packages/vault-core/src/core/strip.ts). - -Notes and open edges - -- Adapter discovery during migrations currently uses a dynamic import and id matching inside [migrate()](/packages/vault-core/src/core/vault.ts). Vite or code-based-migration-definitions required to avoid this further. -- Views are supported by the contract via Adapter.views, but the Reddit adapter does not define any yet. Adding one or two examples would clarify patterns for future adapters. +Legend and notes + +- VS VaultService → [vault-service.ts](/packages/vault-core/src/core/vault-service.ts) +- MIG Migrations utils → [migrations.ts](/packages/vault-core/src/core/migrations.ts) +- CFG VaultConfig → [config.ts](/packages/vault-core/src/core/config.ts) +- CNV ConventionProfile → [codec.ts](/packages/vault-core/src/core/codec.ts) +- Codec → [markdown.ts](/packages/vault-core/src/codecs/markdown.ts) +- Sync surfaces → [sync.ts](/packages/vault-core/src/core/sync.ts) (SyncEngine), [git.ts](/packages/vault-core/src/sync/git.ts) +- File surfaces → [fs.ts](/packages/vault-core/src/core/fs.ts) (FileStore interface), [local-file-store.ts](/packages/vault-core/src/fs/local-file-store.ts) +- DB is external; core does not create or own the connection +- ArkType validation is only for blob imports; FS imports rely on DB constraints and upsert shaping diff --git a/packages/vault-core/README.md b/packages/vault-core/README.md index 88dcb2d1ce..1079d105d4 100644 --- a/packages/vault-core/README.md +++ b/packages/vault-core/README.md @@ -1,42 +1,150 @@ # Vault Core -This package houses the interfaces & supporting code for Epicenter's upcoming adapter ecosystem. +Vault Core provides the primitives and runtime to parse, validate, persist, and sync third‑party exports into a Git‑friendly plaintext vault. It’s the foundation for Epicenter’s adapter ecosystem. -> This spec is in alpha, and will likely change significantly in the near future. Breaking changes will occur up until the 1.0 release. +> Status: Alpha. APIs may change before 1.0. -## Goal +## Architecture at a glance -The goal of the adapter system is two-fold. +- Importers: end‑to‑end units that parse a source blob, validate with ArkType, and upsert into a Drizzle database. +- Adapters: carry the Drizzle schema and migrations config, plus optional human‑readable metadata for tables/columns. +- VaultService: the orchestrator that runs migrations, imports blobs, and syncs data to/from a filesystem using a single injected codec and a convention profile. +- Conventions & Codec: conventions control file layout (paths, dataset keys); a single Markdown codec serializes/parses records deterministically. +- SyncEngine: a VCS‑focused abstraction (Git implementation provided) that offers a FileStore for read/write/list and simple pull/commit/push. -1. Create a modular, extensible, centralized hub for your exported third-party data -2. Expose available tables/features/metadata for access via SQLite explorers, LLMs, MCP, and other tools +This separation keeps Importers/Adapters pure (schema + parsing + upsert) and pushes all DB/filesystem glue and VCS specifics into the service layer. -## Summary +## Key concepts -Adapters are build on [Drizzle ORM](https://orm.drizzle.team/) and [ArkType](https://arktype.dev/). They expose: +- Adapter + - Drizzle schema (tables) and migrations config (`drizzleConfig`). + - Optional metadata for human‑readable names/descriptions. -- SQLite table schema for persisting data - - Natural language mappings for tables and columns -- ArkType schema for data parsing -- Supporting parse and upsert functions +- Importer + - `id`, `name`, `adapter`, `metadata` (optional), `validator` (ArkType), `parse(blob)`, `upsert(db, data)`. + - Owns the source‑specific parsing and validation; calls into the Adapter’s schema when upserting. -> Formal specs for standardizing adapter behavior and capabilities are forthcoming. +- VaultService + - Owns the database and the set of Importers. + - Runs migrations for installed Importers. + - Import/export flows: + - `importBlob(blob, importerId)` → parse/validate/upsert into DB. + - `export(importerId, store)` → DB → files using the configured codec (e.g., Markdown). + - `import(importerId, store)` → files → DB using the configured codec (handles null/undefined normalization and light type coercions). + - Optional Git helpers when a SyncEngine is provided: `gitPull()`, `gitCommit(msg)`, `gitPush()`. + - Accepts a single `codec` and a `conventions` profile to control layout and serialization. -## Lifecycle +- Conventions & Codec + - ConventionProfile: provides `pathFor(adapterId, tableName, pkValues)` and `datasetKeyFor(adapterId, tableName)`. + - Markdown codec: YAML frontmatter + body. Deterministic, quotes numeric‑like strings to avoid accidental type shifts on re‑import. Also provides optional value normalization hooks. + - Conventions: + - Omit nulls on export; on import, `VaultService` normalizes `null → undefined` and applies light primitive coercions for common text fields. + - Paths are derived from sorted primary‑key values to minimize churn. -> This may change as drastically as we determine our requirements. +- SyncEngine + - Interface: `getStore()`, `pull()`, `commit(message)`, `push()`. + - `GitSyncEngine`: shells out to `git` and exposes a FileStore rooted at the repo. + - `LocalFileStore`: read/write/list operations used by both the service and sync engines. -A core concept of adapters is modularity. Since we are relying on Drizzle for our schemas, we can easily add/remove/migrate tables at runtime, allowing for greater flexibility and adaptability to changing data requirements. +## Typical flows -> It hasn't been decided yet how adapters can be added/removed, but we are considering options such as configuration files, admin interfaces, or programmatic APIs. +1. Blob → DB (Importer‑only) -## Future Concerns +- Call `VaultService.importBlob(blob, importerId)` to parse, validate (ArkType), and upsert using the Importer. -- Virtual tables via Drizzle -- Type-safety to prevent table-name collisions +2. DB → Filesystem (Export) -## Status +- Call `VaultService.export(importerId, store)`; the service writes deterministic files using the configured codec under `vault//
/...` (e.g., `.md` for Markdown). -- [x] Primitive interfaces -- [ ] Supporting code -- [ ] Finalized interfaces +3. Filesystem → DB (Import) + +- Call `VaultService.import(importerId, store)`; values are parsed/denormalized via the configured codec, nulls are dropped, and common primitive coercions are applied. ArkType validation is not run for filesystem imports (only for first‑ingest via `importBlob`). + +## DB ↔ FS version compatibility + +Vault files (via the Sync Engine) reflect a point-in-time schema. Importing them into a DB with a different schema can fail or silently coerce data. Version awareness lets us reproducibly rebuild state, minimize surprises, and keep migrations the single source of truth for structural changes. + +Behavior matrix (per Importer) + +| DB State | Sync Engine version | Behavior | Outcome | +|-----------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| Matches head | N/A | No-op | Success (no change) | +| Older than head | N/A | Existing data in database will be migrated | Success | +| Newer than head | N/A | Plan not found in journal → throw | Error (requires updating adapter) | +| Any | Matches head | Import directly with `import()` (no validator), then optionally `migrateFunc` (no-ops) | Success | +| Any | Older than head | Use `migrateImportMigrate(targetTag)`: drop importer tables → apply SQL up to target → import → `migrateFunc` to head | Success if migrations are forward-only and include needed data transforms | +| Any | Newer than head | Plan not found in journal → throw | Error (requires updating adapter) | +| Any | Tag not present in journal | Cannot compute plan → throw | Error | +| Any | No FS version available | Require explicit `targetTag` (CLI flag or manifest) | Error until specified | + +Notes + +- `migrateImportMigrate(importerId, store, { targetTag })` orchestrates the safe path when FS is at an older schema: it drops only the importer’s tables, applies SQL to reach the FS version, imports without ArkType, then migrates forward to head. +- Exports do not consult FS; the DB is the source of truth and defines the serialized shape. +- Data transforms belong in migrations when schema changes are not shape‑compatible. Without them, forward migration after import can fail. + + + +## Minimal wiring example (service) + +Pseudocode for a Node environment using LibSQL, Git sync, a single Markdown codec, and default conventions: + +```ts +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import { migrate } from "drizzle-orm/libsql/migrator"; +import { VaultService, markdownFormat, defaultConvention, GitSyncEngine } from "@repo/vault-core"; + +// importers: an array of Importer instances (each has id, parse, validator, upsert, adapter) +const importers = [redditImporter /*, ...*/]; + +const client = createClient({ url: "file:/abs/path/to.db" }); +const db = drizzle(client); + +const svc = await VaultService.create({ + importers, + database: db, + migrateFunc: migrate, + syncEngine: new GitSyncEngine(process.cwd()), + codec: markdownFormat, + conventions: defaultConvention(), +}); + +// Import an export ZIP +await svc.importBlob( + new Blob( + [ + /* bytes */ + ], + { type: "application/zip" } + ), + "reddit" +); + +// Export to files, commit, and push +const store = await svc.gitPull().then(() => svc["syncEngine"]!.getStore()); +await svc.export("reddit", store); +await svc.gitCommit("Export vault"); +await svc.gitPush(); +``` + +Notes + +- You can also use `VaultService`. +- The demo CLI in `apps/demo-mcp` shows how to stand up a tiny importer‑driven pipeline with LibSQL. + +## Merge‑friendly plaintext by design + +- Deterministic serialization (stable key order, consistent newlines). +- YAML frontmatter quotes numeric‑like strings to avoid accidental number/boolean/null coercion on re‑import. +- File paths built from sorted primary keys; nulls omitted to reduce diff noise. +- Import normalization handles `null → undefined` and light primitive coercions for common text fields. + +## Roadmap / open questions + +- Potential future codecs (YAML, TOML, MDX) and richer frontmatter support. +- More sync engines (e.g., cloud/object storage backends). +- Declarative, column‑aware coercion/normalization derived from Drizzle types. +- Formal import/export test suites per Importer. diff --git a/packages/vault-core/src/adapters/reddit/index.ts b/packages/vault-core/src/adapters/reddit/index.ts index 2cfd0f1e64..8420b1093f 100644 --- a/packages/vault-core/src/adapters/reddit/index.ts +++ b/packages/vault-core/src/adapters/reddit/index.ts @@ -1 +1 @@ -export { redditAdapter as reddit } from './src'; +export * from './src'; diff --git a/packages/vault-core/src/adapters/reddit/src/adapter.ts b/packages/vault-core/src/adapters/reddit/src/adapter.ts new file mode 100644 index 0000000000..c8187eb062 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/src/adapter.ts @@ -0,0 +1,8 @@ +import { defineAdapter } from '@repo/vault-core'; +import * as schema from './schema'; + +// Minimal, browser-usable adapter (no Node-only imports here) +export const redditAdapter = defineAdapter(() => ({ + id: 'reddit', + schema, +})); diff --git a/packages/vault-core/src/adapters/reddit/src/importer.ts b/packages/vault-core/src/adapters/reddit/src/importer.ts new file mode 100644 index 0000000000..dd913523e3 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/src/importer.ts @@ -0,0 +1,24 @@ +import { type ColumnDescriptions, defineImporter } from '@repo/vault-core'; +import { redditAdapter } from './adapter'; +import type { RedditAdapterConfig } from './config'; +import drizzleConfig from './drizzle.config'; +import { metadata } from './metadata'; +import { parseRedditExport } from './parse'; +import { upsertRedditData } from './upsert'; +import { parseSchema } from './validation'; + +export type { ParsedRedditExport, ParseResult } from './types'; + +import type * as schema from './schema'; + +// Node-only Importer export, composed from the web-safe adapter + node parts +export const redditImporter = (args?: RedditAdapterConfig) => + defineImporter(redditAdapter(), { + name: 'Reddit', + // TODO: fill out remaining tables; cast for now to satisfy type + metadata: metadata as unknown as ColumnDescriptions, + validator: parseSchema, + drizzleConfig, + parse: parseRedditExport, + upsert: upsertRedditData, + }); diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index cb7df709dc..a9839af809 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -1,38 +1,2 @@ -import { defineAdapter } from '@repo/vault-core'; -import type { RedditAdapterConfig } from './config'; -import drizzleConfig from './drizzle.config'; -import { metadata } from './metadata'; -import { parseRedditExport } from './parse'; -import * as tables from './schema'; -import { upsertRedditData } from './upsert'; -import { parseSchema } from './validation'; - -// Expose all tables from schema module (runtime values only; TS types are erased) -export const schema = tables; -// ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. -// Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. -type Arrayify = T extends readonly [infer E] ? E[] : T; -type Inferred = (typeof parseSchema)['infer']; -export type ParsedRedditExport = { - [K in keyof Inferred]: Arrayify; -}; -// Back-compat for consumers still importing ParseResult from this module -export type ParseResult = ParsedRedditExport; - -// Adapter export -export const redditAdapter = defineAdapter((args: RedditAdapterConfig) => { - args; // TODO - - const adapter = { - id: 'reddit', - name: 'Reddit Adapter', - schema, - metadata, - validator: parseSchema, - drizzleConfig, - parse: parseRedditExport, - upsert: upsertRedditData, - }; - - return adapter; -}); +export { redditAdapter } from './adapter'; +export { redditImporter } from './importer'; diff --git a/packages/vault-core/src/adapters/reddit/src/types.ts b/packages/vault-core/src/adapters/reddit/src/types.ts new file mode 100644 index 0000000000..a1b41fc201 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/src/types.ts @@ -0,0 +1,12 @@ +import type { parseSchema } from './validation'; + +// ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. +// Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. +type Arrayify = T extends readonly [infer E] ? E[] : T; +type Inferred = (typeof parseSchema)['infer']; +export type ParsedRedditExport = { + [K in keyof Inferred]: Arrayify; +}; + +// Back-compat alias +export type ParseResult = ParsedRedditExport; diff --git a/packages/vault-core/src/adapters/reddit/src/upsert.ts b/packages/vault-core/src/adapters/reddit/src/upsert.ts index 1a19a36d60..ec397a7255 100644 --- a/packages/vault-core/src/adapters/reddit/src/upsert.ts +++ b/packages/vault-core/src/adapters/reddit/src/upsert.ts @@ -1,9 +1,6 @@ -import type { - AnySQLiteColumn, - BaseSQLiteDatabase, - SQLiteTable, -} from 'drizzle-orm/sqlite-core'; -import type { ParsedRedditExport } from '.'; +import type { CompatibleDB } from '@repo/vault-core'; +import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; +import type * as schema from './schema'; import { reddit_account_gender, reddit_announcements, @@ -46,8 +43,7 @@ import { reddit_twitter, reddit_user_preferences, } from './schema'; - -// Parser now emits Date objects for all timestamp fields per parseSchema; no extra coercion needed here. +import type { ParsedRedditExport } from './types'; /** * Small utility to chunk arrays for batched inserts to keep statements reasonable. @@ -129,12 +125,12 @@ async function upsertMany( * - We intentionally avoid adding FKs in v1 per export inconsistencies. */ export async function upsertRedditData( - db: BaseSQLiteDatabase<'sync' | 'async', Record>, + db: CompatibleDB, data: ParsedRedditExport, ): Promise { const provider: - | BaseSQLiteDatabase<'sync' | 'async', Record> - | { transaction: (fn: (tx: unknown) => Promise) => Promise } = + | { transaction: (fn: (tx: unknown) => Promise) => Promise } + | CompatibleDB = typeof (db as unknown as { transaction?: unknown }).transaction === 'function' ? db diff --git a/packages/vault-core/src/codecs/index.ts b/packages/vault-core/src/codecs/index.ts new file mode 100644 index 0000000000..1433df45a0 --- /dev/null +++ b/packages/vault-core/src/codecs/index.ts @@ -0,0 +1,2 @@ +export * from './json'; +export * from './markdown'; diff --git a/packages/vault-core/src/codecs/json.ts b/packages/vault-core/src/codecs/json.ts new file mode 100644 index 0000000000..c0789dba33 --- /dev/null +++ b/packages/vault-core/src/codecs/json.ts @@ -0,0 +1,27 @@ +import { defineCodec, type FormatCodec } from '../core/codec'; + +function stableStringify(value: unknown): string { + return JSON.stringify(value, replacer, 2); +} + +function replacer(_key: string, value: unknown) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const out: Record = {}; + for (const k of Object.keys(value as Record).sort()) + out[k] = (value as Record)[k]; + return out; + } + return value; +} + +export const jsonFormat = defineCodec({ + id: 'json', + fileExtension: 'json', + parse(text) { + const obj = JSON.parse(text); + return obj; + }, + stringify(rec) { + return stableStringify(rec ?? {}); + }, +}); diff --git a/packages/vault-core/src/codecs/markdown.ts b/packages/vault-core/src/codecs/markdown.ts new file mode 100644 index 0000000000..d0e8e4c125 --- /dev/null +++ b/packages/vault-core/src/codecs/markdown.ts @@ -0,0 +1,71 @@ +import { defineCodec, type FormatCodec } from '../core/codec'; + +// Very small deterministic Markdown with YAML headers: ---\n\n---\n +// For now, we do not include a YAML lib; caller will provide headers already normalized. +function headersToYaml(headers: Record): string { + const keys = Object.keys(headers).sort(); + const lines = keys.map((k) => `${k}: ${encode(headers[k])}`); + return lines.join('\n'); +} + +function encode(v: unknown): string { + if (v === undefined) return ''; + if (v === null) return 'null'; + if (typeof v === 'string') { + // Quote only when necessary; keep simple for now + // Also quote if the scalar looks like a JSON literal (number/boolean/null) + // to avoid it being parsed as a non-string on re-import. + if ( + /[:\n#\-]/.test(v) || + /^(?:true|false|null)$/i.test(v) || + /^-?\d+(?:\.\d+)?$/.test(v) + ) + return JSON.stringify(v); + return v; + } + return JSON.stringify(v); +} + +function parseYaml(text: string): Record { + // Minimal, non-compliant YAML parser for simple k: v lines (no arrays/nesting) + const lines = text.split(/\r?\n/); + const out: Record = {}; + for (const line of lines) { + const idx = line.indexOf(':'); + if (idx < 0) continue; + const key = line.slice(0, idx).trim(); + const raw = line.slice(idx + 1).trim(); + if (!key) continue; + // Attempt simple JSON parse for numbers/booleans/quoted strings + try { + if (raw === '') out[key] = undefined; + else out[key] = JSON.parse(raw); + } catch { + out[key] = raw; + } + } + return out; +} + +// TODO figure out condition for body prop (name based??) +export const markdownFormat = defineCodec({ + id: 'markdown', + fileExtension: 'md', + parse(text) { + const fmMatch = text.match(/^---\n([\s\S]*?)\n---\n?/); + if (!fmMatch) return { body: text }; + const headers = parseYaml(fmMatch[1] ?? ''); + const body = text.slice(fmMatch[0].length); + return { ...headers, body }; + }, + stringify(rec) { + const { body, ...rest } = rec ?? ({} as Record); + const head = headersToYaml(rest); + const bodyText = + typeof body === 'string' ? body : body == null ? '' : String(body); + const sep = head.length ? `---\n${head}\n---\n` : ''; + // Ensure final newline + const out = `${sep}${bodyText}`; + return out.endsWith('\n') ? out : `${out}\n`; + }, +}); diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index 5f19a1bb6b..915030be39 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -1,26 +1,19 @@ -import { type Type, type } from 'arktype'; import type { defineConfig } from 'drizzle-kit'; -import type { ColumnsSelection } from 'drizzle-orm'; import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import { - type BaseSQLiteDatabase, - integer, - type SQLiteTable, - type SubqueryWithSelection, - sqliteTable, - text, -} from 'drizzle-orm/sqlite-core'; +import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core'; +// Shared types type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> ? R : never; + type ResultSet = ExtractedResult; -// Bootstrapped type to represent compatible Drizzle database types across the codebase +// Represents compatible Drizzle DB types across the codebase export type CompatibleDB> = BaseSQLiteDatabase<'sync' | 'async', TSchema | ResultSet>; -type DrizzleConfig = ReturnType; +export type DrizzleConfig = ReturnType; export type ColumnDescriptions> = { [K in keyof T]: { @@ -28,167 +21,28 @@ export type ColumnDescriptions> = { }; }; -type View< - T extends string, - TSelection extends ColumnsSelection, - TSchema extends Record, - TDatabase extends CompatibleDB, -> = { - name: T; - definition: (db: TDatabase) => SubqueryWithSelection; -}; - +// Adapter is schema-only. Importers wire lifecycle, parsing, views, etc. export interface Adapter< TID extends string = string, TSchema extends Record = Record, - TDatabase extends CompatibleDB = CompatibleDB, - TParserShape extends Type = Type, - TParsed = TParserShape['infer'], > { - /** - * Unique identifier for the adapter - * - * Should be lowercase, no spaces, alpha-numeric. - * @example "twitter" - */ + /** Unique identifier for the adapter (lowercase, no spaces, alphanumeric) */ id: TID; - - /** - * User-facing name - * @example "Reddit Adapter" - */ - name: string; - /** Database schema */ schema: TSchema; - - /** Column descriptions for every table/column */ - metadata: ColumnDescriptions; - - /** - * ArkType schema for parsing/validation - * - * This will be used by the MCP server to validate data returned from the `parse` method. - */ - validator: TParserShape; - - /** - * Predefined views/CTEs - * - * Should be used for common queries that a user will want to query for. This is especially helpful if the data storage format is complex/unintuitive. - * - * @example - * "recently_played": { - * description: "Recently played songs", - * definition: (db) => db.select().from(songs).where(...) - * } - */ - views?: { - [Alias in string]: View; - }; - - /** - * Drizzle config - * - * @example - * defineConfig({ - * dialect: 'sqlite', - * schema: './src/schema.ts', - * out: './migrations', - * migrations: { - * table: 'test_migrations', - * }, - * }) - */ - drizzleConfig: DrizzleConfig; - - // Lifecycle hooks - - /** - * Parse a blob into a parsed representation - * @example - * const text = await b.text(); - * return JSON.parse(text); - */ - parse: (file: Blob) => Promise; - - /** Upsert data into the database */ - upsert: (db: TDatabase, data: TParsed) => Promise; } -// Note: If a generic only appears in a function parameter position, TS won't infer it and will fall back to the constraint (e.g. `object`). -// These overloads infer the full function type `F` instead, preserving the args type. -export function defineAdapter< - // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories - F extends () => Adapter, ->(adapter: F): F; +// Note: If a generic only appears in a function parameter position, TS won't infer it and will +// fall back to the constraint (e.g. `object`). These overloads infer the full function type `F` instead. +export function defineAdapter Adapter>( + adapter: F, +): F; export function defineAdapter< // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories - F extends (args: any) => Adapter, + F extends (args: any) => Adapter, >(adapter: F): F; -// Implementation signature can be broad; overloads provide strong typing to callers export function defineAdapter unknown>( adapter: F, ): F { return adapter; } - -// Example -// TODO remove - -const songs = sqliteTable('songs', { - id: integer('id').primaryKey(), - title: text('title'), - artist: text('artist'), - album: text('album'), - year: integer('year'), -}); - -const testAdapter = defineAdapter(() => ({ - id: 'test', - name: 'Test Adapter', - validator: type({ - id: 'number', - title: 'string', - artist: 'string', - album: 'string', - year: 'number', - }), - schema: { - songs, - }, - drizzleConfig: { - dialect: 'sqlite', - schema: './src/schema.ts', - casing: 'snake_case', - strict: true, - out: './migrations', - migrations: { - table: 'test_migrations', - }, - }, - parse: (file) => file.text().then(JSON.parse), - upsert: (db, data) => - db - .insert(songs) - .values(data) - .onConflictDoUpdate({ - target: songs.id, - set: { - title: data.title, - artist: data.artist, - album: data.album, - year: data.year, - }, - }) - .then(() => undefined), - metadata: { - songs: { - id: 'Unique identifier for the song', - title: 'Title of the song', - artist: 'Artist of the song', - album: 'Album of the song', - year: 'Year of release', - }, - }, -})); diff --git a/packages/vault-core/src/core/codec.ts b/packages/vault-core/src/core/codec.ts new file mode 100644 index 0000000000..89248c3a12 --- /dev/null +++ b/packages/vault-core/src/core/codec.ts @@ -0,0 +1,108 @@ +import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; + +// Language-level codec (Markdown, JSON, TOML+body, etc.) +export interface FormatCodec { + /** Unique identifier (e.g., 'markdown', 'json', 'toml', 'yaml-md') */ + id: TID; + /** Default file extension without dot + * @example 'md' + */ + fileExtension: Omit; + /** + * Parse file text into a flat record. If a free-form body is present, + * codecs should use the reserved key 'body' to carry it. + */ + parse(text: string): Record; + /** + * Stringify a flat record into file text. If a 'body' key is present, + * codecs that support bodies should place it appropriately (e.g., after + * frontmatter); others may serialize it as a normal field. + */ + stringify(rec: Record): string; + /** Optional value normalization before writing (e.g., Date -> ISO string) */ + normalize?(value: unknown, columnName: string): unknown; + /** Optional value denormalization after reading (e.g., ISO string -> Date) */ + denormalize?(value: unknown, columnName: string): unknown; +} + +export type Codec = FormatCodec; + +// Runtime view of a Drizzle table +export type TableEntry = [name: string, table: SQLiteTable]; +export type ColumnEntry = [name: string, column: SQLiteColumn]; + +// Per-codec convention profile that derives mapping decisions from schema + naming +export interface ConventionProfile { + // compute relative path from table + pk values + pathFor( + adapterId: string, + tableName: string, + pkValues: Record, + ): string; + // map a table name to a dataset key used by Importer.validator/upsert + datasetKeyFor(adapterId: string, tableName: string): string; +} + +// Helpers +export function listTables(schema: Record): TableEntry[] { + return Object.entries(schema) as TableEntry[]; +} + +export function listColumns(table: SQLiteTable): ColumnEntry[] { + return Object.entries(table) as ColumnEntry[]; +} + +// Best-effort PK detection from Drizzle runtime objects. +// Falls back to common naming if metadata is absent. +export function detectPrimaryKey( + tableName: string, + table: SQLiteTable, +): string[] | undefined { + const cols = listColumns(table); + const pkCols: string[] = []; + for (const [name, col] of cols) { + // Drizzle columns expose some shape at runtime; check common fields defensively. + const anyCol = col; + if (anyCol.primary === true && !pkCols.includes(name)) { + pkCols.push(name); + } + } + if (pkCols.length > 0) return pkCols; + // Heuristic fallback by naming + if ('id' in table) return ['id']; + if (`${tableName}_id` in table) return [`${tableName}_id`]; + return undefined; +} + +// Choose body column by common names, prefer notNull string-like columns named body/content/text +// (Body selection moved to the codecs themselves.) + +// Default per-codec convention profile (opinionated) +// Picks a body-capable format (prefer 'markdown') when body column exists; else 'json'. +export function defaultConvention(): ConventionProfile { + return { + pathFor(adapterId, tableName, pkValues) { + const parts = Object.entries(pkValues) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([, v]) => String(v)); + const fileId = parts.length > 0 ? parts.join('__') : 'row'; + // extension decided by mode at callsite; we return a directory path root here + return `vault/${adapterId}/${tableName}/${fileId}`; + }, + datasetKeyFor(adapterId, tableName) { + return tableName.startsWith(`${adapterId}_`) + ? tableName.slice(adapterId.length + 1) + : tableName; + }, + }; +} + +/** + * defineFormat: identity helper for a single FormatCodec (markdown, json, etc.). + */ +export function defineCodec< + const TID extends string, + F extends FormatCodec, +>(codec: F): F { + return codec; +} diff --git a/packages/vault-core/src/core/config.ts b/packages/vault-core/src/core/config.ts index 8d57bbdd12..70259c9446 100644 --- a/packages/vault-core/src/core/config.ts +++ b/packages/vault-core/src/core/config.ts @@ -1,37 +1,76 @@ import type { MigrationConfig } from 'drizzle-orm/migrator'; import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; import type { Adapter } from './adapter'; +import type { Codec, ConventionProfile } from './codec'; +import type { Importer } from './importer'; +import type { SyncEngine } from './sync'; +// Deprecated: use VaultServiceConfig or VaultClientConfig export interface VaultConfig< TDatabase extends BaseSQLiteDatabase<'sync' | 'async', unknown>, - TAdapters extends Adapter[], + TImporters extends Importer[], +> { + adapters: TImporters; + database: TDatabase; + migrateFunc: (db: TDatabase, config: MigrationConfig) => Promise; +} + +// Service config: owns DB and Importers +export interface VaultServiceConfig< + TDatabase extends BaseSQLiteDatabase<'sync' | 'async', unknown>, + TImporters extends Importer[], > { /** - * List of adapters to include + * Importers installed on the service. * - * @see {Adapter} + * Importers encapsulate end-to-end behavior for a source: parse(blob), validate, upsert(db), + * and also reference an Adapter which carries the Drizzle schema + migrations config. + * + * @see Importer */ - adapters: TAdapters; - + importers: TImporters; /** - * Database connection instance - * @example - * import { createClient } from '@libsql/client'; - * const client = createClient({ url: dbUrl }); + * Database connection instance used by the service. + * + * Example (libsql): + * const client = createClient({ url, authToken }); * const db = drizzle(client); - * ... - * database: db, */ database: TDatabase; - /** - * Drizzle platform-specific migration function - * @example + * Drizzle platform-specific migration function used to run migrations for each importer. + * + * Example (libsql): * import { migrate } from 'drizzle-orm/libsql/migrator'; - * ... - * migrateFunc: migrate, - * @see {MigrationConfig} - * @todo Implement in-house migration procedure, which doesn't rely on `node:fs`. + * migrate(db, { migrationsFolder: '...' }) */ migrateFunc: (db: TDatabase, config: MigrationConfig) => Promise; + /** + * A SyncEngine implementation injected by the host. + * Controls DB <-> Filesystem sync flows; keeps Importers/Adapters pure. + */ + syncEngine?: SyncEngine; + /** Active text codec (markdown/json/etc.) and the conventions. */ + codec?: Codec; + conventions?: ConventionProfile; +} + +// Client config: only needs adapters for schema/metadata typing and UI +export interface VaultClientConfig { + /** + * Adapters provide schema (and optional metadata) for type-safety in the client. + * + * The client does not perform database operations; it uses adapters to render UI, + * build queries, and display human-readable table/column info. + * + * @see Adapter + */ + adapters: TAdapters; + /** + * Optional transport configuration placeholder. + * + * The specific RPC transport between client and service is intentionally + * left undefined here; applications should provide their own wiring. + */ + transport?: unknown; } diff --git a/packages/vault-core/src/core/fs.ts b/packages/vault-core/src/core/fs.ts new file mode 100644 index 0000000000..108d177ea2 --- /dev/null +++ b/packages/vault-core/src/core/fs.ts @@ -0,0 +1,69 @@ +/** + * Filesystem driver abstraction (host app provides). Kept tiny on purpose. + */ +export interface FileStore { + read(path: string): Promise; // undefined for missing + write(path: string, contents: string): Promise; + remove(path: string): Promise; + list(prefix?: string): Promise; // returns relative file paths +} + +/** + * Merge driver hook: allows custom resolution for frontmatter or structured text. + * Implementations can be wired via .gitattributes to reduce human conflicts. + */ +export interface MergeDriver { + /** Returns merged contents or undefined if it can’t auto-merge. */ + merge( + base: string | undefined, + current: string | undefined, + incoming: string | undefined, + path: string, + ): Promise; +} + +// Atomic write intent; the sync engine can batch these under a transaction/journal +export type WriteIntent = { + /** Full relative path (POSIX style) */ + path: string; + /** UTF-8 text content; caller performs deterministic formatting first */ + contents: string; +}; + +// Captures a filesystem snapshot used for 3-way merges +export type FsSnapshot = { + /** content hash per path (e.g., sha256 of file contents) */ + files: Record; + /** optional marker to match schema revisions */ + schemaVersion?: number; + /** timestamp of snapshot creation */ + createdAt: string; +}; + +// --- Sync planning types (DB <-> FS) --- + +export type SyncIntent = { + writes: WriteIntent[]; + deletes: string[]; // relative paths to remove +}; + +export type Conflict = + | { type: 'text'; path: string } + | { type: 'entity'; ref: EntityRef; reason: string }; + +export type SyncPlan = { + intents: SyncIntent; + conflicts: Conflict[]; + /** baseline snapshot used to compute the plan */ + base?: FsSnapshot; +}; + +// A single entity in the domain (table row, document, etc.) +export type EntityRef = { kind: string; id: string }; + +// A minimal change unit from parsing a plaintext document +export type EntityPatch = { + ref: EntityRef; + /** Partial update to apply (already validated against Importer.validator) */ + data: T; +}; diff --git a/packages/vault-core/src/core/importer.ts b/packages/vault-core/src/core/importer.ts new file mode 100644 index 0000000000..4e7fcee493 --- /dev/null +++ b/packages/vault-core/src/core/importer.ts @@ -0,0 +1,83 @@ +import { type Type, type } from 'arktype'; +import type { ColumnsSelection } from 'drizzle-orm'; +import type { + SQLiteTable, + SubqueryWithSelection, +} from 'drizzle-orm/sqlite-core'; +import type { + Adapter, + ColumnDescriptions, + CompatibleDB, + DrizzleConfig, +} from './adapter'; + +export type View< + T extends string, + TSelection extends ColumnsSelection, + TSchema extends Record, + TDatabase extends CompatibleDB, +> = { + name: T; + definition: (db: TDatabase) => SubqueryWithSelection; +}; + +export interface Importer< + TID extends string = string, + TSchema extends Record = Record, + TDatabase extends CompatibleDB = CompatibleDB, + TParserShape extends Type = Type, + TParsed = TParserShape['infer'], +> { + /** Unique identifier for the importer (lowercase, no spaces, alphanumeric) */ + id: TID; + /** User-facing name */ + name: string; + /** Adapter (schema provider) */ + adapter: Adapter; + /** Column descriptions for every table/column */ + metadata: ColumnDescriptions; + /** ArkType schema for parsing/validation */ + validator: TParserShape; + /** Predefined views/CTEs for common queries */ + views?: { + [Alias in string]: View; + }; + /** Drizzle config for this schema (migrations, casing, etc.) */ + drizzleConfig: DrizzleConfig; + /** Parse a blob into a parsed representation */ + parse: (file: Blob) => Promise; + /** Upsert data into the database */ + upsert: (db: TDatabase, data: TParsed) => Promise; +} + +// Helper to compose an Importer from a web-safe Adapter + Node-only pieces +export type ImporterNodeParts< + TID extends string, + TSchema extends Record, + TDatabase extends CompatibleDB, + TParserShape extends Type, + TParsed, +> = Omit< + Importer, + 'id' | 'adapter' +>; + +export function defineImporter< + TID extends string, + TSchema extends Record, + TDatabase extends CompatibleDB, + TParserShape extends Type, + TParsed = TParserShape['infer'], +>( + adapter: Adapter, + parts: ImporterNodeParts, +): Importer { + return { + id: adapter.id, + adapter, + ...parts, + }; +} + +// Back-compat alias; prefer defineImporter going forward +export const composeImporter = defineImporter; diff --git a/packages/vault-core/src/core/index.ts b/packages/vault-core/src/core/index.ts index e2d5c5fc60..85adffe295 100644 --- a/packages/vault-core/src/core/index.ts +++ b/packages/vault-core/src/core/index.ts @@ -1,2 +1,9 @@ +export * from '../codecs'; +export * from '../fs/local-file-store'; +export * from '../sync/git'; export * from './adapter'; -export * from './vault'; +export * from './codec'; +export * from './importer'; +export * from './sync'; +export * from './vault-client'; +export * from './vault-service'; diff --git a/packages/vault-core/src/core/migrations.ts b/packages/vault-core/src/core/migrations.ts new file mode 100644 index 0000000000..8b1a824acc --- /dev/null +++ b/packages/vault-core/src/core/migrations.ts @@ -0,0 +1,155 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; +import type { CompatibleDB } from './adapter'; +import type { Importer } from './importer'; + +export type JournalEntry = { + /** Drizzle migration tag, usually the filename without extension (e.g., 0001_add_posts) */ + tag: string; + /** ISO timestamp or number; shape depends on drizzle-kit version */ + when?: unknown; + /** Optional checksum/hash; presence depends on drizzle version */ + hash?: string; +}; + +export type MigrationJournal = { + entries: JournalEntry[]; +}; + +export type SqlStep = { + tag: string; // matches journal tag + direction: 'up' | 'down'; + file: string; // absolute path to .sql +}; + +export type MigrationPlan = { + from?: string; // current DB version tag (if known) + to: string; // target version tag + steps: SqlStep[]; // ordered +}; + +/** Resolve the migrations directory for an importer. */ +export function resolveMigrationsDir(importer: Importer): string { + // Drizzle config usually sets `out` to a folder containing SQL files and meta/_journal.json + // TODO: Ensure this is absolute; if relative, decide base (adapter package dir vs process.cwd()). + const out = importer.drizzleConfig.out ?? ''; + if (!path.isAbsolute(out)) { + // Fallback: treat relative to process.cwd(); callers can pre-resolve if needed + return path.resolve(process.cwd(), out); + } + return out; +} + +/** Read Drizzle's meta/_journal.json */ +export async function readMigrationJournal( + migrationsDir: string, +): Promise { + const journalPath = path.join(migrationsDir, 'meta', '_journal.json'); + const raw = await fs.readFile(journalPath, 'utf8'); + const parsed = JSON.parse(raw) as { entries?: JournalEntry[] }; + return { entries: parsed.entries ?? [] }; +} + +/** List available SQL files and map to tags/directions. */ +export async function listSqlSteps(migrationsDir: string): Promise { + const files = await fs.readdir(migrationsDir); + const steps: SqlStep[] = []; + for (const f of files) { + if (!f.endsWith('.sql')) continue; + const full = path.join(migrationsDir, f); + // Heuristic: drizzle names like 0001_name.sql with two statements (up/down) or separate up/down files + // TODO: Detect drizzle style; for now assume paired files 0001_name.sql contains both up and down separated by comments. + // Placeholder: treat every .sql as an 'up' step (down not used in forward application). + const tag = f.replace(/\.sql$/, ''); + steps.push({ tag, direction: 'up', file: full }); + } + return steps; +} + +/** Compute a forward-only plan from current -> target using journal ordering. */ +export function planToVersion( + journal: MigrationJournal, + allSteps: SqlStep[], + currentTag: string | undefined, + targetTag: string, +): MigrationPlan { + const order = new Map(journal.entries.map((e, i) => [e.tag, i] as const)); + if (!order.has(targetTag)) { + throw new Error(`Target migration tag not found in journal: ${targetTag}`); + } + const currentIdx = currentTag != null ? (order.get(currentTag) ?? -1) : -1; + const targetIdx = order.get(targetTag); + if (targetIdx === undefined) { + throw new Error(`Target migration tag not found in journal: ${targetTag}`); + } + const forward = journal.entries + .slice(currentIdx + 1, targetIdx + 1) + .map((e) => e.tag); + const steps = forward + .map((tag) => allSteps.find((s) => s.tag === tag && s.direction === 'up')) + .filter((s): s is SqlStep => !!s); + return { from: currentTag, to: targetTag, steps }; +} + +/** Drop all tables owned by the importer (best-effort). */ +export async function dropAdapterTables( + db: CompatibleDB, + importer: Importer, +): Promise { + // TODO: Use Drizzle metadata to get table names reliably; for now, list keys from schema object. + const schema = (importer.adapter.schema ?? {}) as Record; + // TODO: Acquire a raw SQL runner; Drizzle's libsql adapter doesn't expose raw execution directly here. + const runSql = getSqlRunner(db); + for (const [name] of Object.entries(schema)) { + // TODO: Quote name properly for SQLite identifiers + await runSql(`DROP TABLE IF EXISTS "${name}";`); + } +} + +/** Inspect Drizzle migrations table for current version tag (implementation TBD). */ +export async function getCurrentDbMigrationTag( + db: CompatibleDB, + importer: Importer, +): Promise { + // TODO: Query importer.drizzleConfig.migrations?.table (default likely 'drizzle_migrations') to get last applied tag/name. + // This requires a query API or raw SQL; wire up a small query once the DB flavor is known. + throw new Error('Not implemented: getCurrentDbMigrationTag'); +} + +/** Apply a sequence of SQL files in order. */ +export async function applySqlPlan( + db: CompatibleDB, + importer: Importer, + plan: MigrationPlan, +) { + const runSql = getSqlRunner(db); + for (const step of plan.steps) { + const sql = await fs.readFile(step.file, 'utf8'); + // TODO: If a single file contains both up/down, split by sentinel comments. + // Importer-specific handling can occur here (e.g., feature flags, schema qualifiers) + void importer; // placeholder to acknowledge importer until used + await runSql(sql); + } +} + +/** Mark a migration tag as applied in the Drizzle migrations table (implementation TBD). */ +export async function markApplied( + db: CompatibleDB, + importer: Importer, + toTag: string, +) { + // TODO: Insert into migrations table or update state consistent with drizzle-orm expectations. + throw new Error('Not implemented: markApplied'); +} + +/** Obtain a raw SQL runner from a CompatibleDB. Placeholder until concrete DBs are wired. */ +export function getSqlRunner( + _db: CompatibleDB, +): (sql: string) => Promise { + // TODO: For libsql: keep a handle to the underlying client and call client.execute(sql) + // TODO: For better-sqlite3: use db.exec(sql) + return async (_sql: string) => { + throw new Error('Not implemented: raw SQL execution for migrations'); + }; +} diff --git a/packages/vault-core/src/core/sync.ts b/packages/vault-core/src/core/sync.ts new file mode 100644 index 0000000000..129643e158 --- /dev/null +++ b/packages/vault-core/src/core/sync.ts @@ -0,0 +1,18 @@ +import type { FileStore, FsSnapshot } from './fs'; + +/** + * SyncEngine: Git-oriented engine used by VaultService. + * - Provides a FileStore view of the working tree + * - Performs VCS operations (pull/commit/push) + * Glue logic (DB<->files via formats/conventions) is handled in VaultService. + */ +export interface SyncEngine { + /** Return a FileStore rooted at the sync workspace (e.g., repo dir). */ + getStore(): Promise; + /** Ensure local workspace is up-to-date; return snapshot after pull. */ + pull(): Promise; + /** Stage and commit current workspace changes with a message. */ + commit(message: string): Promise; + /** Push committed changes to remote. */ + push(): Promise; +} diff --git a/packages/vault-core/src/core/vault-client.ts b/packages/vault-core/src/core/vault-client.ts new file mode 100644 index 0000000000..669752f9d3 --- /dev/null +++ b/packages/vault-core/src/core/vault-client.ts @@ -0,0 +1,28 @@ +import type { Adapter } from './adapter'; +import type { VaultClientConfig } from './config'; +/** + * VaultClient runs in the app (web/desktop). It holds adapters for type-safety + * and schema/metadata introspection. It talks to VaultService via RPC (not implemented). + */ +export class VaultClient { + readonly adapters: TAdapters; + readonly transport: unknown; + + constructor(config: VaultClientConfig) { + this.adapters = config.adapters; + this.transport = config.transport; + } + + /** + * Ask service to import a blob with a given importer. + * Placeholder: must be implemented in host app via RPC. + */ + async importBlob( + _adapterId: TAdapters[number]['id'], + _file: Blob, + ): Promise { + throw new Error( + 'VaultClient.importBlob not implemented: provide RPC transport', + ); + } +} diff --git a/packages/vault-core/src/core/vault-service.ts b/packages/vault-core/src/core/vault-service.ts new file mode 100644 index 0000000000..7ff431475b --- /dev/null +++ b/packages/vault-core/src/core/vault-service.ts @@ -0,0 +1,274 @@ +import type { MigrationConfig } from 'drizzle-orm/migrator'; +import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; +import type { CompatibleDB } from './adapter'; +import type { ConventionProfile } from './codec'; +import { detectPrimaryKey, listColumns, listTables } from './codec'; +import type { VaultServiceConfig } from './config'; +import type { FileStore } from './fs'; +import type { Importer } from './importer'; +import { + applySqlPlan, + dropAdapterTables, + getCurrentDbMigrationTag, + listSqlSteps, + planToVersion, + readMigrationJournal, + resolveMigrationsDir, +} from './migrations'; +import type { SyncEngine } from './sync'; + +export class VaultService< + TDatabase extends CompatibleDB, + TImporters extends Importer[], +> { + readonly importers: TImporters; + readonly db: TDatabase; + readonly migrateFunc: ( + db: TDatabase, + config: MigrationConfig, + ) => Promise; + readonly syncEngine?: SyncEngine; + readonly codec?: import('./codec').Codec; + readonly conventions?: ConventionProfile; + + constructor(config: VaultServiceConfig) { + this.importers = config.importers; + this.db = config.database; + this.migrateFunc = config.migrateFunc; + this.syncEngine = config.syncEngine; + this.codec = config.codec; + this.conventions = config.conventions; + } + + static async create< + TDatabase extends CompatibleDB, + TImporters extends Importer[], + >(config: VaultServiceConfig) { + const svc = new VaultService(config); + await svc.migrate(); + return svc; + } + + /** + * Run migrations for installed importers. + */ + private async migrate() { + for (const importer of this.importers) { + await this.migrateFunc(this.db, { + migrationsFolder: importer.drizzleConfig.out ?? '', + migrationsSchema: importer.drizzleConfig.migrations?.schema ?? '', + migrationsTable: importer.drizzleConfig.migrations?.table ?? '', + }); + } + } + + /** + * Parse a blob with a specific importer and upsert into the database. + * Returns the parsed payload for auditing if needed. + */ + async importBlob(blob: Blob, importerId: string) { + const importer = this.importers.find((i) => i.id === importerId); + if (!importer) throw new Error(`Importer not found: ${importerId}`); + + const parsed = await importer.parse(blob); + const valid = importer.validator.assert(parsed); + await importer.upsert(this.db, valid); + return { importer: importer.name, parsed }; + } + + /** + * Export DB state using the injected SyncEngine and codec. + */ + async export(importerId: string, store: FileStore) { + const importer = this.importers.find((i) => i.id === importerId); + if (!importer) throw new Error(`Importer not found: ${importerId}`); + if (!this.codec || !this.conventions) + throw new Error('No formats/conventions configured'); + const { codec: format, conventions } = this; + const adapterId = importer.id; + // Support both shapes: importer.adapter.schema (new) and importer.schema (legacy) + const schema: Record = + (( + importer as unknown as { adapter?: { schema: Record } } + )?.adapter?.schema as Record) ?? + (importer as unknown as { schema?: Record }) + .schema ?? + ({} as Record); + const files: Record = {}; + + for (const [tableName, table] of listTables(schema)) { + // Query all rows + const rows: Record[] = await ( + this.db as unknown as { + select: () => { + from: (t: unknown) => Promise[]>; + }; + } + ) + .select() + .from(table); + + const pkCols = detectPrimaryKey(tableName, table) ?? []; + + for (const row of rows) { + // Build a flat record deterministically for the codec + const rec: Record = {}; + const tableCols = new Set( + listColumns(table).map(([name]: [string, unknown]) => name), + ); + for (const [k, v] of Object.entries(row)) { + if (!tableCols.has(k)) continue; // ignore non-column fields (if any) + if (v === undefined || v === null) continue; // omit nulls -> undefined on re-import + rec[k] = format.normalize ? format.normalize(v, k) : v; + } + + // Compute path using PK values + const pkValues: Record = {}; + for (const pk of pkCols) pkValues[pk] = row[pk]; + const basePath = conventions.pathFor(adapterId, tableName, pkValues); + const path = `${basePath}.${format.fileExtension}`; + + const text = format.stringify(rec); + await store.write(path, text); + files[path] = ''; + } + } + + return { files, createdAt: new Date().toISOString() }; + } + + /** + * Import DB state from filesystem into DB using the injected SyncEngine and codec. + */ + async import(importerId: string, store: FileStore) { + const importer = this.importers.find((i) => i.id === importerId); + if (!importer) throw new Error(`Importer not found: ${importerId}`); + if (!this.codec || !this.conventions) + throw new Error('No formats/conventions configured'); + const { codec: configuredCodec, conventions } = this; + const adapterId = importer.id; + const schema: Record = + (( + importer as unknown as { adapter?: { schema: Record } } + )?.adapter?.schema as Record) ?? + (importer as unknown as { schema?: Record }) + .schema ?? + ({} as Record); + + // Collect rows per dataset key for a single upsert call + const dataset: Record = {}; + // Initialize all dataset keys to empty arrays to satisfy validators expecting present arrays + for (const [tableName] of listTables(schema)) { + const key = conventions.datasetKeyFor(adapterId, tableName); + if (!dataset[key]) dataset[key] = []; + } + const prefix = `vault/${adapterId}/`; + const paths = await store.list(prefix); + + for (const path of paths) { + if (!path.startsWith(prefix)) continue; + const rel = path.slice(prefix.length); + const parts = rel.split('/'); + if (parts.length < 2) continue; + const tableName = + parts[0] as keyof typeof importer.adapter.schema as string; + const file = parts.slice(1).join('/'); + const dot = file.lastIndexOf('.'); + if (dot < 0) continue; + const fileId = file.slice(0, dot); + const ext = file.slice(dot + 1); + + // Use configured codec; skip files with other extensions + const format = configuredCodec; + if (format.fileExtension !== ext) continue; + + const table = schema[tableName as keyof typeof schema] as SQLiteTable; + if (!table) continue; + + const text = await store.read(path); + if (text == null) continue; + const rec = format.parse(text); + const row: Record = {}; + + const tableCols = new Set( + listColumns(table).map(([name]: [string, unknown]) => name), + ); + for (const [k, v] of Object.entries(rec ?? {})) { + if (!tableCols.has(k)) continue; + row[k] = format.denormalize ? format.denormalize(v, k) : v; + } + + // Ensure PK values exist; if missing in headers, try to reconstruct from filename + const pkCols = detectPrimaryKey(tableName, table) ?? []; + const fileParts = fileId.split('__'); + for (let i = 0; i < pkCols.length; i++) { + const key = pkCols[i]; + if (row[key] === undefined) row[key] = fileParts[i]; + } + + const key = conventions.datasetKeyFor(adapterId, tableName); + if (!dataset[key]) dataset[key] = []; + (dataset[key] as unknown[]).push(row); + } + + // Prepare and upsert (no ArkType validation here): + // Normalize dataset values: convert null -> undefined and coerce numeric-like to strings + for (const key of Object.keys(dataset)) { + const rows = dataset[key] as Record[]; + for (const row of rows) { + for (const [k, v] of Object.entries(row)) { + if (v === null) { + row[k] = undefined; + continue; + } + // Heuristic: fields commonly textual but sometimes numeric in exports + if ( + typeof v === 'number' && + /(?:^|_)(?:id|name|slug|subreddit|channel|parent|media|value|image|url|stake|selection)(?:$|_)/.test( + k, + ) + ) { + row[k] = String(v); + } + } + } + } + // ArkType validator is used only for first-ingest (blob imports), not for FS re-imports. + // We rely on DB constraints/migrations and importer.upsert to handle shaping. + await importer.upsert(this.db, dataset); + } + + /** + * Prepare DB schema for vault import by migrating to a target version, import, then migrate to head. + * This does not run ArkType validation; it assumes the vault content is trusted. + */ + async migrateImportMigrate( + importerId: string, + store: FileStore, + options: { targetTag: string }, + ) { + const importer = this.importers.find((i) => i.id === importerId); + if (!importer) throw new Error(`Importer not found: ${importerId}`); + // 1) Drop adapter tables (scoped) + await dropAdapterTables(this.db, importer); + // 2) Read journal + steps and compute plan to target + const dir = resolveMigrationsDir(importer); + const journal = await readMigrationJournal(dir); + const steps = await listSqlSteps(dir); + // TODO: Read current DB tag from drizzle migrations table + const current = await getCurrentDbMigrationTag(this.db, importer); + const plan = planToVersion(journal, steps, current, options.targetTag); + // 3) Apply plan + await applySqlPlan(this.db, importer, plan); + // TODO: mark applied in drizzle migrations table + // await markApplied(this.db, importer, options.targetTag); + // 4) Import vault content without validation + await this.import(importerId, store); + // 5) Migrate to head using provided migrateFunc (drizzle’s migrator) + await this.migrateFunc(this.db, { + migrationsFolder: importer.drizzleConfig.out ?? '', + migrationsSchema: importer.drizzleConfig.migrations?.schema ?? '', + migrationsTable: importer.drizzleConfig.migrations?.table ?? '', + }); + } +} diff --git a/packages/vault-core/src/core/vault.ts b/packages/vault-core/src/core/vault.ts deleted file mode 100644 index 070d5608fa..0000000000 --- a/packages/vault-core/src/core/vault.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { MigrationConfig } from 'drizzle-orm/migrator'; -import type { Adapter, CompatibleDB } from './adapter'; -import type { VaultConfig } from './config'; -import { readableSchemaInfo } from './strip'; - -export type ImportCounts = Record; - -export type ImportReport = { - adapter: string; - migrated: boolean; - counts: ImportCounts; - // Raw parsed payload for advanced callers; leave as unknown to avoid tight coupling - parsed: unknown; -}; - -export type ImportSummary = { - reports: ImportReport[]; - totalTables: number; - totalRecords: number; -}; - -function countRecords(parsed: unknown): ImportCounts { - const out: ImportCounts = {}; - if (parsed && typeof parsed === 'object') { - for (const [k, v] of Object.entries(parsed as Record)) { - out[k] = Array.isArray(v) ? v.length : 0; - } - } - return out; -} - -export class Vault< - TDatabase extends CompatibleDB, - TAdapters extends Adapter[], -> { - readonly adapters: TAdapters; - readonly db: TDatabase; - readonly migrateFunc: ( - db: TDatabase, - config: MigrationConfig, - ) => Promise; - - constructor(config: VaultConfig) { - this.adapters = config.adapters; - this.db = config.database; - this.migrateFunc = config.migrateFunc; - } - - /** - * Create and initialize the Vault. Runs migrations for selected adapters before returning. - */ - static async create< - TDatabase extends CompatibleDB, - TAdapters extends Adapter[], - >(config: VaultConfig) { - const vault = new Vault(config); - await vault.migrate(); - return vault; - } - - /** - * Run migrations for the selected adapters (defaults to all). - * Uses adapter.drizzleConfig.out verbatim; resolution/existence is delegated to migrateFunc implementation. - */ - private async migrate() { - // TODO something better than whatever this is - const modulePath = import.meta.resolve('../adapters'); - const mod = (await import(modulePath)) as Record; - - for (const adapter of this.adapters) { - for (const func of Object.values(mod)) { - if (typeof func !== 'function') continue; - const a = func(); - this.adapters[0]?.schema['']; - - // TODO again, not amazing - if (!a || typeof a === 'object' || !('id' in a) || a.id !== adapter.id) - continue; - - await this.migrateFunc(this.db, { - migrationsFolder: adapter.drizzleConfig.out ?? '', - migrationsSchema: adapter.drizzleConfig.migrations?.schema ?? '', - migrationsTable: adapter.drizzleConfig.migrations?.table ?? '', - }); - } - } - } - - /** - * Parse a file/blob with each selected adapter and upsert into the database. - * Returns a summary with per-adapter counts. - */ - async importBlob( - blob: Blob, - adapterId: (TAdapters[number]['id'] & object) | string, // Allow any string but keep intellisense - ) { - const reports: ImportReport[] = []; - - const adapter = this.adapters.find((a) => a.id === adapterId); - if (!adapter) throw new Error(`Adapter not found: ${adapterId}`); - - // Parse the blob via the selected adapter - const parsed = await adapter.parse(blob); - // Validate the parsed data against the adapter's schema, throw if invalid - const valid = adapter.validator.assert(parsed); - // Insert the data into the database - await adapter.upsert(this.db, valid); - - reports.push({ - adapter: adapter.name, - migrated: true, - counts: countRecords(parsed), - parsed, - }); - - const totalTables = reports.reduce( - (acc, r) => acc + Object.keys(r.counts).length, - 0, - ); - const totalRecords = reports.reduce( - (acc, r) => acc + Object.values(r.counts).reduce((a, b) => a + b, 0), - 0, - ); - - return { reports, totalTables, totalRecords }; - } - - getCurrentLayout(adapterId?: (TAdapters[number]['id'] & object) | string) { - const schemas = []; - - for (const adapter of this.adapters) { - if (adapterId && adapter.id !== adapterId) continue; - - const humanReadable = readableSchemaInfo( - adapter.schema, - // Adapter.metadata: table -> column -> description - adapter.metadata, - ); - schemas.push(humanReadable); - } - - return schemas; - } -} diff --git a/packages/vault-core/src/fs/index.ts b/packages/vault-core/src/fs/index.ts new file mode 100644 index 0000000000..fabcd349d3 --- /dev/null +++ b/packages/vault-core/src/fs/index.ts @@ -0,0 +1 @@ +export * from './local-file-store'; diff --git a/packages/vault-core/src/fs/local-file-store.ts b/packages/vault-core/src/fs/local-file-store.ts new file mode 100644 index 0000000000..c4b348b2fc --- /dev/null +++ b/packages/vault-core/src/fs/local-file-store.ts @@ -0,0 +1,91 @@ +import { createHash } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import { dirname, join } from 'node:path'; +import type { FileStore, FsSnapshot } from '../core/fs'; + +async function ensureDir(filePath: string) { + await fs.mkdir(dirname(filePath), { recursive: true }); +} + +async function* walk(dir: string): AsyncGenerator { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const e of entries) { + const full = join(dir, e.name); + if (e.isDirectory()) yield* walk(full); + else if (e.isFile()) yield full; + } +} + +function toPosixRelative(root: string, absPath: string): string { + const rel = absPath.slice(root.length + (root.endsWith('/') ? 0 : 1)); + return rel.split(/\\/g).join('/'); +} + +export class LocalFileStore implements FileStore { + constructor(private rootDir: string) {} + + async read(path: string): Promise { + const p = join(this.rootDir, path); + try { + return await fs.readFile(p, 'utf8'); + } catch (e: unknown) { + if ((e as { code?: string })?.code === 'ENOENT') return undefined; + throw e; + } + } + + async write(path: string, contents: string): Promise { + const p = join(this.rootDir, path); + await ensureDir(p); + // Normalize to LF and ensure trailing newline for stability + const normalized = contents.replace(/\r\n/g, '\n'); + const finalText = normalized.endsWith('\n') + ? normalized + : `${normalized}\n`; + await fs.writeFile(p, finalText, 'utf8'); + } + + async remove(path: string): Promise { + const p = join(this.rootDir, path); + try { + await fs.unlink(p); + } catch (e: unknown) { + if ((e as { code?: string })?.code === 'ENOENT') return; + throw e; + } + } + + async list(prefix?: string): Promise { + const out: string[] = []; + const root = this.rootDir; + try { + for await (const abs of walk(root)) { + const rel = toPosixRelative(root, abs); + if (!prefix || rel.startsWith(prefix)) out.push(rel); + } + } catch (e: unknown) { + if ((e as { code?: string })?.code === 'ENOENT') return []; + throw e; + } + return out; + } +} + +export async function snapshotLocal( + rootDir: string, + prefix = 'vault/', +): Promise { + const files: Record = {}; + try { + for await (const abs of walk(rootDir)) { + const rel = toPosixRelative(rootDir, abs); + if (!rel.startsWith(prefix)) continue; + const data = await fs.readFile(abs); + const hash = createHash('sha256').update(data).digest('hex'); + files[rel] = hash; + } + } catch { + // ignore + } + return { files, createdAt: new Date().toISOString() }; +} diff --git a/packages/vault-core/src/sync/git.ts b/packages/vault-core/src/sync/git.ts new file mode 100644 index 0000000000..4fce27e104 --- /dev/null +++ b/packages/vault-core/src/sync/git.ts @@ -0,0 +1,39 @@ +import { exec as _exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import type { FileStore, FsSnapshot } from '../core/fs'; +import type { SyncEngine } from '../core/sync'; +import { LocalFileStore, snapshotLocal } from '../fs/local-file-store'; + +const exec = promisify(_exec); + +export class GitSyncEngine implements SyncEngine { + private store: LocalFileStore; + constructor( + private repoDir: string, + private vaultRoot = 'vault/', + ) { + this.store = new LocalFileStore(repoDir); + } + + async getStore(): Promise { + return this.store; + } + + async pull(): Promise { + await exec('git fetch --all --prune', { cwd: this.repoDir }); + await exec('git pull --ff-only', { cwd: this.repoDir }); + return snapshotLocal(this.repoDir, this.vaultRoot); + } + + async commit(message: string): Promise { + await exec(`git add ${this.vaultRoot}`, { cwd: this.repoDir }); + // Allow empty commit to record a sync event when nothing changed + await exec(`git commit --allow-empty -m ${JSON.stringify(message)}`, { + cwd: this.repoDir, + }); + } + + async push(): Promise { + await exec('git push', { cwd: this.repoDir }); + } +} diff --git a/packages/vault-core/src/sync/index.ts b/packages/vault-core/src/sync/index.ts new file mode 100644 index 0000000000..6c038b427b --- /dev/null +++ b/packages/vault-core/src/sync/index.ts @@ -0,0 +1 @@ +export * from './git'; From 748a70de9b782cf60babc0597b5dfb3acfad97c8 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 19 Aug 2025 19:50:39 +0000 Subject: [PATCH 05/21] refactor: `Codec` usage. --- packages/vault-core/src/codecs/json.ts | 2 +- packages/vault-core/src/codecs/markdown.ts | 2 +- packages/vault-core/src/core/codec.ts | 17 ++++++++--------- packages/vault-core/src/core/vault-service.ts | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/vault-core/src/codecs/json.ts b/packages/vault-core/src/codecs/json.ts index c0789dba33..3265b90a93 100644 --- a/packages/vault-core/src/codecs/json.ts +++ b/packages/vault-core/src/codecs/json.ts @@ -1,4 +1,4 @@ -import { defineCodec, type FormatCodec } from '../core/codec'; +import { defineCodec } from '../core/codec'; function stableStringify(value: unknown): string { return JSON.stringify(value, replacer, 2); diff --git a/packages/vault-core/src/codecs/markdown.ts b/packages/vault-core/src/codecs/markdown.ts index d0e8e4c125..5af43b3690 100644 --- a/packages/vault-core/src/codecs/markdown.ts +++ b/packages/vault-core/src/codecs/markdown.ts @@ -1,4 +1,4 @@ -import { defineCodec, type FormatCodec } from '../core/codec'; +import { defineCodec } from '../core/codec'; // Very small deterministic Markdown with YAML headers: ---\n\n---\n // For now, we do not include a YAML lib; caller will provide headers already normalized. diff --git a/packages/vault-core/src/core/codec.ts b/packages/vault-core/src/core/codec.ts index 89248c3a12..033717f491 100644 --- a/packages/vault-core/src/core/codec.ts +++ b/packages/vault-core/src/core/codec.ts @@ -1,13 +1,13 @@ import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; // Language-level codec (Markdown, JSON, TOML+body, etc.) -export interface FormatCodec { +export interface Codec { /** Unique identifier (e.g., 'markdown', 'json', 'toml', 'yaml-md') */ id: TID; /** Default file extension without dot * @example 'md' */ - fileExtension: Omit; + fileExtension: TExt; /** * Parse file text into a flat record. If a free-form body is present, * codecs should use the reserved key 'body' to carry it. @@ -25,8 +25,6 @@ export interface FormatCodec { denormalize?(value: unknown, columnName: string): unknown; } -export type Codec = FormatCodec; - // Runtime view of a Drizzle table export type TableEntry = [name: string, table: SQLiteTable]; export type ColumnEntry = [name: string, column: SQLiteColumn]; @@ -97,12 +95,13 @@ export function defaultConvention(): ConventionProfile { }; } +type NoDotPrefix = T extends `.${string}` ? never : T; + /** - * defineFormat: identity helper for a single FormatCodec (markdown, json, etc.). + * defineFormat: identity helper for a single Codec (markdown, json, etc.). */ -export function defineCodec< - const TID extends string, - F extends FormatCodec, ->(codec: F): F { +export function defineCodec( + codec: Codec>, +) { return codec; } diff --git a/packages/vault-core/src/core/vault-service.ts b/packages/vault-core/src/core/vault-service.ts index 7ff431475b..fce7a45643 100644 --- a/packages/vault-core/src/core/vault-service.ts +++ b/packages/vault-core/src/core/vault-service.ts @@ -1,7 +1,7 @@ import type { MigrationConfig } from 'drizzle-orm/migrator'; import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; import type { CompatibleDB } from './adapter'; -import type { ConventionProfile } from './codec'; +import type { Codec, ConventionProfile } from './codec'; import { detectPrimaryKey, listColumns, listTables } from './codec'; import type { VaultServiceConfig } from './config'; import type { FileStore } from './fs'; @@ -28,7 +28,7 @@ export class VaultService< config: MigrationConfig, ) => Promise; readonly syncEngine?: SyncEngine; - readonly codec?: import('./codec').Codec; + readonly codec?: Codec; readonly conventions?: ConventionProfile; constructor(config: VaultServiceConfig) { From 95f9a2d52e01df381899da1250247788e0a7e9ba Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 19 Aug 2025 21:17:18 +0000 Subject: [PATCH 06/21] refactor: prototype for SQLite remote queries --- packages/vault-core/src/core/adapter.ts | 196 +++++++++++++++++- packages/vault-core/src/core/config.ts | 2 +- packages/vault-core/src/core/importer.ts | 6 +- packages/vault-core/src/core/migrations.ts | 10 +- packages/vault-core/src/core/vault-client.ts | 35 +++- packages/vault-core/src/core/vault-service.ts | 17 +- 6 files changed, 232 insertions(+), 34 deletions(-) diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index 915030be39..42c0450825 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -10,8 +10,10 @@ type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> type ResultSet = ExtractedResult; // Represents compatible Drizzle DB types across the codebase -export type CompatibleDB> = - BaseSQLiteDatabase<'sync' | 'async', TSchema | ResultSet>; +export type CompatibleDB = BaseSQLiteDatabase< + 'sync' | 'async', + TSchema | ResultSet +>; export type DrizzleConfig = ReturnType; @@ -21,10 +23,29 @@ export type ColumnDescriptions> = { }; }; +// --- Schema & table-name helpers ------------------------------------------------- + +/** Union of table name literals from the schema's keys. */ +export type SchemaTableNames> = + Extract; + +/** Ensure all tables in a schema have names prefixed with the given Adapter ID. */ +// Check that all table names in the schema start with `${TID}_` +export type SchemaTablesAllPrefixed< + TID extends string, + TSchema extends Record, +> = Exclude, `${TID}_${string}`> extends never + ? 1 + : 0; + // Adapter is schema-only. Importers wire lifecycle, parsing, views, etc. export interface Adapter< TID extends string = string, - TSchema extends Record = Record, + TTableNames extends string = string, + TSchema extends Record = Record< + string, + SQLiteTable + >, > { /** Unique identifier for the adapter (lowercase, no spaces, alphanumeric) */ id: TID; @@ -34,15 +55,172 @@ export interface Adapter< // Note: If a generic only appears in a function parameter position, TS won't infer it and will // fall back to the constraint (e.g. `object`). These overloads infer the full function type `F` instead. -export function defineAdapter Adapter>( - adapter: F, -): F; +type KeysOf = Extract; +type PrefixedAdapter< + TID extends string, + S extends Record, +> = KeysOf extends `${TID}_${string}` ? Adapter, S> : never; + +export function defineAdapter< + TID extends string, + S extends Record, +>(adapter: () => PrefixedAdapter): () => PrefixedAdapter; export function defineAdapter< - // biome-ignore lint/suspicious/noExplicitAny: Variance-friendly identity for adapter factories - F extends (args: any) => Adapter, ->(adapter: F): F; + TID extends string, + S extends Record, + A extends unknown[], +>( + adapter: (...args: A) => PrefixedAdapter, +): (...args: A) => PrefixedAdapter; export function defineAdapter unknown>( adapter: F, ): F { return adapter; } + +// --- Cross-adapter utilities ----------------------------------------------------- + +/** Get table-name union for a single Adapter-like value (Adapter or { schema }). */ +export type TableNamesOfAdapterLike = T extends { schema: infer S } + ? S extends Record + ? SchemaTableNames + : never + : never; + +/** Get table-name union for an Importer-like value (has adapter.schema). */ +export type TableNamesOfImporterLike = T extends { + adapter: { schema: infer S }; +} + ? S extends Record + ? SchemaTableNames + : never + : never; + +/** Compute table-name union for any array of Adapters or Importers. */ +export type TableNamesOfMany = + T[number] extends infer E + ? E extends { schema: Record } + ? TableNamesOfAdapterLike + : E extends { adapter: { schema: Record } } + ? TableNamesOfImporterLike + : never + : never; + +/** Compute collisions (duplicates) of table names across an array of Adapters/Importers. */ +export type TableNameCollisions< + T extends readonly unknown[], + Acc extends string = never, +> = T extends readonly [infer H, ...infer R] + ? H extends + | { schema: Record } + | { + adapter: { schema: Record }; + } + ? + | Extract, TableNamesOfMany> + | TableNameCollisions + : TableNameCollisions + : Acc; + +/** Assert there are no duplicate table names; resolves to T when valid, else never. */ +export type NoTableNameCollisions = + TableNameCollisions extends never ? T : never; + +/** Ensure all adapters' schemas are correctly prefixed. Returns T when OK, else never. */ +export type AllAdaptersPrefixed = Exclude< + T[number] extends Adapter + ? SchemaTablesAllPrefixed> + : 1, + 1 +> extends never + ? T + : never; + +/** Ensure all importers' adapter schemas are correctly prefixed. Returns T when OK, else never. */ +export type AllImportersPrefixed = Exclude< + T[number] extends { id: infer ID; adapter: { schema: infer S } } + ? SchemaTablesAllPrefixed> + : 1, + 1 +> extends never + ? T + : never; + +// Utility to merge a union of schema records into a single record via intersection +export type UnionToIntersection = ( + U extends unknown + ? (k: U) => void + : never +) extends (k: infer I) => void + ? I + : never; + +/** + * Build a joined schema record from: + * - a VaultService-like object (has `importers`) + * - a VaultClient-like object (has `adapters`) + * - an array of Importers (have `adapter.schema`) + * - an array of Adapters (have `schema`) + */ +/** + * Normalize any schema-like type to a concrete Record. + * - If S is already a schema record, it's returned unchanged. + * - Otherwise, returns a broad Record so downstream + * utilities (e.g. JoinedSchema<...>) produce a usable record instead of never + * when inputs are unknown/never/empty unions. + */ +type EnsureSchema = S extends Record + ? S + : Record; +export type JoinedSchema = EnsureSchema< + T extends { importers: infer I } + ? UnionToIntersection< + SchemaOfMany + > + : T extends { adapters: infer A } + ? UnionToIntersection< + SchemaOfMany + > + : T extends readonly unknown[] + ? UnionToIntersection> + : never +>; + +type SchemaOfAdapterLike = T extends { schema: infer S } + ? S extends Record + ? S + : never + : never; +type SchemaOfImporterLike = T extends { adapter: { schema: infer S } } + ? S extends Record + ? S + : never + : never; +type SchemaOfMany = [T] extends [never] + ? never + : T[number] extends infer E + ? SchemaOfAdapterLike | SchemaOfImporterLike + : never; + +// Validate arrays of adapters/importers for prefixing and collisions +export type CheckNoCollisions = + TableNameCollisions extends never ? T : never; + +/** + * Validate an array/tuple of Importers at compile time: + * - Ensures each importer's adapter schema keys are prefixed with `${ID}_`. + * - Ensures there are no duplicate table names across all importers. + * Resolves to T when valid; otherwise resolves to never to surface a type error. + */ +export type EnsureImportersOK = + AllImportersPrefixed extends never ? never : CheckNoCollisions; +/** + * Validate an array/tuple of Adapters at compile time: + * - Ensures each adapter's schema keys are prefixed with `${id}_`. + * - Ensures there are no duplicate table names across all adapters. + * Resolves to T when valid; otherwise resolves to never to surface a type error. + */ +export type EnsureAdaptersOK = + AllAdaptersPrefixed extends never + ? never + : CheckNoCollisions; diff --git a/packages/vault-core/src/core/config.ts b/packages/vault-core/src/core/config.ts index 70259c9446..a01762d829 100644 --- a/packages/vault-core/src/core/config.ts +++ b/packages/vault-core/src/core/config.ts @@ -51,7 +51,7 @@ export interface VaultServiceConfig< */ syncEngine?: SyncEngine; /** Active text codec (markdown/json/etc.) and the conventions. */ - codec?: Codec; + codec?: Codec; conventions?: ConventionProfile; } diff --git a/packages/vault-core/src/core/importer.ts b/packages/vault-core/src/core/importer.ts index 4e7fcee493..c8bf857fea 100644 --- a/packages/vault-core/src/core/importer.ts +++ b/packages/vault-core/src/core/importer.ts @@ -1,4 +1,4 @@ -import { type Type, type } from 'arktype'; +import type { Type } from 'arktype'; import type { ColumnsSelection } from 'drizzle-orm'; import type { SQLiteTable, @@ -33,7 +33,7 @@ export interface Importer< /** User-facing name */ name: string; /** Adapter (schema provider) */ - adapter: Adapter; + adapter: Adapter, TSchema>; /** Column descriptions for every table/column */ metadata: ColumnDescriptions; /** ArkType schema for parsing/validation */ @@ -69,7 +69,7 @@ export function defineImporter< TParserShape extends Type, TParsed = TParserShape['infer'], >( - adapter: Adapter, + adapter: Adapter, TSchema>, parts: ImporterNodeParts, ): Importer { return { diff --git a/packages/vault-core/src/core/migrations.ts b/packages/vault-core/src/core/migrations.ts index 8b1a824acc..4cfcf0f6c3 100644 --- a/packages/vault-core/src/core/migrations.ts +++ b/packages/vault-core/src/core/migrations.ts @@ -94,7 +94,7 @@ export function planToVersion( /** Drop all tables owned by the importer (best-effort). */ export async function dropAdapterTables( - db: CompatibleDB, + db: CompatibleDB, importer: Importer, ): Promise { // TODO: Use Drizzle metadata to get table names reliably; for now, list keys from schema object. @@ -109,7 +109,7 @@ export async function dropAdapterTables( /** Inspect Drizzle migrations table for current version tag (implementation TBD). */ export async function getCurrentDbMigrationTag( - db: CompatibleDB, + db: CompatibleDB, importer: Importer, ): Promise { // TODO: Query importer.drizzleConfig.migrations?.table (default likely 'drizzle_migrations') to get last applied tag/name. @@ -119,7 +119,7 @@ export async function getCurrentDbMigrationTag( /** Apply a sequence of SQL files in order. */ export async function applySqlPlan( - db: CompatibleDB, + db: CompatibleDB, importer: Importer, plan: MigrationPlan, ) { @@ -135,7 +135,7 @@ export async function applySqlPlan( /** Mark a migration tag as applied in the Drizzle migrations table (implementation TBD). */ export async function markApplied( - db: CompatibleDB, + db: CompatibleDB, importer: Importer, toTag: string, ) { @@ -145,7 +145,7 @@ export async function markApplied( /** Obtain a raw SQL runner from a CompatibleDB. Placeholder until concrete DBs are wired. */ export function getSqlRunner( - _db: CompatibleDB, + _db: CompatibleDB, ): (sql: string) => Promise { // TODO: For libsql: keep a handle to the underlying client and call client.execute(sql) // TODO: For better-sqlite3: use db.exec(sql) diff --git a/packages/vault-core/src/core/vault-client.ts b/packages/vault-core/src/core/vault-client.ts index 669752f9d3..ea73caae1b 100644 --- a/packages/vault-core/src/core/vault-client.ts +++ b/packages/vault-core/src/core/vault-client.ts @@ -1,10 +1,15 @@ -import type { Adapter } from './adapter'; +import type { + Adapter, + CompatibleDB, + EnsureAdaptersOK, + JoinedSchema, +} from './adapter'; import type { VaultClientConfig } from './config'; -/** - * VaultClient runs in the app (web/desktop). It holds adapters for type-safety - * and schema/metadata introspection. It talks to VaultService via RPC (not implemented). - */ -export class VaultClient { +export class VaultClient< + TDatabase extends CompatibleDB>, + TAdapters extends Adapter[], +> { + private declare readonly _ensureAdaptersOK: EnsureAdaptersOK; readonly adapters: TAdapters; readonly transport: unknown; @@ -25,4 +30,22 @@ export class VaultClient { 'VaultClient.importBlob not implemented: provide RPC transport', ); } + + /** + * Query the database using a SQL builder function. + * @example + * const result = await client.querySQL((db, tables) => + * db.select().from(tables.users).where(equals(tables.users.id, 1)) + * ); + */ + async querySQL( + builder: ( + db: TDatabase, + tables: JoinedSchema, + ) => Pick, + ) { + throw new Error( + 'VaultClient.querySQL not implemented: provide RPC transport', + ); + } } diff --git a/packages/vault-core/src/core/vault-service.ts b/packages/vault-core/src/core/vault-service.ts index fce7a45643..a74c3488d6 100644 --- a/packages/vault-core/src/core/vault-service.ts +++ b/packages/vault-core/src/core/vault-service.ts @@ -1,6 +1,6 @@ import type { MigrationConfig } from 'drizzle-orm/migrator'; import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; -import type { CompatibleDB } from './adapter'; +import type { CompatibleDB, EnsureImportersOK } from './adapter'; import type { Codec, ConventionProfile } from './codec'; import { detectPrimaryKey, listColumns, listTables } from './codec'; import type { VaultServiceConfig } from './config'; @@ -18,9 +18,11 @@ import { import type { SyncEngine } from './sync'; export class VaultService< - TDatabase extends CompatibleDB, + TSchema extends Record, + TDatabase extends CompatibleDB, TImporters extends Importer[], > { + private declare readonly _ensureImportersOK: EnsureImportersOK; readonly importers: TImporters; readonly db: TDatabase; readonly migrateFunc: ( @@ -41,8 +43,9 @@ export class VaultService< } static async create< - TDatabase extends CompatibleDB, TImporters extends Importer[], + TDatabase extends CompatibleDB, + TSchema extends Record, >(config: VaultServiceConfig) { const svc = new VaultService(config); await svc.migrate(); @@ -147,13 +150,7 @@ export class VaultService< throw new Error('No formats/conventions configured'); const { codec: configuredCodec, conventions } = this; const adapterId = importer.id; - const schema: Record = - (( - importer as unknown as { adapter?: { schema: Record } } - )?.adapter?.schema as Record) ?? - (importer as unknown as { schema?: Record }) - .schema ?? - ({} as Record); + const schema = importer.adapter?.schema; // Collect rows per dataset key for a single upsert call const dataset: Record = {}; From eae56552332183dd83085f87c339636a7d567584 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Thu, 11 Sep 2025 16:15:13 +0000 Subject: [PATCH 07/21] removed FS related code --- packages/vault-core/src/core/fs.ts | 69 -------------- packages/vault-core/src/core/index.ts | 3 - packages/vault-core/src/core/sync.ts | 18 ---- packages/vault-core/src/fs/index.ts | 1 - .../vault-core/src/fs/local-file-store.ts | 91 ------------------- packages/vault-core/src/sync/git.ts | 39 -------- packages/vault-core/src/sync/index.ts | 1 - 7 files changed, 222 deletions(-) delete mode 100644 packages/vault-core/src/core/fs.ts delete mode 100644 packages/vault-core/src/core/sync.ts delete mode 100644 packages/vault-core/src/fs/index.ts delete mode 100644 packages/vault-core/src/fs/local-file-store.ts delete mode 100644 packages/vault-core/src/sync/git.ts delete mode 100644 packages/vault-core/src/sync/index.ts diff --git a/packages/vault-core/src/core/fs.ts b/packages/vault-core/src/core/fs.ts deleted file mode 100644 index 108d177ea2..0000000000 --- a/packages/vault-core/src/core/fs.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Filesystem driver abstraction (host app provides). Kept tiny on purpose. - */ -export interface FileStore { - read(path: string): Promise; // undefined for missing - write(path: string, contents: string): Promise; - remove(path: string): Promise; - list(prefix?: string): Promise; // returns relative file paths -} - -/** - * Merge driver hook: allows custom resolution for frontmatter or structured text. - * Implementations can be wired via .gitattributes to reduce human conflicts. - */ -export interface MergeDriver { - /** Returns merged contents or undefined if it can’t auto-merge. */ - merge( - base: string | undefined, - current: string | undefined, - incoming: string | undefined, - path: string, - ): Promise; -} - -// Atomic write intent; the sync engine can batch these under a transaction/journal -export type WriteIntent = { - /** Full relative path (POSIX style) */ - path: string; - /** UTF-8 text content; caller performs deterministic formatting first */ - contents: string; -}; - -// Captures a filesystem snapshot used for 3-way merges -export type FsSnapshot = { - /** content hash per path (e.g., sha256 of file contents) */ - files: Record; - /** optional marker to match schema revisions */ - schemaVersion?: number; - /** timestamp of snapshot creation */ - createdAt: string; -}; - -// --- Sync planning types (DB <-> FS) --- - -export type SyncIntent = { - writes: WriteIntent[]; - deletes: string[]; // relative paths to remove -}; - -export type Conflict = - | { type: 'text'; path: string } - | { type: 'entity'; ref: EntityRef; reason: string }; - -export type SyncPlan = { - intents: SyncIntent; - conflicts: Conflict[]; - /** baseline snapshot used to compute the plan */ - base?: FsSnapshot; -}; - -// A single entity in the domain (table row, document, etc.) -export type EntityRef = { kind: string; id: string }; - -// A minimal change unit from parsing a plaintext document -export type EntityPatch = { - ref: EntityRef; - /** Partial update to apply (already validated against Importer.validator) */ - data: T; -}; diff --git a/packages/vault-core/src/core/index.ts b/packages/vault-core/src/core/index.ts index 85adffe295..d370a07bd1 100644 --- a/packages/vault-core/src/core/index.ts +++ b/packages/vault-core/src/core/index.ts @@ -1,9 +1,6 @@ export * from '../codecs'; -export * from '../fs/local-file-store'; -export * from '../sync/git'; export * from './adapter'; export * from './codec'; export * from './importer'; -export * from './sync'; export * from './vault-client'; export * from './vault-service'; diff --git a/packages/vault-core/src/core/sync.ts b/packages/vault-core/src/core/sync.ts deleted file mode 100644 index 129643e158..0000000000 --- a/packages/vault-core/src/core/sync.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { FileStore, FsSnapshot } from './fs'; - -/** - * SyncEngine: Git-oriented engine used by VaultService. - * - Provides a FileStore view of the working tree - * - Performs VCS operations (pull/commit/push) - * Glue logic (DB<->files via formats/conventions) is handled in VaultService. - */ -export interface SyncEngine { - /** Return a FileStore rooted at the sync workspace (e.g., repo dir). */ - getStore(): Promise; - /** Ensure local workspace is up-to-date; return snapshot after pull. */ - pull(): Promise; - /** Stage and commit current workspace changes with a message. */ - commit(message: string): Promise; - /** Push committed changes to remote. */ - push(): Promise; -} diff --git a/packages/vault-core/src/fs/index.ts b/packages/vault-core/src/fs/index.ts deleted file mode 100644 index fabcd349d3..0000000000 --- a/packages/vault-core/src/fs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './local-file-store'; diff --git a/packages/vault-core/src/fs/local-file-store.ts b/packages/vault-core/src/fs/local-file-store.ts deleted file mode 100644 index c4b348b2fc..0000000000 --- a/packages/vault-core/src/fs/local-file-store.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { createHash } from 'node:crypto'; -import { promises as fs } from 'node:fs'; -import { dirname, join } from 'node:path'; -import type { FileStore, FsSnapshot } from '../core/fs'; - -async function ensureDir(filePath: string) { - await fs.mkdir(dirname(filePath), { recursive: true }); -} - -async function* walk(dir: string): AsyncGenerator { - const entries = await fs.readdir(dir, { withFileTypes: true }); - for (const e of entries) { - const full = join(dir, e.name); - if (e.isDirectory()) yield* walk(full); - else if (e.isFile()) yield full; - } -} - -function toPosixRelative(root: string, absPath: string): string { - const rel = absPath.slice(root.length + (root.endsWith('/') ? 0 : 1)); - return rel.split(/\\/g).join('/'); -} - -export class LocalFileStore implements FileStore { - constructor(private rootDir: string) {} - - async read(path: string): Promise { - const p = join(this.rootDir, path); - try { - return await fs.readFile(p, 'utf8'); - } catch (e: unknown) { - if ((e as { code?: string })?.code === 'ENOENT') return undefined; - throw e; - } - } - - async write(path: string, contents: string): Promise { - const p = join(this.rootDir, path); - await ensureDir(p); - // Normalize to LF and ensure trailing newline for stability - const normalized = contents.replace(/\r\n/g, '\n'); - const finalText = normalized.endsWith('\n') - ? normalized - : `${normalized}\n`; - await fs.writeFile(p, finalText, 'utf8'); - } - - async remove(path: string): Promise { - const p = join(this.rootDir, path); - try { - await fs.unlink(p); - } catch (e: unknown) { - if ((e as { code?: string })?.code === 'ENOENT') return; - throw e; - } - } - - async list(prefix?: string): Promise { - const out: string[] = []; - const root = this.rootDir; - try { - for await (const abs of walk(root)) { - const rel = toPosixRelative(root, abs); - if (!prefix || rel.startsWith(prefix)) out.push(rel); - } - } catch (e: unknown) { - if ((e as { code?: string })?.code === 'ENOENT') return []; - throw e; - } - return out; - } -} - -export async function snapshotLocal( - rootDir: string, - prefix = 'vault/', -): Promise { - const files: Record = {}; - try { - for await (const abs of walk(rootDir)) { - const rel = toPosixRelative(rootDir, abs); - if (!rel.startsWith(prefix)) continue; - const data = await fs.readFile(abs); - const hash = createHash('sha256').update(data).digest('hex'); - files[rel] = hash; - } - } catch { - // ignore - } - return { files, createdAt: new Date().toISOString() }; -} diff --git a/packages/vault-core/src/sync/git.ts b/packages/vault-core/src/sync/git.ts deleted file mode 100644 index 4fce27e104..0000000000 --- a/packages/vault-core/src/sync/git.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { exec as _exec } from 'node:child_process'; -import { promisify } from 'node:util'; -import type { FileStore, FsSnapshot } from '../core/fs'; -import type { SyncEngine } from '../core/sync'; -import { LocalFileStore, snapshotLocal } from '../fs/local-file-store'; - -const exec = promisify(_exec); - -export class GitSyncEngine implements SyncEngine { - private store: LocalFileStore; - constructor( - private repoDir: string, - private vaultRoot = 'vault/', - ) { - this.store = new LocalFileStore(repoDir); - } - - async getStore(): Promise { - return this.store; - } - - async pull(): Promise { - await exec('git fetch --all --prune', { cwd: this.repoDir }); - await exec('git pull --ff-only', { cwd: this.repoDir }); - return snapshotLocal(this.repoDir, this.vaultRoot); - } - - async commit(message: string): Promise { - await exec(`git add ${this.vaultRoot}`, { cwd: this.repoDir }); - // Allow empty commit to record a sync event when nothing changed - await exec(`git commit --allow-empty -m ${JSON.stringify(message)}`, { - cwd: this.repoDir, - }); - } - - async push(): Promise { - await exec('git push', { cwd: this.repoDir }); - } -} diff --git a/packages/vault-core/src/sync/index.ts b/packages/vault-core/src/sync/index.ts deleted file mode 100644 index 6c038b427b..0000000000 --- a/packages/vault-core/src/sync/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './git'; From 37a69d98ee6d2bca9409e118cfa2cefa62005f6d Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Thu, 11 Sep 2025 16:33:25 +0000 Subject: [PATCH 08/21] removed vault client-service architecture --- .../src/adapters/reddit/src/index.ts | 1 - packages/vault-core/src/core/config.ts | 10 +- packages/vault-core/src/core/index.ts | 3 - packages/vault-core/src/core/vault-client.ts | 51 ---- packages/vault-core/src/core/vault-service.ts | 271 ------------------ 5 files changed, 4 insertions(+), 332 deletions(-) delete mode 100644 packages/vault-core/src/core/vault-client.ts delete mode 100644 packages/vault-core/src/core/vault-service.ts diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index a9839af809..8d3edf26a3 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -1,2 +1 @@ export { redditAdapter } from './adapter'; -export { redditImporter } from './importer'; diff --git a/packages/vault-core/src/core/config.ts b/packages/vault-core/src/core/config.ts index a01762d829..eb360f0661 100644 --- a/packages/vault-core/src/core/config.ts +++ b/packages/vault-core/src/core/config.ts @@ -3,7 +3,6 @@ import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; import type { Adapter } from './adapter'; import type { Codec, ConventionProfile } from './codec'; import type { Importer } from './importer'; -import type { SyncEngine } from './sync'; // Deprecated: use VaultServiceConfig or VaultClientConfig export interface VaultConfig< @@ -16,6 +15,10 @@ export interface VaultConfig< } // Service config: owns DB and Importers +/** + * @deprecated The service/client architecture has been removed from vault-core. + * Prefer a pure core API with an injected Drizzle DB and per-call codec injection. + */ export interface VaultServiceConfig< TDatabase extends BaseSQLiteDatabase<'sync' | 'async', unknown>, TImporters extends Importer[], @@ -45,11 +48,6 @@ export interface VaultServiceConfig< * migrate(db, { migrationsFolder: '...' }) */ migrateFunc: (db: TDatabase, config: MigrationConfig) => Promise; - /** - * A SyncEngine implementation injected by the host. - * Controls DB <-> Filesystem sync flows; keeps Importers/Adapters pure. - */ - syncEngine?: SyncEngine; /** Active text codec (markdown/json/etc.) and the conventions. */ codec?: Codec; conventions?: ConventionProfile; diff --git a/packages/vault-core/src/core/index.ts b/packages/vault-core/src/core/index.ts index d370a07bd1..d8fc0434e5 100644 --- a/packages/vault-core/src/core/index.ts +++ b/packages/vault-core/src/core/index.ts @@ -1,6 +1,3 @@ -export * from '../codecs'; export * from './adapter'; export * from './codec'; export * from './importer'; -export * from './vault-client'; -export * from './vault-service'; diff --git a/packages/vault-core/src/core/vault-client.ts b/packages/vault-core/src/core/vault-client.ts deleted file mode 100644 index ea73caae1b..0000000000 --- a/packages/vault-core/src/core/vault-client.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { - Adapter, - CompatibleDB, - EnsureAdaptersOK, - JoinedSchema, -} from './adapter'; -import type { VaultClientConfig } from './config'; -export class VaultClient< - TDatabase extends CompatibleDB>, - TAdapters extends Adapter[], -> { - private declare readonly _ensureAdaptersOK: EnsureAdaptersOK; - readonly adapters: TAdapters; - readonly transport: unknown; - - constructor(config: VaultClientConfig) { - this.adapters = config.adapters; - this.transport = config.transport; - } - - /** - * Ask service to import a blob with a given importer. - * Placeholder: must be implemented in host app via RPC. - */ - async importBlob( - _adapterId: TAdapters[number]['id'], - _file: Blob, - ): Promise { - throw new Error( - 'VaultClient.importBlob not implemented: provide RPC transport', - ); - } - - /** - * Query the database using a SQL builder function. - * @example - * const result = await client.querySQL((db, tables) => - * db.select().from(tables.users).where(equals(tables.users.id, 1)) - * ); - */ - async querySQL( - builder: ( - db: TDatabase, - tables: JoinedSchema, - ) => Pick, - ) { - throw new Error( - 'VaultClient.querySQL not implemented: provide RPC transport', - ); - } -} diff --git a/packages/vault-core/src/core/vault-service.ts b/packages/vault-core/src/core/vault-service.ts deleted file mode 100644 index a74c3488d6..0000000000 --- a/packages/vault-core/src/core/vault-service.ts +++ /dev/null @@ -1,271 +0,0 @@ -import type { MigrationConfig } from 'drizzle-orm/migrator'; -import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; -import type { CompatibleDB, EnsureImportersOK } from './adapter'; -import type { Codec, ConventionProfile } from './codec'; -import { detectPrimaryKey, listColumns, listTables } from './codec'; -import type { VaultServiceConfig } from './config'; -import type { FileStore } from './fs'; -import type { Importer } from './importer'; -import { - applySqlPlan, - dropAdapterTables, - getCurrentDbMigrationTag, - listSqlSteps, - planToVersion, - readMigrationJournal, - resolveMigrationsDir, -} from './migrations'; -import type { SyncEngine } from './sync'; - -export class VaultService< - TSchema extends Record, - TDatabase extends CompatibleDB, - TImporters extends Importer[], -> { - private declare readonly _ensureImportersOK: EnsureImportersOK; - readonly importers: TImporters; - readonly db: TDatabase; - readonly migrateFunc: ( - db: TDatabase, - config: MigrationConfig, - ) => Promise; - readonly syncEngine?: SyncEngine; - readonly codec?: Codec; - readonly conventions?: ConventionProfile; - - constructor(config: VaultServiceConfig) { - this.importers = config.importers; - this.db = config.database; - this.migrateFunc = config.migrateFunc; - this.syncEngine = config.syncEngine; - this.codec = config.codec; - this.conventions = config.conventions; - } - - static async create< - TImporters extends Importer[], - TDatabase extends CompatibleDB, - TSchema extends Record, - >(config: VaultServiceConfig) { - const svc = new VaultService(config); - await svc.migrate(); - return svc; - } - - /** - * Run migrations for installed importers. - */ - private async migrate() { - for (const importer of this.importers) { - await this.migrateFunc(this.db, { - migrationsFolder: importer.drizzleConfig.out ?? '', - migrationsSchema: importer.drizzleConfig.migrations?.schema ?? '', - migrationsTable: importer.drizzleConfig.migrations?.table ?? '', - }); - } - } - - /** - * Parse a blob with a specific importer and upsert into the database. - * Returns the parsed payload for auditing if needed. - */ - async importBlob(blob: Blob, importerId: string) { - const importer = this.importers.find((i) => i.id === importerId); - if (!importer) throw new Error(`Importer not found: ${importerId}`); - - const parsed = await importer.parse(blob); - const valid = importer.validator.assert(parsed); - await importer.upsert(this.db, valid); - return { importer: importer.name, parsed }; - } - - /** - * Export DB state using the injected SyncEngine and codec. - */ - async export(importerId: string, store: FileStore) { - const importer = this.importers.find((i) => i.id === importerId); - if (!importer) throw new Error(`Importer not found: ${importerId}`); - if (!this.codec || !this.conventions) - throw new Error('No formats/conventions configured'); - const { codec: format, conventions } = this; - const adapterId = importer.id; - // Support both shapes: importer.adapter.schema (new) and importer.schema (legacy) - const schema: Record = - (( - importer as unknown as { adapter?: { schema: Record } } - )?.adapter?.schema as Record) ?? - (importer as unknown as { schema?: Record }) - .schema ?? - ({} as Record); - const files: Record = {}; - - for (const [tableName, table] of listTables(schema)) { - // Query all rows - const rows: Record[] = await ( - this.db as unknown as { - select: () => { - from: (t: unknown) => Promise[]>; - }; - } - ) - .select() - .from(table); - - const pkCols = detectPrimaryKey(tableName, table) ?? []; - - for (const row of rows) { - // Build a flat record deterministically for the codec - const rec: Record = {}; - const tableCols = new Set( - listColumns(table).map(([name]: [string, unknown]) => name), - ); - for (const [k, v] of Object.entries(row)) { - if (!tableCols.has(k)) continue; // ignore non-column fields (if any) - if (v === undefined || v === null) continue; // omit nulls -> undefined on re-import - rec[k] = format.normalize ? format.normalize(v, k) : v; - } - - // Compute path using PK values - const pkValues: Record = {}; - for (const pk of pkCols) pkValues[pk] = row[pk]; - const basePath = conventions.pathFor(adapterId, tableName, pkValues); - const path = `${basePath}.${format.fileExtension}`; - - const text = format.stringify(rec); - await store.write(path, text); - files[path] = ''; - } - } - - return { files, createdAt: new Date().toISOString() }; - } - - /** - * Import DB state from filesystem into DB using the injected SyncEngine and codec. - */ - async import(importerId: string, store: FileStore) { - const importer = this.importers.find((i) => i.id === importerId); - if (!importer) throw new Error(`Importer not found: ${importerId}`); - if (!this.codec || !this.conventions) - throw new Error('No formats/conventions configured'); - const { codec: configuredCodec, conventions } = this; - const adapterId = importer.id; - const schema = importer.adapter?.schema; - - // Collect rows per dataset key for a single upsert call - const dataset: Record = {}; - // Initialize all dataset keys to empty arrays to satisfy validators expecting present arrays - for (const [tableName] of listTables(schema)) { - const key = conventions.datasetKeyFor(adapterId, tableName); - if (!dataset[key]) dataset[key] = []; - } - const prefix = `vault/${adapterId}/`; - const paths = await store.list(prefix); - - for (const path of paths) { - if (!path.startsWith(prefix)) continue; - const rel = path.slice(prefix.length); - const parts = rel.split('/'); - if (parts.length < 2) continue; - const tableName = - parts[0] as keyof typeof importer.adapter.schema as string; - const file = parts.slice(1).join('/'); - const dot = file.lastIndexOf('.'); - if (dot < 0) continue; - const fileId = file.slice(0, dot); - const ext = file.slice(dot + 1); - - // Use configured codec; skip files with other extensions - const format = configuredCodec; - if (format.fileExtension !== ext) continue; - - const table = schema[tableName as keyof typeof schema] as SQLiteTable; - if (!table) continue; - - const text = await store.read(path); - if (text == null) continue; - const rec = format.parse(text); - const row: Record = {}; - - const tableCols = new Set( - listColumns(table).map(([name]: [string, unknown]) => name), - ); - for (const [k, v] of Object.entries(rec ?? {})) { - if (!tableCols.has(k)) continue; - row[k] = format.denormalize ? format.denormalize(v, k) : v; - } - - // Ensure PK values exist; if missing in headers, try to reconstruct from filename - const pkCols = detectPrimaryKey(tableName, table) ?? []; - const fileParts = fileId.split('__'); - for (let i = 0; i < pkCols.length; i++) { - const key = pkCols[i]; - if (row[key] === undefined) row[key] = fileParts[i]; - } - - const key = conventions.datasetKeyFor(adapterId, tableName); - if (!dataset[key]) dataset[key] = []; - (dataset[key] as unknown[]).push(row); - } - - // Prepare and upsert (no ArkType validation here): - // Normalize dataset values: convert null -> undefined and coerce numeric-like to strings - for (const key of Object.keys(dataset)) { - const rows = dataset[key] as Record[]; - for (const row of rows) { - for (const [k, v] of Object.entries(row)) { - if (v === null) { - row[k] = undefined; - continue; - } - // Heuristic: fields commonly textual but sometimes numeric in exports - if ( - typeof v === 'number' && - /(?:^|_)(?:id|name|slug|subreddit|channel|parent|media|value|image|url|stake|selection)(?:$|_)/.test( - k, - ) - ) { - row[k] = String(v); - } - } - } - } - // ArkType validator is used only for first-ingest (blob imports), not for FS re-imports. - // We rely on DB constraints/migrations and importer.upsert to handle shaping. - await importer.upsert(this.db, dataset); - } - - /** - * Prepare DB schema for vault import by migrating to a target version, import, then migrate to head. - * This does not run ArkType validation; it assumes the vault content is trusted. - */ - async migrateImportMigrate( - importerId: string, - store: FileStore, - options: { targetTag: string }, - ) { - const importer = this.importers.find((i) => i.id === importerId); - if (!importer) throw new Error(`Importer not found: ${importerId}`); - // 1) Drop adapter tables (scoped) - await dropAdapterTables(this.db, importer); - // 2) Read journal + steps and compute plan to target - const dir = resolveMigrationsDir(importer); - const journal = await readMigrationJournal(dir); - const steps = await listSqlSteps(dir); - // TODO: Read current DB tag from drizzle migrations table - const current = await getCurrentDbMigrationTag(this.db, importer); - const plan = planToVersion(journal, steps, current, options.targetTag); - // 3) Apply plan - await applySqlPlan(this.db, importer, plan); - // TODO: mark applied in drizzle migrations table - // await markApplied(this.db, importer, options.targetTag); - // 4) Import vault content without validation - await this.import(importerId, store); - // 5) Migrate to head using provided migrateFunc (drizzle’s migrator) - await this.migrateFunc(this.db, { - migrationsFolder: importer.drizzleConfig.out ?? '', - migrationsSchema: importer.drizzleConfig.migrations?.schema ?? '', - migrationsTable: importer.drizzleConfig.migrations?.table ?? '', - }); - } -} From 61ef7cbe863119d458d39357f84c7e3ea98f7588 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Sat, 13 Sep 2025 00:20:53 +0000 Subject: [PATCH 09/21] feat: added std utils for adapters/etc. --- bun.lock | 29 +-- packages/vault-core/package.json | 9 +- packages/vault-core/src/codecs/json.spec.ts | 29 +++ packages/vault-core/src/codecs/json.ts | 42 +++- .../vault-core/src/utils/archive/index.ts | 2 + .../src/utils/archive/tar/index.spec.ts | 49 ++++ .../vault-core/src/utils/archive/tar/index.ts | 195 +++++++++++++++ .../src/utils/archive/zip/index.spec.ts | 41 +++ .../vault-core/src/utils/archive/zip/index.ts | 37 +++ .../src/utils/encoding/gzip/index.spec.ts | 54 ++++ .../src/utils/encoding/gzip/index.ts | 107 ++++++++ .../vault-core/src/utils/encoding/index.ts | 1 + .../src/utils/format/csv/index.spec.ts | 157 ++++++++++++ .../vault-core/src/utils/format/csv/index.ts | 233 ++++++++++++++++++ packages/vault-core/src/utils/format/index.ts | 5 + .../src/utils/format/jsonc/index.spec.ts | 47 ++++ .../src/utils/format/jsonc/index.ts | 37 +++ .../src/utils/format/jsonl/index.spec.ts | 39 +++ .../src/utils/format/jsonl/index.ts | 50 ++++ .../src/utils/format/markdown/index.spec.ts | 51 ++++ .../src/utils/format/markdown/index.ts | 59 +++++ .../vault-core/src/utils/format/yaml/index.ts | 29 +++ 22 files changed, 1281 insertions(+), 21 deletions(-) create mode 100644 packages/vault-core/src/codecs/json.spec.ts create mode 100644 packages/vault-core/src/utils/archive/index.ts create mode 100644 packages/vault-core/src/utils/archive/tar/index.spec.ts create mode 100644 packages/vault-core/src/utils/archive/tar/index.ts create mode 100644 packages/vault-core/src/utils/archive/zip/index.spec.ts create mode 100644 packages/vault-core/src/utils/archive/zip/index.ts create mode 100644 packages/vault-core/src/utils/encoding/gzip/index.spec.ts create mode 100644 packages/vault-core/src/utils/encoding/gzip/index.ts create mode 100644 packages/vault-core/src/utils/encoding/index.ts create mode 100644 packages/vault-core/src/utils/format/csv/index.spec.ts create mode 100644 packages/vault-core/src/utils/format/csv/index.ts create mode 100644 packages/vault-core/src/utils/format/index.ts create mode 100644 packages/vault-core/src/utils/format/jsonc/index.spec.ts create mode 100644 packages/vault-core/src/utils/format/jsonc/index.ts create mode 100644 packages/vault-core/src/utils/format/jsonl/index.spec.ts create mode 100644 packages/vault-core/src/utils/format/jsonl/index.ts create mode 100644 packages/vault-core/src/utils/format/markdown/index.spec.ts create mode 100644 packages/vault-core/src/utils/format/markdown/index.ts create mode 100644 packages/vault-core/src/utils/format/yaml/index.ts diff --git a/bun.lock b/bun.lock index 428d39ed86..d7512f30ea 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,7 @@ "version": "0.0.0", "dependencies": { "@libsql/client": "^0.11.0", + "@repo/vault-core": "workspace:*", "drizzle-orm": "catalog:", }, }, @@ -312,9 +313,13 @@ "name": "@repo/vault-core", "version": "0.0.0", "dependencies": { + "@vanillaes/csv": "^3.0.4", "arktype": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", + "fflate": "^0.8.2", + "toml": "^3.0.0", + "yaml": "^2.8.1", }, "devDependencies": { "typescript": "catalog:", @@ -1035,21 +1040,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@volar/kit": ["@volar/kit@2.4.23", "", { "dependencies": { "@volar/language-service": "2.4.23", "@volar/typescript": "2.4.23", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ=="], - - "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], - - "@volar/language-server": ["@volar/language-server@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "@volar/language-service": "2.4.23", "@volar/typescript": "2.4.23", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-k0iO+tybMGMMyrNdWOxgFkP0XJTdbH0w+WZlM54RzJU3WZSjHEupwL30klpM7ep4FO6qyQa03h+VcGHD4Q8gEg=="], - - "@volar/language-service": ["@volar/language-service@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-h5mU9DZ/6u3LCB9xomJtorNG6awBNnk9VuCioGsp6UtFiM8amvS5FcsaC3dabdL9zO0z+Gq9vIEMb/5u9K6jGQ=="], - - "@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="], - - "@volar/typescript": ["@volar/typescript@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag=="], - - "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="], - - "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + "@vanillaes/csv": ["@vanillaes/csv@3.0.4", "", {}, "sha512-cMJ/pAljVGpsHvqgd5N4EpNJOvMjFubg7x+9ehjVgQUFi2h+u4Nc4O4C0ErNLV53rO3rI0W1JnKX3wQz0pWgIA=="], "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], @@ -2235,6 +2226,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], @@ -2451,7 +2444,7 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], "yaml-language-server": ["yaml-language-server@1.15.0", "", { "dependencies": { "ajv": "^8.11.0", "lodash": "4.17.21", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2", "yaml": "2.2.2" }, "optionalDependencies": { "prettier": "2.8.7" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw=="], @@ -2689,6 +2682,10 @@ "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "postcss-load-config/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + + "posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], "protobufjs/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index 9ba0e63d53..77d4262ecd 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -5,7 +5,8 @@ "type": "module", "exports": { ".": "./src/index.ts", - "./adapters/*": "./src/adapters/index.ts" + "./adapters/*": "./src/adapters/*/index.ts", + "./utils/*": "./src/utils/*/index.ts" }, "devDependencies": { "typescript": "catalog:" @@ -17,8 +18,12 @@ "check": "echo \"TODO add this in a later commit\"" }, "dependencies": { + "@vanillaes/csv": "^3.0.4", "arktype": "catalog:", "drizzle-kit": "catalog:", - "drizzle-orm": "catalog:" + "drizzle-orm": "catalog:", + "fflate": "^0.8.2", + "toml": "^3.0.0", + "yaml": "^2.8.1" } } diff --git a/packages/vault-core/src/codecs/json.spec.ts b/packages/vault-core/src/codecs/json.spec.ts new file mode 100644 index 0000000000..3f091d0ce5 --- /dev/null +++ b/packages/vault-core/src/codecs/json.spec.ts @@ -0,0 +1,29 @@ +import { describe, test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { jsonFormat } from './json'; + +describe('JSON', () => { + test('stringify converts Date objects into {$date}', () => { + const input = { exportedAt: new Date('1970-01-01T00:00:00.000Z') }; + + const out = jsonFormat.stringify(input); + const parsed = JSON.parse(out); + + // Validate structure and type + assert.ok(typeof parsed.exportedAt.$date === 'string'); + assert.strictEqual(parsed.exportedAt.$date, '1970-01-01T00:00:00.000Z'); + }); + + test('parse revives {$date} back to a real Date', () => { + const text = JSON.stringify({ + exportedAt: { $date: '2020-01-02T03:04:05.000Z' }, + }); + const revived = jsonFormat.parse(text); + + assert.ok(revived.exportedAt instanceof Date); + assert.strictEqual( + revived.exportedAt.toISOString(), + '2020-01-02T03:04:05.000Z', + ); + }); +}); diff --git a/packages/vault-core/src/codecs/json.ts b/packages/vault-core/src/codecs/json.ts index 3265b90a93..b5bdb326a4 100644 --- a/packages/vault-core/src/codecs/json.ts +++ b/packages/vault-core/src/codecs/json.ts @@ -18,10 +18,46 @@ export const jsonFormat = defineCodec({ id: 'json', fileExtension: 'json', parse(text) { - const obj = JSON.parse(text); - return obj; + return JSON.parse(text, (_, value) => { + // Revive pseudo-date objects + if (isJsonDate(value)) { + return fromJSONDate(value); + } + return value; + }); }, stringify(rec) { - return stableStringify(rec ?? {}); + // Need to override `Date.toJSON` to get desired format + // Otherwise we get ISO strings directly + const originalDateStringifier = Date.prototype.toJSON; + Date.prototype.toJSON = function () { + return toJSONDate(this) as unknown as string; + }; + + try { + return JSON.stringify(rec, null, 2); + } finally { + Date.prototype.toJSON = originalDateStringifier; + } }, }); + +// Pseudo-date because we can't serialize Date objects in JSON, unlike YAML +type JsonDate = { $date: string }; // Is that a BSON reference??? + +function isJsonDate(v: unknown): v is JsonDate { + return ( + typeof v === 'object' && + v !== null && + '$date' in v && + typeof v.$date === 'string' + ); +} + +function toJSONDate(date: Date): JsonDate { + return { $date: date.toISOString() }; +} + +function fromJSONDate(jsonDate: JsonDate): Date { + return new Date(jsonDate.$date); +} diff --git a/packages/vault-core/src/utils/archive/index.ts b/packages/vault-core/src/utils/archive/index.ts new file mode 100644 index 0000000000..a04ccfbcbe --- /dev/null +++ b/packages/vault-core/src/utils/archive/index.ts @@ -0,0 +1,2 @@ +export * from './tar'; +export * from './zip'; diff --git a/packages/vault-core/src/utils/archive/tar/index.spec.ts b/packages/vault-core/src/utils/archive/tar/index.spec.ts new file mode 100644 index 0000000000..2d4725e519 --- /dev/null +++ b/packages/vault-core/src/utils/archive/tar/index.spec.ts @@ -0,0 +1,49 @@ +import { describe, it } from 'bun:test'; +import assert from 'node:assert/strict'; +import { TAR } from './index'; + +describe('TAR', () => { + it('packs and unpacks single text file', async () => { + const files = { 'hello.txt': 'hello world' }; + const tar = await TAR.pack(files); + assert.ok(tar instanceof Uint8Array); + assert.equal(tar.length % 512, 0); + const unpacked = await TAR.unpack(tar); + assert.equal(Object.keys(unpacked).length, 1); + assert.equal( + new TextDecoder().decode(unpacked['hello.txt']), + 'hello world', + ); + }); + + it('packs and unpacks binary file', async () => { + const bin = new Uint8Array(100); + for (let i = 0; i < bin.length; i++) bin[i] = i; + const files = { 'data.bin': bin }; + const tar = await TAR.pack(files); + const unpacked = await TAR.unpack(tar); + assert.equal(unpacked['data.bin']?.length, 100); + assert.equal(unpacked['data.bin']?.[50], 50); + }); + + it('handles multiple files and preserves content', async () => { + const files = { + 'a.txt': 'A', + 'path/b.txt': 'Bee', + 'nested/deep/c.txt': 'Sea', + }; + const tar = await TAR.pack(files); + const unpacked = await TAR.unpack(tar); + assert.deepEqual(Object.keys(unpacked).sort(), Object.keys(files).sort()); + for (const k of Object.keys(files) as Array) { + assert.equal(new TextDecoder().decode(unpacked[k]), files[k]); + } + }); + + it('aligns file data to 512-byte boundaries (padding check)', async () => { + const content = 'x'.repeat(700); // crosses one 512 boundary, requires padding + const tar = await TAR.pack({ 'pad.txt': content }); + // header (512) + data padded to 1024 (data + padding) + two zero-blocks (1024) => total 2560 + assert.equal(tar.length, 2560); + }); +}); diff --git a/packages/vault-core/src/utils/archive/tar/index.ts b/packages/vault-core/src/utils/archive/tar/index.ts new file mode 100644 index 0000000000..63510c010a --- /dev/null +++ b/packages/vault-core/src/utils/archive/tar/index.ts @@ -0,0 +1,195 @@ +type TarEntryInput = string | Uint8Array; +type TarFilesInput = Record; +type TarUnpacked = Record; + +const BLOCK_SIZE = 512; +const USTAR_MAGIC = 'ustar'; + +export type TarNamespace = { + /** Create a tar archive (Uint8Array) from a map of filename->content */ + pack(files: TarFilesInput, options?: { mtime?: number }): Promise; + /** Extract a tar archive into a map of filename->bytes */ + unpack(tarBytes: Uint8Array): Promise; +}; + +function textToBytes(text: string): Uint8Array { + const enc = new TextEncoder(); + return enc.encode(text); +} + +function bytesToText(bytes: Uint8Array): string { + const dec = new TextDecoder(); + return dec.decode(bytes); +} + +function pad(str: string, length: number, padChar = '\0'): string { + if (str.length >= length) return str.slice(0, length); + return str + padChar.repeat(length - str.length); +} + +function octal(value: number, length: number): string { + const str = value.toString(8); + const padLen = length - str.length - 1; // reserve one byte for null + const padded = `${'0'.repeat(Math.max(0, padLen))}${str}\0`; + return pad(padded, length, ' '); +} + +function computeChecksum(block: Uint8Array): number { + let sum = 0; + for (let i = 0; i < block.length; i++) { + const byte = block[i]; + if (byte === undefined) continue; + sum += byte; + } + return sum; +} + +function setString( + buf: Uint8Array, + offset: number, + length: number, + value: string, +) { + for (let i = 0; i < length; i++) { + buf[offset + i] = i < value.length ? value.charCodeAt(i) : 0; // null padding + } +} + +function createHeader(name: string, size: number, mtime: number): Uint8Array { + const block = new Uint8Array(BLOCK_SIZE); + // Set all with zeros (already) + if (name.length > 100) + throw new Error(`tar: filename too long (>${100}): ${name}`); + + setString(block, 0, 100, name); // name + setString(block, 100, 8, pad('0000644', 8)); // mode + setString(block, 108, 8, pad('0000000', 8)); // uid + setString(block, 116, 8, pad('0000000', 8)); // gid + setString(block, 124, 12, octal(size, 12)); // size + setString(block, 136, 12, octal(mtime, 12)); // mtime + // checksum field initially filled with spaces (0x20) + for (let i = 148; i < 156; i++) block[i] = 0x20; + setString(block, 156, 1, '0'); // typeflag '0' regular file + // linkname (unused) 157-256 + setString(block, 257, 6, `${USTAR_MAGIC}\0`); // magic 'ustar\0' + setString(block, 263, 2, '00'); // version + // uname / gname (empty) + // compute checksum + const checksum = computeChecksum(block); + const chkStr = octal(checksum, 8); // includes null + space + setString(block, 148, 8, chkStr); + return block; +} + +function concat(chunks: Uint8Array[]): Uint8Array { + let total = 0; + for (const c of chunks) total += c.length; + const out = new Uint8Array(total); + let offset = 0; + for (const c of chunks) { + out.set(c, offset); + offset += c.length; + } + return out; +} + +function normalizeInput(data: TarEntryInput): Uint8Array { + return typeof data === 'string' ? textToBytes(data) : data; +} + +function padToBlock(data: Uint8Array): Uint8Array { + if (data.length % BLOCK_SIZE === 0) return data; + const padLen = BLOCK_SIZE - (data.length % BLOCK_SIZE); + const padArr = new Uint8Array(padLen); + return concat([data, padArr]); +} + +function isEndBlock(block: Uint8Array): boolean { + for (let i = 0; i < BLOCK_SIZE; i++) if (block[i] !== 0) return false; + return true; +} + +function parseOctal(bytes: Uint8Array, offset: number, length: number): number { + let str = ''; + for (let i = 0; i < length; i++) { + const c = bytes[offset + i]; + if (c === undefined || c === 0 || c === 32) break; // null or space + str += String.fromCharCode(c); + } + if (!str) return 0; + return Number.parseInt(str.trim(), 8) || 0; +} + +function readString(bytes: Uint8Array, offset: number, length: number): string { + let end = offset; + const max = offset + length; + while (end < max && bytes[end] !== 0) end++; + return bytesToText(bytes.subarray(offset, end)); +} + +function packSync(files: TarFilesInput, mtimeOverride?: number): Uint8Array { + const chunks: Uint8Array[] = []; + const now = mtimeOverride ?? Math.floor(Date.now() / 1000); + for (const rawName in files) { + const name = rawName.replace(/\\+/g, '/'); + const file = files[rawName]; + if (!file) continue; // skip empty + const content = normalizeInput(file); + const header = createHeader(name, content.length, now); + chunks.push(header); + chunks.push(content); + if (content.length % BLOCK_SIZE !== 0) { + const padLen = BLOCK_SIZE - (content.length % BLOCK_SIZE); + chunks.push(new Uint8Array(padLen)); + } + } + // two zero blocks terminator + chunks.push(new Uint8Array(BLOCK_SIZE)); + chunks.push(new Uint8Array(BLOCK_SIZE)); + return concat(chunks); +} + +function unpackSync(tarBytes: Uint8Array): TarUnpacked { + const out: TarUnpacked = {}; + let offset = 0; + const len = tarBytes.length; + let zeroCount = 0; + while (offset + BLOCK_SIZE <= len) { + const block = tarBytes.subarray(offset, offset + BLOCK_SIZE); + offset += BLOCK_SIZE; + if (isEndBlock(block)) { + zeroCount++; + if (zeroCount === 2) break; + continue; + } + zeroCount = 0; + + const name = readString(block, 0, 100); + if (!name) continue; // skip invalid + const size = parseOctal(block, 124, 12); + const typeflag = block[156]; + // Only supporting regular files '0' or 0 + if (typeflag !== 48 /* '0' */ && typeflag !== 0) { + // Skip unsupported file types; still need to advance + } + const fileData = tarBytes.subarray(offset, offset + size); + out[name] = fileData.slice(); // copy + // advance with padding + const dataAndPad = padToBlock(fileData); + offset += dataAndPad.length; + } + return out; +} + +const pack: TarNamespace['pack'] = async (files, options) => { + return packSync(files, options?.mtime); +}; + +const unpack: TarNamespace['unpack'] = async (bytes) => { + return unpackSync(bytes); +}; + +export const TAR = { + pack, + unpack, +} satisfies TarNamespace; diff --git a/packages/vault-core/src/utils/archive/zip/index.spec.ts b/packages/vault-core/src/utils/archive/zip/index.spec.ts new file mode 100644 index 0000000000..1001e9172f --- /dev/null +++ b/packages/vault-core/src/utils/archive/zip/index.spec.ts @@ -0,0 +1,41 @@ +// Node.js built-in test runner specs for ZIP utilities +// Run with: node --test (after TypeScript compilation if needed) + +import { describe, it } from 'bun:test'; +import assert from 'node:assert/strict'; +import { ZIP } from './index'; + +function bytes(n: number): Uint8Array { + const a = new Uint8Array(n); + for (let i = 0; i < n; i++) a[i] = (i * 19 + 7) % 256; + return a; +} + +describe('ZIP.archive/extract', () => { + it('archives and extracts multiple files preserving content', async () => { + const files = { + 'a.txt': 'Hello world', + 'b.bin': bytes(256), + 'nested/c.txt': 'Nested file', + }; + const zipped = await ZIP.pack(files, { level: 6 }); + assert.ok(zipped.length > 20); + const out = await ZIP.unpack(zipped); + // Basic presence checks + assert.ok(out['a.txt']); + assert.ok(out['b.bin']); + assert.ok(out['nested/c.txt']); + // Content verification + const decoder = new TextDecoder(); + assert.equal(decoder.decode(out['a.txt']), 'Hello world'); + assert.equal(decoder.decode(out['nested/c.txt']), 'Nested file'); + const originalBin = files['b.bin'] as Uint8Array; + const extractedBin = out['b.bin']; + assert.equal(extractedBin.length, originalBin.length); + for (let i = 0; i < originalBin.length; i++) { + if (originalBin[i] !== extractedBin[i]) { + assert.fail(`byte mismatch at ${i}`); + } + } + }); +}); diff --git a/packages/vault-core/src/utils/archive/zip/index.ts b/packages/vault-core/src/utils/archive/zip/index.ts new file mode 100644 index 0000000000..170a0167bf --- /dev/null +++ b/packages/vault-core/src/utils/archive/zip/index.ts @@ -0,0 +1,37 @@ +import { strToU8, unzipSync, zipSync } from 'fflate'; + +export type ZipInputFile = Uint8Array | string; + +export type ZipNamespace = { + /** Create a zip archive (Uint8Array) from a map of filename->content */ + pack( + files: Record, + options?: { level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }, + ): Promise; + /** Extract a zip archive into a map of filename->bytes */ + unpack(bytes: Uint8Array): Promise>; +}; + +function normalizeFiles( + files: Record, +): Record { + const out: Record = {}; + for (const [name, value] of Object.entries(files)) { + out[name] = typeof value === 'string' ? strToU8(value) : value; + } + return out; +} + +const pack: ZipNamespace['pack'] = async (files, options) => { + const normalized = normalizeFiles(files); + return zipSync(normalized, { level: options?.level ?? 6 }); // default is 6 +}; + +const unpack: ZipNamespace['unpack'] = async (bytes) => { + return unzipSync(bytes); +}; + +export const ZIP = { + pack, + unpack, +} satisfies ZipNamespace; diff --git a/packages/vault-core/src/utils/encoding/gzip/index.spec.ts b/packages/vault-core/src/utils/encoding/gzip/index.spec.ts new file mode 100644 index 0000000000..769a3d83ed --- /dev/null +++ b/packages/vault-core/src/utils/encoding/gzip/index.spec.ts @@ -0,0 +1,54 @@ +// Node.js built-in test runner specs for GZIP utilities +// Run with: node --test (after TypeScript compilation if needed) + +import { describe, it } from 'bun:test'; +import assert from 'node:assert/strict'; +import { GZIP } from './index'; + +const textSample = 'The quick brown fox jumps over the lazy dog'; + +function randomBytes(len: number): Uint8Array { + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) arr[i] = (i * 31 + 17) % 256; // deterministic pattern + return arr; +} + +describe('GZIP.compress/decompress', () => { + it('roundtrips UTF-8 text (bytes path)', async () => { + const gz = await GZIP.encode(textSample); // default bytes output + const outBytes = await GZIP.decode(gz); // default bytes + const decoded = new TextDecoder().decode(outBytes as Uint8Array); + assert.equal(decoded, textSample); + }); + + it('roundtrips binary data', async () => { + const bytes = randomBytes(1024); + const gz = await GZIP.encode(bytes, { level: 9 }); + const raw = await GZIP.decode(gz); + assert.equal(raw.length, bytes.length); + for (let i = 0; i < raw.length; i++) { + if (raw[i] !== bytes[i]) { + assert.fail(`mismatch at index ${i}`); + } + } + }); + it('produces base64 output and decodes back to text', async () => { + const b64 = await GZIP.encode(textSample, { output: 'base64' }); + assert.equal(typeof b64, 'string'); + const decoded = await GZIP.decode(b64 as string, { + inputEncoding: 'base64', + output: 'string', + }); + assert.equal(decoded, textSample); + }); + + it('errors when passing string without base64 flag', async () => { + let threw = false; + try { + await GZIP.decode('not base64 data'); + } catch { + threw = true; + } + assert.ok(threw); + }); +}); diff --git a/packages/vault-core/src/utils/encoding/gzip/index.ts b/packages/vault-core/src/utils/encoding/gzip/index.ts new file mode 100644 index 0000000000..50e9bac9b3 --- /dev/null +++ b/packages/vault-core/src/utils/encoding/gzip/index.ts @@ -0,0 +1,107 @@ +import { gunzipSync, gzipSync, strFromU8, strToU8 } from 'fflate'; + +type Level = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + +export type GzipCompressOptions = { + level?: Level; + output?: 'bytes' | 'base64'; +}; + +export type GzipDecompressOptions = { + inputEncoding?: 'raw' | 'base64'; + output?: 'bytes' | 'string'; +}; + +export type GzipNamespace = { + /** Gzip-compress data (Uint8Array or string) into bytes (default) or base64 string */ + encode: typeof encode; + /** Gzip-decompress data (Uint8Array or base64 string) into bytes (default) or string */ + decode: typeof decode; +}; + +const toUint8 = (data: Uint8Array | string): Uint8Array => + typeof data === 'string' ? strToU8(data) : data; + +const toBase64 = (bytes: Uint8Array): string => { + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + const byte = bytes[i]; + if (byte === undefined) continue; + binary += String.fromCharCode(byte); + } + // btoa is available in browsers; for environments without btoa, a polyfill would be needed upstream. + return btoa(binary); +}; + +const fromBase64 = (b64: string): Uint8Array => { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return arr; +}; + +// Overloaded encode implementation +function encode(data: Uint8Array | string): Promise; +function encode( + data: Uint8Array | string, + options: { output: 'bytes'; level?: Level }, +): Promise; +function encode( + data: Uint8Array | string, + options: { output: 'base64'; level?: Level }, +): Promise; +function encode( + data: Uint8Array | string, + options?: GzipCompressOptions, +): Promise; +async function encode( + data: Uint8Array | string, + options?: GzipCompressOptions, +) { + const raw = gzipSync(toUint8(data), { level: options?.level ?? 6 }); // default is 6 + if (options?.output === 'base64') return toBase64(raw); + return raw; +} + +// Overloaded decode implementation +function decode(data: Uint8Array): Promise; +function decode( + data: Uint8Array, + options: { output: 'bytes'; inputEncoding?: 'raw' | 'base64' }, +): Promise; +function decode( + data: Uint8Array, + options: { output: 'string'; inputEncoding?: 'raw' | 'base64' }, +): Promise; +function decode( + data: string, + options: { inputEncoding: 'base64'; output?: 'bytes' }, +): Promise; +function decode( + data: string, + options: { inputEncoding: 'base64'; output: 'string' }, +): Promise; +function decode( + data: Uint8Array | string, + options?: GzipDecompressOptions, +): Promise; +async function decode( + data: Uint8Array | string, + options?: GzipDecompressOptions, +) { + let bytes: Uint8Array; + if (typeof data === 'string') { + if ((options?.inputEncoding ?? 'raw') !== 'base64') { + throw new Error('String input requires options.inputEncoding = "base64"'); + } + bytes = fromBase64(data); + } else { + bytes = data; + } + const out = gunzipSync(bytes); + if (options?.output === 'string') return strFromU8(out); + return out; +} + +// Export using functions with overloads +export const GZIP = { encode, decode } satisfies GzipNamespace; diff --git a/packages/vault-core/src/utils/encoding/index.ts b/packages/vault-core/src/utils/encoding/index.ts new file mode 100644 index 0000000000..cd6b661872 --- /dev/null +++ b/packages/vault-core/src/utils/encoding/index.ts @@ -0,0 +1 @@ +export * from './gzip'; diff --git a/packages/vault-core/src/utils/format/csv/index.spec.ts b/packages/vault-core/src/utils/format/csv/index.spec.ts new file mode 100644 index 0000000000..000e647b30 --- /dev/null +++ b/packages/vault-core/src/utils/format/csv/index.spec.ts @@ -0,0 +1,157 @@ +import { describe, it } from 'bun:test'; // Run with: node --test dist/... after build +import assert from 'node:assert/strict'; +import { CSV } from './index'; + +describe('csv.CSV.parse', () => { + it('parses with headers (default) into objects', () => { + const input = 'name,age\nAlice,30\nBob,25'; + const result = CSV.parse(input); + assert.equal(result.length, 2); + assert.deepEqual(result[0], { name: 'Alice', age: '30' }); + assert.deepEqual(result[1], { name: 'Bob', age: '25' }); + }); + + it('parses without headers into row arrays', () => { + const input = 'name,age\nAlice,30'; + const result = CSV.parse(input, { headers: false }); + assert.deepEqual(result, [ + ['name', 'age'], + ['Alice', '30'], + ]); + }); + + it('handles quotes, escaped quotes and commas', () => { + const input = 'col1,col2\n"a,b","c""d"'; + const result = CSV.parse(input); + assert.deepEqual(result[0], { col1: 'a,b', col2: 'c"d' }); + }); + + it('parses CRLF line endings and embedded newline inside quoted field', () => { + const input = 'h1,h2\r\n"line1\nline2",value\r\nlast,entry'; + const result = CSV.parse(input); + assert.equal(result.length, 2); + assert.deepEqual(result[0], { h1: 'line1\nline2', h2: 'value' }); + assert.deepEqual(result[1], { h1: 'last', h2: 'entry' }); + }); +}); + +describe('csv.CSV.stringify', () => { + it('stringifies object rows with headers', () => { + const data = [ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 25 }, + ]; + const text = CSV.stringify(data); + const lines = text.split('\n'); + assert.equal(lines[0], 'name,age'); + assert.equal(lines[1], 'Alice,30'); + assert.equal(lines[2], 'Bob,25'); + }); + + it('stringifies raw rows without headers', () => { + const rows = [ + ['name', 'age'], + ['Alice', '30'], + ]; + const text = CSV.stringify(rows, { headers: false }); + assert.equal(text, 'name,age\nAlice,30'); + }); + + it('quotes fields containing delimiter, newline, quotes, or surrounding space', () => { + const rows = [['a,comma', 'multi\nline', '"quoted"', ' spaced ']]; + const text = CSV.stringify(rows, { headers: false }); + assert.equal(text, '"a,comma","multi\nline","""quoted"""," spaced "'); + }); + + it('roundtrips object rows', () => { + const original = [ + { a: '1', b: 'x,y' }, + { a: '2', b: 'z' }, + ]; + const csvText = CSV.stringify(original); + const parsed = CSV.parse(csvText); + assert.deepEqual(parsed, original); + }); +}); + +describe('csv options', () => { + it('parses with custom delimiter ; and headers', () => { + const input = 'name;age\nAlice;30\nBob;25'; + const result = CSV.parse(input, { delimiter: ';' }); + assert.deepEqual(result[0], { name: 'Alice', age: '30' }); + }); + + it('stringifies with custom delimiter ;', () => { + const data = [ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 25 }, + ]; + const text = CSV.stringify(data, { delimiter: ';' }); + assert.equal(text.split('\n')[0], 'name;age'); + }); + + it("parses with custom quote ' and escape '", () => { + const manual = "name,quote\n'alice','say ''hi'''"; // easier to read + const result = CSV.parse(manual, { quote: "'", escape: "'" }); + assert.deepEqual(result[0], { name: 'alice', quote: "say 'hi'" }); + }); + + it('preserves whitespace when trim=false', () => { + const input = 'name,age\n Alice , 30 '; // spaces around fields + const result = CSV.parse(input, { trim: false }); + const first = result[0]; + assert.equal(first?.name, ' Alice '); + assert.equal(first?.age, ' 30 '); + }); + + it('includes empty line when skipEmptyLines=false', () => { + const input = 'a,b\n1,2\n\n3,4'; + const rows = CSV.parse(input, { headers: false, skipEmptyLines: false }); + // rows: header, first, empty, last + assert.equal(rows.length, 4); + assert.deepEqual(rows[2], ['']); // an empty row (single empty field) due to parser logic + }); + + it('skips comment lines with custom comment char ;', () => { + const input = ';ignored line\nname,age\nAlice,30'; + const result = CSV.parse(input, { comment: ';' }); + assert.equal(result.length, 1); // only Alice row + }); + + it('roundtrips with mixed options (custom delimiter & quote)', () => { + const data = [{ path: 'C;\\temp', text: "O'hara" }]; + const csvText = CSV.stringify(data, { + delimiter: ';', + quote: "'", + escape: "'", + }); + const parsed = CSV.parse(csvText, { + delimiter: ';', + quote: "'", + escape: "'", + }); + assert.deepEqual(parsed, [{ path: 'C;\\temp', text: "O'hara" }]); + }); + + it('handles very large field containing delimiters and quotes', () => { + const longSegment = 'segment,'; // contains delimiter + const repeated = `${Array.from({ length: 500 }, () => longSegment).join('')}"tail"`; + const data = [{ big: repeated, other: 'x' }]; + const csvText = CSV.stringify(data); // default options + // Ensure it quoted the first line's big field (starts with header line then quoted field) + const lines = csvText.split('\n'); + assert.ok(lines[1]?.startsWith('"')); + const parsed = CSV.parse(csvText); + assert.equal(parsed[0]?.big, repeated); + assert.equal(parsed[0]?.other, 'x'); + }); + + it('treats comment char inside quotes as literal text', () => { + const input = + '#comment line\nname,remark\nAlice,"#not a comment"\nBob,valid'; + const parsed = CSV.parse(input, { comment: '#' }); + assert.equal(parsed.length, 2); // two data rows + assert.deepEqual(parsed[0], { name: 'Alice', remark: '#not a comment' }); + assert.deepEqual(parsed[1], { name: 'Bob', remark: 'valid' }); + }); +}); diff --git a/packages/vault-core/src/utils/format/csv/index.ts b/packages/vault-core/src/utils/format/csv/index.ts new file mode 100644 index 0000000000..1ec1221027 --- /dev/null +++ b/packages/vault-core/src/utils/format/csv/index.ts @@ -0,0 +1,233 @@ +export type CsvOptions = { + /** Character used to separate values. Default is comma (,). */ + delimiter?: string; + /** Character used to quote values. Default is double quote ("). */ + quote?: string; + /** Character used to escape quotes inside quoted values. Default is double quote (") (e.g. "foo ""bar"" baz"). */ + escape?: string; + /** Whether to trim whitespace around values. Default is true. */ + trim?: boolean; + /** Whether the first row contains headers. Default is true. */ + headers?: boolean; + /** Whether to skip empty lines. Default is true. */ + skipEmptyLines?: boolean; + /** If comments are included, the character used to denote them. Default is #. */ + comment?: string; +}; + +export type CsvNamespace = { + /** + * Parse CSV text. Default (headers omitted or true) returns array of objects (keyed by header row). + * When headers=false, returns a 2D string array of raw rows. + */ + parse = Record>( + text: string, + options?: CsvOptions & { headers?: true }, + ): T[]; + parse(text: string, options: CsvOptions & { headers: false }): string[][]; + parse = Record>( + text: string, + options?: CsvOptions & { headers?: boolean }, + ): T[] | string[][]; + + /** + * Stringify object rows (default, headers omitted or true) or raw 2D string arrays (headers=false). + */ + stringify( + data: Record[], + options?: CsvOptions & { headers?: true }, + ): string; + stringify(data: string[][], options: CsvOptions & { headers: false }): string; + stringify( + data: Record[] | string[][], + options?: CsvOptions & { headers?: boolean }, + ): string; +}; + +const defaultOpts: Required = { + delimiter: ',', + quote: '"', + escape: '"', + trim: true, + headers: true, + skipEmptyLines: true, + comment: '#', +}; + +/** + * Parse a CSV string into objects (default, headers=true) or raw row arrays (headers=false). + */ +function parseCsv = Record>( + input: string, + options?: CsvOptions & { headers?: true }, +): T[]; +function parseCsv( + input: string, + options: CsvOptions & { headers: false }, +): string[][]; +function parseCsv = Record>( + input: string, + options?: CsvOptions & { headers?: boolean }, +): T[] | string[][] { + const opts = { ...defaultOpts, ...options }; + + const rows: string[][] = []; + let current: string[] = []; + let field = ''; + let inQuotes = false; + + const delimiterOpt = opts.delimiter; + const quoteOpt = opts.quote; + const escapeOpt = opts.escape; + + const pushField = () => { + let val = field; + if (opts.trim) val = val.trim(); + current.push(val); + field = ''; + }; + + const pushRow = () => { + if (!(opts.skipEmptyLines && current.length === 1 && current[0] === '')) { + rows.push(current); + } + current = []; + }; + + for (let i = 0; i < input.length; i++) { + const char = input[i]; + const next = input[i + 1]; + + // Handle comments at start of line + if ( + !inQuotes && + char === opts.comment && + (i === 0 || input[i - 1] === '\n' || input[i - 1] === '\r') + ) { + // Skip until end of line + while (i < input.length && input[i] !== '\n') i++; + continue; + } + + if (inQuotes) { + if (char === escapeOpt && next === quoteOpt) { + field += quoteOpt; + i++; // skip escaped quote + } else if (char === quoteOpt) { + inQuotes = false; + } else { + field += char; + } + } else { + if (char === quoteOpt) { + inQuotes = true; + } else if (char === delimiterOpt) { + pushField(); + } else if (char === '\n') { + pushField(); + pushRow(); + } else if (char === '\r') { + // CRLF support + } else { + field += char; + } + } + } + // Flush last field & row + pushField(); + pushRow(); + + // If headers !== false → return array of objects + if (opts.headers !== false && rows.length > 0) { + const [headerRow, ...body] = rows; + return body.map((row) => { + const obj: Record = {}; + for (const [idx, key] of headerRow?.entries() ?? []) { + obj[key] = row[idx] ?? ''; + } + return obj as T; + }); + } + + return rows; +} + +// (Removed conditional utility types in favor of overloads for clearer typing.) + +/** + * Stringify arrays or objects into a CSV string. + */ +// Type guards to discriminate between object row input and 2D string array input. +function isObjectRowArray( + data: Record[] | string[][], +): data is Record[] { + return data.length === 0 || !Array.isArray(data[0]); +} + +function is2DStringArray( + data: Record[] | string[][], +): data is string[][] { + return data.length === 0 || Array.isArray(data[0]); +} + +// Overloads: callers get proper type expectations without needing casts. +function stringifyCsv( + data: Record[], + options?: CsvOptions & { headers?: true }, +): string; +function stringifyCsv( + data: string[][], + options: CsvOptions & { headers: false }, +): string; +function stringifyCsv( + data: Record[] | string[][], + options?: CsvOptions & { headers?: boolean }, +): string { + const opts = { ...defaultOpts, ...options }; + + const rows: string[][] = []; + + // Treat undefined headers as true (consistent with defaults) + if (opts.headers !== false && isObjectRowArray(data) && data.length > 0) { + const firstRow = data[0]; + if (firstRow === undefined) return ''; + const keys = Object.keys(firstRow); + rows.push(keys); + for (const obj of data) { + rows.push(keys.map((k) => String(obj[k] ?? ''))); + } + } else if (is2DStringArray(data)) { + // Either headers explicitly false or user passed raw rows. + for (const row of data) { + // Ensure all values are stringified (in case of accidental non-string entries) + rows.push(row.map((v) => String(v))); + } + } + + const needsQuoting = (val: string) => + val.includes(opts.delimiter) || + val.includes('\n') || + val.includes('\r') || + val.includes(opts.quote) || + /^\s|\s$/.test(val); + + return rows + .map((row) => + row + .map((val) => { + let v = String(val); + if (needsQuoting(v)) { + v = v.replaceAll(opts.quote, opts.escape + opts.quote); + return opts.quote + v + opts.quote; + } + return v; + }) + .join(opts.delimiter), + ) + .join('\n'); +} + +export const CSV = { + parse: parseCsv, + stringify: stringifyCsv, +} satisfies CsvNamespace; diff --git a/packages/vault-core/src/utils/format/index.ts b/packages/vault-core/src/utils/format/index.ts new file mode 100644 index 0000000000..a2956837bd --- /dev/null +++ b/packages/vault-core/src/utils/format/index.ts @@ -0,0 +1,5 @@ +export * from './csv'; +export * from './jsonc'; +export * from './jsonl'; +export * from './markdown'; +export * from './yaml'; diff --git a/packages/vault-core/src/utils/format/jsonc/index.spec.ts b/packages/vault-core/src/utils/format/jsonc/index.spec.ts new file mode 100644 index 0000000000..31ebdbc4a5 --- /dev/null +++ b/packages/vault-core/src/utils/format/jsonc/index.spec.ts @@ -0,0 +1,47 @@ +import { describe, it } from 'bun:test'; +import assert from 'node:assert/strict'; +import { JSONC } from './index'; + +describe('JSONC.parse', () => { + it('parses object with single-line comments', () => { + const input = `{ + // comment about a + "a": 1, + "b": 2 // trailing comment + }`; + const result = JSONC.parse(input); + assert.deepEqual(result, { a: 1, b: 2 }); + }); + + it('parses object with multi-line comments and trailing commas', () => { + const input = `{ + /* block comment */ + "a": 1, + "b": 2, + }`; + const result = JSONC.parse(input); + assert.deepEqual(result, { a: 1, b: 2 }); + }); + + it('applies reviver function', () => { + const input = '{"a":1,"b":2}'; + const result = JSONC.parse(input, (_, v) => + typeof v === 'number' ? v * 10 : v, + ); + assert.deepEqual(result, { a: 10, b: 20 }); + }); +}); + +describe('JSONC.stringify', () => { + it('stringifies object using JSON.stringify behavior', () => { + const obj = { a: 1, b: 'x' }; + const text = JSONC.stringify(obj); + assert.equal(text, JSON.stringify(obj)); + }); + + it('supports replacer and space parameters', () => { + const obj = { a: 1, b: 2 }; + const text = JSONC.stringify(obj, (k, v) => (k === 'b' ? undefined : v), 2); + assert.equal(text, JSON.stringify({ a: 1 }, null, 2)); + }); +}); diff --git a/packages/vault-core/src/utils/format/jsonc/index.ts b/packages/vault-core/src/utils/format/jsonc/index.ts new file mode 100644 index 0000000000..01451882c5 --- /dev/null +++ b/packages/vault-core/src/utils/format/jsonc/index.ts @@ -0,0 +1,37 @@ +export type JsoncNamespace = { + /** + * Minimal JSONC parser/stringifier supporting comments and trailing commas. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse MDN reference} + */ + parse( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown, + ): Record; + /** + * Minimal JSONC stringifier (currently identical to JSON.stringify). + * Does not add comments or trailing commas. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify MDN reference} + */ + stringify( + obj: Record, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + space?: string | number, + ): string; +}; + +const parse: JsoncNamespace['parse'] = (text, reviver) => { + // Simple JSONC parser that removes comments and trailing commas + const noComments = text + .replace(/\/\/.*$/gm, '') // Remove single-line comments + .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments + .replace(/,\s*([}\]])/g, '$1'); // Remove trailing commas + + return JSON.parse(noComments, reviver); +}; + +const stringify: JsoncNamespace['stringify'] = JSON.stringify; + +export const JSONC = { + parse, + stringify, +} satisfies JsoncNamespace; diff --git a/packages/vault-core/src/utils/format/jsonl/index.spec.ts b/packages/vault-core/src/utils/format/jsonl/index.spec.ts new file mode 100644 index 0000000000..40304444bf --- /dev/null +++ b/packages/vault-core/src/utils/format/jsonl/index.spec.ts @@ -0,0 +1,39 @@ +import { describe, it } from 'bun:test'; // Run with: node --test dist/... after build +import assert from 'node:assert/strict'; +import { JSONL } from './index'; + +describe('JSONL.parse', () => { + it('parses multiple lines into objects', () => { + const input = '{"a":1}\n{"b":2}\n'; + const result = JSONL.parse(input); + assert.deepEqual(result, [{ a: 1 }, { b: 2 }]); + }); + + it('returns empty array for blank input', () => { + const result = JSONL.parse(' '); + assert.deepEqual(result, []); + }); + + it('throws with line number on invalid JSON', () => { + const input = '{"a":1}\n{"b":}\n{"c":3}'; + try { + JSONL.parse(input); + assert.fail('Expected error'); + } catch (e) { + assert.ok(e instanceof Error); + assert.match(e.message, /Invalid JSONL at line 2/); + } + }); +}); + +describe('JSONL.stringify', () => { + it('stringifies array of objects to JSONL', () => { + const arr = [{ a: 1 }, { b: 2 }]; + const text = JSONL.stringify(arr); + assert.equal(text, '{"a":1}\n{"b":2}'); + }); + + it('throws on empty array', () => { + assert.throws(() => JSONL.stringify([]), /non-empty array/); + }); +}); diff --git a/packages/vault-core/src/utils/format/jsonl/index.ts b/packages/vault-core/src/utils/format/jsonl/index.ts new file mode 100644 index 0000000000..49e938c6fe --- /dev/null +++ b/packages/vault-core/src/utils/format/jsonl/index.ts @@ -0,0 +1,50 @@ +export type JsonlNamespace = { + /** + * Parses a JSON Lines (JSONL) formatted string into an array of objects. + * Each line should be a valid JSON object. + * @see {@link https://jsonlines.org/ JSON Lines specification} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse MDN reference} + */ + parse(text: string): Record[]; + /** + * Converts an array of objects into a JSON Lines (JSONL) formatted string. + * Each object will be serialized as a JSON string on its own line. + * @see {@link https://jsonlines.org/ JSON Lines specification} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify MDN reference} + */ + stringify(arr: Record[]): string; +}; + +const parse: JsonlNamespace['parse'] = (text) => { + if (!text || text.trim().length === 0) return []; + const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0); + const result = []; + + // Parse each line as JSON, collecting objects and throwing on errors + for (const [i, line] of lines.entries()) { + try { + const obj = JSON.parse(line); + if (obj && typeof obj === 'object' && !Array.isArray(obj)) { + result.push(obj as Record); + } + } catch (error) { + // Re-throw with line number for easier debugging + throw new Error(`Invalid JSONL at line ${i + 1}: ${line}`, { + cause: error, + }); + } + } + return result; +}; + +const stringify: JsonlNamespace['stringify'] = (arr) => { + if (!Array.isArray(arr) || arr.length === 0) + throw new Error('Input must be a non-empty array'); + const lines = arr.map((obj) => JSON.stringify(obj)); + return lines.join('\n'); +}; + +export const JSONL = { + parse, + stringify, +} satisfies JsonlNamespace; diff --git a/packages/vault-core/src/utils/format/markdown/index.spec.ts b/packages/vault-core/src/utils/format/markdown/index.spec.ts new file mode 100644 index 0000000000..3967314b2a --- /dev/null +++ b/packages/vault-core/src/utils/format/markdown/index.spec.ts @@ -0,0 +1,51 @@ +import { describe, it } from 'bun:test'; // Run with: node --test dist/... after build +import assert from 'node:assert/strict'; +import { Markdown } from './index'; + +describe('Markdown.parse', () => { + it('parses body without frontmatter gracefully', () => { + const input = 'Hello world'; + const result = Markdown.parse(input); + assert.deepEqual(result, { body: 'Hello world', frontmatter: {} }); + }); + + it('parses YAML frontmatter and body', () => { + const input = + '---\n' + + 'title: Test Doc\n' + + 'draft: true\n' + + '---\n\n' + + 'Content here.'; + const result = Markdown.parse(input); + assert.equal(result.body.trim(), 'Content here.'); + assert.deepEqual(result.frontmatter, { title: 'Test Doc', draft: true }); + }); +}); + +describe('Markdown.stringify', () => { + it('stringifies with frontmatter', () => { + const obj = { + body: 'Content here.', + frontmatter: { title: 'Test Doc', draft: true }, + }; + const md = Markdown.stringify(obj); + assert.match(md, /---/); + assert.match(md, /title: Test Doc/); + assert.match(md, /draft: true/); + assert.match(md, /Content here\./); + }); + + it('stringifies without frontmatter', () => { + const obj = { body: 'Just content', frontmatter: {} }; + const md = Markdown.stringify(obj); + assert.equal(md, 'Just content'); + }); + + it('roundtrips frontmatter + body', () => { + const original = { body: 'Body text', frontmatter: { a: 1 } }; + const md = Markdown.stringify(original); + const parsed = Markdown.parse(md); + assert.deepEqual(parsed.frontmatter, { a: 1 }); + assert.equal(parsed.body.trim(), 'Body text'); + }); +}); diff --git a/packages/vault-core/src/utils/format/markdown/index.ts b/packages/vault-core/src/utils/format/markdown/index.ts new file mode 100644 index 0000000000..01873be2d5 --- /dev/null +++ b/packages/vault-core/src/utils/format/markdown/index.ts @@ -0,0 +1,59 @@ +import { YAML } from '../yaml'; + +type Result = { body: string; frontmatter?: Record }; + +export type MarkdownNamespace = { + /** + * Minimal Markdown parser that parses a Markdown string into body text & frontmatter. + * Currently supports YAML frontmatter only. + * @see {@link ../yaml/index.ts YAML parser} + */ + parse(text: string): Result; + /** + * Minimal Markdown stringifier that combines body text & frontmatter into a Markdown string. + * Currently supports YAML frontmatter only. + * @see {@link ../yaml/index.ts YAML stringifier} + */ + stringify(data: Result): string; +}; + +const FRONTMATTER_REGEX = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/; + +const parse: MarkdownNamespace['parse'] = (text) => { + if (!text || text.trim().length === 0) return { body: '', frontmatter: {} }; + + let body = text; + let frontmatter: Record = {}; + + const match = text.match(FRONTMATTER_REGEX); + if (match) { + const yamlText = match[1]; + if (yamlText === undefined) return { body: text }; + body = text.slice(match[0].length); + try { + frontmatter = YAML.parse(yamlText); + } catch (error) { + throw new Error('Invalid YAML frontmatter', { cause: error }); + } + } + + return { body, frontmatter }; +}; + +const stringify: MarkdownNamespace['stringify'] = (data) => { + if (!data || typeof data.body !== 'string') + throw new Error('Input must have a body string'); + + let frontmatterText = ''; + if (data.frontmatter && Object.keys(data.frontmatter).length > 0) { + const yamlText = YAML.stringify(data.frontmatter); + frontmatterText = `---\n${yamlText}---\n\n`; + } + + return `${frontmatterText}${data.body}`; +}; + +export const Markdown = { + parse, + stringify, +} satisfies MarkdownNamespace; diff --git a/packages/vault-core/src/utils/format/yaml/index.ts b/packages/vault-core/src/utils/format/yaml/index.ts new file mode 100644 index 0000000000..c9927266ef --- /dev/null +++ b/packages/vault-core/src/utils/format/yaml/index.ts @@ -0,0 +1,29 @@ +import { parse, stringify } from 'yaml'; + +// Wanted to use `bun` but apparently YAML stringification is just... not a thing in most implementations... + +export type YamlNamespace = { + /** + * Compliant YAML parser. Follows YAML 1.2 spec (superset of JSON). + * @see {@link https://yaml.org/spec/1.2.2/ YAML 1.2 specification} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse MDN reference} + */ + parse( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown, + ): Record; + /** + * Compliant YAML stringifier. Follows YAML 1.2 spec (superset of JSON). + * @see {@link https://yaml.org/spec/1.2.2/ YAML 1.2 specification} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify MDN reference} + */ + stringify( + obj: Record, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + ): string; +}; + +export const YAML = { + parse, + stringify, +} satisfies YamlNamespace; From 4c430dd1516a731b7e6ce0b822a379465510dd20 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Sat, 13 Sep 2025 20:28:41 +0000 Subject: [PATCH 10/21] added standard schema validation package --- bun.lock | 2 +- packages/vault-core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index d7512f30ea..c7e8be30c5 100644 --- a/bun.lock +++ b/bun.lock @@ -313,8 +313,8 @@ "name": "@repo/vault-core", "version": "0.0.0", "dependencies": { + "@standard-schema/spec": "^1.0.0", "@vanillaes/csv": "^3.0.4", - "arktype": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", "fflate": "^0.8.2", diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index 77d4262ecd..dc91446f3b 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -18,8 +18,8 @@ "check": "echo \"TODO add this in a later commit\"" }, "dependencies": { + "@standard-schema/spec": "^1.0.0", "@vanillaes/csv": "^3.0.4", - "arktype": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", "fflate": "^0.8.2", From 154ede29cb990b84bc55ffa714c40fb1c982bae6 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Sat, 13 Sep 2025 23:09:43 +0000 Subject: [PATCH 11/21] feat: markdown parser dogfoods yaml parser --- packages/vault-core/src/codecs/markdown.ts | 63 ++-------------------- 1 file changed, 3 insertions(+), 60 deletions(-) diff --git a/packages/vault-core/src/codecs/markdown.ts b/packages/vault-core/src/codecs/markdown.ts index 5af43b3690..27bafabc22 100644 --- a/packages/vault-core/src/codecs/markdown.ts +++ b/packages/vault-core/src/codecs/markdown.ts @@ -1,71 +1,14 @@ import { defineCodec } from '../core/codec'; - -// Very small deterministic Markdown with YAML headers: ---\n\n---\n -// For now, we do not include a YAML lib; caller will provide headers already normalized. -function headersToYaml(headers: Record): string { - const keys = Object.keys(headers).sort(); - const lines = keys.map((k) => `${k}: ${encode(headers[k])}`); - return lines.join('\n'); -} - -function encode(v: unknown): string { - if (v === undefined) return ''; - if (v === null) return 'null'; - if (typeof v === 'string') { - // Quote only when necessary; keep simple for now - // Also quote if the scalar looks like a JSON literal (number/boolean/null) - // to avoid it being parsed as a non-string on re-import. - if ( - /[:\n#\-]/.test(v) || - /^(?:true|false|null)$/i.test(v) || - /^-?\d+(?:\.\d+)?$/.test(v) - ) - return JSON.stringify(v); - return v; - } - return JSON.stringify(v); -} - -function parseYaml(text: string): Record { - // Minimal, non-compliant YAML parser for simple k: v lines (no arrays/nesting) - const lines = text.split(/\r?\n/); - const out: Record = {}; - for (const line of lines) { - const idx = line.indexOf(':'); - if (idx < 0) continue; - const key = line.slice(0, idx).trim(); - const raw = line.slice(idx + 1).trim(); - if (!key) continue; - // Attempt simple JSON parse for numbers/booleans/quoted strings - try { - if (raw === '') out[key] = undefined; - else out[key] = JSON.parse(raw); - } catch { - out[key] = raw; - } - } - return out; -} +import { YAML } from '../utils/format/yaml'; // TODO figure out condition for body prop (name based??) export const markdownFormat = defineCodec({ id: 'markdown', fileExtension: 'md', parse(text) { - const fmMatch = text.match(/^---\n([\s\S]*?)\n---\n?/); - if (!fmMatch) return { body: text }; - const headers = parseYaml(fmMatch[1] ?? ''); - const body = text.slice(fmMatch[0].length); - return { ...headers, body }; + return YAML.parse(text); }, stringify(rec) { - const { body, ...rest } = rec ?? ({} as Record); - const head = headersToYaml(rest); - const bodyText = - typeof body === 'string' ? body : body == null ? '' : String(body); - const sep = head.length ? `---\n${head}\n---\n` : ''; - // Ensure final newline - const out = `${sep}${bodyText}`; - return out.endsWith('\n') ? out : `${out}\n`; + return YAML.stringify(rec); }, }); From b8f1de68c878a0ccb600c158b0bd62f8afe442d2 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Fri, 3 Oct 2025 22:08:16 +0000 Subject: [PATCH 12/21] adapter/core refactor and reddit adapter migration --- ...1003T220750 vault-core-minimal-overview.md | 63 ++ packages/vault-core/src/adapters/index.ts | 2 +- .../vault-core/src/adapters/reddit/README.md | 33 + .../src/adapters/reddit/drizzle.config.ts | 7 + .../vault-core/src/adapters/reddit/index.ts | 2 +- ...te_magdalene.sql => 0000_silent_zaran.sql} | 22 +- .../adapters/reddit/migrations/manifest.ts | 1 + .../reddit/migrations/meta/0000_snapshot.json | 89 +- .../reddit/migrations/meta/_journal.json | 4 +- .../adapters/reddit/migrations/transforms.ts | 9 + .../adapters/reddit/migrations/versions.ts | 284 +++++ .../src/adapters/reddit/src/adapter.ts | 16 +- .../src/adapters/reddit/src/config.ts | 1 + .../src/adapters/reddit/src/csv-parse.d.ts | 18 - .../src/adapters/reddit/src/drizzle.config.ts | 24 - .../src/adapters/reddit/src/importer.ts | 24 - .../src/adapters/reddit/src/ingestor.ts | 15 + .../src/adapters/reddit/src/metadata.ts | 13 +- .../src/adapters/reddit/src/parse.ts | 593 +---------- .../src/adapters/reddit/src/schema.ts | 146 +-- .../src/adapters/reddit/src/types.ts | 12 - .../src/adapters/reddit/src/upsert.ts | 467 --------- .../src/adapters/reddit/src/validation.ts | 192 ++-- packages/vault-core/src/codecs/json.ts | 15 +- packages/vault-core/src/codecs/markdown.ts | 1 + packages/vault-core/src/core/adapter.ts | 389 +++---- packages/vault-core/src/core/codec.ts | 56 +- packages/vault-core/src/core/config.ts | 145 +-- packages/vault-core/src/core/db.ts | 21 + .../src/core/import/importPipeline.ts | 98 ++ .../src/core/import/migrationMetadata.ts | 99 ++ packages/vault-core/src/core/importer.ts | 83 -- packages/vault-core/src/core/index.ts | 4 +- packages/vault-core/src/core/ingestor.ts | 24 + packages/vault-core/src/core/migrations.ts | 979 ++++++++++++++++-- packages/vault-core/src/core/strip.ts | 16 +- packages/vault-core/src/core/vault.ts | 300 ++++++ .../vault-core/src/utils/format/toml/index.ts | 14 + 38 files changed, 2423 insertions(+), 1858 deletions(-) create mode 100644 docs/specs/20251003T220750 vault-core-minimal-overview.md create mode 100644 packages/vault-core/src/adapters/reddit/drizzle.config.ts rename packages/vault-core/src/adapters/reddit/migrations/{0000_polite_magdalene.sql => 0000_silent_zaran.sql} (88%) create mode 100644 packages/vault-core/src/adapters/reddit/migrations/manifest.ts create mode 100644 packages/vault-core/src/adapters/reddit/migrations/transforms.ts create mode 100644 packages/vault-core/src/adapters/reddit/migrations/versions.ts delete mode 100644 packages/vault-core/src/adapters/reddit/src/csv-parse.d.ts delete mode 100644 packages/vault-core/src/adapters/reddit/src/drizzle.config.ts delete mode 100644 packages/vault-core/src/adapters/reddit/src/importer.ts create mode 100644 packages/vault-core/src/adapters/reddit/src/ingestor.ts delete mode 100644 packages/vault-core/src/adapters/reddit/src/types.ts delete mode 100644 packages/vault-core/src/adapters/reddit/src/upsert.ts create mode 100644 packages/vault-core/src/core/db.ts create mode 100644 packages/vault-core/src/core/import/importPipeline.ts create mode 100644 packages/vault-core/src/core/import/migrationMetadata.ts delete mode 100644 packages/vault-core/src/core/importer.ts create mode 100644 packages/vault-core/src/core/ingestor.ts create mode 100644 packages/vault-core/src/core/vault.ts create mode 100644 packages/vault-core/src/utils/format/toml/index.ts diff --git a/docs/specs/20251003T220750 vault-core-minimal-overview.md b/docs/specs/20251003T220750 vault-core-minimal-overview.md new file mode 100644 index 0000000000..abee8676e5 --- /dev/null +++ b/docs/specs/20251003T220750 vault-core-minimal-overview.md @@ -0,0 +1,63 @@ +# Vault Core Minimal Overview + +- Date: 2025-10-03 +- Status: Draft +- Owner: Vault core maintainers + +## Architecture Snapshot + +- **Adapters** expose prefixed Drizzle schema plus: + - `versions`: ordered tuple of `{ tag: '0000', sql: string[] }` + - `transforms`: registry keyed by non-baseline tags + - `validator`: Standard Schema parser for ingest payloads + - `ingestors` (optional): file parsers returning validator-ready payloads + - `metadata`: table/column descriptions (typed via `AdapterMetadata`) +- **Vault** orchestrator (see `packages/vault-core/src/core/vault.ts`) wires adapters into import, ingest, and export flows without owning IO. + +## Migration Workflow (Plan A) + +1. Adapter author runs `drizzle-kit` locally, copies SQL into `versions[n].sql`. +2. `createVault` calls `runStartupSqlMigrations(adapter.id, adapter.versions, db)` before touching tables; SQL arrays replay sequentially and ledger tables are managed automatically. +3. Export pipeline writes `__meta__/migration.json` via `createMigrationMetadataFile`. + +> Plan B (inline diff using Drizzle internals) is commented out but preserved inside `migrations.ts` for future reference. + +## Import Path (Files ➜ DB) + +1. Host collects adapter files + codec (`ImportOptions`). +2. `createVault.importData`: + - Runs migrations + - Rehydrates dataset from codec files (skipping metadata directory) + - Detects source tag from metadata when available +3. `runImportPipeline` selects effective versions/transforms, runs `transformAndValidate`, and accepts optional overrides for tests. +4. A **required** `dataValidator` (drizzle-arktype) morphs + validates the transformed dataset. +5. `replaceAdapterTables` truncates and inserts into each adapter table. + +## Ingest Path (File ➜ DB) + +1. `createVault.ingestData` picks the matching `Ingestor.matches`. +2. `Ingestor.parse` returns the payload. +3. Adapter Standard Schema `validator` is mandatory; morphs value via `runValidation`. +4. `replaceAdapterTables` writes rows (same helper as import). + +## Export Path (DB ➜ Files) + +1. Each adapter table is read via Drizzle. +2. Codec transforms rows (`normalize` / `denormalize`) and writes deterministic file paths using adapter conventions. +3. Migration metadata file is added to the export bundle. + +## Host Responsibilities + +- Supply a Drizzle DB instance (core manages migration ledger tables automatically). +- Pass adapter list to vault (`UniqueAdapterIds` enforces unique IDs). +- Provide codecs for import/export and drizzle-arktype validators for import. +- Offer UI/CLI to run adapter transforms or ingestion pipelines as needed. + +## Key Entry Points + +- [`packages/vault-core/src/core/vault.ts`](packages/vault-core/src/core/vault.ts) +- [`packages/vault-core/src/core/migrations.ts`](packages/vault-core/src/core/migrations.ts) +- [`packages/vault-core/src/core/import/importPipeline.ts`](packages/vault-core/src/core/import/importPipeline.ts) +- [`packages/vault-core/src/core/adapter.ts`](packages/vault-core/src/core/adapter.ts) + +This document is the minimal reference for contributors implementing adapters or host integrations going forward. diff --git a/packages/vault-core/src/adapters/index.ts b/packages/vault-core/src/adapters/index.ts index 4db245e24f..9e653bc150 100644 --- a/packages/vault-core/src/adapters/index.ts +++ b/packages/vault-core/src/adapters/index.ts @@ -1 +1 @@ -export * from './reddit/index'; +export * from './reddit'; diff --git a/packages/vault-core/src/adapters/reddit/README.md b/packages/vault-core/src/adapters/reddit/README.md index 43a211f16c..c29ffc68a6 100644 --- a/packages/vault-core/src/adapters/reddit/README.md +++ b/packages/vault-core/src/adapters/reddit/README.md @@ -18,3 +18,36 @@ To migrate: - `cd` project directory - `bun run migrate` + +## Migrations (Plan A) + +This adapter ships two kinds of migrations: + +1. SQL schema migrations (forward-only; embedded inline) + +- Embedded directly in ./migrations/manifest.ts via redditVersions using "sql" (string[]) or "sqlText" (string) +- No node:fs required; core will split "sqlText" on drizzle "--> statement-breakpoint" markers or on semicolons as a fallback +- Note: legacy .sql files may exist in ./migrations/ for reference, but the manifest's inline SQL is the source of truth + +2. JS data transforms (version-to-version) + +- Location: ./migrations/transforms.ts +- A TransformRegistry keyed by the target tag; each function converts data from the previous version into the current target version +- Typed with defineTransformRegistry and RequiredTransformTags so every forward step is covered + +How hosts run this (no node: imports required in core): + +- Startup SQL migrations (schema) + - Call `runStartupSqlMigrations(adapter.id, adapter.versions, db, reporter)` from `@repo/vault-core` + - Pass the same Drizzle DB that Vault uses; core ensures the ledger tables exist and replays the embedded SQL + +- Data transforms + validation (content) + - Use transformAndValidate(manifest, transforms, dataset, sourceTag, validator?) + - transforms is the TransformRegistry that converts dataset from sourceTag up to manifest.currentTag + - validator is optional; when provided, it should return the morphed value or throw on failure (see redditDataValidator) + +Quick reference + +- Manifest: ./migrations/manifest.ts +- Transforms: ./migrations/transforms.ts +- SQL artifacts: ./migrations/\*.sql diff --git a/packages/vault-core/src/adapters/reddit/drizzle.config.ts b/packages/vault-core/src/adapters/reddit/drizzle.config.ts new file mode 100644 index 0000000000..d86cf3b743 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + dialect: 'sqlite', + schema: './src/schema.ts', + out: './migrations', +}); diff --git a/packages/vault-core/src/adapters/reddit/index.ts b/packages/vault-core/src/adapters/reddit/index.ts index 8420b1093f..511d919a21 100644 --- a/packages/vault-core/src/adapters/reddit/index.ts +++ b/packages/vault-core/src/adapters/reddit/index.ts @@ -1 +1 @@ -export * from './src'; +export { redditAdapter } from './src/adapter'; diff --git a/packages/vault-core/src/adapters/reddit/migrations/0000_polite_magdalene.sql b/packages/vault-core/src/adapters/reddit/migrations/0000_silent_zaran.sql similarity index 88% rename from packages/vault-core/src/adapters/reddit/migrations/0000_polite_magdalene.sql rename to packages/vault-core/src/adapters/reddit/migrations/0000_silent_zaran.sql index 871a7ed0ed..16516567eb 100644 --- a/packages/vault-core/src/adapters/reddit/migrations/0000_polite_magdalene.sql +++ b/packages/vault-core/src/adapters/reddit/migrations/0000_silent_zaran.sql @@ -65,12 +65,10 @@ CREATE TABLE `reddit_comments` ( `id` text PRIMARY KEY NOT NULL, `permalink` text NOT NULL, `date` integer NOT NULL, - `created_utc` integer NOT NULL, `ip` text, `subreddit` text NOT NULL, `gildings` integer, `link` text NOT NULL, - `post_id` text, `parent` text, `body` text, `media` text @@ -99,38 +97,34 @@ CREATE TABLE `reddit_friends` ( ); --> statement-breakpoint CREATE TABLE `reddit_gilded_content` ( - `content_link` text, + `content_link` text PRIMARY KEY NOT NULL, `award` text, `amount` text, `date` integer ); --> statement-breakpoint -CREATE UNIQUE INDEX `reddit_gilded_content_content_award_date_uq` ON `reddit_gilded_content` (`content_link`,`award`,`date`);--> statement-breakpoint CREATE TABLE `reddit_gold_received` ( - `content_link` text, + `content_link` text PRIMARY KEY NOT NULL, `gold_received` text, `gilder_username` text, `date` integer ); --> statement-breakpoint -CREATE UNIQUE INDEX `reddit_gold_received_content_date_uq` ON `reddit_gold_received` (`content_link`,`date`);--> statement-breakpoint CREATE TABLE `reddit_hidden_posts` ( `id` text PRIMARY KEY NOT NULL, `permalink` text NOT NULL ); --> statement-breakpoint CREATE TABLE `reddit_ip_logs` ( - `date` integer, + `date` integer PRIMARY KEY NOT NULL, `ip` text ); --> statement-breakpoint -CREATE UNIQUE INDEX `reddit_ip_logs_date_ip_uq` ON `reddit_ip_logs` (`date`,`ip`);--> statement-breakpoint CREATE TABLE `reddit_linked_identities` ( - `issuer_id` text, - `subject_id` text + `issuer_id` text PRIMARY KEY NOT NULL, + `subject_id` text NOT NULL ); --> statement-breakpoint -CREATE UNIQUE INDEX `reddit_linked_identities_issuer_subject_uq` ON `reddit_linked_identities` (`issuer_id`,`subject_id`);--> statement-breakpoint CREATE TABLE `reddit_linked_phone_number` ( `phone_number` text PRIMARY KEY NOT NULL ); @@ -198,7 +192,7 @@ CREATE TABLE `reddit_multireddits` ( --> statement-breakpoint CREATE TABLE `reddit_payouts` ( `payout_amount_usd` text, - `date` integer, + `date` integer PRIMARY KEY NOT NULL, `payout_id` text ); --> statement-breakpoint @@ -208,7 +202,7 @@ CREATE TABLE `reddit_persona` ( ); --> statement-breakpoint CREATE TABLE `reddit_poll_votes` ( - `post_id` text, + `post_id` text PRIMARY KEY NOT NULL, `user_selection` text, `text` text, `image_url` text, @@ -216,7 +210,6 @@ CREATE TABLE `reddit_poll_votes` ( `stake_amount` text ); --> statement-breakpoint -CREATE UNIQUE INDEX `reddit_poll_votes_post_user_uq` ON `reddit_poll_votes` (`post_id`,`user_selection`);--> statement-breakpoint CREATE TABLE `reddit_post_headers` ( `id` text PRIMARY KEY NOT NULL, `permalink` text NOT NULL, @@ -237,7 +230,6 @@ CREATE TABLE `reddit_posts` ( `id` text PRIMARY KEY NOT NULL, `permalink` text NOT NULL, `date` integer NOT NULL, - `created_utc` integer NOT NULL, `ip` text, `subreddit` text NOT NULL, `gildings` integer, diff --git a/packages/vault-core/src/adapters/reddit/migrations/manifest.ts b/packages/vault-core/src/adapters/reddit/migrations/manifest.ts new file mode 100644 index 0000000000..09494eeae9 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/migrations/manifest.ts @@ -0,0 +1 @@ +export { redditVersions as redditMigrations } from './versions'; diff --git a/packages/vault-core/src/adapters/reddit/migrations/meta/0000_snapshot.json b/packages/vault-core/src/adapters/reddit/migrations/meta/0000_snapshot.json index a0e97dc616..7723658b8b 100644 --- a/packages/vault-core/src/adapters/reddit/migrations/meta/0000_snapshot.json +++ b/packages/vault-core/src/adapters/reddit/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "582cf3d6-8a6e-4223-b2bf-faa81e65cd39", + "id": "fcd987c1-fe5c-40ee-8df4-ad2cbdb10ed3", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "reddit_account_gender": { @@ -383,13 +383,6 @@ "notNull": true, "autoincrement": false }, - "created_utc": { - "name": "created_utc", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "ip": { "name": "ip", "type": "text", @@ -418,13 +411,6 @@ "notNull": true, "autoincrement": false }, - "post_id": { - "name": "post_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, "parent": { "name": "parent", "type": "text", @@ -591,8 +577,8 @@ "content_link": { "name": "content_link", "type": "text", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "award": { @@ -617,13 +603,7 @@ "autoincrement": false } }, - "indexes": { - "reddit_gilded_content_content_award_date_uq": { - "name": "reddit_gilded_content_content_award_date_uq", - "columns": ["content_link", "award", "date"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, @@ -635,8 +615,8 @@ "content_link": { "name": "content_link", "type": "text", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "gold_received": { @@ -661,13 +641,7 @@ "autoincrement": false } }, - "indexes": { - "reddit_gold_received_content_date_uq": { - "name": "reddit_gold_received_content_date_uq", - "columns": ["content_link", "date"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, @@ -703,8 +677,8 @@ "date": { "name": "date", "type": "integer", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "ip": { @@ -715,13 +689,7 @@ "autoincrement": false } }, - "indexes": { - "reddit_ip_logs_date_ip_uq": { - "name": "reddit_ip_logs_date_ip_uq", - "columns": ["date", "ip"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, @@ -733,25 +701,19 @@ "issuer_id": { "name": "issuer_id", "type": "text", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "subject_id": { "name": "subject_id", "type": "text", "primaryKey": false, - "notNull": false, + "notNull": true, "autoincrement": false } }, - "indexes": { - "reddit_linked_identities_issuer_subject_uq": { - "name": "reddit_linked_identities_issuer_subject_uq", - "columns": ["issuer_id", "subject_id"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, @@ -1148,8 +1110,8 @@ "date": { "name": "date", "type": "integer", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "payout_id": { @@ -1195,8 +1157,8 @@ "post_id": { "name": "post_id", "type": "text", - "primaryKey": false, - "notNull": false, + "primaryKey": true, + "notNull": true, "autoincrement": false }, "user_selection": { @@ -1235,13 +1197,7 @@ "autoincrement": false } }, - "indexes": { - "reddit_poll_votes_post_user_uq": { - "name": "reddit_poll_votes_post_user_uq", - "columns": ["post_id", "user_selection"], - "isUnique": true - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, @@ -1361,13 +1317,6 @@ "notNull": true, "autoincrement": false }, - "created_utc": { - "name": "created_utc", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "ip": { "name": "ip", "type": "text", diff --git a/packages/vault-core/src/adapters/reddit/migrations/meta/_journal.json b/packages/vault-core/src/adapters/reddit/migrations/meta/_journal.json index 8bafe11bda..667719c66f 100644 --- a/packages/vault-core/src/adapters/reddit/migrations/meta/_journal.json +++ b/packages/vault-core/src/adapters/reddit/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1755190101550, - "tag": "0000_polite_magdalene", + "when": 1760567304216, + "tag": "0000_silent_zaran", "breakpoints": true } ] diff --git a/packages/vault-core/src/adapters/reddit/migrations/transforms.ts b/packages/vault-core/src/adapters/reddit/migrations/transforms.ts new file mode 100644 index 0000000000..205d7d0c08 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/migrations/transforms.ts @@ -0,0 +1,9 @@ +import { defineTransformRegistry } from '../../../core/migrations'; + +/** + * Reddit transform registry: keyed by target tag. + * 0001: baseline forward step; currently a no-op. Replace with real transforms as schema evolves. + * + * Note: we pass the required tag union to enforce compile-time coverage. + */ +export const redditTransforms = defineTransformRegistry({}); diff --git a/packages/vault-core/src/adapters/reddit/migrations/versions.ts b/packages/vault-core/src/adapters/reddit/migrations/versions.ts new file mode 100644 index 0000000000..b04c9c03c9 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/migrations/versions.ts @@ -0,0 +1,284 @@ +import { defineVersions } from '../../../core/migrations'; + +/** + * Reddit adapter migration versions (Plan A baseline). + * - Integer version tags; sqlId maps to the SQL artifact name. + * - Owned resources enumerate adapter-owned DB objects for reset/export scoping. + * + * We export 'redditVersions' as a readonly tuple so transform registries can derive + * required keys at compile-time. + */ +export const redditVersions = defineVersions({ + tag: '0000', + // Generated by migrations-add script + sql: [ + `CREATE TABLE \`reddit_account_gender\` ( + \`id\` text PRIMARY KEY DEFAULT 'singleton' NOT NULL, + \`account_gender\` text + );`, + `CREATE TABLE \`reddit_announcements\` ( + \`announcement_id\` text PRIMARY KEY NOT NULL, + \`sent_at\` integer, + \`read_at\` integer, + \`from_id\` text, + \`from_username\` text, + \`subject\` text, + \`body\` text, + \`url\` text + );`, + `CREATE TABLE \`reddit_approved_submitter_subreddits\` ( + \`subreddit\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_birthdate\` ( + \`id\` text PRIMARY KEY DEFAULT 'singleton' NOT NULL, + \`birthdate\` integer, + \`verified_birthdate\` integer, + \`verification_state\` text, + \`verification_method\` text + );`, + `CREATE TABLE \`reddit_chat_history\` ( + \`message_id\` text PRIMARY KEY NOT NULL, + \`created_at\` integer, + \`updated_at\` integer, + \`username\` text, + \`message\` text, + \`thread_parent_message_id\` text, + \`channel_url\` text, + \`subreddit\` text, + \`channel_name\` text, + \`conversation_type\` text + );`, + `CREATE TABLE \`reddit_checkfile\` ( + \`filename\` text PRIMARY KEY NOT NULL, + \`sha256\` text + );`, + `CREATE TABLE \`reddit_comment_headers\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`date\` integer NOT NULL, + \`ip\` text, + \`subreddit\` text NOT NULL, + \`gildings\` integer, + \`link\` text NOT NULL, + \`parent\` text + );`, + `CREATE TABLE \`reddit_comment_votes\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`direction\` text NOT NULL + );`, + `CREATE TABLE \`reddit_comments\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`date\` integer NOT NULL, + \`ip\` text, + \`subreddit\` text NOT NULL, + \`gildings\` integer, + \`link\` text NOT NULL, + \`parent\` text, + \`body\` text, + \`media\` text + );`, + `CREATE TABLE \`reddit_drafts\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`title\` text, + \`body\` text, + \`kind\` text, + \`created\` integer, + \`spoiler\` text, + \`nsfw\` text, + \`original_content\` text, + \`content_category\` text, + \`flair_id\` text, + \`flair_text\` text, + \`send_replies\` text, + \`subreddit\` text, + \`is_public_link\` text + );`, + `CREATE TABLE \`reddit_friends\` ( + \`username\` text PRIMARY KEY NOT NULL, + \`note\` text + );`, + `CREATE TABLE \`reddit_gilded_content\` ( + \`content_link\` text PRIMARY KEY NOT NULL, + \`award\` text, + \`amount\` text, + \`date\` integer + );`, + `CREATE TABLE \`reddit_gold_received\` ( + \`content_link\` text PRIMARY KEY NOT NULL, + \`gold_received\` text, + \`gilder_username\` text, + \`date\` integer + );`, + `CREATE TABLE \`reddit_hidden_posts\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL + );`, + `CREATE TABLE \`reddit_ip_logs\` ( + \`date\` integer PRIMARY KEY NOT NULL, + \`ip\` text + );`, + `CREATE TABLE \`reddit_linked_identities\` ( + \`issuer_id\` text PRIMARY KEY NOT NULL, + \`subject_id\` text NOT NULL + );`, + `CREATE TABLE \`reddit_linked_phone_number\` ( + \`phone_number\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_message_headers\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`thread_id\` text, + \`date\` integer, + \`ip\` text, + \`from\` text, + \`to\` text + );`, + `CREATE TABLE \`reddit_messages\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`thread_id\` text, + \`date\` integer, + \`ip\` text, + \`from\` text, + \`to\` text, + \`subject\` text, + \`body\` text + );`, + `CREATE TABLE \`reddit_messages_archive\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`thread_id\` text, + \`date\` integer, + \`ip\` text, + \`from\` text, + \`to\` text, + \`subject\` text, + \`body\` text + );`, + `CREATE TABLE \`reddit_messages_archive_headers\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`thread_id\` text, + \`date\` integer, + \`ip\` text, + \`from\` text, + \`to\` text + );`, + `CREATE TABLE \`reddit_moderated_subreddits\` ( + \`subreddit\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_multireddits\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`display_name\` text, + \`date\` integer, + \`description\` text, + \`privacy\` text, + \`subreddits\` text, + \`image_url\` text, + \`is_owner\` text, + \`favorited\` text, + \`followers\` text + );`, + `CREATE TABLE \`reddit_payouts\` ( + \`payout_amount_usd\` text, + \`date\` integer PRIMARY KEY NOT NULL, + \`payout_id\` text + );`, + 'CREATE UNIQUE INDEX `reddit_payouts_payout_date_uq` ON `reddit_payouts` (`payout_id`,`date`);', + `CREATE TABLE \`reddit_persona\` ( + \`persona_inquiry_id\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_poll_votes\` ( + \`post_id\` text PRIMARY KEY NOT NULL, + \`user_selection\` text, + \`text\` text, + \`image_url\` text, + \`is_prediction\` text, + \`stake_amount\` text + );`, + `CREATE TABLE \`reddit_post_headers\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`date\` integer NOT NULL, + \`ip\` text, + \`subreddit\` text NOT NULL, + \`gildings\` integer, + \`url\` text + );`, + `CREATE TABLE \`reddit_post_votes\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`direction\` text NOT NULL + );`, + `CREATE TABLE \`reddit_posts\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL, + \`date\` integer NOT NULL, + \`ip\` text, + \`subreddit\` text NOT NULL, + \`gildings\` integer, + \`title\` text, + \`url\` text, + \`body\` text + );`, + `CREATE TABLE \`reddit_purchases\` ( + \`processor\` text, + \`transaction_id\` text PRIMARY KEY NOT NULL, + \`product\` text, + \`date\` integer, + \`cost\` text, + \`currency\` text, + \`status\` text + );`, + `CREATE TABLE \`reddit_saved_comments\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL + );`, + `CREATE TABLE \`reddit_saved_posts\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`permalink\` text NOT NULL + );`, + `CREATE TABLE \`reddit_scheduled_posts\` ( + \`scheduled_post_id\` text PRIMARY KEY NOT NULL, + \`subreddit\` text, + \`title\` text, + \`body\` text, + \`url\` text, + \`submission_time\` integer, + \`recurrence\` text + );`, + `CREATE TABLE \`reddit_sensitive_ads_preferences\` ( + \`type\` text PRIMARY KEY NOT NULL, + \`preference\` text + );`, + `CREATE TABLE \`reddit_statistics\` ( + \`statistic\` text PRIMARY KEY NOT NULL, + \`value\` text + );`, + `CREATE TABLE \`reddit_stripe\` ( + \`stripe_account_id\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_subscribed_subreddits\` ( + \`subreddit\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_subscriptions\` ( + \`processor\` text, + \`subscription_id\` text PRIMARY KEY NOT NULL, + \`product\` text, + \`product_id\` text, + \`product_name\` text, + \`status\` text, + \`start_date\` integer, + \`end_date\` integer + );`, + `CREATE TABLE \`reddit_twitter\` ( + \`username\` text PRIMARY KEY NOT NULL + );`, + `CREATE TABLE \`reddit_user_preferences\` ( + \`preference\` text PRIMARY KEY NOT NULL, + \`value\` text + );`, + ], +}); diff --git a/packages/vault-core/src/adapters/reddit/src/adapter.ts b/packages/vault-core/src/adapters/reddit/src/adapter.ts index c8187eb062..417e3e40aa 100644 --- a/packages/vault-core/src/adapters/reddit/src/adapter.ts +++ b/packages/vault-core/src/adapters/reddit/src/adapter.ts @@ -1,8 +1,20 @@ import { defineAdapter } from '@repo/vault-core'; +import { redditTransforms } from '../migrations/transforms'; +import { redditVersions } from '../migrations/versions'; +import type { RedditAdapterConfig } from './config'; +import { redditZipIngestor } from './ingestor'; +import { metadata } from './metadata'; import * as schema from './schema'; +import { parseSchema } from './validation'; -// Minimal, browser-usable adapter (no Node-only imports here) -export const redditAdapter = defineAdapter(() => ({ +// Unified Reddit adapter wired for core-orchestrated validation and ingestion. +// Tag alignment between versions and transforms is enforced by core defineAdapter typing. +export const redditAdapter = defineAdapter((_?: RedditAdapterConfig) => ({ id: 'reddit', schema, + metadata, + validator: parseSchema, + ingestors: [redditZipIngestor], + versions: redditVersions, + transforms: redditTransforms, })); diff --git a/packages/vault-core/src/adapters/reddit/src/config.ts b/packages/vault-core/src/adapters/reddit/src/config.ts index 9c950b62f2..97062edfb3 100644 --- a/packages/vault-core/src/adapters/reddit/src/config.ts +++ b/packages/vault-core/src/adapters/reddit/src/config.ts @@ -1,4 +1,5 @@ // biome-ignore lint/complexity/noBannedTypes: nothing to configure yet export type RedditAdapterConfig = {} | undefined; +// biome-ignore lint/correctness/noUnusedVariables: Future config options const DEFAULT_CONFIG = {} satisfies Exclude; diff --git a/packages/vault-core/src/adapters/reddit/src/csv-parse.d.ts b/packages/vault-core/src/adapters/reddit/src/csv-parse.d.ts deleted file mode 100644 index 5ca7dae8a5..0000000000 --- a/packages/vault-core/src/adapters/reddit/src/csv-parse.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare module 'csv-parse' { - export type Options = { - columns?: boolean | string[]; - bom?: boolean; - skip_empty_lines?: boolean; - relax_column_count?: boolean; - trim?: boolean; - }; - - export function parse( - input: string, - options: Options, - callback: ( - err: unknown | null, - records: Record[], - ) => void, - ): void; -} diff --git a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts b/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts deleted file mode 100644 index fe2fcd3a6d..0000000000 --- a/packages/vault-core/src/adapters/reddit/src/drizzle.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from 'drizzle-kit'; -import { fileURLToPath } from 'node:url'; - -// Resolve paths relative to this module so they work regardless of process CWD -// Migrations live at the adapter root (../migrations), not inside src/ -// TODO custom migration format/process that doesn't rely on node:fs (??) -const out = fileURLToPath(new URL('../migrations', import.meta.url)); -const schema = fileURLToPath(new URL('./schema.ts', import.meta.url)) as string; - -export default defineConfig({ - // Using sqlite dialect; schema is in this package - dialect: 'sqlite', - casing: 'snake_case', - strict: true, - out, - - // Use absolute schema path for CLI compatibility as well - schema, - - // Every adapter *must* have a unique migrations table name, in order for everything to play nicely with other adapters - migrations: { - table: 'reddit_migrations', - }, -}); diff --git a/packages/vault-core/src/adapters/reddit/src/importer.ts b/packages/vault-core/src/adapters/reddit/src/importer.ts deleted file mode 100644 index dd913523e3..0000000000 --- a/packages/vault-core/src/adapters/reddit/src/importer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type ColumnDescriptions, defineImporter } from '@repo/vault-core'; -import { redditAdapter } from './adapter'; -import type { RedditAdapterConfig } from './config'; -import drizzleConfig from './drizzle.config'; -import { metadata } from './metadata'; -import { parseRedditExport } from './parse'; -import { upsertRedditData } from './upsert'; -import { parseSchema } from './validation'; - -export type { ParsedRedditExport, ParseResult } from './types'; - -import type * as schema from './schema'; - -// Node-only Importer export, composed from the web-safe adapter + node parts -export const redditImporter = (args?: RedditAdapterConfig) => - defineImporter(redditAdapter(), { - name: 'Reddit', - // TODO: fill out remaining tables; cast for now to satisfy type - metadata: metadata as unknown as ColumnDescriptions, - validator: parseSchema, - drizzleConfig, - parse: parseRedditExport, - upsert: upsertRedditData, - }); diff --git a/packages/vault-core/src/adapters/reddit/src/ingestor.ts b/packages/vault-core/src/adapters/reddit/src/ingestor.ts new file mode 100644 index 0000000000..e47e8ae6d0 --- /dev/null +++ b/packages/vault-core/src/adapters/reddit/src/ingestor.ts @@ -0,0 +1,15 @@ +import { defineIngestor } from '@repo/vault-core'; +import { parseRedditExport as parse } from './parse'; + +/** + * ZIP ingestor for Reddit GDPR export. + * - Matches a single .zip File + * - Parses via existing parseRedditExport (Blob-compatible) + * - Returns normalized payload ready for validation/upsert + */ +export const redditZipIngestor = defineIngestor({ + matches(file) { + return /\.zip$/i.test(file.name ?? ''); + }, + parse, +}); diff --git a/packages/vault-core/src/adapters/reddit/src/metadata.ts b/packages/vault-core/src/adapters/reddit/src/metadata.ts index a6410209eb..952c30f58b 100644 --- a/packages/vault-core/src/adapters/reddit/src/metadata.ts +++ b/packages/vault-core/src/adapters/reddit/src/metadata.ts @@ -1,9 +1,11 @@ +import type { AdapterMetadata } from '@repo/vault-core'; +import type * as schema from './schema'; + export const metadata = { reddit_posts: { id: 'Reddit post id (base36)', permalink: 'Full permalink URL to the post', - date: 'Original timestamp string from export (e.g. 2025-05-18 04:35:32 UTC)', - created_utc: 'Unix epoch seconds derived from date', + date: 'Timestamp (UTC). Coerced to Date from export string/epoch', ip: 'Recorded IP address associated with the post event, if present', subreddit: 'Subreddit name where the post was made (e.g. sveltejs)', gildings: 'Number of gildings on the post (integer)', @@ -14,17 +16,14 @@ export const metadata = { reddit_comments: { id: 'Reddit comment id (base36)', permalink: 'Full permalink URL to the comment', - date: 'Original timestamp string from export (e.g. 2025-05-18 04:35:32 UTC)', - created_utc: 'Unix epoch seconds derived from date', + date: 'Timestamp (UTC). Coerced to Date from export string/epoch', ip: 'Recorded IP address associated with the comment event, if present', subreddit: 'Subreddit name where the comment was made', gildings: 'Number of gildings on the comment (integer)', link: 'Permalink URL to the parent post of this comment (CSV “link” field)', - post_id: - 'Derived base36 id of the parent post when extractable from link/permalink; NULL otherwise', parent: 'CSV “parent” field; thing id of parent post or comment when present', body: 'Comment body text', media: 'Media info field from CSV when present', }, -} as const; +} satisfies AdapterMetadata; diff --git a/packages/vault-core/src/adapters/reddit/src/parse.ts b/packages/vault-core/src/adapters/reddit/src/parse.ts index 328fa5f12e..78803ac6c5 100644 --- a/packages/vault-core/src/adapters/reddit/src/parse.ts +++ b/packages/vault-core/src/adapters/reddit/src/parse.ts @@ -1,552 +1,67 @@ -import { parse as csvParse } from 'csv-parse'; -import { unzipSync } from 'fflate'; -import type { ParsedRedditExport } from './index'; +import { ZIP } from '../../../utils/archive/zip'; +import { CSV } from '../../../utils/format/csv'; -/** - * CSV record shape directly from export files - * We keep fields loose (string | undefined) and coerce downstream. - */ -type RawRecord = Record; - -// Result type comes from ArkType schema in index.ts -// We return a ParsedRedditExport object, matching parseSchema. - -export async function parseRedditExport( - file: Blob, -): Promise { +export async function parseRedditExport(file: Blob) { // Read entire zip as Uint8Array const ab = await file.arrayBuffer(); - const zipMap = unzipSync(new Uint8Array(ab)); // { [filename]: Uint8Array } + const zipMap = await ZIP.unpack(new Uint8Array(ab)); // { [filename]: Uint8Array } // Read+parse helpers const decode = (bytes: Uint8Array) => new TextDecoder('utf-8', { fatal: false, ignoreBOM: true }).decode(bytes); - const readCsvText = (name: string): string => { + const readCsv = async (name: string) => { const bytes = zipMap[name]; - return bytes ? decode(bytes) : ''; + const csvText = bytes ? decode(bytes) : ''; + return CSV.parse(csvText); }; - const readCsv = async (name: string): Promise => - parseCsv(readCsvText(name)); - - // Parse CSVs used today - const postsRecords = await readCsv('posts.csv'); - const commentsRecords = await readCsv('comments.csv'); - - // Map to normalized shapes with coercions/derivations - const posts = postsRecords - .map((r) => mapPost(r)) - .filter((p) => p !== undefined); - - const comments = commentsRecords - .map((r) => mapComment(r)) - .filter((c) => c !== undefined); - - // Lightly mapped datasets (string fields only; optional everywhere) - const post_headers = (await readCsv('post_headers.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - date: toDate(u(r.date)), - ip: u(r.ip), - subreddit: u(r.subreddit), - gildings: numOrUndefined(r.gildings), - url: u(r.url), - })); - - const comment_headers = (await readCsv('comment_headers.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - date: toDate(u(r.date)), - ip: u(r.ip), - subreddit: u(r.subreddit), - gildings: numOrUndefined(r.gildings), - link: u(r.link), - parent: u(r.parent), - })); - - const post_votes = (await readCsv('post_votes.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - direction: u(r.direction), - })); - - const comment_votes = (await readCsv('comment_votes.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - direction: u(r.direction), - })); - - const saved_posts = (await readCsv('saved_posts.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - })); - - const saved_comments = (await readCsv('saved_comments.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - })); - - const hidden_posts = (await readCsv('hidden_posts.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - })); - - const message_headers = (await readCsv('message_headers.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - thread_id: u(r.thread_id), - date: toDate(u(r.date)), - ip: u(r.ip), - from: u(r.from), - to: u(r.to), - })); - - const messages = (await readCsv('messages.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - thread_id: u(r.thread_id), - date: toDate(u(r.date)), - ip: u(r.ip), - from: u(r.from), - to: u(r.to), - subject: u(r.subject), - body: u(r.body), - })); - - const messages_archive_headers = ( - await readCsv('messages_archive_headers.csv') - ).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - thread_id: u(r.thread_id), - date: toDate(u(r.date)), - ip: u(r.ip), - from: u(r.from), - to: u(r.to), - })); - - const messages_archive = (await readCsv('messages_archive.csv')).map((r) => ({ - id: u(r.id), - permalink: u(r.permalink), - thread_id: u(r.thread_id), - date: toDate(u(r.date)), - ip: u(r.ip), - from: u(r.from), - to: u(r.to), - subject: u(r.subject), - body: u(r.body), - })); - - const chat_history = (await readCsv('chat_history.csv')).map((r) => ({ - message_id: u(r.message_id), - created_at: toDate(u(r.created_at)), - updated_at: toDate(u(r.updated_at)), - username: u(r.username), - message: u(r.message), - thread_parent_message_id: u(r.thread_parent_message_id), - channel_url: u(r.channel_url), - subreddit: u(r.subreddit), - channel_name: u(r.channel_name), - conversation_type: u(r.conversation_type), - })); - - const account_gender = (await readCsv('account_gender.csv')).map((r) => ({ - account_gender: u(r.account_gender), - })); - - const sensitive_ads_preferences = ( - await readCsv('sensitive_ads_preferences.csv') - ).map((r) => ({ - type: u(r.type), - preference: u(r.preference), - })); - - const birthdate = (await readCsv('birthdate.csv')).map((r) => ({ - birthdate: toDate(u(r.birthdate)), - verified_birthdate: toDate(u(r.verified_birthdate)), - verification_state: u(r.verification_state), - verification_method: u(r.verification_method), - })); - - const user_preferences = (await readCsv('user_preferences.csv')).map((r) => ({ - preference: u(r.preference), - value: u(r.value), - })); - - const linked_identities = (await readCsv('linked_identities.csv')).map( - (r) => ({ - issuer_id: u(r.issuer_id), - subject_id: u(r.subject_id), - }), - ); - - const linked_phone_number = (await readCsv('linked_phone_number.csv')).map( - (r) => ({ - phone_number: u(r.phone_number), - }), - ); - - const twitter = (await readCsv('twitter.csv')).map((r) => ({ - username: u(r.username), - })); - - const approved_submitter_subreddits = ( - await readCsv('approved_submitter_subreddits.csv') - ).map((r) => ({ - subreddit: u(r.subreddit), - })); - - const moderated_subreddits = (await readCsv('moderated_subreddits.csv')).map( - (r) => ({ - subreddit: u(r.subreddit), - }), + const files = [ + 'posts', + 'comments', + 'post_headers', + 'comment_headers', + 'post_votes', + 'comment_votes', + 'saved_posts', + 'saved_comments', + 'hidden_posts', + 'message_headers', + 'messages', + 'messages_archive_headers', + 'messages_archive', + 'chat_history', + 'account_gender', + 'sensitive_ads_preferences', + 'birthdate', + 'user_preferences', + 'linked_identities', + 'linked_phone_number', + 'twitter', + 'approved_submitter_subreddits', + 'moderated_subreddits', + 'subscribed_subreddits', + 'multireddits', + 'purchases', + 'subscriptions', + 'payouts', + 'stripe', + 'announcements', + 'drafts', + 'friends', + 'gilded_content', + 'gold_received', + 'ip_logs', + 'persona', + 'poll_votes', + 'scheduled_posts', + 'statistics', + 'checkfile', + ] as const; + + return Object.fromEntries[]>( + await Promise.all( + files.map(async (f) => [f, await readCsv(`${f}.csv`)] as const), + ), ); - - const subscribed_subreddits = ( - await readCsv('subscribed_subreddits.csv') - ).map((r) => ({ - subreddit: u(r.subreddit), - })); - - const multireddits = (await readCsv('multireddits.csv')).map((r) => ({ - id: u(r.id), - display_name: u(r.display_name), - date: toDate(u(r.date)), - description: u(r.description), - privacy: u(r.privacy), - subreddits: u(r.subreddits), - image_url: u(r.image_url), - is_owner: u(r.is_owner), - favorited: u(r.favorited), - followers: u(r.followers), - })); - - const purchases = (await readCsv('purchases.csv')).map((r) => ({ - processor: u(r.processor), - transaction_id: u(r.transaction_id), - product: u(r.product), - date: toDate(u(r.date)), - cost: u(r.cost), - currency: u(r.currency), - status: u(r.status), - })); - - const subscriptions = (await readCsv('subscriptions.csv')).map((r) => ({ - processor: u(r.processor), - subscription_id: u(r.subscription_id), - product: u(r.product), - product_id: u(r.product_id), - product_name: u(r.product_name), - status: u(r.status), - start_date: toDate(u(r.start_date)), - end_date: toDate(u(r.end_date)), - })); - - const payouts = (await readCsv('payouts.csv')).map((r) => ({ - payout_amount_usd: u(r.payout_amount_usd), - date: toDate(u(r.date)), - payout_id: u(r.payout_id), - })); - - const stripe = (await readCsv('stripe.csv')).map((r) => ({ - stripe_account_id: u(r.stripe_account_id), - })); - - const announcements = (await readCsv('announcements.csv')).map((r) => ({ - announcement_id: u(r.announcement_id), - sent_at: toDate(u(r.sent_at)), - read_at: toDate(u(r.read_at)), - from_id: u(r.from_id), - from_username: u(r.from_username), - subject: u(r.subject), - body: u(r.body), - url: u(r.url), - })); - - const drafts = (await readCsv('drafts.csv')).map((r) => ({ - id: u(r.id), - title: u(r.title), - body: u(r.body), - kind: u(r.kind), - created: toDate(u(r.created)), - spoiler: u(r.spoiler), - nsfw: u(r.nsfw), - original_content: u(r.original_content), - content_category: u(r.content_category), - flair_id: u(r.flair_id), - flair_text: u(r.flair_text), - send_replies: u(r.send_replies), - subreddit: u(r.subreddit), - is_public_link: u(r.is_public_link), - })); - - const friends = (await readCsv('friends.csv')).map((r) => ({ - username: u(r.username), - note: u(r.note), - })); - - const gilded_content = (await readCsv('gilded_content.csv')).map((r) => ({ - content_link: u(r.content_link), - award: u(r.award), - amount: u(r.amount), - date: toDate(u(r.date)), - })); - - const gold_received = (await readCsv('gold_received.csv')).map((r) => ({ - content_link: u(r.content_link), - gold_received: u(r.gold_received), - gilder_username: u(r.gilder_username), - date: toDate(u(r.date)), - })); - - const ip_logs = (await readCsv('ip_logs.csv')).map((r) => ({ - date: toDate(u(r.date)), - ip: u(r.ip), - })); - - const persona = (await readCsv('persona.csv')).map((r) => ({ - persona_inquiry_id: u(r.persona_inquiry_id), - })); - - const poll_votes = (await readCsv('poll_votes.csv')).map((r) => ({ - post_id: u(r.post_id), - user_selection: u(r.user_selection), - text: u(r.text), - image_url: u(r.image_url), - is_prediction: u(r.is_prediction), - stake_amount: u(r.stake_amount), - })); - - const scheduled_posts = (await readCsv('scheduled_posts.csv')).map((r) => ({ - scheduled_post_id: u(r.scheduled_post_id), - subreddit: u(r.subreddit), - title: u(r.title), - body: u(r.body), - url: u(r.url), - submission_time: toDate(u(r.submission_time)), - recurrence: u(r.recurrence), - })); - - const statistics = (await readCsv('statistics.csv')).map((r) => ({ - statistic: u(r.statistic), - value: u(r.value), - })); - - const checkfile = (await readCsv('checkfile.csv')).map((r) => ({ - filename: u(r.filename), - sha256: u(r.sha256), - })); - - return { - // Core content - posts: dropEmptyRows(posts), - post_headers: dropEmptyRows(post_headers), - comments: dropEmptyRows(comments), - comment_headers: dropEmptyRows(comment_headers), - - // Votes / visibility / saves - post_votes: dropEmptyRows(post_votes), - comment_votes: dropEmptyRows(comment_votes), - saved_posts: dropEmptyRows(saved_posts), - saved_comments: dropEmptyRows(saved_comments), - hidden_posts: dropEmptyRows(hidden_posts), - - // Messaging - message_headers: dropEmptyRows(message_headers), - messages: dropEmptyRows(messages), - messages_archive_headers: dropEmptyRows(messages_archive_headers), - messages_archive: dropEmptyRows(messages_archive), - - // Chat - chat_history: dropEmptyRows(chat_history), - - // Account and preferences - account_gender: dropEmptyRows(account_gender), - sensitive_ads_preferences: dropEmptyRows(sensitive_ads_preferences), - birthdate: dropEmptyRows(birthdate), - user_preferences: dropEmptyRows(user_preferences), - linked_identities: dropEmptyRows(linked_identities), - linked_phone_number: dropEmptyRows(linked_phone_number), - twitter: dropEmptyRows(twitter), - - // Moderation / subscriptions / subreddits - approved_submitter_subreddits: dropEmptyRows(approved_submitter_subreddits), - moderated_subreddits: dropEmptyRows(moderated_subreddits), - subscribed_subreddits: dropEmptyRows(subscribed_subreddits), - multireddits: dropEmptyRows(multireddits), - - // Commerce and payouts - purchases: dropEmptyRows(purchases), - subscriptions: dropEmptyRows(subscriptions), - payouts: dropEmptyRows(payouts), - stripe: dropEmptyRows(stripe), - - // Misc - announcements: dropEmptyRows(announcements), - drafts: dropEmptyRows(drafts), - friends: dropEmptyRows(friends), - gilded_content: dropEmptyRows(gilded_content), - gold_received: dropEmptyRows(gold_received), - ip_logs: dropEmptyRows(ip_logs), - persona: dropEmptyRows(persona), - poll_votes: dropEmptyRows(poll_votes), - scheduled_posts: dropEmptyRows(scheduled_posts), - statistics: dropEmptyRows(statistics), - checkfile: dropEmptyRows(checkfile), - }; -} - -/** - * csv-parse promise wrapper for convenience - */ -function parseCsv(input: string): Promise { - return new Promise((resolve, reject) => { - if (!input || input.trim().length === 0) return resolve([]); - csvParse( - input, - { - columns: true, - bom: true, - skip_empty_lines: true, - relax_column_count: true, - trim: true, - }, - (err: unknown | null, records: RawRecord[]) => { - if (err) reject(err); - else resolve(records); - }, - ); - }); -} - -/** - * Coercion helpers - */ -function u(v: string | undefined): string | undefined { - return blankToUndefined(v); -} -function blankToUndefined(v: string | undefined): string | undefined { - if (v == null) return undefined; - const t = v.trim(); - return t === '' ? undefined : t; -} -function numOrUndefined(v: string | undefined): number | undefined { - const t = blankToUndefined(v); - if (t == null) return undefined; - const n = Number(t); - return Number.isFinite(n) ? n : undefined; -} -function toDate(dateStr: string | undefined): Date | undefined { - const s = blankToUndefined(dateStr); - if (!s) return undefined; - const num = Number(s); - if (Number.isFinite(num)) { - const ms = num < 1e12 ? num * 1000 : num; - const d = new Date(ms); - return Number.isNaN(d.getTime()) ? undefined : d; - } - const d = new Date(s); - return Number.isNaN(d.getTime()) ? undefined : d; -} -function extractPostIdFromUrl(urlStr: string | undefined): string | undefined { - if (!urlStr) return undefined; - try { - const u = new URL(urlStr); - // Example: /r/sveltejs/comments/1kp9tv3/transitions.../mswmz2d/ - const parts = u.pathname.split('/').filter(Boolean); - const idx = parts.indexOf('comments'); - if (idx >= 0 && parts.length > idx + 1) { - const candidate = parts[idx + 1]?.trim(); - if (candidate) return candidate; - } - } catch { - // non-URL or malformed; ignore - } - return undefined; -} - -/** - * Filter helpers to remove rows that are entirely empty after coercion - * (i.e., every property is undefined or null). This protects downstream upserts - * from generating "No values to set" when a CSV contains a single blank row. - */ -function isNonEmptyRow(obj: Record): boolean { - for (const v of Object.values(obj)) { - if (v !== undefined && v !== null) return true; - } - return false; -} -function dropEmptyRows>(rows: T[]): T[] { - return rows.filter((r) => isNonEmptyRow(r as Record)); -} - -/** - * Mapping: posts.csv - * Headers: id,permalink,date,ip,subreddit,gildings,title,url,body - */ -function mapPost( - r: RawRecord, -): ParsedRedditExport['posts'][number] | undefined { - const id = u(r.id); - const permalink = u(r.permalink); - const dateStr = u(r.date); - const subreddit = u(r.subreddit); - - if (!id || !permalink || !dateStr || !subreddit) return undefined; - - const date = toDate(dateStr); - if (date == null) return undefined; - - const created_utc = date; - - return { - id, - permalink, - date, - created_utc, - ip: u(r.ip), - subreddit, - gildings: numOrUndefined(r.gildings), - title: u(r.title), - url: u(r.url), - body: u(r.body), - }; -} - -/** - * Mapping: comments.csv - * Headers: id,permalink,date,ip,subreddit,gildings,link,parent,body,media - */ -function mapComment( - r: RawRecord, -): ParsedRedditExport['comments'][number] | undefined { - const id = u(r.id); - const permalink = u(r.permalink); - const dateStr = u(r.date); - const subreddit = u(r.subreddit); - const link = u(r.link); - - if (!id || !permalink || !dateStr || !subreddit || !link) return undefined; - - const date = toDate(dateStr); - if (date == null) return undefined; - - const created_utc = date; - const post_id = extractPostIdFromUrl(link) ?? extractPostIdFromUrl(permalink); - - return { - id, - permalink, - date, - created_utc, - ip: u(r.ip), - subreddit, - gildings: numOrUndefined(r.gildings), - link, - post_id, - parent: u(r.parent), - body: u(r.body), - media: u(r.media), - }; } diff --git a/packages/vault-core/src/adapters/reddit/src/schema.ts b/packages/vault-core/src/adapters/reddit/src/schema.ts index 91b8c199b2..d18f45eb26 100644 --- a/packages/vault-core/src/adapters/reddit/src/schema.ts +++ b/packages/vault-core/src/adapters/reddit/src/schema.ts @@ -9,10 +9,9 @@ import { * Core content tables */ export const reddit_posts = sqliteTable('reddit_posts', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), date: integer('date', { mode: 'timestamp' }).notNull(), - created_utc: integer('created_utc', { mode: 'timestamp' }).notNull(), ip: text('ip'), subreddit: text('subreddit').notNull(), gildings: integer('gildings'), @@ -22,7 +21,7 @@ export const reddit_posts = sqliteTable('reddit_posts', { }); export const reddit_post_headers = sqliteTable('reddit_post_headers', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), date: integer('date', { mode: 'timestamp' }).notNull(), ip: text('ip'), @@ -32,22 +31,20 @@ export const reddit_post_headers = sqliteTable('reddit_post_headers', { }); export const reddit_comments = sqliteTable('reddit_comments', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), date: integer('date', { mode: 'timestamp' }).notNull(), - created_utc: integer('created_utc', { mode: 'timestamp' }).notNull(), ip: text('ip'), subreddit: text('subreddit').notNull(), gildings: integer('gildings'), link: text('link').notNull(), - post_id: text('post_id'), parent: text('parent'), body: text('body'), media: text('media'), }); export const reddit_comment_headers = sqliteTable('reddit_comment_headers', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), date: integer('date', { mode: 'timestamp' }).notNull(), ip: text('ip'), @@ -61,29 +58,29 @@ export const reddit_comment_headers = sqliteTable('reddit_comment_headers', { * Votes, saves, visibility */ export const reddit_post_votes = sqliteTable('reddit_post_votes', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), direction: text('direction').notNull(), // up/down/none }); export const reddit_comment_votes = sqliteTable('reddit_comment_votes', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), direction: text('direction').notNull(), }); export const reddit_saved_posts = sqliteTable('reddit_saved_posts', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), }); export const reddit_saved_comments = sqliteTable('reddit_saved_comments', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), }); export const reddit_hidden_posts = sqliteTable('reddit_hidden_posts', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), }); @@ -91,7 +88,7 @@ export const reddit_hidden_posts = sqliteTable('reddit_hidden_posts', { * Messaging */ export const reddit_message_headers = sqliteTable('reddit_message_headers', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), thread_id: text('thread_id'), date: integer('date', { mode: 'timestamp' }), @@ -101,7 +98,7 @@ export const reddit_message_headers = sqliteTable('reddit_message_headers', { }); export const reddit_messages = sqliteTable('reddit_messages', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), thread_id: text('thread_id'), date: integer('date', { mode: 'timestamp' }), @@ -115,7 +112,7 @@ export const reddit_messages = sqliteTable('reddit_messages', { export const reddit_messages_archive_headers = sqliteTable( 'reddit_messages_archive_headers', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), thread_id: text('thread_id'), date: integer('date', { mode: 'timestamp' }), @@ -126,7 +123,7 @@ export const reddit_messages_archive_headers = sqliteTable( ); export const reddit_messages_archive = sqliteTable('reddit_messages_archive', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), permalink: text('permalink').notNull(), thread_id: text('thread_id'), date: integer('date', { mode: 'timestamp' }), @@ -141,7 +138,7 @@ export const reddit_messages_archive = sqliteTable('reddit_messages_archive', { * Chat */ export const reddit_chat_history = sqliteTable('reddit_chat_history', { - message_id: text('message_id').primaryKey(), + message_id: text('message_id').primaryKey().notNull(), created_at: integer('created_at', { mode: 'timestamp' }), updated_at: integer('updated_at', { mode: 'timestamp' }), username: text('username'), @@ -179,32 +176,27 @@ export const reddit_birthdate = sqliteTable('reddit_birthdate', { }); export const reddit_user_preferences = sqliteTable('reddit_user_preferences', { - preference: text('preference').primaryKey(), + preference: text('preference').primaryKey().notNull(), value: text('value'), }); export const reddit_linked_identities = sqliteTable( 'reddit_linked_identities', { - issuer_id: text('issuer_id'), - subject_id: text('subject_id'), + issuer_id: text('issuer_id').primaryKey().notNull(), + subject_id: text('subject_id').notNull(), }, - (t) => ({ - uq_issuer_subject: uniqueIndex( - 'reddit_linked_identities_issuer_subject_uq', - ).on(t.issuer_id, t.subject_id), - }), ); export const reddit_linked_phone_number = sqliteTable( 'reddit_linked_phone_number', { - phone_number: text('phone_number').primaryKey(), + phone_number: text('phone_number').primaryKey().notNull(), }, ); export const reddit_twitter = sqliteTable('reddit_twitter', { - username: text('username').primaryKey(), + username: text('username').primaryKey().notNull(), }); /** @@ -213,26 +205,26 @@ export const reddit_twitter = sqliteTable('reddit_twitter', { export const reddit_approved_submitter_subreddits = sqliteTable( 'reddit_approved_submitter_subreddits', { - subreddit: text('subreddit').primaryKey(), + subreddit: text('subreddit').primaryKey().notNull(), }, ); export const reddit_moderated_subreddits = sqliteTable( 'reddit_moderated_subreddits', { - subreddit: text('subreddit').primaryKey(), + subreddit: text('subreddit').primaryKey().notNull(), }, ); export const reddit_subscribed_subreddits = sqliteTable( 'reddit_subscribed_subreddits', { - subreddit: text('subreddit').primaryKey(), + subreddit: text('subreddit').primaryKey().notNull(), }, ); export const reddit_multireddits = sqliteTable('reddit_multireddits', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), display_name: text('display_name'), date: integer('date', { mode: 'timestamp' }), description: text('description'), @@ -249,7 +241,7 @@ export const reddit_multireddits = sqliteTable('reddit_multireddits', { */ export const reddit_purchases = sqliteTable('reddit_purchases', { processor: text('processor'), - transaction_id: text('transaction_id').primaryKey(), + transaction_id: text('transaction_id').primaryKey().notNull(), product: text('product'), date: integer('date', { mode: 'timestamp' }), cost: text('cost'), @@ -259,7 +251,7 @@ export const reddit_purchases = sqliteTable('reddit_purchases', { export const reddit_subscriptions = sqliteTable('reddit_subscriptions', { processor: text('processor'), - subscription_id: text('subscription_id').primaryKey(), + subscription_id: text('subscription_id').primaryKey().notNull(), product: text('product'), product_id: text('product_id'), product_name: text('product_name'), @@ -272,7 +264,7 @@ export const reddit_payouts = sqliteTable( 'reddit_payouts', { payout_amount_usd: text('payout_amount_usd'), - date: integer('date', { mode: 'timestamp' }), + date: integer('date', { mode: 'timestamp' }).primaryKey().notNull(), payout_id: text('payout_id'), }, (t) => ({ @@ -284,14 +276,14 @@ export const reddit_payouts = sqliteTable( ); export const reddit_stripe = sqliteTable('reddit_stripe', { - stripe_account_id: text('stripe_account_id').primaryKey(), + stripe_account_id: text('stripe_account_id').primaryKey().notNull(), }); /** * Misc content and utility */ export const reddit_announcements = sqliteTable('reddit_announcements', { - announcement_id: text('announcement_id').primaryKey(), + announcement_id: text('announcement_id').primaryKey().notNull(), sent_at: integer('sent_at', { mode: 'timestamp' }), read_at: integer('read_at', { mode: 'timestamp' }), from_id: text('from_id'), @@ -302,7 +294,7 @@ export const reddit_announcements = sqliteTable('reddit_announcements', { }); export const reddit_drafts = sqliteTable('reddit_drafts', { - id: text('id').primaryKey(), + id: text('id').primaryKey().notNull(), title: text('title'), body: text('body'), kind: text('kind'), @@ -323,69 +315,37 @@ export const reddit_friends = sqliteTable('reddit_friends', { note: text('note'), }); -export const reddit_gilded_content = sqliteTable( - 'reddit_gilded_content', - { - content_link: text('content_link'), - award: text('award'), - amount: text('amount'), - date: integer('date', { mode: 'timestamp' }), - }, - (t) => ({ - uq_content_award_date: uniqueIndex( - 'reddit_gilded_content_content_award_date_uq', - ).on(t.content_link, t.award, t.date), - }), -); +export const reddit_gilded_content = sqliteTable('reddit_gilded_content', { + content_link: text('content_link').primaryKey().notNull(), + award: text('award'), + amount: text('amount'), + date: integer('date', { mode: 'timestamp' }), +}); -export const reddit_gold_received = sqliteTable( - 'reddit_gold_received', - { - content_link: text('content_link'), - gold_received: text('gold_received'), - gilder_username: text('gilder_username'), - date: integer('date', { mode: 'timestamp' }), - }, - (t) => ({ - uq_content_date: uniqueIndex('reddit_gold_received_content_date_uq').on( - t.content_link, - t.date, - ), - }), -); +export const reddit_gold_received = sqliteTable('reddit_gold_received', { + content_link: text('content_link').primaryKey().notNull(), + gold_received: text('gold_received'), + gilder_username: text('gilder_username'), + date: integer('date', { mode: 'timestamp' }), +}); -export const reddit_ip_logs = sqliteTable( - 'reddit_ip_logs', - { - date: integer('date', { mode: 'timestamp' }), - ip: text('ip'), - }, - (t) => ({ - uq_date_ip: uniqueIndex('reddit_ip_logs_date_ip_uq').on(t.date, t.ip), - }), -); +export const reddit_ip_logs = sqliteTable('reddit_ip_logs', { + date: integer('date', { mode: 'timestamp' }).primaryKey().notNull(), + ip: text('ip'), +}); export const reddit_persona = sqliteTable('reddit_persona', { persona_inquiry_id: text('persona_inquiry_id').primaryKey(), }); -export const reddit_poll_votes = sqliteTable( - 'reddit_poll_votes', - { - post_id: text('post_id'), - user_selection: text('user_selection'), - text: text('text'), - image_url: text('image_url'), - is_prediction: text('is_prediction'), - stake_amount: text('stake_amount'), - }, - (t) => ({ - uq_post_user: uniqueIndex('reddit_poll_votes_post_user_uq').on( - t.post_id, - t.user_selection, - ), - }), -); +export const reddit_poll_votes = sqliteTable('reddit_poll_votes', { + post_id: text('post_id').primaryKey().notNull(), + user_selection: text('user_selection'), + text: text('text'), + image_url: text('image_url'), + is_prediction: text('is_prediction'), + stake_amount: text('stake_amount'), +}); export const reddit_scheduled_posts = sqliteTable('reddit_scheduled_posts', { scheduled_post_id: text('scheduled_post_id').primaryKey(), diff --git a/packages/vault-core/src/adapters/reddit/src/types.ts b/packages/vault-core/src/adapters/reddit/src/types.ts deleted file mode 100644 index a1b41fc201..0000000000 --- a/packages/vault-core/src/adapters/reddit/src/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { parseSchema } from './validation'; - -// ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. -// Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. -type Arrayify = T extends readonly [infer E] ? E[] : T; -type Inferred = (typeof parseSchema)['infer']; -export type ParsedRedditExport = { - [K in keyof Inferred]: Arrayify; -}; - -// Back-compat alias -export type ParseResult = ParsedRedditExport; diff --git a/packages/vault-core/src/adapters/reddit/src/upsert.ts b/packages/vault-core/src/adapters/reddit/src/upsert.ts deleted file mode 100644 index ec397a7255..0000000000 --- a/packages/vault-core/src/adapters/reddit/src/upsert.ts +++ /dev/null @@ -1,467 +0,0 @@ -import type { CompatibleDB } from '@repo/vault-core'; -import type { AnySQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; -import type * as schema from './schema'; -import { - reddit_account_gender, - reddit_announcements, - reddit_approved_submitter_subreddits, - reddit_birthdate, - reddit_chat_history, - reddit_checkfile, - reddit_comment_headers, - reddit_comment_votes, - reddit_comments, - reddit_drafts, - reddit_friends, - reddit_gilded_content, - reddit_gold_received, - reddit_hidden_posts, - reddit_ip_logs, - reddit_linked_identities, - reddit_linked_phone_number, - reddit_message_headers, - reddit_messages, - reddit_messages_archive, - reddit_messages_archive_headers, - reddit_moderated_subreddits, - reddit_multireddits, - reddit_payouts, - reddit_persona, - reddit_poll_votes, - reddit_post_headers, - reddit_post_votes, - reddit_posts, - reddit_purchases, - reddit_saved_comments, - reddit_saved_posts, - reddit_scheduled_posts, - reddit_sensitive_ads_preferences, - reddit_statistics, - reddit_stripe, - reddit_subscribed_subreddits, - reddit_subscriptions, - reddit_twitter, - reddit_user_preferences, -} from './schema'; -import type { ParsedRedditExport } from './types'; - -/** - * Small utility to chunk arrays for batched inserts to keep statements reasonable. - */ -function chunk(arr: T[], size: number): T[][] { - if (size <= 0) return [arr]; - const out: T[][] = []; - for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)); - return out; -} - -/** - * Remove undefined/null fields from a row. If nothing remains, it's an "empty" row. - */ -function pruneRow>(row: T): Partial { - const cleaned: Partial = {}; - for (const [k, v] of Object.entries(row)) { - if (v !== undefined && v !== null) { - (cleaned as Record)[k] = v; - } - } - return cleaned; -} - -/** - * Generic, safe onConflict upsert for a single row object. - * Falls back to per-row inserts to preserve idempotency and simplicity. - */ -async function upsertOne( - tx: unknown, - table: SQLiteTable, - row: T, - target: unknown, // allow column or composite array -): Promise { - // Guard against empty CSV rows that parse into all-undefined fields. - const cleaned = pruneRow( - row as unknown as Record, - ) as Partial; - if (Object.keys(cleaned).length === 0) { - // Nothing to insert/update; skip silently. - return; - } - const anyTx = tx as { - insert: (tbl: SQLiteTable) => { - values: (v: Partial) => { - onConflictDoUpdate: (args: { - target: unknown; - set: Partial; - }) => Promise; - }; - }; - }; - await anyTx.insert(table).values(cleaned).onConflictDoUpdate({ - target, - set: cleaned, - }); -} - -/** - * Upsert many rows by iterating records with conflict handling. - * Uses small per-row statements to stay simple and avoid driver limits. - */ -async function upsertMany( - tx: unknown, - table: SQLiteTable, - rows: T[], - target: AnySQLiteColumn | readonly AnySQLiteColumn[], -): Promise { - for (const r of rows) { - await upsertOne(tx, table, r, target); - } -} - -/** - * Implement the Adapter.upsert contract. - * - * Notes: - * - Upserts are performed in a single transaction. - * - We intentionally avoid adding FKs in v1 per export inconsistencies. - */ -export async function upsertRedditData( - db: CompatibleDB, - data: ParsedRedditExport, -): Promise { - const provider: - | { transaction: (fn: (tx: unknown) => Promise) => Promise } - | CompatibleDB = - typeof (db as unknown as { transaction?: unknown }).transaction === - 'function' - ? db - : { - transaction: async (fn: (tx: unknown) => Promise) => - fn(db as unknown), - }; - - await provider.transaction(async (tx: unknown) => { - // Parser already returns Date objects for timestamp fields. Use data as-is. - const posts = data.posts ?? []; - const post_headers = data.post_headers ?? []; - const comments = data.comments ?? []; - const comment_headers = data.comment_headers ?? []; - const message_headers = data.message_headers ?? []; - const messages = data.messages ?? []; - const messages_archive_headers = data.messages_archive_headers ?? []; - const messages_archive = data.messages_archive ?? []; - const chat_history = data.chat_history ?? []; - const birthdate = data.birthdate ?? []; - const multireddits = data.multireddits ?? []; - const purchases = data.purchases ?? []; - const subscriptions = data.subscriptions ?? []; - const payouts = data.payouts ?? []; - const announcements = data.announcements ?? []; - const drafts = data.drafts ?? []; - const gilded_content = data.gilded_content ?? []; - const gold_received = data.gold_received ?? []; - const ip_logs = data.ip_logs ?? []; - const scheduled_posts = data.scheduled_posts ?? []; - // Core content - if (posts?.length) { - await upsertMany(tx, reddit_posts, posts, reddit_posts.id); - } - if (post_headers?.length) { - await upsertMany( - tx, - reddit_post_headers, - post_headers, - reddit_post_headers.id, - ); - } - if (comments?.length) { - await upsertMany(tx, reddit_comments, comments, reddit_comments.id); - } - if (comment_headers?.length) { - await upsertMany( - tx, - reddit_comment_headers, - comment_headers, - reddit_comment_headers.id, - ); - } - - // Votes / visibility / saves - if (data.post_votes?.length) { - // Use id as the conflict target to satisfy SQLite UNIQUE/PK requirements in v1 - await upsertMany( - tx, - reddit_post_votes, - data.post_votes, - reddit_post_votes.id, - ); - } - if (data.comment_votes?.length) { - // Use id as the conflict target to satisfy SQLite UNIQUE/PK requirements in v1 - await upsertMany( - tx, - reddit_comment_votes, - data.comment_votes, - reddit_comment_votes.id, - ); - } - if (data.saved_posts?.length) { - await upsertMany( - tx, - reddit_saved_posts, - data.saved_posts, - reddit_saved_posts.id, - ); - } - if (data.saved_comments?.length) { - await upsertMany( - tx, - reddit_saved_comments, - data.saved_comments, - reddit_saved_comments.id, - ); - } - if (data.hidden_posts?.length) { - await upsertMany( - tx, - reddit_hidden_posts, - data.hidden_posts, - reddit_hidden_posts.id, - ); - } - - // Messaging - if (message_headers?.length) { - await upsertMany( - tx, - reddit_message_headers, - message_headers, - reddit_message_headers.id, - ); - } - if (messages?.length) { - await upsertMany(tx, reddit_messages, messages, reddit_messages.id); - } - if (messages_archive_headers?.length) { - await upsertMany( - tx, - reddit_messages_archive_headers, - messages_archive_headers, - reddit_messages_archive_headers.id, - ); - } - if (messages_archive?.length) { - await upsertMany( - tx, - reddit_messages_archive, - messages_archive, - reddit_messages_archive.id, - ); - } - - // Chat - if (chat_history?.length) { - await upsertMany( - tx, - reddit_chat_history, - chat_history, - reddit_chat_history.message_id, - ); - } - - // Account & preferences - if (data.account_gender?.length) { - await upsertMany( - tx, - reddit_account_gender, - data.account_gender, - reddit_account_gender.id, // Single-row sentinel - ); - } - if (data.sensitive_ads_preferences?.length) { - await upsertMany( - tx, - reddit_sensitive_ads_preferences, - data.sensitive_ads_preferences, - reddit_sensitive_ads_preferences.type, - ); - } - if (birthdate?.length) { - // Single-row table; use sentinel primary key - await upsertMany(tx, reddit_birthdate, birthdate, reddit_birthdate.id); - } - if (data.user_preferences?.length) { - await upsertMany( - tx, - reddit_user_preferences, - data.user_preferences, - reddit_user_preferences.preference, - ); - } - if (data.linked_identities?.length) { - await upsertMany(tx, reddit_linked_identities, data.linked_identities, [ - reddit_linked_identities.issuer_id, - reddit_linked_identities.subject_id, - ]); - } - if (data.linked_phone_number?.length) { - await upsertMany( - tx, - reddit_linked_phone_number, - data.linked_phone_number, - reddit_linked_phone_number.phone_number, - ); - } - if (data.twitter?.length) { - await upsertMany( - tx, - reddit_twitter, - data.twitter, - reddit_twitter.username, - ); - } - - // Moderation & subs - if (data.approved_submitter_subreddits?.length) { - await upsertMany( - tx, - reddit_approved_submitter_subreddits, - data.approved_submitter_subreddits, - reddit_approved_submitter_subreddits.subreddit, - ); - } - if (data.moderated_subreddits?.length) { - await upsertMany( - tx, - reddit_moderated_subreddits, - data.moderated_subreddits, - reddit_moderated_subreddits.subreddit, - ); - } - if (data.subscribed_subreddits?.length) { - await upsertMany( - tx, - reddit_subscribed_subreddits, - data.subscribed_subreddits, - reddit_subscribed_subreddits.subreddit, - ); - } - if (multireddits?.length) { - await upsertMany( - tx, - reddit_multireddits, - multireddits, - reddit_multireddits.id, - ); - } - - // Commerce & payouts - if (purchases?.length) { - await upsertMany( - tx, - reddit_purchases, - purchases, - reddit_purchases.transaction_id, - ); - } - if (subscriptions?.length) { - await upsertMany( - tx, - reddit_subscriptions, - subscriptions, - reddit_subscriptions.subscription_id, - ); - } - if (payouts?.length) { - await upsertMany(tx, reddit_payouts, payouts, [ - reddit_payouts.payout_id, - reddit_payouts.date, - ]); - } - if (data.stripe?.length) { - await upsertMany( - tx, - reddit_stripe, - data.stripe, - reddit_stripe.stripe_account_id, - ); - } - - // Misc - if (announcements?.length) { - await upsertMany( - tx, - reddit_announcements, - announcements, - reddit_announcements.announcement_id, - ); - } - if (drafts?.length) { - await upsertMany(tx, reddit_drafts, drafts, reddit_drafts.id); - } - if (data.friends?.length) { - await upsertMany( - tx, - reddit_friends, - data.friends, - reddit_friends.username, - ); - } - if (gilded_content?.length) { - await upsertMany(tx, reddit_gilded_content, gilded_content, [ - reddit_gilded_content.content_link, - reddit_gilded_content.award, - reddit_gilded_content.date, - ]); - } - if (gold_received?.length) { - await upsertMany(tx, reddit_gold_received, gold_received, [ - reddit_gold_received.content_link, - reddit_gold_received.date, - ]); - } - if (ip_logs?.length) { - await upsertMany(tx, reddit_ip_logs, ip_logs, [ - reddit_ip_logs.date, - reddit_ip_logs.ip, - ]); - } - if (data.persona?.length) { - await upsertMany( - tx, - reddit_persona, - data.persona, - reddit_persona.persona_inquiry_id, - ); - } - if (data.poll_votes?.length) { - await upsertMany(tx, reddit_poll_votes, data.poll_votes, [ - reddit_poll_votes.post_id, - reddit_poll_votes.user_selection, - ]); - } - if (scheduled_posts?.length) { - await upsertMany( - tx, - reddit_scheduled_posts, - scheduled_posts, - reddit_scheduled_posts.scheduled_post_id, - ); - } - if (data.statistics?.length) { - await upsertMany( - tx, - reddit_statistics, - data.statistics, - reddit_statistics.statistic, - ); - } - if (data.checkfile?.length) { - await upsertMany( - tx, - reddit_checkfile, - data.checkfile, - reddit_checkfile.filename, - ); - } - }); -} diff --git a/packages/vault-core/src/adapters/reddit/src/validation.ts b/packages/vault-core/src/adapters/reddit/src/validation.ts index 2e215c28f0..e27112c077 100644 --- a/packages/vault-core/src/adapters/reddit/src/validation.ts +++ b/packages/vault-core/src/adapters/reddit/src/validation.ts @@ -1,5 +1,13 @@ import { type } from 'arktype'; +const date = type('string.date.parse'); +const dateOpt = type('string') + .pipe((v) => (v === '' ? undefined : v)) + .to('string.date.parse | undefined'); +const registrationDate = type('string') + .pipe((v) => (v === 'registration ip' ? undefined : v)) + .to('string.date.parse | undefined'); + // ArkType parse schema // explicit object-array schemas for all other datasets to avoid 'unknown'. export const parseSchema = type({ @@ -7,109 +15,107 @@ export const parseSchema = type({ posts: type({ id: 'string', permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', + date: date, + created_utc: date, + ip: 'string.ip', subreddit: 'string', - gildings: 'number | undefined', + gildings: 'string.numeric.parse', title: 'string | undefined', url: 'string | undefined', body: 'string | undefined', }).array(), post_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', + id: 'string', + permalink: 'string', + date: date, + ip: 'string.ip', + subreddit: 'string', + gildings: 'string.numeric.parse', url: 'string | undefined', }).array(), comments: type({ id: 'string', permalink: 'string', - date: 'Date', - created_utc: 'Date', - ip: 'string | undefined', + date: date, + ip: 'string.ip', subreddit: 'string', - gildings: 'number | undefined', - link: 'string', - post_id: 'string | undefined', + gildings: 'string.numeric.parse', + link: 'string.url', parent: 'string | undefined', body: 'string | undefined', media: 'string | undefined', }).array(), comment_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', - subreddit: 'string | undefined', - gildings: 'number | undefined', - link: 'string | undefined', + id: 'string', + permalink: 'string', + date: date, + ip: 'string.ip', + subreddit: 'string', + gildings: 'string.numeric.parse', + link: 'string', parent: 'string | undefined', }).array(), // Votes / visibility / saves post_votes: type({ - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', + id: 'string', + permalink: 'string', + direction: 'string', }).array(), comment_votes: type({ - id: 'string | undefined', - permalink: 'string | undefined', - direction: 'string | undefined', + id: 'string', + permalink: 'string', + direction: 'string', }).array(), saved_posts: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', }).array(), saved_comments: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', }).array(), hidden_posts: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', }).array(), // Messaging message_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', + date: dateOpt, + ip: 'string.ip', from: 'string | undefined', to: 'string | undefined', }).array(), messages: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', + date: dateOpt, + ip: 'string.ip', from: 'string | undefined', to: 'string | undefined', subject: 'string | undefined', body: 'string | undefined', }).array(), messages_archive_headers: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', + date: dateOpt, + ip: 'string.ip', from: 'string | undefined', to: 'string | undefined', }).array(), messages_archive: type({ - id: 'string | undefined', - permalink: 'string | undefined', + id: 'string', + permalink: 'string', thread_id: 'string | undefined', - date: 'Date | undefined', - ip: 'string | undefined', + date: dateOpt, + ip: 'string.ip', from: 'string | undefined', to: 'string | undefined', subject: 'string | undefined', @@ -118,9 +124,9 @@ export const parseSchema = type({ // Chat chat_history: type({ - message_id: 'string | undefined', - created_at: 'Date | undefined', - updated_at: 'Date | undefined', + message_id: 'string', + created_at: dateOpt, + updated_at: dateOpt, username: 'string | undefined', message: 'string | undefined', thread_parent_message_id: 'string | undefined', @@ -131,38 +137,40 @@ export const parseSchema = type({ }).array(), // Account and preferences - account_gender: type({ account_gender: 'string | undefined' }).array(), + account_gender: type({ + account_gender: 'string | undefined', + }).array(), sensitive_ads_preferences: type({ - type: 'string | undefined', + type: 'string', preference: 'string | undefined', }).array(), birthdate: type({ - birthdate: 'Date | undefined', - verified_birthdate: 'Date | undefined', + birthdate: dateOpt, + verified_birthdate: dateOpt, verification_state: 'string | undefined', verification_method: 'string | undefined', }).array(), user_preferences: type({ - preference: 'string | undefined', + preference: 'string', value: 'string | undefined', }).array(), linked_identities: type({ - issuer_id: 'string | undefined', - subject_id: 'string | undefined', + issuer_id: 'string', + subject_id: 'string', }).array(), - linked_phone_number: type({ phone_number: 'string | undefined' }).array(), - twitter: type({ username: 'string | undefined' }).array(), + linked_phone_number: type({ phone_number: 'string' }).array(), + twitter: type({ username: 'string' }).array(), // Moderation / subscriptions / subreddits approved_submitter_subreddits: type({ - subreddit: 'string | undefined', + subreddit: 'string', }).array(), - moderated_subreddits: type({ subreddit: 'string | undefined' }).array(), - subscribed_subreddits: type({ subreddit: 'string | undefined' }).array(), + moderated_subreddits: type({ subreddit: 'string' }).array(), + subscribed_subreddits: type({ subreddit: 'string' }).array(), multireddits: type({ - id: 'string | undefined', + id: 'string', display_name: 'string | undefined', - date: 'Date | undefined', + date: dateOpt, description: 'string | undefined', privacy: 'string | undefined', subreddits: 'string | undefined', @@ -175,35 +183,35 @@ export const parseSchema = type({ // Commerce and payouts purchases: type({ processor: 'string | undefined', - transaction_id: 'string | undefined', + transaction_id: 'string', product: 'string | undefined', - date: 'Date | undefined', + date: dateOpt, cost: 'string | undefined', currency: 'string | undefined', status: 'string | undefined', }).array(), subscriptions: type({ processor: 'string | undefined', - subscription_id: 'string | undefined', + subscription_id: 'string', product: 'string | undefined', product_id: 'string | undefined', product_name: 'string | undefined', status: 'string | undefined', - start_date: 'Date | undefined', - end_date: 'Date | undefined', + start_date: dateOpt, + end_date: dateOpt, }).array(), payouts: type({ payout_amount_usd: 'string | undefined', - date: 'Date | undefined', + date: date, payout_id: 'string | undefined', }).array(), - stripe: type({ stripe_account_id: 'string | undefined' }).array(), + stripe: type({ stripe_account_id: 'string' }).array(), // Misc announcements: type({ - announcement_id: 'string | undefined', - sent_at: 'Date | undefined', - read_at: 'Date | undefined', + announcement_id: 'string', + sent_at: dateOpt, + read_at: dateOpt, from_id: 'string | undefined', from_username: 'string | undefined', subject: 'string | undefined', @@ -211,11 +219,11 @@ export const parseSchema = type({ url: 'string | undefined', }).array(), drafts: type({ - id: 'string | undefined', + id: 'string', title: 'string | undefined', body: 'string | undefined', kind: 'string | undefined', - created: 'Date | undefined', + created: dateOpt, spoiler: 'string | undefined', nsfw: 'string | undefined', original_content: 'string | undefined', @@ -227,25 +235,25 @@ export const parseSchema = type({ is_public_link: 'string | undefined', }).array(), friends: type({ - username: 'string | undefined', + username: 'string', note: 'string | undefined', }).array(), gilded_content: type({ - content_link: 'string | undefined', + content_link: 'string', award: 'string | undefined', amount: 'string | undefined', - date: 'Date | undefined', + date: dateOpt, }).array(), gold_received: type({ - content_link: 'string | undefined', + content_link: 'string', gold_received: 'string | undefined', gilder_username: 'string | undefined', - date: 'Date | undefined', + date: dateOpt, }).array(), - ip_logs: type({ date: 'Date | undefined', ip: 'string | undefined' }).array(), - persona: type({ persona_inquiry_id: 'string | undefined' }).array(), + ip_logs: type({ date: registrationDate, ip: 'string.ip' }).array(), + persona: type({ persona_inquiry_id: 'string' }).array(), poll_votes: type({ - post_id: 'string | undefined', + post_id: 'string', user_selection: 'string | undefined', text: 'string | undefined', image_url: 'string | undefined', @@ -253,20 +261,20 @@ export const parseSchema = type({ stake_amount: 'string | undefined', }).array(), scheduled_posts: type({ - scheduled_post_id: 'string | undefined', + scheduled_post_id: 'string', subreddit: 'string | undefined', title: 'string | undefined', body: 'string | undefined', url: 'string | undefined', - submission_time: 'Date | undefined', + submission_time: dateOpt, recurrence: 'string | undefined', }).array(), statistics: type({ - statistic: 'string | undefined', + statistic: 'string', value: 'string | undefined', }).array(), checkfile: type({ - filename: 'string | undefined', + filename: 'string', sha256: 'string | undefined', }).array(), }); diff --git a/packages/vault-core/src/codecs/json.ts b/packages/vault-core/src/codecs/json.ts index b5bdb326a4..343e822c19 100644 --- a/packages/vault-core/src/codecs/json.ts +++ b/packages/vault-core/src/codecs/json.ts @@ -1,22 +1,9 @@ import { defineCodec } from '../core/codec'; -function stableStringify(value: unknown): string { - return JSON.stringify(value, replacer, 2); -} - -function replacer(_key: string, value: unknown) { - if (value && typeof value === 'object' && !Array.isArray(value)) { - const out: Record = {}; - for (const k of Object.keys(value as Record).sort()) - out[k] = (value as Record)[k]; - return out; - } - return value; -} - export const jsonFormat = defineCodec({ id: 'json', fileExtension: 'json', + mimeType: 'application/json', parse(text) { return JSON.parse(text, (_, value) => { // Revive pseudo-date objects diff --git a/packages/vault-core/src/codecs/markdown.ts b/packages/vault-core/src/codecs/markdown.ts index 27bafabc22..0635b2f588 100644 --- a/packages/vault-core/src/codecs/markdown.ts +++ b/packages/vault-core/src/codecs/markdown.ts @@ -5,6 +5,7 @@ import { YAML } from '../utils/format/yaml'; export const markdownFormat = defineCodec({ id: 'markdown', fileExtension: 'md', + mimeType: 'text/markdown', parse(text) { return YAML.parse(text); }, diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index 42c0450825..b2cfa2de15 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -1,44 +1,77 @@ -import type { defineConfig } from 'drizzle-kit'; -import type { LibSQLDatabase } from 'drizzle-orm/libsql'; -import type { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core'; - -// Shared types -type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> - ? R - : never; - -type ResultSet = ExtractedResult; - -// Represents compatible Drizzle DB types across the codebase -export type CompatibleDB = BaseSQLiteDatabase< - 'sync' | 'async', - TSchema | ResultSet ->; - -export type DrizzleConfig = ReturnType; - +import type { StandardSchemaV1 } from '@standard-schema/spec'; +import type { ColumnsSelection, InferSelectModel } from 'drizzle-orm'; +import type { + SQLiteTable, + SubqueryWithSelection, +} from 'drizzle-orm/sqlite-core'; +import type { CompatibleDB } from './db'; +import type { Ingestor } from './ingestor'; +import type { + RequiredTransformTags, + Tag4, + TransformRegistry, + VersionDef, +} from './migrations'; + +/** Column-level metadata */ +export type ColumnInfo = string; + +/** Per-table simple column descriptions or rich ColumnInfo for each column. */ export type ColumnDescriptions> = { [K in keyof T]: { - [C in keyof T[K]['_']['columns']]: string; + [C in keyof T[K]['_']['columns']]: ColumnInfo; + }; +}; + +/** Table metadata in human readable format. */ +export type AdapterMetadata> = { + [K in keyof TSchema]?: { + [C in keyof TSchema[K]['_']['columns']]?: ColumnInfo; }; }; -// --- Schema & table-name helpers ------------------------------------------------- +/** View helper used by adapters for predefined queries (optional). */ +export type View< + T extends string, + TSelection extends ColumnsSelection, + TSchema extends Record, + TDatabase extends CompatibleDB, +> = { + name: T; + definition: (db: TDatabase) => SubqueryWithSelection; +}; -/** Union of table name literals from the schema's keys. */ -export type SchemaTableNames> = - Extract; +// TODO remove once https://github.com/drizzle-team/drizzle-orm/issues/2745 is resolved +/** Convert `null` properties in a type to `undefined` */ +type NullToUndefined = { + [K in keyof T]: T[K] extends null + ? undefined + : T[K] extends (infer U)[] + ? NullToUndefined[] + : Exclude | ([null] extends [T[K]] ? undefined : never); +}; -/** Ensure all tables in a schema have names prefixed with the given Adapter ID. */ -// Check that all table names in the schema start with `${TID}_` -export type SchemaTablesAllPrefixed< +/** Translate schema to object, strip prefix of table names */ +/** + * Map a prefixed schema record to an object whose keys are the table names with the + * adapter prefix removed and whose values are arrays of the inferred row type. + * + * This represents the natural shape for bulk ingestion: each table produces many rows. + * + * @example `reddit` ID and `reddit_posts` table become `posts` + */ +export type SchemaMappedToObject< TID extends string, - TSchema extends Record, -> = Exclude, `${TID}_${string}`> extends never - ? 1 - : 0; + TObj extends Record, +> = { + [K in keyof TObj as K extends `${TID}_${infer Rest}` + ? Rest + : K]: NullToUndefined>[]; +}; -// Adapter is schema-only. Importers wire lifecycle, parsing, views, etc. +/** + * Unified Adapter: schema + parsing/upsert lifecycle. + */ export interface Adapter< TID extends string = string, TTableNames extends string = string, @@ -46,181 +79,171 @@ export interface Adapter< string, SQLiteTable >, + TVersions extends readonly VersionDef[] = readonly VersionDef[], + TPreparsed = unknown, + TParsed = unknown, > { /** Unique identifier for the adapter (lowercase, no spaces, alphanumeric) */ id: TID; - /** Database schema */ - schema: TSchema; -} -// Note: If a generic only appears in a function parameter position, TS won't infer it and will -// fall back to the constraint (e.g. `object`). These overloads infer the full function type `F` instead. -type KeysOf = Extract; -type PrefixedAdapter< - TID extends string, - S extends Record, -> = KeysOf extends `${TID}_${string}` ? Adapter, S> : never; + /** Drizzle schema object. */ + schema: TID extends string + ? TSchema + : EnsureAllTablesArePrefixedWith extends never + ? never + : EnsureSchemaHasPrimaryKeys; + + /** Adapter metadata for UI/help */ + metadata?: AdapterMetadata; + + /** Optional predefined views + * Note: to avoid function parameter variance issues in structural assignability, + * we use a base schema shape for the DB parameter rather than the adapter's TSchema. + */ + views?: { + [Alias in string]: View< + Alias, + ColumnsSelection, + Record, + CompatibleDB> + >; + }; + + /** Pipelines for importing new data. Validation/morphing happens via `adapter.validator` */ + ingestors?: readonly Ingestor[]; + + /** + * Optional Standard Schema validator for parsed payload (ingest pipeline). + */ + validator?: StandardSchemaV1; + + /** + * Authoring-time versions tuple used for JS transform tag alignment checks + * and runtime transform planning. + */ + versions: TVersions; + + /** + * Transform registry; when provided with 'versions', tag alignment is + * enforced at compile time and verified at runtime. + */ + transforms: TransformRegistry>; +} +/** + * Define a new adapter where the validator's parsed output must match the schema's InferSelect shape, + * where all tables are prefixed and have primary keys, and (optionally) enforce JS transform tag alignment when 'versions' and 'transforms' are provided. + */ export function defineAdapter< - TID extends string, - S extends Record, ->(adapter: () => PrefixedAdapter): () => PrefixedAdapter; + const TID extends string, + TSchema extends Record, + TVersions extends readonly VersionDef[] = readonly VersionDef[], + TPreparsed = unknown, + TParsed = SchemaMappedToObject, +>( + adapter: () => PrefixedAdapter, +): () => PrefixedAdapter; export function defineAdapter< - TID extends string, - S extends Record, - A extends unknown[], + const TID extends string, + TSchema extends Record, + TPreparsed, + TVersions extends readonly VersionDef[], + TParsed = SchemaMappedToObject, + TArgs extends unknown[] = [], >( - adapter: (...args: A) => PrefixedAdapter, -): (...args: A) => PrefixedAdapter; + adapter: ( + ...args: TArgs + ) => PrefixedAdapter, +): ( + ...args: TArgs +) => PrefixedAdapter; export function defineAdapter unknown>( adapter: F, ): F { return adapter; } -// --- Cross-adapter utilities ----------------------------------------------------- - -/** Get table-name union for a single Adapter-like value (Adapter or { schema }). */ -export type TableNamesOfAdapterLike = T extends { schema: infer S } - ? S extends Record - ? SchemaTableNames - : never - : never; - -/** Get table-name union for an Importer-like value (has adapter.schema). */ -export type TableNamesOfImporterLike = T extends { - adapter: { schema: infer S }; -} - ? S extends Record - ? SchemaTableNames - : never - : never; - -/** Compute table-name union for any array of Adapters or Importers. */ -export type TableNamesOfMany = - T[number] extends infer E - ? E extends { schema: Record } - ? TableNamesOfAdapterLike - : E extends { adapter: { schema: Record } } - ? TableNamesOfImporterLike - : never +/** + * Compile-time detection of whether a table has a primary key. + * Produces the table name union if any table is missing a primary key; else never. + */ +type TableHasPrimaryKey = { + [K in keyof TColumns]: TColumns[K] extends { _: { isPrimaryKey: true } } + ? K : never; +}[keyof TColumns] extends never + ? false + : true; +type EnsureSchemaHasPrimaryKeys> = { + [K in keyof S]: TableHasPrimaryKey extends false ? never : K & string; +}[keyof S]; -/** Compute collisions (duplicates) of table names across an array of Adapters/Importers. */ -export type TableNameCollisions< - T extends readonly unknown[], - Acc extends string = never, -> = T extends readonly [infer H, ...infer R] - ? H extends - | { schema: Record } - | { - adapter: { schema: Record }; - } - ? - | Extract, TableNamesOfMany> - | TableNameCollisions - : TableNameCollisions - : Acc; - -/** Assert there are no duplicate table names; resolves to T when valid, else never. */ -export type NoTableNameCollisions = - TableNameCollisions extends never ? T : never; - -/** Ensure all adapters' schemas are correctly prefixed. Returns T when OK, else never. */ -export type AllAdaptersPrefixed = Exclude< - T[number] extends Adapter - ? SchemaTablesAllPrefixed> - : 1, - 1 -> extends never - ? T - : never; +type KeysOf = Extract; -/** Ensure all importers' adapter schemas are correctly prefixed. Returns T when OK, else never. */ -export type AllImportersPrefixed = Exclude< - T[number] extends { id: infer ID; adapter: { schema: infer S } } - ? SchemaTablesAllPrefixed> - : 1, - 1 -> extends never - ? T - : never; +/** + * Compile time check for table name prefixing + */ +type PrefixedAdapter< + TID extends string, + Schema extends Record, + TVersions extends readonly VersionDef[], + TPreparsed, + TParsed, +> = Adapter, Schema, TVersions, TPreparsed, TParsed> & + // If any table names are NOT prefixed with `${TID}_`, attach an impossible property + // so TS surfaces a clear, actionable error including the offending keys. + (MissingPrefixedTables extends never + ? unknown + : { + __error__schema_table_prefix_mismatch__: `Expected all tables to start with "${TID}_"`; + }) & + // If any tables are missing primary keys, surface them similarly + (MissingPrimaryKeyTables extends never + ? unknown + : { + __error__missing_primary_keys__: MissingPrimaryKeyTables; + }); + +// Compute the set of schema keys that are NOT prefixed with `${TID}_` +type MissingPrefixedTables< + TID extends string, + TSchema extends Record, +> = Exclude, `${TID}_${string}`>; -// Utility to merge a union of schema records into a single record via intersection -export type UnionToIntersection = ( - U extends unknown - ? (k: U) => void - : never -) extends (k: infer I) => void - ? I +// Compute the set of tables that do not declare a primary key +type MissingPrimaryKeyTables> = { + [K in keyof S]: TableHasPrimaryKey extends false ? K & string : never; +}[keyof S]; + +type EnsureAllTablesArePrefixedWith< + TID extends string, + TSchema extends Record, +> = Exclude, `${TID}_${string}`> extends never + ? TSchema : never; +type SchemaTableNames> = Extract< + keyof TSchema, + string +>; /** - * Build a joined schema record from: - * - a VaultService-like object (has `importers`) - * - a VaultClient-like object (has `adapters`) - * - an array of Importers (have `adapter.schema`) - * - an array of Adapters (have `schema`) - */ -/** - * Normalize any schema-like type to a concrete Record. - * - If S is already a schema record, it's returned unchanged. - * - Otherwise, returns a broad Record so downstream - * utilities (e.g. JoinedSchema<...>) produce a usable record instead of never - * when inputs are unknown/never/empty unions. + * Compile-time detection of duplicate adapter IDs in a tuple. + * Produces the first duplicate ID union if any; otherwise never. */ -type EnsureSchema = S extends Record - ? S - : Record; -export type JoinedSchema = EnsureSchema< - T extends { importers: infer I } - ? UnionToIntersection< - SchemaOfMany - > - : T extends { adapters: infer A } - ? UnionToIntersection< - SchemaOfMany - > - : T extends readonly unknown[] - ? UnionToIntersection> - : never ->; - -type SchemaOfAdapterLike = T extends { schema: infer S } - ? S extends Record - ? S - : never - : never; -type SchemaOfImporterLike = T extends { adapter: { schema: infer S } } - ? S extends Record - ? S +type NoDuplicateAdapter< + T extends readonly { id: string }[], + Seen extends string = never, +> = T extends readonly [infer H, ...infer R] + ? H extends { id: infer ID extends string } + ? ID extends Seen + ? ID | NoDuplicateAdapter, Seen> + : NoDuplicateAdapter, Seen | ID> : never : never; -type SchemaOfMany = [T] extends [never] - ? never - : T[number] extends infer E - ? SchemaOfAdapterLike | SchemaOfImporterLike - : never; - -// Validate arrays of adapters/importers for prefixing and collisions -export type CheckNoCollisions = - TableNameCollisions extends never ? T : never; /** - * Validate an array/tuple of Importers at compile time: - * - Ensures each importer's adapter schema keys are prefixed with `${ID}_`. - * - Ensures there are no duplicate table names across all importers. - * Resolves to T when valid; otherwise resolves to never to surface a type error. - */ -export type EnsureImportersOK = - AllImportersPrefixed extends never ? never : CheckNoCollisions; -/** - * Validate an array/tuple of Adapters at compile time: - * - Ensures each adapter's schema keys are prefixed with `${id}_`. - * - Ensures there are no duplicate table names across all adapters. - * Resolves to T when valid; otherwise resolves to never to surface a type error. + * Enforce unique adapter IDs at the type level for tuple literals. + * Evaluates to T when no duplicates; else never (surfacing a type error). */ -export type EnsureAdaptersOK = - AllAdaptersPrefixed extends never - ? never - : CheckNoCollisions; +export type UniqueAdapters = + NoDuplicateAdapter extends never ? T : never; diff --git a/packages/vault-core/src/core/codec.ts b/packages/vault-core/src/core/codec.ts index 033717f491..4df2492b13 100644 --- a/packages/vault-core/src/core/codec.ts +++ b/packages/vault-core/src/core/codec.ts @@ -8,6 +8,8 @@ export interface Codec { * @example 'md' */ fileExtension: TExt; + /** MIME type for file exports (e.g., 'text/markdown', 'application/json') */ + mimeType: string; /** * Parse file text into a flat record. If a free-form body is present, * codecs should use the reserved key 'body' to carry it. @@ -32,13 +34,7 @@ export type ColumnEntry = [name: string, column: SQLiteColumn]; // Per-codec convention profile that derives mapping decisions from schema + naming export interface ConventionProfile { // compute relative path from table + pk values - pathFor( - adapterId: string, - tableName: string, - pkValues: Record, - ): string; - // map a table name to a dataset key used by Importer.validator/upsert - datasetKeyFor(adapterId: string, tableName: string): string; + pathFor(adapterId: string, tableName: string, pkValues: string[]): string; } // Helpers @@ -50,26 +46,24 @@ export function listColumns(table: SQLiteTable): ColumnEntry[] { return Object.entries(table) as ColumnEntry[]; } -// Best-effort PK detection from Drizzle runtime objects. -// Falls back to common naming if metadata is absent. -export function detectPrimaryKey( - tableName: string, - table: SQLiteTable, -): string[] | undefined { +/** + * Find primary key columns in a table. + * + * Due to type-safety in adapter.ts, *all* tables should have a primary key. + * @throws if no primary key found + */ +export function listPrimaryKeys(tableName: string, table: SQLiteTable) { const cols = listColumns(table); - const pkCols: string[] = []; - for (const [name, col] of cols) { - // Drizzle columns expose some shape at runtime; check common fields defensively. - const anyCol = col; - if (anyCol.primary === true && !pkCols.includes(name)) { - pkCols.push(name); - } + const pkCols = []; + for (const col of cols) { + const [, drizzleCol] = col; + if (drizzleCol._.isPrimaryKey) pkCols.push(col); } - if (pkCols.length > 0) return pkCols; - // Heuristic fallback by naming - if ('id' in table) return ['id']; - if (`${tableName}_id` in table) return [`${tableName}_id`]; - return undefined; + + if (pkCols.length === 0) + throw new Error(`Table ${tableName} has no primary key`); + + return pkCols; } // Choose body column by common names, prefer notNull string-like columns named body/content/text @@ -80,18 +74,14 @@ export function detectPrimaryKey( export function defaultConvention(): ConventionProfile { return { pathFor(adapterId, tableName, pkValues) { - const parts = Object.entries(pkValues) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([, v]) => String(v)); + // Merge PK values with __, sorted by key name for determinism + const parts = pkValues + .toSorted((a, b) => a.localeCompare(b)) + .map((v) => String(v)); const fileId = parts.length > 0 ? parts.join('__') : 'row'; // extension decided by mode at callsite; we return a directory path root here return `vault/${adapterId}/${tableName}/${fileId}`; }, - datasetKeyFor(adapterId, tableName) { - return tableName.startsWith(`${adapterId}_`) - ? tableName.slice(adapterId.length + 1) - : tableName; - }, }; } diff --git a/packages/vault-core/src/core/config.ts b/packages/vault-core/src/core/config.ts index eb360f0661..038cbe99b5 100644 --- a/packages/vault-core/src/core/config.ts +++ b/packages/vault-core/src/core/config.ts @@ -1,74 +1,99 @@ -import type { MigrationConfig } from 'drizzle-orm/migrator'; -import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; -import type { Adapter } from './adapter'; +import type { Adapter, UniqueAdapters } from './adapter'; import type { Codec, ConventionProfile } from './codec'; -import type { Importer } from './importer'; +import type { DrizzleDb } from './db'; +import type { + DataValidator, + Tag4, + TransformRegistry, + VersionDef, +} from './migrations'; -// Deprecated: use VaultServiceConfig or VaultClientConfig -export interface VaultConfig< - TDatabase extends BaseSQLiteDatabase<'sync' | 'async', unknown>, - TImporters extends Importer[], -> { - adapters: TImporters; - database: TDatabase; - migrateFunc: (db: TDatabase, config: MigrationConfig) => Promise; -} +/** Construct a Vault around a Drizzle DB. */ +export type CoreOptions = { + database: DrizzleDb; + adapters: UniqueAdapters; +}; + +/** Helper to get Adapter ID from Adapter */ +export type AdapterIDs = T[number]['id']; -// Service config: owns DB and Importers /** - * @deprecated The service/client architecture has been removed from vault-core. - * Prefer a pure core API with an injected Drizzle DB and per-call codec injection. + * Per-call codec and conventions (override defaults). + * Caller must provide the Adapter that owns the schema to export. */ -export interface VaultServiceConfig< - TDatabase extends BaseSQLiteDatabase<'sync' | 'async', unknown>, - TImporters extends Importer[], -> { - /** - * Importers installed on the service. - * - * Importers encapsulate end-to-end behavior for a source: parse(blob), validate, upsert(db), - * and also reference an Adapter which carries the Drizzle schema + migrations config. - * - * @see Importer - */ - importers: TImporters; +export type ExportOptions = { + /** Adapters to export (compile-time unique by id for tuple literals). Defaults to all adapters. */ + adapterIDs?: AdapterIDs>[]; + /** Codec (format) to use for exports */ + codec: Codec; + /** Optional conventions override (otherwise uses built-in default) */ + conventions?: ConventionProfile; +}; + +/** + * Provide the target Adapter, a files map (path -> contents), + * and the codec used to parse these files. Conventions may be overridden per-call. + */ +export type ImportOptions = { + /** The target Adapter to use for importing data */ + adapterID: AdapterIDs<[UniqueAdapters[number]]>; + files: Map; + /** Codec (format) to use for imports. Must match the exported format */ + codec: Codec; + /** Optional conventions override (otherwise uses built-in default) */ + conventions?: ConventionProfile; + /** - * Database connection instance used by the service. - * - * Example (libsql): - * const client = createClient({ url, authToken }); - * const db = drizzle(client); + * Plan A (optional): ordered version definitions used to plan data transforms forward-only. + * When provided together with transforms, core will run the transform chain and then validation before upsert. + * Falls back to adapter-level versions when omitted. */ - database: TDatabase; + versions?: readonly VersionDef[]; + /** - * Drizzle platform-specific migration function used to run migrations for each importer. - * - * Example (libsql): - * import { migrate } from 'drizzle-orm/libsql/migrator'; - * migrate(db, { migrationsFolder: '...' }) + * Plan A (optional): registry of data transforms keyed by target version tag. + * Must cover all forward steps when versions are provided. */ - migrateFunc: (db: TDatabase, config: MigrationConfig) => Promise; - /** Active text codec (markdown/json/etc.) and the conventions. */ - codec?: Codec; - conventions?: ConventionProfile; -} + transforms?: TransformRegistry; -// Client config: only needs adapters for schema/metadata typing and UI -export interface VaultClientConfig { /** - * Adapters provide schema (and optional metadata) for type-safety in the client. - * - * The client does not perform database operations; it uses adapters to render UI, - * build queries, and display human-readable table/column info. - * - * @see Adapter + * Plan A (optional): runtime validator (e.g., drizzle-arktype) invoked after transforms. + * If omitted and versions/transforms are not provided, core falls back to adapter-level validator. */ - adapters: TAdapters; + dataValidator?: DataValidator; + /** - * Optional transport configuration placeholder. - * - * The specific RPC transport between client and service is intentionally - * left undefined here; applications should provide their own wiring. + * Plan A (optional): source dataset version tag; used as the starting point for the transform chain. + * If omitted, the host/importer should provide a sensible default (e.g., '0000') or encode version in the bundle metadata. */ - transport?: unknown; -} + sourceTag?: string; +}; + +/** IngestOptions variant for one-time single-file ingestors (e.g., ZIP). */ +export type IngestOptions = { + adapter: Adapter; + file: File; +}; + +export type AdapterTables = TAdapter['schema']; +export type AdapterTableMap = { + [AdapterID in AdapterIDs]: AdapterTables< + Extract + >; +}; +export type QueryInterface = { + /** Map of adapter ID -> table name -> table object */ + tables: AdapterTableMap; + db: DrizzleDb; +}; + +/** + * Vault: minimal API surface. + * Methods use object method shorthand as per project conventions. + */ +export type Vault = { + exportData(options: ExportOptions): Promise>; + importData(options: ImportOptions): Promise; + ingestData(options: IngestOptions): Promise; + getQueryInterface(): QueryInterface; +}; diff --git a/packages/vault-core/src/core/db.ts b/packages/vault-core/src/core/db.ts new file mode 100644 index 0000000000..0f8a01cd61 --- /dev/null +++ b/packages/vault-core/src/core/db.ts @@ -0,0 +1,21 @@ +import type { AnyTable as AnyTableGeneric } from 'drizzle-orm'; +import type { LibSQLDatabase } from 'drizzle-orm/libsql'; +import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; + +// Shared types +type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> + ? R + : never; + +type ResultSet = ExtractedResult; + +// Represents compatible Drizzle DB types across the codebase +export type CompatibleDB = BaseSQLiteDatabase< + 'sync' | 'async', + TSchema | ResultSet +>; + +/** Minimal Drizzle-DB type expected by core. Hosts pass a concrete Drizzle instance. */ +export type DrizzleDb = CompatibleDB; + +export type AnyTable = AnyTableGeneric<{ name: string }>; // simplify usage diff --git a/packages/vault-core/src/core/import/importPipeline.ts b/packages/vault-core/src/core/import/importPipeline.ts new file mode 100644 index 0000000000..b31843fd14 --- /dev/null +++ b/packages/vault-core/src/core/import/importPipeline.ts @@ -0,0 +1,98 @@ +/** + * Lightweight orchestrator that lifts adapter metadata (versions + transforms) + * into a data-fix pipeline before rows touch the database. Hosts supply the raw + * dataset gathered from disk/ingestors; we return the fully-normalized rows. + * + * High-level flow: + * 1. Determine which transform registry + version tuple to use (allowing runtime overrides). + * 2. If a manifest/versions tuple exists, run every forward transform and optional validator. + * 3. Otherwise run the validator directly (legacy adapters). + * 4. Hand the morphed dataset back to the caller for ingestion. + */ +import type { Adapter } from '../adapter'; +import type { + DataValidator, + Tag4, + TransformRegistry, + TransformRegistryForVersions, + VersionDef, +} from '../migrations'; +import { transformAndValidate } from '../migrations'; + +/** + * Runtime configuration for a single pipeline run. + * + * dataset — Adapter-shaped record of unprefixed table keys mapped to rows. + * adapter — The adapter we’re importing (used to grab default versions/transforms). + * transformsOverride / versionsOverride + * — Allows tests or hosts to inject a different transform chain. + * dataValidator — Optional Standard Schema validator; defaults to adapter.validator upstream. + * sourceTag — Tag provided explicitly (e.g., host UI choice). + * detectedTag — Tag auto-detected out of the import metadata (migration.json). + */ +export type ImportPipelineInput = { + dataset: Record; + adapter: Adapter; + transformsOverride?: TransformRegistry; + versionsOverride?: readonly VersionDef[]; + dataValidator?: DataValidator; + sourceTag?: string; + detectedTag?: string; +}; + +/** + * Executes the import pipeline, producing a dataset that matches the adapter’s current schema. + * - Chooses effective version + transform chain (preferring overrides over adapter defaults). + * - Runs transform chain + validator when available. + * - Falls back to direct validation for legacy adapters without versions/transforms. + */ +export async function runImportPipeline( + input: ImportPipelineInput, +): Promise> { + const { + dataset, + adapter, + transformsOverride, + versionsOverride, + dataValidator, + sourceTag, + detectedTag, + } = input; + + const transforms = (transformsOverride ?? adapter.transforms) as + | TransformRegistry + | undefined; + const baseVersions = adapter.versions; + const effectiveVersions = (versionsOverride ?? baseVersions) as + | readonly VersionDef[] + | undefined; + const resolvedSourceTag = sourceTag ?? detectedTag; + + // Any transforms/validation happen purely in-memory; we never mutate the original dataset object. + let pipelineOutput: Record = dataset; + + if (effectiveVersions && transforms) { + // Both versions + transforms exist → run the forward chain plus validation. + const typedVersions = effectiveVersions; + const typedTransforms = transforms as TransformRegistryForVersions< + typeof typedVersions + >; + const typedDataset = dataset as { + [key: string]: Record[]; + }; + const result = await transformAndValidate( + typedVersions, + typedTransforms, + typedDataset, + resolvedSourceTag, + dataValidator, + ); + pipelineOutput = result as Record; + } else if (dataValidator) { + // Legacy adapter path: run a validator if provided, but skip transform orchestration. + const validated = await dataValidator(dataset); + pipelineOutput = validated as Record; + } + + return pipelineOutput; +} diff --git a/packages/vault-core/src/core/import/migrationMetadata.ts b/packages/vault-core/src/core/import/migrationMetadata.ts new file mode 100644 index 0000000000..f1f1a4ad2e --- /dev/null +++ b/packages/vault-core/src/core/import/migrationMetadata.ts @@ -0,0 +1,99 @@ +/** + * Utilities for producing metadata files that describe an adapter’s migration state. + * + * When we export data, we attach a JSON manifest so future imports know: + * - Which version tag the vault was on (ledger vs. adapter default) + * - Which tags are declared in the adapter today + * - When the export snapshot was taken + * + * Host tooling can read this file to pre-populate “source version” selectors, drive + * transform planning, or display drift warnings (ledger vs. declared versions). + */ +import type { Adapter } from '../adapter'; +import type { DrizzleDb } from '../db'; +import { ensureVaultLedgerTables, getVaultLedgerTag } from '../migrations'; + +export const MIGRATION_META_DIR = '__meta__'; +export const MIGRATION_META_FILENAME = 'migration.json'; + +/** + * Shape of the emitted metadata file (written as JSON under __meta__/ADAPTER/migration.json). + */ +export type MigrationMetadata = { + adapterId: string; + tag: string | null; + source: 'ledger' | 'adapter'; + ledgerTag: string | null; + latestDeclaredTag: string | null; + versions: string[]; + exportedAt: string; +}; + +/** + * Helper: fetch the last-applied tag for an adapter from the migration store. + * Returns undefined when the host does not provide a store or when no tag is stored yet, + * which signals downstream logic to fall back to adapter-declared versions. + */ +async function resolveLedgerTag( + adapterId: string, + db?: DrizzleDb, +): Promise { + if (!db) return undefined; + await ensureVaultLedgerTables(db); + return (await getVaultLedgerTag(db, adapterId)) ?? undefined; +} + +/** + * Produce migration metadata for a single adapter. + * + * Priority order for `tag`: + * 1. ledgerTag (vault-migrations table) when available + * 2. latest declared tag from the adapter manifest + * 3. null when neither exists (fresh adapter) + * + * `versions` is emitted as the manifest’s ordered tag list so consumers can plan forward chains. + */ +export async function createMigrationMetadata( + adapter: Adapter, + db?: DrizzleDb, + clock: () => Date = () => new Date(), +): Promise { + const versions = adapter.versions ?? []; + const declaredTags = versions.map((v) => v.tag); + const ledgerTag = await resolveLedgerTag(adapter.id, db); + const latestDeclaredTag = declaredTags.length + ? declaredTags[declaredTags.length - 1] + : undefined; + const resolvedTag = ledgerTag ?? latestDeclaredTag ?? null; + return { + adapterId: adapter.id, + tag: resolvedTag, + source: ledgerTag ? 'ledger' : 'adapter', + ledgerTag: ledgerTag ?? null, + latestDeclaredTag: latestDeclaredTag ?? null, + versions: declaredTags, + exportedAt: clock().toISOString(), + }; +} + +/** + * Convenience: build the on-disk metadata file alongside the in-memory metadata object. + * Returns both so callers can stash the file in an export archive and keep the parsed metadata. + */ +export async function createMigrationMetadataFile( + adapter: Adapter, + db?: DrizzleDb, + clock?: () => Date, +): Promise<{ path: string; file: File; metadata: MigrationMetadata }> { + const metadata = await createMigrationMetadata(adapter, db, clock); + const file = new File( + [JSON.stringify(metadata, null, 4)], + MIGRATION_META_FILENAME, + { type: 'application/json' }, + ); + return { + path: `${MIGRATION_META_DIR}/${adapter.id}/${MIGRATION_META_FILENAME}`, + file, + metadata, + }; +} diff --git a/packages/vault-core/src/core/importer.ts b/packages/vault-core/src/core/importer.ts deleted file mode 100644 index c8bf857fea..0000000000 --- a/packages/vault-core/src/core/importer.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { Type } from 'arktype'; -import type { ColumnsSelection } from 'drizzle-orm'; -import type { - SQLiteTable, - SubqueryWithSelection, -} from 'drizzle-orm/sqlite-core'; -import type { - Adapter, - ColumnDescriptions, - CompatibleDB, - DrizzleConfig, -} from './adapter'; - -export type View< - T extends string, - TSelection extends ColumnsSelection, - TSchema extends Record, - TDatabase extends CompatibleDB, -> = { - name: T; - definition: (db: TDatabase) => SubqueryWithSelection; -}; - -export interface Importer< - TID extends string = string, - TSchema extends Record = Record, - TDatabase extends CompatibleDB = CompatibleDB, - TParserShape extends Type = Type, - TParsed = TParserShape['infer'], -> { - /** Unique identifier for the importer (lowercase, no spaces, alphanumeric) */ - id: TID; - /** User-facing name */ - name: string; - /** Adapter (schema provider) */ - adapter: Adapter, TSchema>; - /** Column descriptions for every table/column */ - metadata: ColumnDescriptions; - /** ArkType schema for parsing/validation */ - validator: TParserShape; - /** Predefined views/CTEs for common queries */ - views?: { - [Alias in string]: View; - }; - /** Drizzle config for this schema (migrations, casing, etc.) */ - drizzleConfig: DrizzleConfig; - /** Parse a blob into a parsed representation */ - parse: (file: Blob) => Promise; - /** Upsert data into the database */ - upsert: (db: TDatabase, data: TParsed) => Promise; -} - -// Helper to compose an Importer from a web-safe Adapter + Node-only pieces -export type ImporterNodeParts< - TID extends string, - TSchema extends Record, - TDatabase extends CompatibleDB, - TParserShape extends Type, - TParsed, -> = Omit< - Importer, - 'id' | 'adapter' ->; - -export function defineImporter< - TID extends string, - TSchema extends Record, - TDatabase extends CompatibleDB, - TParserShape extends Type, - TParsed = TParserShape['infer'], ->( - adapter: Adapter, TSchema>, - parts: ImporterNodeParts, -): Importer { - return { - id: adapter.id, - adapter, - ...parts, - }; -} - -// Back-compat alias; prefer defineImporter going forward -export const composeImporter = defineImporter; diff --git a/packages/vault-core/src/core/index.ts b/packages/vault-core/src/core/index.ts index d8fc0434e5..b45b869931 100644 --- a/packages/vault-core/src/core/index.ts +++ b/packages/vault-core/src/core/index.ts @@ -1,3 +1,5 @@ export * from './adapter'; export * from './codec'; -export * from './importer'; +export * from './ingestor'; +export * from './migrations'; +export * from './vault'; diff --git a/packages/vault-core/src/core/ingestor.ts b/packages/vault-core/src/core/ingestor.ts new file mode 100644 index 0000000000..e0a4590456 --- /dev/null +++ b/packages/vault-core/src/core/ingestor.ts @@ -0,0 +1,24 @@ +/** + * An ingestor is responsible to parsing one or more input files into a normalized + * payload that can be validated and upserted by an Adapter. This is completely separate + * from the vault import/export lifecycle. + */ +export type Ingestor = { + /** Return true if this ingestor can handle the provided files */ + matches(file: File): boolean; + /** Parse files into a normalized payload expected by validator/upsert */ + parse(file: File): Promise; +}; + +/** + * Define an Ingestor with full type inference. + * + * @param ingestor The ingestor implementation + * @param T The shape of the parsed payload (default: unknown) + * @returns The same ingestor, with types inferred + */ +export function defineIngestor( + ingestor: Ingestor, +): Ingestor { + return ingestor; +} diff --git a/packages/vault-core/src/core/migrations.ts b/packages/vault-core/src/core/migrations.ts index 4cfcf0f6c3..3220491d5c 100644 --- a/packages/vault-core/src/core/migrations.ts +++ b/packages/vault-core/src/core/migrations.ts @@ -1,9 +1,21 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; +/** + * Environment-agnostic migration primitives for vault-core. + * + * This module intentionally performs no IO and imports no node: modules. + * Hosts must provide any filesystem/database access and drizzle-kit integration. + * + * Design: + * - Core accepts already-parsed data structures (journals, step metadata) and + * provides pure planning helpers and progress event types. + * - Execution is performed by host-injected strategies; core only defines shapes. + */ + +import type { InferInsertModel } from 'drizzle-orm'; +import { sql } from 'drizzle-orm'; import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; -import type { CompatibleDB } from './adapter'; -import type { Importer } from './importer'; +import type { DrizzleDb } from './db'; +/** Drizzle migration journal entry (parsed from meta/_journal.json by the host). */ export type JournalEntry = { /** Drizzle migration tag, usually the filename without extension (e.g., 0001_add_posts) */ tag: string; @@ -13,143 +25,890 @@ export type JournalEntry = { hash?: string; }; +/** Parsed Drizzle journal object (host-provided; core does not read files). */ export type MigrationJournal = { entries: JournalEntry[]; }; -export type SqlStep = { - tag: string; // matches journal tag - direction: 'up' | 'down'; - file: string; // absolute path to .sql -}; - +/** + * A pure planning result representing the ordered set of tags needed to move + * from the current tag (if any) to the target tag. Core does not include SQL + * statements here; hosts generate SQL via drizzle-kit as needed. + */ export type MigrationPlan = { - from?: string; // current DB version tag (if known) - to: string; // target version tag - steps: SqlStep[]; // ordered + /** Current DB version tag (if known). */ + from?: string; + /** Target version tag (must exist in the journal). */ + to: string; + /** Ordered list of tags to apply to reach the target. */ + tags: string[]; }; -/** Resolve the migrations directory for an importer. */ -export function resolveMigrationsDir(importer: Importer): string { - // Drizzle config usually sets `out` to a folder containing SQL files and meta/_journal.json - // TODO: Ensure this is absolute; if relative, decide base (adapter package dir vs process.cwd()). - const out = importer.drizzleConfig.out ?? ''; - if (!path.isAbsolute(out)) { - // Fallback: treat relative to process.cwd(); callers can pre-resolve if needed - return path.resolve(process.cwd(), out); - } - return out; -} - -/** Read Drizzle's meta/_journal.json */ -export async function readMigrationJournal( - migrationsDir: string, -): Promise { - const journalPath = path.join(migrationsDir, 'meta', '_journal.json'); - const raw = await fs.readFile(journalPath, 'utf8'); - const parsed = JSON.parse(raw) as { entries?: JournalEntry[] }; - return { entries: parsed.entries ?? [] }; -} - -/** List available SQL files and map to tags/directions. */ -export async function listSqlSteps(migrationsDir: string): Promise { - const files = await fs.readdir(migrationsDir); - const steps: SqlStep[] = []; - for (const f of files) { - if (!f.endsWith('.sql')) continue; - const full = path.join(migrationsDir, f); - // Heuristic: drizzle names like 0001_name.sql with two statements (up/down) or separate up/down files - // TODO: Detect drizzle style; for now assume paired files 0001_name.sql contains both up and down separated by comments. - // Placeholder: treat every .sql as an 'up' step (down not used in forward application). - const tag = f.replace(/\.sql$/, ''); - steps.push({ tag, direction: 'up', file: full }); - } - return steps; -} - -/** Compute a forward-only plan from current -> target using journal ordering. */ +/** + * Compute a forward-only plan from current -> target using journal ordering. + * Pure function: no IO, no environment assumptions. + */ export function planToVersion( journal: MigrationJournal, - allSteps: SqlStep[], currentTag: string | undefined, targetTag: string, ): MigrationPlan { const order = new Map(journal.entries.map((e, i) => [e.tag, i] as const)); - if (!order.has(targetTag)) { + + const targetIdx = order.get(targetTag); + if (targetIdx == null) { throw new Error(`Target migration tag not found in journal: ${targetTag}`); } + const currentIdx = currentTag != null ? (order.get(currentTag) ?? -1) : -1; - const targetIdx = order.get(targetTag); - if (targetIdx === undefined) { - throw new Error(`Target migration tag not found in journal: ${targetTag}`); + + if (currentIdx > targetIdx) { + // Downgrade paths are not supported by this planner; hosts can implement if needed. + throw new Error( + `Current tag (${currentTag}) is ahead of target tag (${targetTag}); downgrades are not supported in core planner.`, + ); } + const forward = journal.entries .slice(currentIdx + 1, targetIdx + 1) .map((e) => e.tag); - const steps = forward - .map((tag) => allSteps.find((s) => s.tag === tag && s.direction === 'up')) - .filter((s): s is SqlStep => !!s); - return { from: currentTag, to: targetTag, steps }; + + return { from: currentTag, to: targetTag, tags: forward }; } -/** Drop all tables owned by the importer (best-effort). */ -export async function dropAdapterTables( - db: CompatibleDB, - importer: Importer, +/** + * Progress event and reporter types for host-executed migrations. + * These are emitted by host executors while running the planned steps/tags. + */ +export type ProgressEvent = + | { + type: 'start'; + totalSteps: number; + } + | { + type: 'step'; + index: number; // 0-based index in the overall plan + tag: string; // current tag being applied + progress?: number; // optional 0..1 + message?: string; + } + | { + type: 'complete'; + } + | { + type: 'error'; + error: unknown; + }; + +/** Reporter callbacks; hosts pass an implementation to their executor. */ +export type ProgressReporter = { + onStart(event: Extract): void; + onStep(event: Extract): void; + onComplete(event: Extract): void; + onError(event: Extract): void; +}; + +/** + * Host integration points (shapes only; no implementation in core): + * + * - MigrationPlanner: optional advanced planner that can consider multiple adapter + * version tuples, curated steps, or drizzle-kit diffs to assemble a cross-plugin plan. + * This is left opaque. + * + * - MigrationExecutor: applies a plan to the injected database and emits ProgressReporter events. + * Core does not require a specific DB type here to remain environment-agnostic. + */ +export type MigrationPlanner = (...args: unknown[]) => MigrationPlan; + +export type MigrationExecutor = ( + plan: MigrationPlan, + report: ProgressReporter, +) => Promise; + +// ============================== +// Plan B: Inline migration (SQLite/libsql) execution helpers +// Design: keep core environment-agnostic; host supplies Drizzle internals via 'engine'. +// We skip validators on purpose and rely on 'squash' + differ to derive SQL. +// The implementation is commented out below but retained for potential future use. +// ============================== + +// /** Supported sqlite-like dialects for inline diffing. */ +// export type SqliteLikeDialect = 'sqlite' | 'libsql'; + +// /** Optional migration mode forwarded to differ; 'push' mirrors drizzle example semantics. */ +// export type MigrationMode = 'migrate' | 'push'; + +// /** Resolver input/output shapes kept generic yet typed; avoid any. */ +// export type TableResolverInput = { +// created?: unknown[]; +// deleted?: unknown[]; +// [k: string]: unknown; +// }; +// export type TableResolverOutput = { +// created: unknown[]; +// deleted: unknown[]; +// moved: unknown[]; +// renamed: unknown[]; +// }; + +// export type ColumnResolverInput = { +// tableName?: unknown; +// schema?: unknown; +// created?: unknown[]; +// deleted?: unknown[]; +// [k: string]: unknown; +// }; +// export type ColumnResolverOutput = { +// tableName?: unknown; +// schema?: unknown; +// created: unknown[]; +// deleted: unknown[]; +// renamed: unknown[]; +// }; + +// export type ViewResolverInput = { +// created?: unknown[]; +// deleted?: unknown[]; +// [k: string]: unknown; +// }; +// export type ViewResolverOutput = { +// created: unknown[]; +// deleted: unknown[]; +// moved: unknown[]; +// renamed: unknown[]; +// }; + +// /** A minimal set of resolvers used by sqlite/libsql snapshot diff functions. */ +// export type SqliteResolvers = { +// tablesResolver: ( +// input: TableResolverInput, +// ) => Promise | TableResolverOutput; +// columnsResolver: ( +// input: ColumnResolverInput, +// ) => Promise | ColumnResolverOutput; +// viewsResolver: ( +// input: ViewResolverInput, +// ) => Promise | ViewResolverOutput; +// /** Optional schemas resolver; not required by sqlite/libsql differ in current examples. */ +// schemasResolver?: ( +// input: TableResolverInput, +// ) => Promise | TableResolverOutput; +// }; + +// /** Default, non-interactive resolvers: create-only for new, drop-only for deleted, no renames/moves. */ +// export const defaultSqliteResolvers: SqliteResolvers = { +// tablesResolver(input: TableResolverInput) { +// Expect shape { created: T[], deleted: T[] } +// return { +// created: Array.isArray(input.created) ? input.created : [], +// deleted: Array.isArray(input.deleted) ? input.deleted : [], +// moved: [], +// renamed: [], +// }; +// }, +// columnsResolver(input: ColumnResolverInput) { +// Expect shape { tableName, created: T[], deleted: T[] } +// return { +// tableName: input.tableName, +// schema: input.schema, +// created: Array.isArray(input.created) ? input.created : [], +// deleted: Array.isArray(input.deleted) ? input.deleted : [], +// renamed: [], +// }; +// }, +// viewsResolver(input: ViewResolverInput) { +// Expect shape { created: T[], deleted: T[] } +// return { +// created: Array.isArray(input.created) ? input.created : [], +// deleted: Array.isArray(input.deleted) ? input.deleted : [], +// moved: [], +// renamed: [], +// }; +// }, +// }; + +// /** Result contract returned by drizzle snapshot differs that we use. */ +// export type SqlDiffResult = { +// sqlStatements: string[]; +// drizzle may also include auxiliary outputs, we keep them if present +// statements?: unknown; +// _meta?: unknown; +// }; + +// /** Function signature of sqlite snapshot differ (applySqliteSnapshotsDiff). */ +// export type ApplySqliteSnapshotsDiff = ( +// squashedPrev: unknown, +// squashedCur: unknown, +// tablesResolver: ( +// input: TableResolverInput, +// ) => Promise | TableResolverOutput, +// columnsResolver: ( +// input: ColumnResolverInput, +// ) => Promise | ColumnResolverOutput, +// viewsResolver: ( +// input: ViewResolverInput, +// ) => Promise | ViewResolverOutput, +// validatedPrev: unknown, +// validatedCur: unknown, +// mode?: 'push', +// ) => Promise | SqlDiffResult; + +// /** Function signature of libsql snapshot differ (applyLibSQLSnapshotsDiff). */ +// export type ApplyLibSQLSnapshotsDiff = ApplySqliteSnapshotsDiff; + +// /** Function signature of sqlite squasher (squashSqliteScheme). */ +// export type SquashSqliteScheme = (snapshot: unknown, mode?: 'push') => unknown; + +// /** Drizzle-engine functions needed to run sqlite/libsql diffs. Host provides these concretions. */ +// export type SqliteEngine = { +// squashSqliteScheme: SquashSqliteScheme; +// applySqliteSnapshotsDiff?: ApplySqliteSnapshotsDiff; +// applyLibSQLSnapshotsDiff?: ApplyLibSQLSnapshotsDiff; +// }; + +// /** Options for SQL generation helpers. */ +// export type GenerateSqlOptions = { +// mode?: MigrationMode; +// resolvers?: Partial; +// engine: SqliteEngine; +// }; + +// /** Merge user resolvers over defaults (shallow). */ +// function mergeResolvers( +// base: SqliteResolvers, +// partial?: Partial, +// ): SqliteResolvers { +// if (!partial) return base; +// return { +// tablesResolver: partial.tablesResolver ?? base.tablesResolver, +// columnsResolver: partial.columnsResolver ?? base.columnsResolver, +// viewsResolver: partial.viewsResolver ?? base.viewsResolver, +// schemasResolver: partial.schemasResolver ?? base.schemasResolver, +// }; +// } + +// /** +// * Pure SQL generator for SQLite using provided drizzle-engine internals. +// * prev/cur are raw snapshots; we skip validators and only squash. +// */ +// export async function generateSqlForSqlite( +// prev: unknown, +// cur: unknown, +// opts: GenerateSqlOptions, +// ): Promise { +// const { engine } = opts; +// if (!engine.squashSqliteScheme || !engine.applySqliteSnapshotsDiff) { +// throw new Error( +// 'SQLite differ not available: ensure engine.squashSqliteScheme and engine.applySqliteSnapshotsDiff are provided', +// ); +// } +// const modePush = opts.mode === 'push' ? 'push' : undefined; +// const resolvers = mergeResolvers(defaultSqliteResolvers, opts.resolvers); + +// const squashedPrev = engine.squashSqliteScheme(prev, modePush); +// const squashedCur = engine.squashSqliteScheme(cur, modePush); + +// const { sqlStatements } = await engine.applySqliteSnapshotsDiff( +// squashedPrev, +// squashedCur, +// resolvers.tablesResolver, +// resolvers.columnsResolver, +// resolvers.viewsResolver, +// We pass raw snapshots through as "validated" to avoid pulling validators +// prev, +// cur, +// modePush, +// ); + +// return sqlStatements ?? []; +// } + +// /** +// * Pure SQL generator for libSQL using provided drizzle-engine internals. +// * prev/cur are raw snapshots; we skip validators and only squash. +// */ +// export async function generateSqlForLibsql( +// prev: unknown, +// cur: unknown, +// opts: GenerateSqlOptions, +// ): Promise { +// const { engine } = opts; +// if (!engine.squashSqliteScheme || !engine.applyLibSQLSnapshotsDiff) { +// throw new Error( +// 'libSQL differ not available: ensure engine.squashSqliteScheme and engine.applyLibSQLSnapshotsDiff are provided', +// ); +// } +// const modePush = opts.mode === 'push' ? 'push' : undefined; +// const resolvers = mergeResolvers(defaultSqliteResolvers, opts.resolvers); + +// const squashedPrev = engine.squashSqliteScheme(prev, modePush); +// const squashedCur = engine.squashSqliteScheme(cur, modePush); + +// const { sqlStatements } = await engine.applyLibSQLSnapshotsDiff( +// squashedPrev, +// squashedCur, +// resolvers.tablesResolver, +// resolvers.columnsResolver, +// resolvers.viewsResolver, +// We pass raw snapshots through as "validated" to avoid pulling validators +// prev, +// cur, +// modePush, +// ); + +// return sqlStatements ?? []; +// } + +// /** Execution helper: sequentially run statements using provided executor. */ +// export async function executeSqlStatements( +// statements: string[], +// execute: (sql: string) => Promise, +// ): Promise { +// for (const sql of statements) { +// Execute in order; SQLite DDL may auto-commit, so we avoid wrapping in a single tx here. +// await execute(sql); +// } +// } + +// /** Snapshot provider per tag used by the orchestrator. */ +// export type SnapshotProvider = ( +// tag: string, +// ) => { prev: unknown; cur: unknown } | Promise<{ prev: unknown; cur: unknown }>; + +// /** Orchestrator options for running a plan end-to-end. */ +// export type RunInlineOptions = { +// dialect: SqliteLikeDialect; +// mode?: MigrationMode; +// engine: SqliteEngine; +// validate?: boolean; // reserved for future; unused because validators are skipped by design +// /** If true and 'execute' provided, statements are applied; otherwise dry-run returns statements only. */ +// apply?: boolean; +// /** Execution callback for applying SQL; required when apply is true. */ +// execute?: (sql: string) => Promise; +// /** Optional progress reporter from this module. */ +// reporter?: ProgressReporter; +// }; + +// /** Orchestrate plan execution: generate per-tag SQL and optionally apply via execute callback. */ +// export async function runPlannedMigrationsInline( +// plan: MigrationPlan, +// getSnapshotsByTag: SnapshotProvider, +// options: RunInlineOptions, +// ): Promise<{ byTag: Record }> { +// const { dialect, mode, engine, apply, execute, reporter } = options; + +// if (apply && !execute) { +// throw new Error('apply is true but no execute callback was provided'); +// } + +// reporter?.onStart({ type: 'start', totalSteps: plan.tags.length }); + +// const byTag: Record = {}; +// for (let i = 0; i < plan.tags.length; i++) { +// const tag = plan.tags[i]; + +// try { +// const pair = await getSnapshotsByTag(tag); +// const prev = pair.prev; +// const cur = pair.cur; + +// let statements: string[]; +// if (dialect === 'sqlite') { +// statements = await generateSqlForSqlite(prev, cur, { mode, engine }); +// } else if (dialect === 'libsql') { +// statements = await generateSqlForLibsql(prev, cur, { mode, engine }); +// } else { +// throw new Error( +// `Unsupported dialect for inline migrations: ${dialect}`, +// ); +// } + +// byTag[tag] = statements; + +// if (apply && execute && statements.length > 0) { +// for (const [idx, sql] of statements.entries()) { +// reporter?.onStep({ +// type: 'step', +// index: i, +// tag, +// progress: statements.length > 0 ? (idx + 1) / statements.length : 1, +// message: `Applying statement ${idx + 1} of ${statements.length}`, +// }); +// await execute(sql); +// } +// } else { +// reporter?.onStep({ +// type: 'step', +// index: i, +// tag, +// progress: 1, +// message: `Generated ${statements.length} statements (dry run)`, +// }); +// } +// } catch (error) { +// reporter?.onError({ type: 'error', error }); +// throw error; +// } +// } + +// reporter?.onComplete({ type: 'complete' }); + +// return { byTag }; +// } + +// ============================== +// Plan A: adapter versions, vault-managed ledger, startup SQL, and data transform chain +// ============================== + +/** Vault-managed migration tables: SQL schema strings hosts can execute. */ +export const VAULT_MIGRATIONS_SQL = ` +CREATE TABLE IF NOT EXISTS vault_migrations ( + adapter_id TEXT PRIMARY KEY, + current_tag TEXT NOT NULL, + updated_at INTEGER NOT NULL +); +`; + +export const VAULT_MIGRATION_JOURNAL_SQL = ` +CREATE TABLE IF NOT EXISTS vault_migration_journal ( + adapter_id TEXT NOT NULL, + tag TEXT NOT NULL, + applied_at INTEGER NOT NULL, + PRIMARY KEY (adapter_id, tag) +); +`; + +const VAULT_MIGRATIONS_TABLE = 'vault_migrations'; +const VAULT_MIGRATION_JOURNAL_TABLE = 'vault_migration_journal'; + +export async function ensureVaultLedgerTables(db: DrizzleDb): Promise { + await db.run(sql.raw(VAULT_MIGRATIONS_SQL)); + await db.run(sql.raw(VAULT_MIGRATION_JOURNAL_SQL)); +} + +export async function getVaultLedgerTag( + db: DrizzleDb, + adapterId: string, +): Promise { + await ensureVaultLedgerTables(db); + const row = await db.get<{ current_tag: string | null }>( + sql`SELECT current_tag FROM ${sql.raw(VAULT_MIGRATIONS_TABLE)} WHERE adapter_id = ${adapterId}`, + ); + return row?.current_tag ?? undefined; +} + +async function setVaultLedgerTag( + db: DrizzleDb, + adapterId: string, + tag: string, +): Promise { + await ensureVaultLedgerTables(db); + const timestamp = Date.now(); + await db.run( + sql`INSERT INTO ${sql.raw(VAULT_MIGRATIONS_TABLE)} (adapter_id, current_tag, updated_at) +VALUES (${adapterId}, ${tag}, ${timestamp}) +ON CONFLICT(adapter_id) DO UPDATE SET current_tag = excluded.current_tag, updated_at = excluded.updated_at`, + ); +} + +async function appendVaultLedgerJournal( + db: DrizzleDb, + adapterId: string, + tag: string, ): Promise { - // TODO: Use Drizzle metadata to get table names reliably; for now, list keys from schema object. - const schema = (importer.adapter.schema ?? {}) as Record; - // TODO: Acquire a raw SQL runner; Drizzle's libsql adapter doesn't expose raw execution directly here. - const runSql = getSqlRunner(db); - for (const [name] of Object.entries(schema)) { - // TODO: Quote name properly for SQLite identifiers - await runSql(`DROP TABLE IF EXISTS "${name}";`); + await ensureVaultLedgerTables(db); + const timestamp = Date.now(); + await db.run( + sql`INSERT INTO ${sql.raw(VAULT_MIGRATION_JOURNAL_TABLE)} (adapter_id, tag, applied_at) +VALUES (${adapterId}, ${tag}, ${timestamp}) +ON CONFLICT(adapter_id, tag) DO NOTHING`, + ); +} + +/** Build a pseudo-journal from a versions tuple to reuse planToVersion. */ +export function buildJournalFromVersions< + TVersions extends readonly VersionDef[], +>(versions: TVersions): MigrationJournal { + return { + entries: versions.map((v) => ({ tag: v.tag })), + }; +} + +/** Split a monolithic SQL string into executable statements. */ +function splitSqlText(text: string): string[] { + // Prefer explicit drizzle 'statement-breakpoint' markers if present + if (text.includes('--> statement-breakpoint')) { + return text + .split(/-->\s*statement-breakpoint\s*/g) + .map((s) => s.trim()) + .filter((s) => s.length > 0); } + // Fallback: split on semicolons at end of statements + return text + .split(/;\s*(?:\r?\n|$)/g) + .map((s) => s.trim()) + .filter((s) => s.length > 0) + .map((s) => (s.endsWith(';') ? s : `${s};`)); } -/** Inspect Drizzle migrations table for current version tag (implementation TBD). */ -export async function getCurrentDbMigrationTag( - db: CompatibleDB, - importer: Importer, -): Promise { - // TODO: Query importer.drizzleConfig.migrations?.table (default likely 'drizzle_migrations') to get last applied tag/name. - // This requires a query API or raw SQL; wire up a small query once the DB flavor is known. - throw new Error('Not implemented: getCurrentDbMigrationTag'); +/** + * Startup SQL migration runner for a single adapter. + * Forward-only: computes steps from the ledger's current tag to the latest version. + */ +export async function runStartupSqlMigrations< + TId extends string, + TVersions extends readonly VersionDef[], +>( + adapterId: TId, + versions: TVersions, + db: DrizzleDb, + reporter?: ProgressReporter, +): Promise<{ applied: string[] }> { + if (!versions || versions.length === 0) { + return { applied: [] }; + } + + await ensureVaultLedgerTables(db); + + const target = getLatestTag(versions); + const current = await getVaultLedgerTag(db, adapterId); + const plan = planToVersion( + buildJournalFromVersions(versions), + current, + target, + ); + + reporter?.onStart({ type: 'start', totalSteps: plan.tags.length }); + + const applied: string[] = []; + + for (let i = 0; i < plan.tags.length; i++) { + const tag = plan.tags[i]; + const ve = versions.find((v) => v.tag === tag); + if (!ve) { + const error = new Error(`Version entry not found for tag ${tag}`); + reporter?.onError({ type: 'error', error }); + throw error; + } + + const statements = ve.sql.flatMap((chunk) => splitSqlText(chunk)); + + if (statements.length === 0) { + reporter?.onStep({ + type: 'step', + index: i, + tag, + progress: 1, + message: 'No SQL statements for this version', + }); + } else { + for (const [idx, statement] of statements.entries()) { + reporter?.onStep({ + type: 'step', + index: i, + tag, + progress: (idx + 1) / statements.length, + message: `Applying statement ${idx + 1} of ${statements.length}`, + }); + await db.run(sql.raw(statement)); + } + } + + await appendVaultLedgerJournal(db, adapterId, tag); + await setVaultLedgerTag(db, adapterId, tag); + applied.push(tag); + } + + reporter?.onComplete({ type: 'complete' }); + + return { applied }; } -/** Apply a sequence of SQL files in order. */ -export async function applySqlPlan( - db: CompatibleDB, - importer: Importer, - plan: MigrationPlan, -) { - const runSql = getSqlRunner(db); - for (const step of plan.steps) { - const sql = await fs.readFile(step.file, 'utf8'); - // TODO: If a single file contains both up/down, split by sentinel comments. - // Importer-specific handling can occur here (e.g., feature flags, schema qualifiers) - void importer; // placeholder to acknowledge importer until used - await runSql(sql); +/** Additional metadata supplied to individual transform functions for better DX. */ +export type DataTransformContext = { + /** Target tag that the transform will produce. */ + toTag: Tag4; + /** Source tag feeding into this transform (previous tag or dataset tag). */ + fromTag?: string; + /** Optional initial source tag provided by the caller. */ + sourceTag?: string; + /** Final tag the chain is targeting. */ + targetTag: Tag4; + /** Zero-based index of this step in the plan. */ + index: number; + /** Total number of steps in the current transform plan. */ + total: number; + /** Whether this step is the final transform in the chain. */ + isLast: boolean; + /** Ordered list of target tags that will be applied (excludes the baseline). */ + plan: readonly Tag4[]; + /** Full adapter versions tuple for additional context. */ + versions: readonly VersionDef[]; +}; + +/** A data transform converts JSON shaped as version A to JSON shaped as version B (adapter-specific). */ +export type DataTransform = ( + input: unknown, + context: DataTransformContext, +) => unknown | Promise; + +/** + * Registry of per-version transforms: + * Map of toTag => transform that expects input of previous tag and produces output of toTag. + * Example: { '0001': t_0000_to_0001, '0002': t_0001_to_0002 } + */ +export type TransformRegistry = Record< + TTag, + DataTransform +>; + +/** Convenience alias for versions tuples to derive required transform keys. */ +export type TransformRegistryForVersions< + TVersions extends readonly VersionDef[], +> = TransformRegistry>; + +/** Determine the forward tag list using the versions tuple, from sourceTag (exclusive) to targetTag (inclusive). */ +export function computeForwardTagsFromVersions< + TVersions extends readonly VersionDef[], +>( + versions: TVersions, + sourceTag: string | undefined, + targetTag: string, +): string[] { + const j = buildJournalFromVersions(versions); + return planToVersion(j, sourceTag, targetTag).tags; +} + +/** + * Run the data transform chain from sourceTag -> latest version. + * The registry must contain a transform for each target tag in the forward plan. + */ +export async function runDataTransformChain< + TID extends string, + TVersions extends readonly VersionDef[], + TSchema extends Record, +>( + versions: TVersions, + registry: TransformRegistryForVersions, + input: { + [Key in keyof TSchema]: InferInsertModel[]; + }, + // If undefined, starts from the baseline (first version in the tuple) + sourceTag: string | undefined, + targetTag?: string, + reporter?: ProgressReporter, +): Promise { + const plannedTarget = (targetTag ?? getLatestTag(versions)) as Tag4; + const tags = computeForwardTagsFromVersions( + versions, + sourceTag, + plannedTarget, + ); + const plannedTags = tags.map((tag) => tag as Tag4); + const previousTagByTarget = new Map(); + for (let i = 0; i < versions.length; i++) { + const currentTag = versions[i]?.tag as Tag4; + const prev = versions[i - 1]?.tag; + previousTagByTarget.set(currentTag, prev); + } + + reporter?.onStart({ type: 'start', totalSteps: tags.length }); + + let acc: unknown = input; + type RequiredTags = RequiredTransformTags; + for (let i = 0; i < plannedTags.length; i++) { + const toTag = plannedTags[i]; + const fn = registry[toTag as RequiredTags]; + if (!fn) { + const err = new Error(`Missing transform for target tag ${toTag}`); + reporter?.onError({ type: 'error', error: err }); + throw err; + } + const fromTag = + i === 0 + ? (sourceTag ?? previousTagByTarget.get(toTag)) + : plannedTags[i - 1]; + acc = await fn(acc, { + toTag, + fromTag, + sourceTag, + targetTag: plannedTarget, + index: i, + total: plannedTags.length, + isLast: i === plannedTags.length - 1, + plan: plannedTags, + versions, + }); + reporter?.onStep({ + type: 'step', + index: i, + tag: toTag, + progress: 1, + message: `Transformed to ${toTag}`, + }); } + + reporter?.onComplete({ type: 'complete' }); + return acc; } -/** Mark a migration tag as applied in the Drizzle migrations table (implementation TBD). */ -export async function markApplied( - db: CompatibleDB, - importer: Importer, - toTag: string, -) { - // TODO: Insert into migrations table or update state consistent with drizzle-orm expectations. - throw new Error('Not implemented: markApplied'); +function getLatestTag[]>( + versions: TVersions, +): TVersions[number]['tag'] { + return versions + .map((v) => [v.tag, Number.parseInt(v.tag, 10)] as const) + .sort((a, b) => a[1] - b[1])[0][0]; } -/** Obtain a raw SQL runner from a CompatibleDB. Placeholder until concrete DBs are wired. */ -export function getSqlRunner( - _db: CompatibleDB, -): (sql: string) => Promise { - // TODO: For libsql: keep a handle to the underlying client and call client.execute(sql) - // TODO: For better-sqlite3: use db.exec(sql) - return async (_sql: string) => { - throw new Error('Not implemented: raw SQL execution for migrations'); - }; +// ============================== +// Version tuple type-safety helpers (authoring-time) +// ============================== + +/** Single decimal digit literal. */ +export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; + +/** Four-digit tag, e.g. '0000', '0001'. */ +export type Tag4 = `${Digit}${Digit}${Digit}${Digit}`; + +/** Version definition for adapter-managed migrations (stricter than runtime). */ +export type VersionDef = { + /** Four-digit version tag (e.g., '0001'). Must be unique within the tuple. */ + tag: TTag; + /** Inline array of statements (preferred for environment-agnostic bundles) */ + sql: string[]; +}; + +/** Tuple utilities */ +type LastOfTuple = T extends readonly [ + ...infer _, + infer L, +] + ? L + : never; + +type FirstOfTuple = T extends readonly [ + infer F, + ...unknown[], +] + ? F + : never; + +/** Extract the union of tags from a version tuple. */ +export type VersionTags[]> = + TVersions[number]['tag']; + +/** First tag from versions tuple. */ +export type FirstTag[]> = + FirstOfTuple extends VersionDef + ? FirstOfTuple['tag'] + : never; + +/** Tag tuple derived from a VersionDef tuple. */ +export type VersionTagTuple[]> = { + [K in keyof TVersions]: TVersions[K] extends VersionDef< + infer TTag extends Tag4 + > + ? TTag + : never; +}; + +/** Tuple of required forward transform tags (all tags except the first/baseline). */ +export type RequiredTransformTagTuple< + TVersions extends readonly VersionDef[], +> = TVersions extends readonly [VersionDef, ...infer Rest] + ? Rest extends readonly VersionDef[] + ? { + [K in keyof Rest]: Rest[K] extends VersionDef + ? Tag + : never; + } + : [] + : []; + +/** + * Compute the tags that require data transforms: + * all version tags except the first (baseline). + */ +export type RequiredTransformTags< + TVersions extends readonly VersionDef[], +> = RequiredTransformTagTuple[number]; + +/** + * Authoring-time transform registry keyed by the required transform tags. + * Example: + * const transforms: TransformRegistryForVersions = + * { '0001': fn, '0002': fn }; + */ + +/** + * Helper to define a transform registry with compile-time keys. + * Provide the union of tags you must cover: + * const transforms = defineTransformRegistry({ '0001': fn, '0002': fn }); + * + * Each transform receives a {@link DataTransformContext} describing the plan, + * which enables richer DX (branching, instrumentation, etc.). + */ +export function defineTransformRegistry< + TRegistry extends Partial>, +>(registry: TRegistry): TRegistry { + return registry; +} + +/** Validate transformed data using an injected validator (e.g., drizzle-arktype). Returns morphed value. */ +export type DataValidator = (value: unknown) => unknown | Promise; + +/** Run chain then validate; returns morphed/validated data if no exception is thrown. */ +export async function transformAndValidate< + TID extends string, + TVersions extends readonly VersionDef[], + TSchema extends Record, +>( + versions: TVersions, + registry: TransformRegistryForVersions, + input: { + [Key in keyof TSchema]: InferInsertModel[]; + }, + sourceTag: string | undefined, + validator?: DataValidator, + targetTag?: string, + reporter?: ProgressReporter, +): Promise { + const transformed = await runDataTransformChain( + versions, + registry, + input, + sourceTag, + targetTag, + reporter, + ); + const validated = validator ? await validator(transformed) : transformed; + return validated; } + +/** + * Helper to define a versions tuple without needing "as const" at call sites. + * Preserves literal tag types across the tuple. + * + * Example: + * const versions = defineVersions( + * { tag: '0000', sqlText: 'CREATE TABLE ...;' }, + * { tag: '0001', sqlText: '' }, + * ); + */ +export function defineVersions[]>( + ...versions: TVersions +): TVersions { + return versions; +} + +/** + * Derive the union of adapter table names from any shape that exposes a "schema" record. + * This avoids coupling to the Adapter type while allowing type-safe table-name usage in hosts. + * + * Example: + * type Tables = AdapterTableNames; + * // Tables is the union of keys of adapter.schema (as strings) + */ +export type AdapterTableNames = A extends { schema: Record } + ? Extract + : never; diff --git a/packages/vault-core/src/core/strip.ts b/packages/vault-core/src/core/strip.ts index ccf44e5640..8de1de3e37 100644 --- a/packages/vault-core/src/core/strip.ts +++ b/packages/vault-core/src/core/strip.ts @@ -1,5 +1,7 @@ -import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core'; +import type { AnyColumn } from 'drizzle-orm'; + import type { ColumnDescriptions } from './adapter'; +import type { AnyTable } from './db'; export type ReadableColumnInfo = { name: string; @@ -18,11 +20,11 @@ export type ReadableTableInfo = { * * When metadata is provided, matching table/column descriptions are included under `description`. */ -export function readableSchemaInfo>( +export function readableSchemaInfo>( schema: TSchema, metadata?: ColumnDescriptions, ): ReadableTableInfo[] { - const tables = Object.entries(schema) as [string, SQLiteTable][]; + const tables = Object.entries(schema); return tables.map(([name, table]) => ({ name, columns: readableTableInfo( @@ -33,10 +35,10 @@ export function readableSchemaInfo>( } function readableTableInfo( - table: SQLiteTable, + table: AnyTable, tableMetadata?: Record, ) { - const columns = Object.entries(table) as [string, SQLiteColumn][]; + const columns = Object.entries(table._.columns); return columns.map(([name, col]) => readableColumnInfo(name, col, tableMetadata?.[name]), ); @@ -44,7 +46,7 @@ function readableTableInfo( function readableColumnInfo( name: string, - column: SQLiteColumn, + column: AnyColumn, description?: string, ): ReadableColumnInfo { // Add other fields here we wish to expose @@ -52,6 +54,6 @@ function readableColumnInfo( name, type: column.dataType, nullable: !column.notNull, - } as const; + }; return description !== undefined ? { ...base, description } : base; } diff --git a/packages/vault-core/src/core/vault.ts b/packages/vault-core/src/core/vault.ts new file mode 100644 index 0000000000..c27d3ba9bd --- /dev/null +++ b/packages/vault-core/src/core/vault.ts @@ -0,0 +1,300 @@ +import type { Adapter, UniqueAdapters } from './adapter'; +import { + defaultConvention, + listColumns, + listPrimaryKeys, + listTables, +} from './codec'; +import type { + AdapterIDs, + AdapterTableMap, + CoreOptions, + ExportOptions, + ImportOptions, + IngestOptions, + Vault, +} from './config'; +import type { CompatibleDB } from './db'; +import { runImportPipeline } from './import/importPipeline'; +import { + createMigrationMetadataFile, + MIGRATION_META_DIR, +} from './import/migrationMetadata'; +import { runStartupSqlMigrations } from './migrations'; + +/** Minimal Drizzle-DB type expected by core. Hosts pass a concrete Drizzle instance. */ +export type DrizzleDb = CompatibleDB; + +/** + * Construct a Vault bound to a Drizzle DB. No IO; pure orchestration. + */ +export function createVault( + options: CoreOptions, +): Vault { + const db = options.database; + + // Ensure migrations have been applied before we touch adapter tables. + async function ensureMigrationsUpToDate(adapter: Adapter) { + const versions = adapter.versions; + if (!versions || versions.length === 0) return; + await runStartupSqlMigrations(adapter.id, versions, db); + } + + // Standard Schema validation runner + async function runValidation( + adapter: Adapter, + value: unknown, + ): Promise { + const { validator } = adapter; + if (!validator) + throw new Error( + `validation required: adapter '${adapter.id}' has no validator`, + ); + + const result = await validator['~standard'].validate(value); + if (result.issues) + throw new Error( + `importData: validation failed for adapter '${adapter.id}': ${result.issues + .map((i) => `${i.path ?? ''} ${i.message ?? ''}`.trim()) + .join('; ')}`, + ); + return (result as unknown as { value?: unknown }).value ?? value; + } + + /** + * Ensure no duplicate adapter IDs at runtime (covers non-literal arrays) + * @throws if duplicate IDs found + */ + function ensureNoDuplicateAdapterIds( + adapters: UniqueAdapters, + ) { + const seen = new Set(); + for (const a of adapters) { + if (seen.has(a.id)) + throw new Error( + `createVault: duplicate adapter ID found at runtime: '${a.id}'`, + ); + seen.add(a.id); + } + } + + /** + * Drop all rows from a table by name, then insert all provided rows. + * @throws if table not found in adapter schema + */ + async function replaceAdapterTables( + adapter: Adapter, + dataset: Record, + ) { + const { schema } = adapter; + for (const [tableName, rows] of Object.entries(dataset)) { + const table = schema[tableName as keyof typeof schema]; + if (!table) + throw new Error( + `replaceAdapterTables: unknown table ${tableName} for adapter '${adapter.id}'`, + ); + + await db.delete(table); + for (const row of rows) { + await db.insert(table).values([row]); + } + } + } + + return { + async exportData(opts: ExportOptions) { + const { + adapterIDs, + codec, + conventions: conv = defaultConvention(), + } = opts; + const adapters = + adapterIDs === undefined + ? options.adapters + : options.adapters.filter((a) => adapterIDs.includes(a.id)); + + ensureNoDuplicateAdapterIds(adapters); + + const files = new Map(); + + // Iterate over each adapter + for (const adapter of adapters) { + await ensureMigrationsUpToDate(adapter); + const { schema } = adapter; + const adapterId = adapter.id; + + // Iterate over each table in the adapter's schema + for (const [tableName, table] of listTables(schema)) { + // Select for all rows from the table + const rows = await db.select().from(table); + + const pkCols = listPrimaryKeys(tableName, table); + const tableCols = new Set(listColumns(table).map(([name]) => name)); + + for (const row of rows) { + // Build a flat record deterministically for the codec + const rec: Record = {}; + for (const [k, v] of Object.entries(row)) { + if (!tableCols.has(k)) continue; + if (v === undefined || v === null) continue; + rec[k] = codec.normalize ? codec.normalize(v, k) : v; + } + + // Compute path using PK values + const pkValues = pkCols.map(([name]) => row[name]); + const basePath = conv.pathFor(adapterId, tableName, pkValues); + + const path = `${basePath}.${codec.fileExtension}`; + const filename = path.split('/').pop(); + if (!filename) throw new Error('invalid filename'); + const text = codec.stringify(rec); + const file = new File([text], filename, { + type: codec.mimeType, + }); + + files.set(path, file); + } + } + const { path: metaPath, file: metaFile } = + await createMigrationMetadataFile(adapter, db); + files.set(metaPath, metaFile); + } + + return files; + }, + async importData(opts: ImportOptions) { + const { adapterID, files, codec } = opts; + const adapter = options.adapters.find((a) => a.id === adapterID); + if (!adapter) + throw new Error(`importData: unknown adapter ID '${adapterID}'`); + + await ensureMigrationsUpToDate(adapter); + + const { schema, id: adapterId } = adapter; + // Build one huge object with all data, run validator, then upsert in one call + + // Collect rows per dataset key for a single upsert call + const dataset: Record = {}; + let detectedTag: string | undefined; + + for (const [path, input] of files) { + const text = await input.text(); + + const parts = path.split('/').filter((segment) => segment.length > 0); + const metaIndex = parts.indexOf(MIGRATION_META_DIR); + // TODO clean this up + if (metaIndex !== -1) { + try { + const parsed = codec.parse(text) as { tag?: string }; + if (typeof parsed?.tag === 'string') detectedTag = parsed.tag; + } catch { + // ignore malformed metadata + } + continue; + } + + if (parts.length < 2) continue; // No nested paths supported + + const tableName = parts[0]; + + // Check that file extension matches codec + const file = parts.slice(1).join('/'); + const dot = file.indexOf('.'); + if (dot < 0) + throw new Error(`importData: file ${path} has no extension`); + const ext = file.slice(dot + 1); + if (ext !== codec.fileExtension) + throw new Error( + `importData: file ${path} has wrong extension (expected ${codec.fileExtension})`, + ); + + // Find matching table in schema + const table = schema[tableName as keyof typeof schema]; + if (!table) throw new Error(`importData: unknown table ${tableName}`); + + // Parse file text into a record + const rec = codec.parse(text); + const row: Record = {}; + const tableCols = new Set(listColumns(table).map(([name]) => name)); + for (const [k, v] of Object.entries(rec ?? {})) { + if (!tableCols.has(k)) continue; + row[k] = codec.denormalize ? codec.denormalize(v, k) : v; + } + + const key = tableName.slice(adapterId.length + 1); + + // Initialize bucket if needed + dataset[key] ??= []; + + const bucket = dataset[key]; + bucket.push(row); + } + + const pipelineOutput = await runImportPipeline({ + adapter, + dataset, + transformsOverride: opts.transforms, + versionsOverride: opts.versions, + dataValidator: undefined, + sourceTag: opts.sourceTag, + detectedTag, + }); + + const schemaValidator = opts.dataValidator; + if (!schemaValidator) + throw new Error( + `importData: dataValidator (drizzle-arktype) is required for adapter '${adapter.id}'`, + ); + + const validatedDataset = await schemaValidator(pipelineOutput); + await replaceAdapterTables( + adapter, + validatedDataset as Record, + ); + }, + async ingestData(opts: IngestOptions) { + const adapter = opts.adapter; + const file = opts.file; + + ensureNoDuplicateAdapterIds([adapter]); + await ensureMigrationsUpToDate(adapter); + + if (!adapter.ingestors || adapter.ingestors.length === 0) + throw new Error( + `ingestData: adapter '${adapter.id}' has no ingestors configured`, + ); + + const ingestor = adapter.ingestors.find((i) => { + try { + return i.matches(file); + } catch { + return false; + } + }); + if (!ingestor) + throw new Error( + `ingestData: no ingestor matched file '${file.name}' for adapter '${adapter.id}'`, + ); + + const dataset = await ingestor.parse(file); + + // Run validation and use morphed value + const validated = await runValidation(adapter, dataset); + await replaceAdapterTables( + adapter, + validated as Record, + ); + }, + getQueryInterface() { + // Populate a map of adapter ID -> table name -> table object + const tables = {} as AdapterTableMap; + for (const adapter of options.adapters) { + tables[adapter.id as AdapterIDs] = adapter.schema; + } + return { + db, + tables, + }; + }, + }; +} diff --git a/packages/vault-core/src/utils/format/toml/index.ts b/packages/vault-core/src/utils/format/toml/index.ts new file mode 100644 index 0000000000..e9ac10b0e9 --- /dev/null +++ b/packages/vault-core/src/utils/format/toml/index.ts @@ -0,0 +1,14 @@ +import { parse } from 'toml'; + +export type TomlNamespace = { + /** Minimal TOML parser/stringifier. + * @see {@link https://toml.io/en/v1.0.0 TOML specification} + */ + parse(text: string): Record; + // I guess TOML stringification is not really a thing?? + // stringify(data: Record): string; +}; + +export const TOML = { + parse, +} satisfies TomlNamespace; From ee7ca2584b3ef61b2ddb38952c28bcd1897de998 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 7 Oct 2025 20:39:04 +0000 Subject: [PATCH 13/21] tests: added core tests & fixes --- bun.lock | 881 ++++++++++-------- ...1003T220750 vault-core-minimal-overview.md | 80 +- packages/vault-core/README.md | 242 ++--- packages/vault-core/package.json | 2 + packages/vault-core/src/adapters/index.ts | 1 - packages/vault-core/src/core/adapter.ts | 127 ++- packages/vault-core/src/core/codec.ts | 6 +- packages/vault-core/src/core/config.ts | 43 +- .../src/core/import/importPipeline.ts | 10 +- .../src/core/import/migrationMetadata.ts | 8 +- packages/vault-core/src/core/migrations.ts | 224 +++-- packages/vault-core/src/core/vault.spec.ts | 159 ++++ packages/vault-core/src/core/vault.ts | 271 ++++-- .../tests/export-import-roundtrip.spec.ts | 104 +++ .../vault-core/tests/fixtures/testAdapter.ts | 123 +++ .../vault-core/tests/import-paths.spec.ts | 135 +++ .../tests/transforms-alignment.spec.ts | 62 ++ packages/vault-core/tsconfig.json | 44 + 18 files changed, 1759 insertions(+), 763 deletions(-) delete mode 100644 packages/vault-core/src/adapters/index.ts create mode 100644 packages/vault-core/src/core/vault.spec.ts create mode 100644 packages/vault-core/tests/export-import-roundtrip.spec.ts create mode 100644 packages/vault-core/tests/fixtures/testAdapter.ts create mode 100644 packages/vault-core/tests/import-paths.spec.ts create mode 100644 packages/vault-core/tests/transforms-alignment.spec.ts create mode 100644 packages/vault-core/tsconfig.json diff --git a/bun.lock b/bun.lock index c7e8be30c5..d85cc3c2bf 100644 --- a/bun.lock +++ b/bun.lock @@ -144,7 +144,7 @@ }, "apps/whispering": { "name": "@repo/whispering", - "version": "7.5.4", + "version": "7.5.5", "dependencies": { "@anthropic-ai/sdk": "^0.55.0", "@aptabase/tauri": "^0.4.1", @@ -322,6 +322,7 @@ "yaml": "^2.8.1", }, "devDependencies": { + "tsx": "^4.20.6", "typescript": "catalog:", }, }, @@ -384,25 +385,31 @@ "@aptabase/web": ["@aptabase/web@0.4.3", "", {}, "sha512-IcFGvgEcc26chrRDmnOxzH/HLeNexrAoDx2DjNFAoTjfZCSJmxrABqseVLlTI7JeGKfDdIVI+IC3AWj5v+2J0A=="], - "@ark/schema": ["@ark/schema@0.46.0", "", { "dependencies": { "@ark/util": "0.46.0" } }, "sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ=="], + "@ark/regex": ["@ark/regex@0.0.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg=="], - "@ark/util": ["@ark/util@0.46.0", "", {}, "sha512-JPy/NGWn/lvf1WmGCPw2VGpBg5utZraE84I7wli18EDF3p3zc/e9WolT35tINeZO3l7C77SjqRJeAUoT0CvMRg=="], + "@ark/schema": ["@ark/schema@0.50.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ=="], - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + "@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], - "@astrojs/check": ["@astrojs/check@0.9.4", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.0.5", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.1" } }, "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ=="], - "@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="], + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.2", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.2" } }, "sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ=="], - "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.2", "", {}, "sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g=="], + "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - "@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="], + "@astrojs/check": ["@astrojs/check@0.9.5", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-88vc8n2eJ1Oua74yXSGo/8ABMeypfQPGEzuoAx2awL9Ju8cE6tZ2Rz9jVx5hIExHK5gKVhpxfZj4WXm7e32g1w=="], - "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.6", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.2", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.4", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bwylYktCTsLMVoCOEHbn2GSUA3c5KT/qilekBKA3CBng0bo1TYjNZPr761vxumRk9kJGqTOtU+fgCAp5Vwokug=="], + "@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="], + + "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.4", "", {}, "sha512-lDA9MqE8WGi7T/t2BMi+EAXhs4Vcvr94Gqx3q15cFEz8oFZMO4/SFBqYr/UcmNlvW+35alowkVj+w9VhLvs5Cw=="], + + "@astrojs/language-server": ["@astrojs/language-server@2.15.5", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.23", "@volar/language-core": "~2.4.23", "@volar/language-server": "~2.4.23", "@volar/language-service": "~2.4.23", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.65", "volar-service-emmet": "0.0.65", "volar-service-html": "0.0.65", "volar-service-prettier": "0.0.65", "volar-service-typescript": "0.0.65", "volar-service-typescript-twoslash-queries": "0.0.65", "volar-service-yaml": "0.0.65", "vscode-html-languageservice": "^5.5.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-GizawjrIytYEOv8c/VUDrzGYo5t584w6S0fUAmVK2u11BtAZcbtXWNjGtwFWYLyR27J9pZe4Ipe/qP8mXUfCWQ=="], + + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.8", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.4", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.13.0", "smol-toml": "^1.4.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-uFNyFWadnULWK2cOw4n0hLKeu+xaVWeuECdP10cQ3K2fkybtTlhb7J7TcScdjmS8Yps7oje9S/ehYMfZrhrgCg=="], "@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], - "@astrojs/svelte": ["@astrojs/svelte@7.1.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.3", "svelte2tsx": "^0.7.39", "vite": "^6.3.5" }, "peerDependencies": { "astro": "^5.0.0", "svelte": "^5.1.16", "typescript": "^5.3.3" } }, "sha512-nNAO7iFgCZXCN31N4xBSS/k7vZAZxeZ/v8V6VWZOKG47gVlxeAJBHzn2GlXMMVkxIamr6dhrkDrhYFKIPzoGpw=="], + "@astrojs/svelte": ["@astrojs/svelte@7.2.0", "", { "dependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", "svelte2tsx": "^0.7.42", "vite": "^6.3.6" }, "peerDependencies": { "astro": "^5.0.0", "svelte": "^5.1.16", "typescript": "^5.3.3" } }, "sha512-6AbtExkKc+m0tHR7Plf4bd3Myx0FMHmAICFfp4eAlv8IavjFmZMIjosRvm2+1l8MTH80p+cQxQmo/R3K+RvXlw=="], "@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="], @@ -412,11 +419,15 @@ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], - "@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], - "@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "@better-auth/core": ["@better-auth/core@1.3.28", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "better-sqlite3": "^12.4.1", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-iZOGKlXaNEIEj0Q3z7+REE94I89YUJ0sel/1pvm1qqdHkm59G+ToTysHtyTcLYby3+UtAeJRKyFAY0nwJH0H7A=="], - "@better-auth/utils": ["@better-auth/utils@0.2.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.3.28", "", { "dependencies": { "@better-auth/core": "1.3.28", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" } }, "sha512-qZtV82IFuyQZc2c37VkiDgO/qfqPnJuWIyeC/iFK1AA5N8RSuC2+CVIH1sNDytPXUAthbYeOzcOCW2YEkgz1Ow=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], @@ -438,7 +449,7 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ=="], - "@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="], + "@capsizecss/unpack": ["@capsizecss/unpack@3.0.0", "", { "dependencies": { "fontkit": "^2.0.2" } }, "sha512-+ntATQe1AlL7nTOYjwjj6w3299CgRot48wL761TUGYpYgAou3AaONZazp0PKZyCyWhudWsjhq1nvRHOvbMzhTA=="], "@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="], @@ -446,30 +457,32 @@ "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], - "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.6.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.19", "workerd": "^1.20250802.0" }, "optionalPeers": ["workerd"] }, "sha512-C7/tW7Qy+wGOCmHXu7xpP1TF3uIhRoi7zVY7dmu/SOSGjPilK+lSQ2lIRILulZsT467ZJNlI0jBxMbd8LzkGRg=="], + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.7.7", "", { "peerDependencies": { "unenv": "2.0.0-rc.21", "workerd": "^1.20250927.0" }, "optionalPeers": ["workerd"] }, "sha512-HtZuh166y0Olbj9bqqySckz0Rw9uHjggJeoGbDx5x+sgezBXlxO6tQSig2RZw5tgObF8mWI8zaPvQMkQZtAODw=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250816.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yN1Rga4ufTdrJPCP4gEqfB47i1lWi3teY5IoeQbUuKnjnCtm4pZvXur526JzCmaw60Jx+AEWf5tizdwRd5hHBQ=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251008.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-yph0H+8mMOK5Z9oDwjb8rI96oTVt4no5lZ43aorcbzsWG9VUIaXSXlBBoB3von6p4YCRW+J3n36fBM9XZ6TLaA=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250816.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WyKPMQhbU+TTf4uDz3SA7ZObspg7WzyJMv/7J4grSddpdx2A4Y4SfPu3wsZleAOIMOAEVi0A1sYDhdltKM7Mxg=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251008.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Yc4lMGSbM4AEtYRpyDpmk77MsHb6X2BSwJgMgGsLVPmckM7ZHivZkJChfcNQjZ/MGR6nkhYc4iF6TcVS+UMEVw=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250816.0", "", { "os": "linux", "cpu": "x64" }, "sha512-NWHOuFnVBaPRhLHw8kjPO9GJmc2P/CTYbnNlNm0EThyi57o/oDx0ldWLJqEHlrdEPOw7zEVGBqM/6M+V9agC6w=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20251008.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AjoQnylw4/5G6SmfhZRsli7EuIK7ZMhmbxtU0jkpciTlVV8H01OsFOgS1d8zaTXMfkWamEfMouy8oH/L7B9YcQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250816.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-FR+/yhaWs7FhfC3GKsM3+usQVrGEweJ9qyh7p+R6HNwnobgKr/h5ATWvJ4obGJF6ZHHodgSe+gOSYR7fkJ1xAQ=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20251008.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hRy9yyvzVq1HsqHZUmFkAr0C8JGjAD/PeeVEGCKL3jln3M9sNCKIrbDXiL+efe+EwajJNNlDxpO+s30uVWVaRg=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250816.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0lqClj2UMhFa8tCBiiX7Zhd5Bjp0V+X8oNBG6V6WsR9p9/HlIHAGgwRAM7aYkyG+8KC8xlbC89O2AXUXLpHx0g=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20251008.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Gm0RR+ehfNMsScn2pUcn3N9PDUpy7FyvV9ecHEyclKttvztyFOcmsF14bxEaSVv7iM4TxWEBn1rclmYHxDM4ow=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20250820.0", "", {}, "sha512-WiGPOVlejDGlKRsTbaREkRKm8Xo5nHtucq5AmR/ObvKmaFOVVv0OQCW5Xht2YQgV0BfYPb/QEkO8oFjDXjBMZg=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20251014.0", "", {}, "sha512-tEW98J/kOa0TdylIUOrLKRdwkUw0rvvYVlo+Ce0mqRH3c8kSoxLzUH9gfCvwLe0M89z1RkzFovSKAW2Nwtyn3w=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="], + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], - "@csstools/css-color-parser": ["@csstools/css-color-parser@3.0.10", "", { "dependencies": { "@csstools/color-helpers": "^5.0.2", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg=="], + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.14", "", { "peerDependencies": { "postcss": "^8.4" } }, "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q=="], + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], @@ -478,7 +491,7 @@ "@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="], - "@emmetio/css-parser": ["@emmetio/css-parser@0.4.0", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw=="], + "@emmetio/css-parser": ["@emmetio/css-parser@github:ramya-rao-a/css-parser#370c480", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "ramya-rao-a-css-parser-370c480"], "@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="], @@ -488,7 +501,7 @@ "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], - "@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], "@epicenter/api": ["@epicenter/api@workspace:apps/api"], @@ -562,17 +575,17 @@ "@eslint/compat": ["@eslint/compat@1.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg=="], - "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="], "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], - "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="], + "@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], @@ -580,11 +593,11 @@ "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.3", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@fontsource-variable/manrope": ["@fontsource-variable/manrope@5.2.6", "", {}, "sha512-8a0INmmJfrcOO+MDThDefifCqreErfhNX1qy9l9VMc0FNCUTJtJi8Lclk8H5RqsDmR5PBYfTot1iUZFZ7qX57A=="], + "@fontsource-variable/manrope": ["@fontsource-variable/manrope@5.2.8", "", {}, "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw=="], "@google/generative-ai": ["@google/generative-ai@0.24.1", "", {}, "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q=="], @@ -600,58 +613,64 @@ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.3" }, "os": "darwin", "cpu": "x64" }, "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.3", "", { "os": "linux", "cpu": "arm" }, "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.3" }, "os": "linux", "cpu": "arm" }, "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.3" }, "os": "linux", "cpu": "s390x" }, "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" }, "os": "linux", "cpu": "arm64" }, "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.3" }, "os": "linux", "cpu": "x64" }, "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.4", "", { "dependencies": { "@emnapi/runtime": "^1.5.0" }, "cpu": "none" }, "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA=="], - "@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="], + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig=="], + + "@internationalized/date": ["@internationalized/date@3.10.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw=="], "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -660,7 +679,7 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], @@ -702,11 +721,11 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@neondatabase/serverless": ["@neondatabase/serverless@1.0.1", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-O6yC5TT0jbw86VZVkmnzCZJB0hfxBl0JJz6f+3KHoZabjb/X08r9eFA+vuY06z1/qaovykvdkrXYq3SPUuvogA=="], + "@neondatabase/serverless": ["@neondatabase/serverless@1.0.2", "", { "dependencies": { "@types/node": "^22.15.30", "@types/pg": "^8.8.0" } }, "sha512-I5sbpSIAHiB+b6UttofhrN/UJXII+4tZPAq1qugzwCwLIL8EZLV7F/JyHUrEIiGgQpEXzpnjlJ+zwcEhheGvCw=="], - "@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="], + "@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="], - "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -756,15 +775,29 @@ "@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="], - "@peculiar/asn1-android": ["@peculiar/asn1-android@2.4.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.4.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg=="], + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw=="], - "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.4.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.4.0", "@peculiar/asn1-x509": "^2.4.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ=="], + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.5.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-pfx": "^2.5.0", "@peculiar/asn1-pkcs8": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "@peculiar/asn1-x509-attr": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A=="], - "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.4.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.4.0", "@peculiar/asn1-x509": "^2.4.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img=="], + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q=="], - "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.4.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ=="], + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.5.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ=="], - "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.4.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.4.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw=="], + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], @@ -774,6 +807,8 @@ "@poppinss/exception": ["@poppinss/exception@1.2.2", "", {}, "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg=="], + "@posthog/core": ["@posthog/core@1.3.0", "", {}, "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw=="], + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -812,121 +847,125 @@ "@ricky0123/vad-web": ["@ricky0123/vad-web@0.0.24", "", { "dependencies": { "onnxruntime-web": "1.14.0" } }, "sha512-uv6GWW/kq8BkVErMQzXp3uwSyYMT3w/3QJiUerVaaKp7EwhOTIRY+96EoyFdG2WOFU5RkLk/2CVGbI7nDlxhEg=="], - "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.3", "", { "os": "android", "cpu": "arm" }, "sha512-UmTdvXnLlqQNOCJnyksjPs1G4GqXNGW1LrzCe8+8QoaLhhDeTXYBgJ3k6x61WIhlHX2U+VzEJ55TtIjR/HTySA=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.3", "", { "os": "android", "cpu": "arm64" }, "sha512-8NoxqLpXm7VyeI0ocidh335D6OKT0UJ6fHdnIxf3+6oOerZZc+O7r+UhvROji6OspyPm+rrIdb1gTXtVIqn+Sg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-csnNavqZVs1+7/hUKtgjMECsNG2cdB8F7XBHP6FfQjqhjF8rzMzb3SLyy/1BG7YSfQ+bG75Ph7DyedbUqwq1rA=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-r2MXNjbuYabSIX5yQqnT8SGSQ26XQc8fmp6UhlYJd95PZJkQD1u82fWP7HqvGUf33IsOC6qsiV+vcuD4SDP6iw=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-uluObTmgPJDuJh9xqxyr7MV61Imq+0IvVsAlWyvxAaBSNzCcmZlhfYcRhCdMaCsy46ccZa7vtDDripgs9Jkqsw=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-AVJXEq9RVHQnejdbFvh1eWEoobohUYN3nqJIPI4mNTMpsyYN01VvcAClxflyk2HIxvLpRcRggpX1m9hkXkpC/A=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.3", "", { "os": "linux", "cpu": "arm" }, "sha512-byyflM+huiwHlKi7VHLAYTKr67X199+V+mt1iRgJenAI594vcmGGddWlu6eHujmcdl6TqSNnvqaXJqZdnEWRGA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aLm3NMIjr4Y9LklrH5cu7yybBqoVCdr4Nvnm8WB7PKCn34fMCGypVNpGK0JQWdPAzR/FnoEoFtlRqZbBBLhVoQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VtilE6eznJRDIoFOzaagQodUksTEfLIsvXymS+UdJiSXrPW7Ai+WG4uapAc3F7Hgs791TwdGh4xyOzbuzIZrnw=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dG3JuS6+cRAL0GQ925Vppafi0qwZnkHdPeuZIxIPXqkCLP02l7ka+OCyBoDEv8S+nKHxfjvjW4OZ7hTdHkx8/w=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.3", "", { "os": "linux", "cpu": "none" }, "sha512-iU8DxnxEKJptf8Vcx4XvAUdpkZfaz0KWfRrnIRrOndL0SvzEte+MTM7nDH4A2Now4FvTZ01yFAgj6TX/mZl8hQ=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VrQZp9tkk0yozJoQvQcqlWiqaPnLM6uY1qPYXvukKePb0fqaiQtOdMJSxNFUZFsGw5oA5vvVokjHrx8a9Qsz2A=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.3", "", { "os": "linux", "cpu": "none" }, "sha512-uf2eucWSUb+M7b0poZ/08LsbcRgaDYL8NCGjUeFMwCWFwOuFcZ8D9ayPl25P3pl+D2FH45EbHdfyUesQ2Lt9wA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.3", "", { "os": "linux", "cpu": "none" }, "sha512-7tnUcDvN8DHm/9ra+/nF7lLzYHDeODKKKrh6JmZejbh1FnCNZS8zMkZY5J4sEipy2OW1d1Ncc4gNHUd0DLqkSg=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-MUpAOallJim8CsJK+4Lc9tQzlfPbHxWDrGXZm2z6biaadNpvh3a5ewcdat478W+tXDoUiHwErX/dOql7ETcLqg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.3", "", { "os": "linux", "cpu": "x64" }, "sha512-F42IgZI4JicE2vM2PWCe0N5mR5vR0gIdORPqhGQ32/u1S1v3kLtbZ0C/mi9FFk7C5T0PgdeyWEPajPjaUpyoKg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.3", "", { "os": "linux", "cpu": "x64" }, "sha512-oLc+JrwwvbimJUInzx56Q3ujL3Kkhxehg7O1gWAYzm8hImCd5ld1F2Gry5YDjR21MNb5WCKhC9hXgU7rRlyegQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-lOrQ+BVRstruD1fkWg9yjmumhowR0oLAAzavB7yFSaGltY8klttmZtCLvOXCmGE9mLIn8IBV/IFrQOWz5xbFPg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-vvrVKPRS4GduGR7VMH8EylCBqsDcw6U+/0nPDuIjXQRbHJc6xOBj+frx8ksfZAh6+Fptw5wHrN7etlMmQnPQVg=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.3", "", { "os": "win32", "cpu": "x64" }, "sha512-fi3cPxCnu3ZeM3EwKZPgXbWoGzm2XHgB/WShKI81uj8wG0+laobmqy5wbgEwzstlbLu4MyO8C19FyhhWseYKNQ=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="], - "@shikijs/core": ["@shikijs/core@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], - "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA=="], + "@shikijs/core": ["@shikijs/core@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA=="], - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw=="], + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg=="], - "@shikijs/langs": ["@shikijs/langs@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw=="], + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg=="], - "@shikijs/themes": ["@shikijs/themes@3.11.0", "", { "dependencies": { "@shikijs/types": "3.11.0" } }, "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q=="], + "@shikijs/langs": ["@shikijs/langs@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ=="], - "@shikijs/types": ["@shikijs/types@3.11.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q=="], + "@shikijs/themes": ["@shikijs/themes@3.13.0", "", { "dependencies": { "@shikijs/types": "3.13.0" } }, "sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg=="], + + "@shikijs/types": ["@shikijs/types@3.13.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw=="], "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - "@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.2", "", {}, "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="], - "@simplewebauthn/server": ["@simplewebauthn/server@13.1.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g=="], + "@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="], - "@sindresorhus/is": ["@sindresorhus/is@7.0.2", "", {}, "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw=="], + "@sindresorhus/is": ["@sindresorhus/is@7.1.0", "", {}, "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA=="], "@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="], "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="], - "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.9", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-aytHXcMi7lb9ljsWUzXYQ0p5X1z9oWud2olu/EpmH7aCu4m84h7QLvb5Wp+CFirKcwoNnYvYWhyP/L8Vh1ztdw=="], + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], - "@sveltejs/kit": ["@sveltejs/kit@2.37.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-xgKtpjQ6Ry4mdShd01ht5AODUsW7+K1iValPDq7QX8zI1hWOKREH9GjG8SRCN5tC4K7UXmMhuQam7gbLByVcnw=="], + "@sveltejs/kit": ["@sveltejs/kit@2.47.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-mbUomaJTiADTrq6GT4ZvQ7v1rs0S+wXGMzrjFwjARAKMEF8FpOUmz2uEJ4M9WMJMQOXCMHpKFzJfdjo9O7M22A=="], - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.1.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-3pppgIeIZs6nrQLazzKcdnTJ2IWiui/UucEPXKyFG35TKaHQrfkWBnv6hyJcLxFuR90t+LaoecrqTs8rJKWfSQ=="], + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", "deepmerge": "^4.3.1", "magic-string": "^0.30.17", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ=="], "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.12" } }, "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ=="], + "@tailwindcss/node": ["@tailwindcss/node@4.1.15", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.15" } }, "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.12", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.12", "@tailwindcss/oxide-darwin-arm64": "4.1.12", "@tailwindcss/oxide-darwin-x64": "4.1.12", "@tailwindcss/oxide-freebsd-x64": "4.1.12", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", "@tailwindcss/oxide-linux-x64-musl": "4.1.12", "@tailwindcss/oxide-wasm32-wasi": "4.1.12", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" } }, "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.15", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.15", "@tailwindcss/oxide-darwin-arm64": "4.1.15", "@tailwindcss/oxide-darwin-x64": "4.1.15", "@tailwindcss/oxide-freebsd-x64": "4.1.15", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", "@tailwindcss/oxide-linux-x64-musl": "4.1.15", "@tailwindcss/oxide-wasm32-wasi": "4.1.15", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" } }, "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.12", "", { "os": "android", "cpu": "arm64" }, "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.15", "", { "os": "android", "cpu": "arm64" }, "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12", "", { "os": "linux", "cpu": "arm" }, "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.15", "", { "os": "linux", "cpu": "arm" }, "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.15", "", { "os": "linux", "cpu": "x64" }, "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.12", "", { "os": "linux", "cpu": "x64" }, "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.15", "", { "os": "linux", "cpu": "x64" }, "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.12", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.15", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.12", "", { "os": "win32", "cpu": "x64" }, "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.15", "", { "os": "win32", "cpu": "x64" }, "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w=="], - "@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="], + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.12", "", { "dependencies": { "@tailwindcss/node": "4.1.12", "@tailwindcss/oxide": "4.1.12", "tailwindcss": "4.1.12" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.15", "", { "dependencies": { "@tailwindcss/node": "4.1.15", "@tailwindcss/oxide": "4.1.15", "tailwindcss": "4.1.15" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.3", "", {}, "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.5", "", {}, "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], - "@tanstack/svelte-query": ["@tanstack/svelte-query@6.0.1", "", { "dependencies": { "@tanstack/query-core": "5.90.3" }, "peerDependencies": { "svelte": "^5.25.0" } }, "sha512-si5xsb/aeUZFvWgF5IxOPDpQ1g1fqTX7NF//bTEFrbLdAk68LmkWzY5QCwknZp5cBdU50qJfGY19hbAfMYeiOQ=="], + "@tanstack/svelte-query": ["@tanstack/svelte-query@6.0.3", "", { "dependencies": { "@tanstack/query-core": "5.90.5" }, "peerDependencies": { "svelte": "^5.25.0" } }, "sha512-+elBl7JMr5C5RVHYu/DMxH06IrDZPpORFm4Rtyg9Qe9ww6gvG9naCvPw5arMoMq7si4Y3pJDTuX8PbQgy/RRMQ=="], "@tanstack/svelte-query-devtools": ["@tanstack/svelte-query-devtools@6.0.0", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1", "esm-env": "^1.2.1" }, "peerDependencies": { "@tanstack/svelte-query": "^6.0.0", "svelte": "^5.25.0" } }, "sha512-YMvrLmQ/itQJ62WAV8+E0d7WsQlNUFOfKgijPxvHlIkBEZalKdF2BH0dIUsXc+as2xOdOZlSbR4Tw7c/hhli4w=="], @@ -934,57 +973,57 @@ "@tanstack/table-core": ["@tanstack/table-core@9.0.0-alpha.10", "", {}, "sha512-f2kEGGL+d+I7evkhU926cID2MyH7nPI8acAPcwpaAR1DrgTNStAMp3NS+tgMDyrYtc8zd+RyTxC8m+NBhHhFmA=="], - "@tauri-apps/api": ["@tauri-apps/api@2.8.0", "", {}, "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw=="], + "@tauri-apps/api": ["@tauri-apps/api@2.9.0", "", {}, "sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw=="], - "@tauri-apps/cli": ["@tauri-apps/cli@2.8.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.8.1", "@tauri-apps/cli-darwin-x64": "2.8.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.8.1", "@tauri-apps/cli-linux-arm64-gnu": "2.8.1", "@tauri-apps/cli-linux-arm64-musl": "2.8.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.8.1", "@tauri-apps/cli-linux-x64-gnu": "2.8.1", "@tauri-apps/cli-linux-x64-musl": "2.8.1", "@tauri-apps/cli-win32-arm64-msvc": "2.8.1", "@tauri-apps/cli-win32-ia32-msvc": "2.8.1", "@tauri-apps/cli-win32-x64-msvc": "2.8.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-ONVAfI7PFUO6MdSq9dh2YwlIb1cAezrzqrWw2+TChVskoqzDyyzncU7yXlcph/H/nR/kNDEY3E1pC8aV3TVCNQ=="], + "@tauri-apps/cli": ["@tauri-apps/cli@2.9.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.0", "@tauri-apps/cli-darwin-x64": "2.9.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.0", "@tauri-apps/cli-linux-arm64-gnu": "2.9.0", "@tauri-apps/cli-linux-arm64-musl": "2.9.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.0", "@tauri-apps/cli-linux-x64-gnu": "2.9.0", "@tauri-apps/cli-linux-x64-musl": "2.9.0", "@tauri-apps/cli-win32-arm64-msvc": "2.9.0", "@tauri-apps/cli-win32-ia32-msvc": "2.9.0", "@tauri-apps/cli-win32-x64-msvc": "2.9.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-Rq67+sgiiUot95kjn+6eP8gTRw9YL839gutPx5bAsGtlQ8n9S6qo2VSQkogYsiHlJs14hQpYACn/EIswH6sHzw=="], - "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.8.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-301XWcDozcvJ79uMRquSvgI4vvAxetFs+reMpBI1U5mSWixjUqxZjxs9UDJAtE+GFXdGYTjSLUxCKe5WBDKZ/A=="], + "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.9.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-A2Wo2gvtPDymSApnLlKGVuX/b6rvVtdlTh80qta7j0jgc+tK0dyX8+puDufthUR3VPBRsVmV+XWfEJKnaqMLjg=="], - "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.8.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-fJpOD/jWNy3sn27mjPGexBxGPTCgoCu29C+7qBV8kKJQGrRB4/zJk2zMqcKMjV/1Dma47n+saQWXLFwGpRUHgQ=="], + "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.9.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RfFB1BB7cqPuPWwKtROXYkN9F760jwYIHpxXgg5AocEQ0c6XynWPMLnYvy77jEyycbYt6cWeIwhiWQYsRbWESA=="], - "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.8.1", "", { "os": "linux", "cpu": "arm" }, "sha512-BcrZiInB3xjdV/Q2yv88aAz4Ajrxomd1+oePUO8ZWVpdhFwMZaAAOMbpPVgrlanGBeSzU7Aim9i1Opz/+JYiDA=="], + "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.9.0", "", { "os": "linux", "cpu": "arm" }, "sha512-n1Gs41458ktY6FMTow/M6AWzy5EYhH1vJ2rdkNAwgX1u086xHCM8PbnowQVgJbRjhrJCUoq7E36EjSy2awHTvA=="], - "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.8.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-uZXaQrcdk55h4qWSe3pngg6LMUwVUIoluxXG/cmKHeq8LddlUdKpj3OaSPahLWip1Ol6hq14ysvywzsrdhM4kA=="], + "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-E2y+egQvm7nZbl6cv2Nt1kYw5H8rJG2IisGj9bzJbd8ygSsWJK4Rdw6KW9Ml9iZL7+GuYGihOtlMcyQ6uykw2g=="], - "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.8.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VK/zwBzQY9SfyK7RSrxlIRQLJyhyssoByYWPK/FJMre8SV/y8zZ071cTQNG9dPWM1f+onI1WPTleG+TBUq/0Gw=="], + "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.9.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TH09uepDx3LE7+DSzn9x04ilM0pouguwD6Cjq+A2NdDOu2UkZ3rWux77lMiiuO5fQAGYQAs0BtLjkzcTDoUHTQ=="], - "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.8.1", "", { "os": "linux", "cpu": "none" }, "sha512-bFw3zK6xkyurDR5kw2QgiU6YFlFNrfgtli3wRdTRv8zSVLZMQ2iZ8keYnd57vpvsbZ9PusFPYAMS7Fkzkf9I4g=="], + "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.9.0", "", { "os": "linux", "cpu": "none" }, "sha512-s0ENNDStw8tLScc/K5gS4xE8VrDaFbyCCgYHylrBsIqKQT4rYZLHH3WyzWxxLXIOhPzkczw6MPxt0GdUVPH97A=="], - "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.8.1", "", { "os": "linux", "cpu": "x64" }, "sha512-zOnFX+Rppuz0UVVSeCi67lMet8le+yT4UIiQ6t/QYGtpoWO/D4GpMoVYehJlR14klNXrC2CRxT9b3BUWTCEBwA=="], + "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-stBAjrxfcrJLdmvF3jQskq/Ks/ar4TRyk45kfpD9/0c/8WWDKKWu+z6+ynGNkDYfm9GpbQOQDAjfX0BPWodZZw=="], - "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.8.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gLy6eisaeOTC6NQirs3a0XZNCVT/i7JPYHkXx6ArH6+Kb9IU8ogthTY4MQoYbkWmdOp3ijKX+RT1dD3IZURrEg=="], + "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.9.0", "", { "os": "linux", "cpu": "x64" }, "sha512-fxR/cG3DVuVFDoBCvAGzbVdNfHAdMfNG32aBR1j6y+0+Ys4ZF+a4SNBbMNGdJ2gQc6/QVciswYMSfSs9hP3GZA=="], - "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.8.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ciZ93Dm847zFDqRyc1e0YRiu/cdWne1bMhvifcZOibbyqSKB9o+b95Y5axMtXqR4Wsd2mHiC5TE+MVF3NDsdEw=="], + "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.9.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YIyRvIaYyPRlf1XB0HOLI3q9rkBpb9a8Cl6+PRopTsnXQqlfZIBG5A2KmQO90PkvmyVC6CprGcvK0U28l4MUow=="], - "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.8.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-uWUa503Pw53XidUvcqWOvVsBY7vpQs+ZlTyQgXSnPuTiMF1l5bFEzqoHMvZfIL3MFG13xCAqVK1bR7lFB/6qMQ=="], + "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.9.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Z6a6J+KT0DvjoWSz/R0EDRUCr0DDl/sp10sL1OuJLGnsl36lXWF10YuhJua3dQHizzJzkHpWAV/k1EBxjf10fQ=="], - "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.8.1", "", { "os": "win32", "cpu": "x64" }, "sha512-KmiT0vI7FMBWfk5YDQg7+WcjzuMdeaHOQ7H0podZ7lyJg2qo2DpbGp8y+fMVCRsmvQx5bW6Cyh1ArfO1kkUInA=="], + "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ja2LCRGhEBV/FxRF3ofGGO8ZAVrZt5P0MKkAyJ2wQGRB7xcFoadmnkKwpF0uFOjT/6ygh4f/RV46cjo3pbZxyA=="], "@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA=="], - "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-cNLo9YeQSC0MF4IgXnotHsqEgJk72MBZLXmQPrLA95qTaaWiiaFQ38hIMdZ6YbGUNkr3oni3EhU+AD5jLHcdUA=="], + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OvXkrEBfWwtd8tzVCEXIvRfNEX87qs2jv6SqmVPiHcJjBhSF/GUvjqUNIDmKByb5N8nvDqVUM7+g1sXwdC/S9w=="], - "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ=="], + "@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig=="], "@tauri-apps/plugin-global-shortcut": ["@tauri-apps/plugin-global-shortcut@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-WbAz0ElhpP+0kzQZRScdCC7UQ7OPH8PAn//fsBNu7+ywihsnVSVOg1L9YhieAtLNtAlnmFI69Yl5AGaA3ge5IQ=="], - "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.1", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-SpQ1azXEdQI0UB2NZTIPljJTDEe0bIaKzHYR/k4UQp6yzRYGLC/ktmIgEfQ2RvKAWus8GcYgGr5K6LJPbo/NZw=="], + "@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-x1mQKHSLDk4mS2S938OTeyk8L7QyLpCrKZCZcjkljGsvTvRMojCvI9SeJ1kaxc7t8xSilkC7WdId8xER9TIGLg=="], - "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-QDwXo9VzAlH97c0veuf19TZI6cRBPfJDl2O6hNEDvI66j60lOO9z+PL6MJrj8A6Y+t55r7mGhe3rQWLmOre2HA=="], + "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-7gqgfANSREKhh35fY1L4j3TUjUdePmU735FYDqRGeIf8nMXWpcx6j4FhN9/4nYz+m0mv79DCTPLqIPTySggGgg=="], - "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.4.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-43VyN8JJtvKWJY72WI/KNZszTpDpzHULFxQs0CJBIYUdCRowQ6Q1feWTDb979N7nldqSuDOaBupZ6wz2nvuWwQ=="], + "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-B0LShOYae4CZjN8leiNDbnfjSrTwoZakqKaWpfoH6nXiJwt6Rgj6RnVIffG3DoJiKsffRhMkjmBV9VeilSb4TA=="], - "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-dm3bDsMuUngpIQdJ1jaMkMfyQpHyDcaTIKTFaAMHoKeUd+Is3UHO2uzhElr6ZZkfytIIyQtSVnCWdW2Kc58f3g=="], + "@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-ty5V8XDUIFbSnrk3zsFoP3kzN+vAufYzalJSlmrVhQTImIZa1aL1a03bOaP2vuBvfR+WDRC6NgV2xBl8G07d+w=="], "@tauri-apps/plugin-process": ["@tauri-apps/plugin-process@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ=="], - "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-6GIRxO2z64uxPX4CCTuhQzefvCC0ew7HjdBhMALiGw74vFBDY95VWueAHOHgNOMV4UOUAFupyidN9YulTe5xlA=="], + "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-jjs2WGDO/9z2pjNlydY/F5yYhNsscv99K5lCmU5uKjsVvQ3dRlDhhtVYoa4OLDmktLtQvgvbQjCFibMl6tgGfw=="], "@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.9.0", "", { "dependencies": { "@tauri-apps/api": "^2.6.0" } }, "sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg=="], - "@trpc/client": ["@trpc/client@11.4.4", "", { "peerDependencies": { "@trpc/server": "11.4.4", "typescript": ">=5.7.2" } }, "sha512-86OZl+Y+Xlt9ITGlhCMImERcsWCOrVzpNuzg3XBlsDSmSs9NGsghKjeCpJQlE36XaG3aze+o9pRukiYYvBqxgQ=="], + "@trpc/client": ["@trpc/client@11.6.0", "", { "peerDependencies": { "@trpc/server": "11.6.0", "typescript": ">=5.7.2" } }, "sha512-DyWbYk2hd50BaVrXWVkaUnaSwgAF5g/lfBkXtkF1Aqlk6BtSzGUo3owPkgqQO2I5LwWy1+ra9TsSfBBvIZpTwg=="], - "@trpc/server": ["@trpc/server@11.4.4", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-VkJb2xnb4rCynuwlCvgPBh5aM+Dco6fBBIo6lWAdJJRYVwtyE5bxNZBgUvRRz/cFSEAy0vmzLxF7aABDJfK5Rg=="], + "@trpc/server": ["@trpc/server@11.6.0", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-skTso0AWbOZck40jwNeYv++AMZXNWLUWdyk+pB5iVaYmEKTuEeMoPrEudR12VafbEU6tZa8HK3QhBfTYYHDCdg=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -1006,7 +1045,7 @@ "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - "@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="], + "@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1018,30 +1057,46 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.40.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/type-utils": "8.40.0", "@typescript-eslint/utils": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.40.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.46.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/type-utils": "8.46.2", "@typescript-eslint/utils": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/typescript-estree": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.46.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.0", "@typescript-eslint/types": "^8.46.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.46.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.46.2", "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0" } }, "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2" } }, "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.46.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "@typescript-eslint/typescript-estree": "8.40.0", "@typescript-eslint/utils": "8.40.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.46.0", "", {}, "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.46.2", "", {}, "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.0", "@typescript-eslint/tsconfig-utils": "8.46.0", "@typescript-eslint/types": "8.46.0", "@typescript-eslint/visitor-keys": "8.46.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.2", "@typescript-eslint/tsconfig-utils": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.40.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/types": "8.40.0", "@typescript-eslint/typescript-estree": "8.40.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.0", "", { "dependencies": { "@typescript-eslint/types": "8.46.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.2", "", { "dependencies": { "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@vanillaes/csv": ["@vanillaes/csv@3.0.4", "", {}, "sha512-cMJ/pAljVGpsHvqgd5N4EpNJOvMjFubg7x+9ehjVgQUFi2h+u4Nc4O4C0ErNLV53rO3rI0W1JnKX3wQz0pWgIA=="], + "@volar/kit": ["@volar/kit@2.4.23", "", { "dependencies": { "@volar/language-service": "2.4.23", "@volar/typescript": "2.4.23", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ=="], + + "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], + + "@volar/language-server": ["@volar/language-server@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "@volar/language-service": "2.4.23", "@volar/typescript": "2.4.23", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-k0iO+tybMGMMyrNdWOxgFkP0XJTdbH0w+WZlM54RzJU3WZSjHEupwL30klpM7ep4FO6qyQa03h+VcGHD4Q8gEg=="], + + "@volar/language-service": ["@volar/language-service@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-h5mU9DZ/6u3LCB9xomJtorNG6awBNnk9VuCioGsp6UtFiM8amvS5FcsaC3dabdL9zO0z+Gq9vIEMb/5u9K6jGQ=="], + + "@volar/source-map": ["@volar/source-map@2.4.23", "", {}, "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q=="], + + "@volar/typescript": ["@volar/typescript@2.4.23", "", { "dependencies": { "@volar/language-core": "2.4.23", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag=="], + + "@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="], + + "@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="], + "@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="], "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], @@ -1080,13 +1135,13 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "arktype": ["arktype@2.1.20", "", { "dependencies": { "@ark/schema": "0.46.0", "@ark/util": "0.46.0" } }, "sha512-IZCEEXaJ8g+Ijd59WtSYwtjnqXiwM8sWQ5EjGamcto7+HVN9eK0C4p0zDlCuAwWhpqr6fIBkxPuYDl4/Mcj/+Q=="], + "arktype": ["arktype@2.1.23", "", { "dependencies": { "@ark/regex": "0.0.0", "@ark/schema": "0.50.0", "@ark/util": "0.50.0" } }, "sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ=="], "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="], - "astro": ["astro@5.13.2", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.2", "@astrojs/markdown-remark": "6.3.6", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "smol-toml": "^1.3.4", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.4", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-yjcXY0Ua3EwjpVd3GoUXa65HQ6qgmURBptA+M9GzE0oYvgfuyM7bIbH8IR/TWIbdefVUJR5b7nZ0oVnMytmyfQ=="], + "astro": ["astro@5.14.7", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.7.4", "@astrojs/markdown-remark": "6.3.8", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^3.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.2.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.1", "deterministic-object-hash": "^2.0.2", "devalue": "^5.3.2", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.18", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.3.0", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.2", "shiki": "^3.12.0", "smol-toml": "^1.4.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.6.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.0", "vfile": "^6.0.3", "vite": "^6.3.6", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.24.6", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-vdZmRN2MFf60ZTjFkZNrQQkrmeeZzTI1c6N3ZRQN55rPGHjywM2VplJwJ68q496DfpaoDoAroDBpdm+eTgHUtQ=="], "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], @@ -1110,15 +1165,21 @@ "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - "better-auth": ["better-auth@1.3.7", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "^1.0.13", "defu": "^6.1.4", "jose": "^5.10.0", "kysely": "^0.28.5", "nanostores": "^0.11.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ=="], + "better-auth": ["better-auth@1.3.28", "", { "dependencies": { "@better-auth/core": "1.3.28", "@better-auth/telemetry": "1.3.28", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-fSaeRsTSkzCSSKREFsm7z7TsTMC8ghGrwCN+mumxCZiyc8Fh/UThUwURlTJmsR0YVB0DMR8ejQH+c38WhdQslQ=="], + + "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], + + "better-sqlite3": ["better-sqlite3@12.4.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ=="], - "better-call": ["better-call@1.0.15", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-u4ZNRB1yBx5j3CltTEbY2ZoFPVcgsuvciAqTEmPvnZpZ483vlZf4LGJ5aVau1yMlrvlyHxOCica3OqXBLhmsUw=="], + "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], "bits-ui": ["bits-ui@2.8.10", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.29.1", "svelte-toolbelt": "^0.9.3", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-MOobkqapDZNrpcNmeL2g664xFmH4tZBOKBTxFmsQYMZQuybSZHQnPXy+AjM5XZEXRmCFx5+XRmo6+fC3vHh1hQ=="], - "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="], @@ -1162,9 +1223,9 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "ci-info": ["ci-info@4.3.0", "", {}, "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ=="], + "ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], @@ -1216,14 +1277,12 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "core-js": ["core-js@3.45.0", "", {}, "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA=="], + "core-js": ["core-js@3.46.0", "", {}, "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], @@ -1232,15 +1291,15 @@ "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + "cssstyle": ["cssstyle@5.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^4.0.3", "@csstools/css-syntax-patches-for-csstree": "^1.0.14", "css-tree": "^3.1.0" } }, "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], - "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], @@ -1250,6 +1309,8 @@ "dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -1278,11 +1339,11 @@ "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="], + "devalue": ["devalue@5.4.1", "", {}, "sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "dexie": ["dexie@4.2.0", "", {}, "sha512-OSeyyWOUetDy9oFWeddJgi83OnRA3hSFh3RrbltmPgqHszE9f24eUCVLI4mPg0ifsWk0lQTdnS+jyGNrPMvhDA=="], + "dexie": ["dexie@4.2.1", "", {}, "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="], "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], @@ -1292,15 +1353,15 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dompurify": ["dompurify@3.2.6", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ=="], + "dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="], "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "drizzle-arktype": ["drizzle-arktype@0.1.3", "", { "peerDependencies": { "arktype": ">=2.0.0", "drizzle-orm": ">=0.36.0" } }, "sha512-X66GB2pz7Nb+NmCZefDXpdoglxjGYnB2yRU5umAK2stVkl4rvV6i6XbMg1+w1HiY/ydC8gJVq4jKAARYazpb3g=="], - "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], + "drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="], - "drizzle-orm": ["drizzle-orm@0.44.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q=="], + "drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -1316,6 +1377,8 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -1344,11 +1407,11 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], + "eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="], "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], - "eslint-plugin-svelte": ["eslint-plugin-svelte@3.12.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.3.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-hD7wPe+vrPgx3U2X2b/wyTMtWobm660PygMGKrWWYTc9lvtY8DpNFDaU2CJQn1szLjGbn/aJ3g8WiXuKakrEkw=="], + "eslint-plugin-svelte": ["eslint-plugin-svelte@3.12.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-4KRG84eAHQfYd9OjZ1K7sCHy0nox+9KwT+s5WCCku3jTim5RV4tVENob274nCwIaApXsYPKAUAZFBxKZ3Wyfjw=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -1382,12 +1445,14 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.5", "", {}, "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -1416,10 +1481,12 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="], @@ -1434,7 +1501,7 @@ "flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="], - "fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="], + "fontace": ["fontace@0.3.1", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-9f5g4feWT1jWT8+SbL85aLIRLIXUaDygaM2xPXRmzPYxrOMNok79Lr3FGJoKVNKibE0WCunNiEVG2mwuE+2qEg=="], "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], @@ -1452,6 +1519,8 @@ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -1460,7 +1529,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="], + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -1470,17 +1539,19 @@ "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "get-tsconfig": ["get-tsconfig@4.12.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw=="], "giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], - "globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="], + "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1528,7 +1599,7 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - "hono": ["hono@4.9.2", "", {}, "sha512-UG2jXGS/gkLH42l/1uROnwXpkjvvxkl3kpopL3LBo27NuaDPI6xHNfuUSilIHcrBkPfl4y0z6y2ByI455TjNRw=="], + "hono": ["hono@4.10.1", "", {}, "sha512-rpGNOfacO4WEPClfkEt1yfl8cbu10uB1lNpiI33AKoiAHwOS8lV748JiLx4b5ozO/u4qLjIvfpFsPXdY5Qjkmg=="], "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], @@ -1550,7 +1621,7 @@ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1558,19 +1629,21 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - "import-meta-resolve": ["import-meta-resolve@4.1.0", "", {}, "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], - "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], @@ -1606,19 +1679,19 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isomorphic-dompurify": ["isomorphic-dompurify@2.26.0", "", { "dependencies": { "dompurify": "^3.2.6", "jsdom": "^26.1.0" } }, "sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw=="], + "isomorphic-dompurify": ["isomorphic-dompurify@2.29.0", "", { "dependencies": { "dompurify": "^3.3.0", "jsdom": "^27.0.0" } }, "sha512-Bgw5M9GMsuGeGSRpS81gk68t9/+r3AwuJJ5WnSxZK+tuazDodlRgmwz4ItMAfNYDgiNaizREYeiefkFQWkG7ow=="], "isomorphic-git": ["isomorphic-git@1.32.1", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "path-browserify": "^1.0.1", "pify": "^4.0.1", "readable-stream": "^3.4.0", "sha.js": "^2.4.9", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-NZCS7qpLkCZ1M/IrujYBD31sM6pd/fMVArK4fz4I7h6m0rUW2AsYU7S7zXeABuHL6HIfW6l53b4UQ/K441CQjg=="], - "jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], + "jose": ["jose@6.1.0", "", {}, "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA=="], "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + "jsdom": ["jsdom@27.0.1", "", { "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -1640,33 +1713,35 @@ "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], - "kysely": ["kysely@0.28.5", "", {}, "sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA=="], + "kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "libsql": ["libsql@0.4.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.4.7", "@libsql/darwin-x64": "0.4.7", "@libsql/linux-arm64-gnu": "0.4.7", "@libsql/linux-arm64-musl": "0.4.7", "@libsql/linux-x64-gnu": "0.4.7", "@libsql/linux-x64-musl": "0.4.7", "@libsql/win32-x64-msvc": "0.4.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw=="], - "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], @@ -1676,27 +1751,21 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash.castarray": ["lodash.castarray@4.4.0", "", {}, "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="], + "marked": ["marked@16.4.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1804,7 +1873,7 @@ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - "miniflare": ["miniflare@4.20250816.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^7.10.0", "workerd": "1.20250816.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-HuakGvmsU8aC60wsHP7Su+BgJFly1GmKbmbR/nqIz0Xlk6wcd/pp3vZ7jtbT3unf+aeBOlEO/CzcUb8xFsJLdA=="], + "miniflare": ["miniflare@4.20251008.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251008.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-sKCNYNzXG6l8qg0Oo7y8WcDKcpbgw0qwZsxNpdZilFTR4EavRow2TlcwuPSVN99jqAjhz0M4VXvTdSGdtJ2VfQ=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -1812,13 +1881,15 @@ "minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], - "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="], + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], "mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="], @@ -1830,9 +1901,11 @@ "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], - "nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + + "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], - "nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -1844,7 +1917,7 @@ "nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="], - "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + "node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="], "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], @@ -1856,14 +1929,12 @@ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "node-mock-http": ["node-mock-http@1.0.2", "", {}, "sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g=="], + "node-mock-http": ["node-mock-http@1.0.3", "", {}, "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "nwsapi": ["nwsapi@2.2.21", "", {}, "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA=="], - "nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1892,7 +1963,7 @@ "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], - "openai": ["openai@5.13.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-Jty97Apw40znKSlXZL2YDap1U2eN9NfXbqm/Rj1rExeOLEnhwezpKQ+v43kIqojavUgm30SR3iuvGlNEBR+AFg=="], + "openai": ["openai@5.23.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -1902,11 +1973,11 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-queue": ["p-queue@8.1.0", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw=="], + "p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="], "p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="], - "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], + "package-manager-detector": ["package-manager-detector@1.5.0", "", {}, "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -1916,12 +1987,10 @@ "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="], - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1972,9 +2041,11 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "posthog-js": ["posthog-js@1.260.1", "", { "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-DD8ZSRpdScacMqtqUIvMFme8lmOWkOvExG8VvjONE7Cm3xpRH5xXpfrwMJE4bayTGWKMx4ij6SfphK6dm/o2ug=="], + "posthog-js": ["posthog-js@1.277.0", "", { "dependencies": { "@posthog/core": "1.3.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-whSyov8KH2IwXkeJVbgu07EkPk6AITXnrJN7Mg5rGAHJQ0LS1w6qh2Ib4LMsLHTrR5UAqwYHcufbjDl6snoESw=="], - "preact": ["preact@10.27.1", "", {}, "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ=="], + "preact": ["preact@10.27.2", "", {}, "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -2000,6 +2071,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -2014,7 +2087,9 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + "raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -2022,6 +2097,8 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -2070,7 +2147,7 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "rollup": ["rollup@4.46.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.3", "@rollup/rollup-android-arm64": "4.46.3", "@rollup/rollup-darwin-arm64": "4.46.3", "@rollup/rollup-darwin-x64": "4.46.3", "@rollup/rollup-freebsd-arm64": "4.46.3", "@rollup/rollup-freebsd-x64": "4.46.3", "@rollup/rollup-linux-arm-gnueabihf": "4.46.3", "@rollup/rollup-linux-arm-musleabihf": "4.46.3", "@rollup/rollup-linux-arm64-gnu": "4.46.3", "@rollup/rollup-linux-arm64-musl": "4.46.3", "@rollup/rollup-linux-loongarch64-gnu": "4.46.3", "@rollup/rollup-linux-ppc64-gnu": "4.46.3", "@rollup/rollup-linux-riscv64-gnu": "4.46.3", "@rollup/rollup-linux-riscv64-musl": "4.46.3", "@rollup/rollup-linux-s390x-gnu": "4.46.3", "@rollup/rollup-linux-x64-gnu": "4.46.3", "@rollup/rollup-linux-x64-musl": "4.46.3", "@rollup/rollup-win32-arm64-msvc": "4.46.3", "@rollup/rollup-win32-ia32-msvc": "4.46.3", "@rollup/rollup-win32-x64-msvc": "4.46.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw=="], + "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="], "rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="], @@ -2078,7 +2155,7 @@ "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], - "run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="], + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -2098,9 +2175,11 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], @@ -2114,7 +2193,7 @@ "sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="], - "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "sharp": ["sharp@0.34.4", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.0", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.4", "@img/sharp-darwin-x64": "0.34.4", "@img/sharp-libvips-darwin-arm64": "1.2.3", "@img/sharp-libvips-darwin-x64": "1.2.3", "@img/sharp-libvips-linux-arm": "1.2.3", "@img/sharp-libvips-linux-arm64": "1.2.3", "@img/sharp-libvips-linux-ppc64": "1.2.3", "@img/sharp-libvips-linux-s390x": "1.2.3", "@img/sharp-libvips-linux-x64": "1.2.3", "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", "@img/sharp-libvips-linuxmusl-x64": "1.2.3", "@img/sharp-linux-arm": "0.34.4", "@img/sharp-linux-arm64": "0.34.4", "@img/sharp-linux-ppc64": "0.34.4", "@img/sharp-linux-s390x": "0.34.4", "@img/sharp-linux-x64": "0.34.4", "@img/sharp-linuxmusl-arm64": "0.34.4", "@img/sharp-linuxmusl-x64": "0.34.4", "@img/sharp-wasm32": "0.34.4", "@img/sharp-win32-arm64": "0.34.4", "@img/sharp-win32-ia32": "0.34.4", "@img/sharp-win32-x64": "0.34.4" } }, "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -2122,7 +2201,7 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "shiki": ["shiki@3.11.0", "", { "dependencies": { "@shikijs/core": "3.11.0", "@shikijs/engine-javascript": "3.11.0", "@shikijs/engine-oniguruma": "3.11.0", "@shikijs/langs": "3.11.0", "@shikijs/themes": "3.11.0", "@shikijs/types": "3.11.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VgKumh/ib38I1i3QkMn6mAQA6XjjQubqaAYhfge71glAll0/4xnt8L2oSuC45Qcr/G5Kbskj4RliMQddGmy/Og=="], + "shiki": ["shiki@3.13.0", "", { "dependencies": { "@shikijs/core": "3.13.0", "@shikijs/engine-javascript": "3.13.0", "@shikijs/engine-oniguruma": "3.13.0", "@shikijs/langs": "3.13.0", "@shikijs/themes": "3.13.0", "@shikijs/types": "3.13.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -2138,11 +2217,11 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - "simple-icons": ["simple-icons@15.11.0", "", {}, "sha512-hHgDvNcbIdE5e6thY19Ao1VEI4CectXDNB0+nXOLCBf3ApuzeMm4tAhWzeR9qZdt/GoeQs1nm9JTVzCVBuX1nA=="], + "simple-icons": ["simple-icons@15.17.0", "", {}, "sha512-viOcugYj+JFYVWJvDh4Ph1xHk9iTGhzt+NoPrfAQYSCADvmZFSQUWyKEbSMuqVRUsaRgvADn+Cczysemsf1N3Q=="], - "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], - "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -2176,23 +2255,23 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="], + "style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow=="], "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "svelte": ["svelte@5.38.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ltBPlkvqk3bgCK7/N323atUpP3O3Y+DrGV4dcULrsSn4fZaaNnOmdplNznwfdWclAgvSr5rxjtzn/zJhRm6TKg=="], + "svelte": ["svelte@5.41.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w=="], - "svelte-check": ["svelte-check@4.3.1", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg=="], + "svelte-check": ["svelte-check@4.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg=="], - "svelte-eslint-parser": ["svelte-eslint-parser@1.3.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg=="], + "svelte-eslint-parser": ["svelte-eslint-parser@1.4.0", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA=="], "svelte-sonner": ["svelte-sonner@1.0.5", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dpGPFqKb/QWudYqGnEz93vuY+NgCEvyNvxoCLMVGw6sDN/3oVeKV1xiEirW2E1N3vJEyj5imSBNOGltQHA7mg=="], "svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], - "svelte2tsx": ["svelte2tsx@0.7.42", "", { "dependencies": { "dedent-js": "^1.0.1", "pascal-case": "^3.1.1" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-PSNrKS16aVdAajoFjpF5M0t6TA7ha7GcKbBajD9RG3M+vooAuvLnWAGUSC6eJL4zEOVbOWKtcS2BuY4rxPljoA=="], + "svelte2tsx": ["svelte2tsx@0.7.45", "", { "dependencies": { "dedent-js": "^1.0.1", "scule": "^1.3.0" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-cSci+mYGygYBHIZLHlm/jYlEc1acjAHqaQaDFHdEBpUueM9kSTnPpvPtSl5VkJOU1qSJ7h1K+6F/LIUYiqC8VA=="], "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], @@ -2202,25 +2281,29 @@ "tailwind-variants": ["tailwind-variants@1.0.0", "", { "dependencies": { "tailwind-merge": "3.0.2" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA=="], - "tailwindcss": ["tailwindcss@4.1.12", "", {}, "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA=="], + "tailwindcss": ["tailwindcss@4.1.15", "", {}, "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "tauri-plugin-macos-permissions-api": ["tauri-plugin-macos-permissions-api@2.3.0", "", { "dependencies": { "@tauri-apps/api": "^2.5.0" } }, "sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww=="], "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + "tldts": ["tldts@7.0.17", "", { "dependencies": { "tldts-core": "^7.0.17" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ=="], - "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + "tldts-core": ["tldts-core@7.0.17", "", {}, "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g=="], - "to-buffer": ["to-buffer@1.2.1", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ=="], + "to-buffer": ["to-buffer@1.2.2", "", { "dependencies": { "isarray": "^2.0.5", "safe-buffer": "^5.2.1", "typed-array-buffer": "^1.0.3" } }, "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -2230,9 +2313,9 @@ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], - "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], @@ -2250,8 +2333,14 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], + + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "turbo": ["turbo@2.5.8", "", { "optionalDependencies": { "turbo-darwin-64": "2.5.8", "turbo-darwin-arm64": "2.5.8", "turbo-linux-64": "2.5.8", "turbo-linux-arm64": "2.5.8", "turbo-windows-64": "2.5.8", "turbo-windows-arm64": "2.5.8" }, "bin": { "turbo": "bin/turbo" } }, "sha512-5c9Fdsr9qfpT3hA0EyYSFRZj1dVVsb6KIWubA9JBYZ/9ZEAijgUEae0BBR/Xl/wekt4w65/lYLTFaP3JmwSO8w=="], "turbo-darwin-64": ["turbo-darwin-64@2.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh5bCACiHO8rUXZLpKw+m3FiHtAp2CkanSyJre+SInEvEr5kIxjGvCK/8MFX8SFRjQuhjtvpIvYYZJB4AGCxNQ=="], @@ -2268,7 +2357,7 @@ "turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="], - "tw-animate-css": ["tw-animate-css@1.3.7", "", {}, "sha512-lvLb3hTIpB5oGsk8JmLoAjeCHV58nKa2zHYn8yWOoG5JJusH3bhJlF2DLAZ/5NmJ+jyH3ssiAx/2KmbhavJy/A=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -2280,11 +2369,11 @@ "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], - "typescript-eslint": ["typescript-eslint@8.40.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "@typescript-eslint/typescript-estree": "8.40.0", "@typescript-eslint/utils": "8.40.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q=="], + "typescript-eslint": ["typescript-eslint@8.46.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.2", "@typescript-eslint/parser": "8.46.2", "@typescript-eslint/typescript-estree": "8.46.2", "@typescript-eslint/utils": "8.46.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg=="], "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], @@ -2298,7 +2387,7 @@ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "unenv": ["unenv@2.0.0-rc.19", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA=="], + "unenv": ["unenv@2.0.0-rc.21", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A=="], "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], @@ -2306,11 +2395,11 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], + "unifont": ["unifont@0.6.0", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-5Fx50fFQMQL5aeHyWnZX9122sSLckcDvcfFiBf3QYeHa7a1MKJooUy52b67moi2MJYkrfo/TWY+CoLdr/w0tTA=="], "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], - "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], @@ -2324,13 +2413,13 @@ "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "unstorage": ["unstorage@1.16.1", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.3", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-gdpZ3guLDhz+zWIlYP1UwQ259tG5T5vYRzDaHMkQ1bBY1SQPutvZnrRjTFaWUUpseErJIgAZS51h6NOcZVZiqQ=="], + "unstorage": ["unstorage@1.17.1", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^4.0.3", "destr": "^2.0.5", "h3": "^1.15.4", "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.7", "ofetch": "^1.4.1", "ufo": "^1.6.1" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6.0.3 || ^7.0.0", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -2350,29 +2439,29 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="], + "vite": ["vite@7.1.11", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg=="], "vite-plugin-devtools-json": ["vite-plugin-devtools-json@1.0.0", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw=="], "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - "volar-service-css": ["volar-service-css@0.0.62", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg=="], + "volar-service-css": ["volar-service-css@0.0.65", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-oaImNguZF/8NfQh5jJZ2lJYBtF3aFS5H2w+6GmH7ykESAgBJ1UC7DrhmH5smBGGF7OOzzc9AzrrnriafoFJBdA=="], - "volar-service-emmet": ["volar-service-emmet@0.0.62", "", { "dependencies": { "@emmetio/css-parser": "^0.4.0", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ=="], + "volar-service-emmet": ["volar-service-emmet@0.0.65", "", { "dependencies": { "@emmetio/css-parser": "ramya-rao-a/css-parser#vscode", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-YkAPlkJnjyAAUZGtG7STgy3ENFy7C0n3dl6MffUYkcovosfUUNgpUOmsj4t1qw1c7t5KMvLfAZHsEC3Ig5Qs3w=="], - "volar-service-html": ["volar-service-html@0.0.62", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ=="], + "volar-service-html": ["volar-service-html@0.0.65", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-AxXckCTbCr5j5z81d3bNiRRL32xCaBSa8lmYhq0QfzBPVPaRv06YYaxp22XizM061f96iizM7ZkSHCu1RuSwRA=="], - "volar-service-prettier": ["volar-service-prettier@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w=="], + "volar-service-prettier": ["volar-service-prettier@0.0.65", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-cJH+5MtYp5q+zL8d6rd9RwQpwXFJuBaRHMuUyG+kasEkeWMbJKVVFYzUzNtbM8HMGMq5yYy+UGn/Zuh3CvSjaA=="], - "volar-service-typescript": ["volar-service-typescript@0.0.62", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.3", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g=="], + "volar-service-typescript": ["volar-service-typescript@0.0.65", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-zPJuLIMs7lkQCvL+Rza8+3/EIoXEIkX8+DL7bNNfPgnbalbvRDhqWLVMJ6Zk3pINjLJafDqyhSbw8srfkUv97w=="], - "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng=="], + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.65", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-aghyUm2Rc4QNjKG1nvEjT2Kdzuvccs5H1TD0fIaM5i7X5d/vnm3QLP6wzIGGffa3sjD5b6YmLZR2XaFfNEusog=="], - "volar-service-yaml": ["volar-service-yaml@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.15.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig=="], + "volar-service-yaml": ["volar-service-yaml@0.0.65", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.15.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-1C67p6M+Y11IVDOHfmbnF4AJiqmH5xaWsJhvM9eO4kazvHzVhk3+kzCtKcNoXIa77uPNmEbbox5CRysage8MNA=="], - "vscode-css-languageservice": ["vscode-css-languageservice@6.3.7", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-5TmXHKllPzfkPhW4UE9sODV3E0bIOJPOk+EERKllf2SmAczjfTmYeq5txco+N3jpF8KIZ6loj/JptpHBQuVQRA=="], + "vscode-css-languageservice": ["vscode-css-languageservice@6.3.8", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ=="], - "vscode-html-languageservice": ["vscode-html-languageservice@5.5.1", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-/ZdEtsZ3OiFSyL00kmmu7crFV9KwWR+MgpzjsxO60DQH7sIfHZM892C/E4iDd11EKocr+NYuvOA4Y7uc3QzLEA=="], + "vscode-html-languageservice": ["vscode-html-languageservice@5.6.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-FIVz83oGw2tBkOr8gQPeiREInnineCKGCz3ZD1Pi6opOuX3nSRkc4y4zLLWsuop+6ttYX//XZCI6SLzGhRzLmA=="], "vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="], @@ -2398,7 +2487,7 @@ "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], - "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + "webidl-conversions": ["webidl-conversions@8.0.0", "", {}, "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA=="], "wellcrafted": ["wellcrafted@0.22.0", "", {}, "sha512-qlAZ8NYJf+kOpqu+LHW3byMuy80yDex/B0fe4GoI5ou6v+20Jaj0R1jrvCF77edrHVgoXaK1774vni3ZPxH8HA=="], @@ -2406,7 +2495,7 @@ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -2420,9 +2509,9 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "workerd": ["workerd@1.20250816.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250816.0", "@cloudflare/workerd-darwin-arm64": "1.20250816.0", "@cloudflare/workerd-linux-64": "1.20250816.0", "@cloudflare/workerd-linux-arm64": "1.20250816.0", "@cloudflare/workerd-windows-64": "1.20250816.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-5gIvHPE/3QVlQR1Sc1NdBkWmqWj/TSgIbY/f/qs9lhiLBw/Da+HbNBTVYGjvwYqEb3NQ+XQM4gAm5b2+JJaUJg=="], + "workerd": ["workerd@1.20251008.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251008.0", "@cloudflare/workerd-darwin-arm64": "1.20251008.0", "@cloudflare/workerd-linux-64": "1.20251008.0", "@cloudflare/workerd-linux-arm64": "1.20251008.0", "@cloudflare/workerd-windows-64": "1.20251008.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-HwaJmXO3M1r4S8x2ea2vy8Rw/y/38HRQuK/gNDRQ7w9cJXn6xSl1sIIqKCffULSUjul3wV3I3Nd/GfbmsRReEA=="], - "wrangler": ["wrangler@4.31.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.6.2", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250816.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250816.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250816.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-blb8NfA4BGscvSzvLm2mEQRuUTmaMCiglkqHiR3EIque78UXG39xxVtFXlKhK32qaVvGI7ejdM//HC9plVPO3w=="], + "wrangler": ["wrangler@4.43.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.7", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251008.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.21", "workerd": "1.20251008.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251008.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-IBNqXlYHSUSCNNWj/tQN4hFiQy94l7fTxEnJWETXyW69+cjUyjQ7MfeoId3vIV9KBgY8y5M5uf2XulU95OikJg=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -2442,7 +2531,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], @@ -2462,11 +2551,11 @@ "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], - "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "zod-openapi": ["zod-openapi@5.3.1", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-lRb4p4+WAhLGBvQCQf5SFxeZtnzO5m3GmCT4IDjOzGNlZFiVOuey9rWN+EVEDSVJC59B54tP2gxah1wVUplONw=="], + "zod-openapi": ["zod-openapi@5.4.3", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-6kJ/gJdvHZtuxjYHoMtkl2PixCwRuZ/s79dVkEr7arHvZGXfx7Cvh53X3HfJ5h9FzGelXOXlnyjwfX0sKEPByw=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], @@ -2480,11 +2569,15 @@ "@aptabase/tauri/@tauri-apps/api": ["@tauri-apps/api@1.6.0", "", {}, "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + + "@asamuzakjp/dom-selector/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "@astrojs/svelte/@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], - "@astrojs/svelte/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "@astrojs/svelte/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], - "@astrojs/yaml2ts/yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "@better-auth/core/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], @@ -2504,14 +2597,10 @@ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], - "@libsql/hrana-client/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], "@libsql/isomorphic-ws/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "@neondatabase/serverless/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - "@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], "@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], @@ -2522,11 +2611,11 @@ "@octokit/endpoint/universal-user-agent": ["universal-user-agent@6.0.1", "", {}, "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ=="], - "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="], + "@octokit/graphql/@octokit/request": ["@octokit/request@10.0.5", "", { "dependencies": { "@octokit/endpoint": "^11.0.1", "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ=="], "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="], + "@octokit/plugin-request-log/@octokit/core": ["@octokit/core@7.0.5", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", "@octokit/request": "^10.0.4", "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q=="], "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], @@ -2536,11 +2625,11 @@ "@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@octokit/rest/@octokit/core": ["@octokit/core@7.0.3", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ=="], + "@octokit/rest/@octokit/core": ["@octokit/core@7.0.5", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", "@octokit/request": "^10.0.4", "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q=="], - "@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.1.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw=="], + "@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="], - "@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="], + "@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="], "@openauthjs/openauth/@standard-schema/spec": ["@standard-schema/spec@1.0.0-beta.3", "", {}, "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw=="], @@ -2548,9 +2637,9 @@ "@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="], - "@poppinss/dumper/supports-color": ["supports-color@10.2.0", "", {}, "sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q=="], + "@poppinss/dumper/supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], - "@repo/constants/@types/node": ["@types/node@20.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow=="], + "@repo/constants/@types/node": ["@types/node@20.19.22", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ=="], "@repo/svelte-utils/svelte": ["svelte@5.14.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "esm-env": "^1.2.1", "esrap": "^1.3.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2iR/UHHA2Dsldo4JdXDcdqT+spueuh+uNYw1FoTKBbpnFEECVISeqSo0uubPS4AfBE0xI6u7DGHxcdq3DTDmoQ=="], @@ -2558,48 +2647,22 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@tailwindcss/oxide/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="], + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/fontkit/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - - "@types/node-fetch/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - - "@types/pg/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - - "@types/ws/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0" } }, "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w=="], - - "@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], - - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.40.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.40.0", "@typescript-eslint/tsconfig-utils": "8.40.0", "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="], - - "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0" } }, "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w=="], - - "@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], - - "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.40.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.40.0", "@typescript-eslint/tsconfig-utils": "8.40.0", "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ=="], - "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], "accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], @@ -2608,21 +2671,27 @@ "astro/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - "astro/devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="], - "astro/diff": ["diff@5.2.0", "", {}, "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="], - "astro/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + + "better-auth/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "bits-ui/runed": ["runed@0.29.2", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA=="], "bits-ui/svelte-toolbelt": ["svelte-toolbelt@0.9.3", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.29.0", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw=="], - "boxen/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], + "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "boxen/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + "boxen/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "c12/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], @@ -2640,16 +2709,18 @@ "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - "gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "groq-sdk/@types/node": ["@types/node@18.19.123", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg=="], + "groq-sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "groq-sdk/form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], "groq-sdk/formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "hast-util-from-html/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], @@ -2660,7 +2731,7 @@ "jsdom/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "lightningcss/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -2668,12 +2739,18 @@ "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + "miniflare/sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="], "node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "paneforge/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], @@ -2686,15 +2763,17 @@ "posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + "prebuild-install/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "protobufjs/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], - "sharp/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], @@ -2704,11 +2783,13 @@ "tailwind-variants/tailwind-merge": ["tailwind-merge@3.0.2", "", {}, "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw=="], - "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], + "tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.40.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.40.0", "@typescript-eslint/types": "8.40.0", "@typescript-eslint/typescript-estree": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.40.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.40.0", "@typescript-eslint/tsconfig-utils": "8.40.0", "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ=="], + "type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="], "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], @@ -2716,6 +2797,8 @@ "vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "yaml-language-server/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -2786,17 +2869,23 @@ "@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], - "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="], + "@octokit/graphql/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA=="], - "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="], + "@octokit/graphql/@octokit/request/@octokit/request-error": ["@octokit/request-error@7.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0" } }, "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA=="], + + "@octokit/graphql/@octokit/request/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/plugin-request-log/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.2", "", { "dependencies": { "@octokit/request": "^10.0.4", "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/request": ["@octokit/request@10.0.5", "", { "dependencies": { "@octokit/endpoint": "^11.0.1", "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0" } }, "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA=="], + + "@octokit/plugin-request-log/@octokit/core/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], "@octokit/plugin-request-log/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], @@ -2808,67 +2897,79 @@ "@octokit/rest/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], - "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.3", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA=="], + "@octokit/rest/@octokit/core/@octokit/graphql": ["@octokit/graphql@9.0.2", "", { "dependencies": { "@octokit/request": "^10.0.4", "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw=="], + + "@octokit/rest/@octokit/core/@octokit/request": ["@octokit/request@10.0.5", "", { "dependencies": { "@octokit/endpoint": "^11.0.1", "@octokit/request-error": "^7.0.1", "@octokit/types": "^15.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ=="], - "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="], + "@octokit/rest/@octokit/core/@octokit/request-error": ["@octokit/request-error@7.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0" } }, "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA=="], + + "@octokit/rest/@octokit/core/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], "@octokit/rest/@octokit/core/before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], + + "@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], + "@repo/svelte-utils/svelte/esrap": ["esrap@1.4.9", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], + "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.40.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.40.0", "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw=="], + "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.40.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw=="], + "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], + "boxen/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "boxen/wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], + "miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.40.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.40.0", "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw=="], + "miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.40.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw=="], + "miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], + "miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - "boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], - "boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - "boxen/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], - "boxen/wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], - "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - "giget/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + "miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], - "giget/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + "miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - "giget/tar/minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + "miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], - "giget/tar/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], - "giget/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], - "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], - "groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "miniflare/sharp/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -2882,64 +2983,50 @@ "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "@typescript-eslint/visitor-keys": "8.40.0" } }, "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w=="], - - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], - - "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], + "widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.40.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.40.0", "@typescript-eslint/types": "^8.40.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.40.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.40.0", "", {}, "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.40.0", "", { "dependencies": { "@typescript-eslint/types": "8.40.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA=="], - - "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], - - "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "yaml-language-server/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="], - "@epicenter/opencode/yargs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@epicenter/opencode/yargs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "@epicenter/opencode/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "@epicenter/opencode/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], + "@epicenter/opencode/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "@epicenter/opencode/yargs/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="], + "@epicenter/opencode/yargs/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "@epicenter/opencode/yargs/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@octokit/graphql/@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA=="], - "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="], + "@octokit/plugin-request-log/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@11.0.1", "", { "dependencies": { "@octokit/types": "^15.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA=="], - "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="], - "giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="], "yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.16.0", "", {}, "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="], - "@epicenter/opencode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "@epicenter/opencode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - "@epicenter/opencode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@epicenter/opencode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@epicenter/opencode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], + "@epicenter/opencode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], } } diff --git a/docs/specs/20251003T220750 vault-core-minimal-overview.md b/docs/specs/20251003T220750 vault-core-minimal-overview.md index abee8676e5..e9b0b032c8 100644 --- a/docs/specs/20251003T220750 vault-core-minimal-overview.md +++ b/docs/specs/20251003T220750 vault-core-minimal-overview.md @@ -1,26 +1,26 @@ # Vault Core Minimal Overview - Date: 2025-10-03 +- Updated: 2025-10-20 - Status: Draft - Owner: Vault core maintainers ## Architecture Snapshot -- **Adapters** expose prefixed Drizzle schema plus: - - `versions`: ordered tuple of `{ tag: '0000', sql: string[] }` - - `transforms`: registry keyed by non-baseline tags - - `validator`: Standard Schema parser for ingest payloads - - `ingestors` (optional): file parsers returning validator-ready payloads - - `metadata`: table/column descriptions (typed via `AdapterMetadata`) -- **Vault** orchestrator (see `packages/vault-core/src/core/vault.ts`) wires adapters into import, ingest, and export flows without owning IO. +- Adapters expose prefixed Drizzle schema plus: + - versions: ordered tuple array of { tag: '0000', sql: string[] } + - transforms: registry keyed by non-baseline tags + - validator: Standard Schema parser for adapter payloads + - ingestors (optional): file parsers returning validator-ready payloads + - metadata: table and column descriptions (typed via AdapterMetadata) +- Vault orchestrator wires adapters into import, ingest, and export flows without owning IO. It performs multi-adapter import by auto-detecting adapters from file paths. See [packages/vault-core/src/core/vault.ts](packages/vault-core/src/core/vault.ts). +- Adapters remain independent; cross-adapter relationships are composed by hosts using the query interface. ## Migration Workflow (Plan A) -1. Adapter author runs `drizzle-kit` locally, copies SQL into `versions[n].sql`. -2. `createVault` calls `runStartupSqlMigrations(adapter.id, adapter.versions, db)` before touching tables; SQL arrays replay sequentially and ledger tables are managed automatically. -3. Export pipeline writes `__meta__/migration.json` via `createMigrationMetadataFile`. - -> Plan B (inline diff using Drizzle internals) is commented out but preserved inside `migrations.ts` for future reference. +1. Adapter authors generate SQL locally (for example with Drizzle) and copy statements into the adapter migrations file. Example: [packages/vault-core/src/adapters/reddit/migrations/versions.ts](packages/vault-core/src/adapters/reddit/migrations/versions.ts). +2. Before touching tables, the vault runs per-adapter SQL migrations; ledger tables are managed automatically. See [packages/vault-core/src/core/migrations.ts](packages/vault-core/src/core/migrations.ts). +3. The export pipeline writes a migration metadata file alongside data files. See [packages/vault-core/src/core/import/migrationMetadata.ts](packages/vault-core/src/core/import/migrationMetadata.ts). ## Import Path (Files ➜ DB) @@ -60,4 +60,58 @@ - [`packages/vault-core/src/core/import/importPipeline.ts`](packages/vault-core/src/core/import/importPipeline.ts) - [`packages/vault-core/src/core/adapter.ts`](packages/vault-core/src/core/adapter.ts) -This document is the minimal reference for contributors implementing adapters or host integrations going forward. +# This document is the minimal reference for contributors implementing adapters or host integrations going forward. + +> Plan B (inline diff using Drizzle internals) remains documented in comments in [packages/vault-core/src/core/migrations.ts](packages/vault-core/src/core/migrations.ts) for future exploration. + +## Import Path (multi-adapter: files to DB) + +1. Host collects a bundle as a map of file paths to File objects and selects a codec. Codec determines file extension and normalize or denormalize behavior. See [packages/vault-core/src/core/codec.ts](packages/vault-core/src/core/codec.ts). +2. The vault import groups files by detected adapter ID using the path convention adapterId/tableName/pk.json. Unknown adapters are skipped. +3. For each detected adapter, the vault: + - runs SQL migrations for that adapter + - parses each file, enforces the codec file extension, and skips the migration metadata directory + - denormalizes records with the codec and filters to actual table columns + - applies the adapter versions and transforms during the import pipeline. See [packages/vault-core/src/core/import/importPipeline.ts](packages/vault-core/src/core/import/importPipeline.ts). + - validates the transformed dataset using `drizzle-arktype` (NOT THE ADAPTER VALIDATOR) + - replaces adapter tables atomically by truncating and inserting rows +4. When present, the migration tag is detected from the metadata file and used for transform selection. + +### Edge cases and errors + +- Unknown adapter in a bundle: skipped +- Unknown table for a detected adapter: error with explicit message +- Wrong codec extension: error for the specific file +- No adapter validator: error for that adapter + +## Ingest Path (single file to DB) + +1. The vault selects the matching ingestor based on adapter ingestors metadata. +2. The ingestor parses the file and returns a payload in the adapter expected shape. +3. The adapter Standard Schema validator morphs and validates the payload. +4. The vault replaces the adapter tables using the same helper as the import path. + +## Export Path (DB to files) + +1. Each adapter table is read via Drizzle from the host-supplied database. +2. The codec normalizes rows and writes deterministic file paths using adapter conventions. +3. A migration metadata file is added to the export bundle. + +## Host Responsibilities + +- Supply a Drizzle-compatible database; vault-core manages migration ledger tables automatically. +- Pass the adapter list into the vault; adapter IDs must be unique. +- Provide a codec for import and export; callers do not pass validators or transform overrides. +- Execute vault operations in an environment that supports DDL (for example a server runtime backed by SQLite) to allow migrations to run. +- Offer UI or CLI to trigger import, export, and ingest operations as appropriate for your app. + +## Key Entry Points + +- [packages/vault-core/src/core/vault.ts](packages/vault-core/src/core/vault.ts) +- [packages/vault-core/src/core/migrations.ts](packages/vault-core/src/core/migrations.ts) +- [packages/vault-core/src/core/import/importPipeline.ts](packages/vault-core/src/core/import/importPipeline.ts) +- [packages/vault-core/src/core/adapter.ts](packages/vault-core/src/core/adapter.ts) +- [packages/vault-core/src/core/import/migrationMetadata.ts](packages/vault-core/src/core/import/migrationMetadata.ts) +- [packages/vault-core/src/core/codec.ts](packages/vault-core/src/core/codec.ts) + +This document is a minimal reference for contributors implementing adapters or host integrations. diff --git a/packages/vault-core/README.md b/packages/vault-core/README.md index 1079d105d4..28086c7bb8 100644 --- a/packages/vault-core/README.md +++ b/packages/vault-core/README.md @@ -1,150 +1,172 @@ -# Vault Core +--- -Vault Core provides the primitives and runtime to parse, validate, persist, and sync third‑party exports into a Git‑friendly plaintext vault. It’s the foundation for Epicenter’s adapter ecosystem. +# vault-core -> Status: Alpha. APIs may change before 1.0. +> This represents a very early, proof-of-concept version of Vault Core. The API, features, architecture, applications, and more are all subject to significant change. +> Take this as a sneak-peek into ongoing work, not a stable library. -## Architecture at a glance +A small, adapter-driven data vault. Each adapter owns its schema, validation, migrations, and ingest rules; the vault orchestrates import, export, and ingest, without coupling adapters together. Apps compose multiple adapters at runtime to build cross-adapter UX. -- Importers: end‑to‑end units that parse a source blob, validate with ArkType, and upsert into a Drizzle database. -- Adapters: carry the Drizzle schema and migrations config, plus optional human‑readable metadata for tables/columns. -- VaultService: the orchestrator that runs migrations, imports blobs, and syncs data to/from a filesystem using a single injected codec and a convention profile. -- Conventions & Codec: conventions control file layout (paths, dataset keys); a single Markdown codec serializes/parses records deterministically. -- SyncEngine: a VCS‑focused abstraction (Git implementation provided) that offers a FileStore for read/write/list and simple pull/commit/push. +Highlights -This separation keeps Importers/Adapters pure (schema + parsing + upsert) and pushes all DB/filesystem glue and VCS specifics into the service layer. +- Independent adapters: schemas are table-prefixed and migration-scoped per adapter +- Deterministic import/export shapes with codec-normalized files +- Per-adapter validation (Standard Schema; arktype-backed) enforced at import/ingest +- Migrations applied automatically before writes +- Multi-adapter import: one call processes a mixed bundle by auto-detecting adapters from file paths +- Runtime traversal: get a Drizzle-compatible db and adapter tables map for app-layer joins -## Key concepts +Quick links -- Adapter - - Drizzle schema (tables) and migrations config (`drizzleConfig`). - - Optional metadata for human‑readable names/descriptions. +- Vault constructor: [`createVault()`](packages/vault-core/src/core/vault.ts:31) +- Import (multi-adapter): [`importData()`](packages/vault-core/src/core/vault.ts:176) +- Export: [`exportData()`](packages/vault-core/src/core/vault.ts:116) +- Ingest (adapter-owned parsers): [`ingestData()`](packages/vault-core/src/core/vault.ts:284) +- Runtime traversal: [`getQueryInterface()`](packages/vault-core/src/core/vault.ts:317) +- Adapter definition: [`defineAdapter`](packages/vault-core/src/core/adapter.ts:137) -- Importer - - `id`, `name`, `adapter`, `metadata` (optional), `validator` (ArkType), `parse(blob)`, `upsert(db, data)`. - - Owns the source‑specific parsing and validation; calls into the Adapter’s schema when upserting. +## Core concepts -- VaultService - - Owns the database and the set of Importers. - - Runs migrations for installed Importers. - - Import/export flows: - - `importBlob(blob, importerId)` → parse/validate/upsert into DB. - - `export(importerId, store)` → DB → files using the configured codec (e.g., Markdown). - - `import(importerId, store)` → files → DB using the configured codec (handles null/undefined normalization and light type coercions). - - Optional Git helpers when a SyncEngine is provided: `gitPull()`, `gitCommit(msg)`, `gitPush()`. - - Accepts a single `codec` and a `conventions` profile to control layout and serialization. +Adapters -- Conventions & Codec - - ConventionProfile: provides `pathFor(adapterId, tableName, pkValues)` and `datasetKeyFor(adapterId, tableName)`. - - Markdown codec: YAML frontmatter + body. Deterministic, quotes numeric‑like strings to avoid accidental type shifts on re‑import. Also provides optional value normalization hooks. - - Conventions: - - Omit nulls on export; on import, `VaultService` normalizes `null → undefined` and applies light primitive coercions for common text fields. - - Paths are derived from sorted primary‑key values to minimize churn. +- An adapter bundles: + - Drizzle schema with table names prefixed by adapter id (e.g., `example_notes_items`) + - Versions and transforms for migrations + - A Standard Schema validator (arktype-backed) for parsed dataset shapes + - Optional ingestors for raw file formats +- Table prefixing and primary keys are compile-time checked; see [`core/adapter.ts`](packages/vault-core/src/core/adapter.ts:86). +- Adapters remain independent; the vault never couples their storage. -- SyncEngine - - Interface: `getStore()`, `pull()`, `commit(message)`, `push()`. - - `GitSyncEngine`: shells out to `git` and exposes a FileStore rooted at the repo. - - `LocalFileStore`: read/write/list operations used by both the service and sync engines. +Validation -## Typical flows +- During import and ingest, the vault runs a Standard Schema validator: + - Import uses the adapter’s validator to validate the parsed dataset before table replacement + - Ingest uses the adapter’s validator on the ingestor output +- Failed validation aborts the operation with detailed path messages; see error formatting in [`runValidation`](packages/vault-core/src/core/vault.ts:44). -1. Blob → DB (Importer‑only) +Migrations -- Call `VaultService.importBlob(blob, importerId)` to parse, validate (ArkType), and upsert using the Importer. +- Before touching an adapter’s tables, the vault ensures its SQL migrations are applied via [`runStartupSqlMigrations`](packages/vault-core/src/core/vault.ts:37). +- The export flow writes a per-adapter migration metadata file; import detects this metadata and records its tag. -2. DB → Filesystem (Export) +Codecs and format -- Call `VaultService.export(importerId, store)`; the service writes deterministic files using the configured codec under `vault//
/...` (e.g., `.md` for Markdown). +- A codec defines parse/stringify and normalization/denormalization rules; JSON is the default via [`jsonFormat`](packages/vault-core/src/codecs/json.ts:3). +- Paths follow a deterministic convention (adapterId/tableName/pk.json) computed with the default convention used by export. -3. Filesystem → DB (Import) +Compatible DB -- Call `VaultService.import(importerId, store)`; values are parsed/denormalized via the configured codec, nulls are dropped, and common primitive coercions are applied. ArkType validation is not run for filesystem imports (only for first‑ingest via `importBlob`). +- The vault expects a Drizzle-compatible, SQLite-compatible DB. Use a server environment for DDL-backed features. Tests may make use of `bun:sqlite` in-memory DB. -## DB ↔ FS version compatibility +## API overview -Vault files (via the Sync Engine) reflect a point-in-time schema. Importing them into a DB with a different schema can fail or silently coerce data. Version awareness lets us reproducibly rebuild state, minimize surprises, and keep migrations the single source of truth for structural changes. +Construct -Behavior matrix (per Importer) +- Create a vault bound to a DB instance and a set of adapters: + - [`createVault(options)`](packages/vault-core/src/core/vault.ts:31) where options include `database` (Drizzle-compatible) and `adapters` (array of adapter instances). -| DB State | Sync Engine version | Behavior | Outcome | -|-----------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| Matches head | N/A | No-op | Success (no change) | -| Older than head | N/A | Existing data in database will be migrated | Success | -| Newer than head | N/A | Plan not found in journal → throw | Error (requires updating adapter) | -| Any | Matches head | Import directly with `import()` (no validator), then optionally `migrateFunc` (no-ops) | Success | -| Any | Older than head | Use `migrateImportMigrate(targetTag)`: drop importer tables → apply SQL up to target → import → `migrateFunc` to head | Success if migrations are forward-only and include needed data transforms | -| Any | Newer than head | Plan not found in journal → throw | Error (requires updating adapter) | -| Any | Tag not present in journal | Cannot compute plan → throw | Error | -| Any | No FS version available | Require explicit `targetTag` (CLI flag or manifest) | Error until specified | +Export -Notes +- Export adapter data to a codec-normalized file bundle: + - [`exportData({ codec })`](packages/vault-core/src/core/vault.ts:116) returns `Map`: `{ path -> File }`. + - Exports all registered adapters by default; per-adapter migration metadata is included. -- `migrateImportMigrate(importerId, store, { targetTag })` orchestrates the safe path when FS is at an older schema: it drops only the importer’s tables, applies SQL to reach the FS version, imports without ArkType, then migrates forward to head. -- Exports do not consult FS; the DB is the source of truth and defines the serialized shape. -- Data transforms belong in migrations when schema changes are not shape‑compatible. Without them, forward migration after import can fail. +Import (multi-adapter) - +Ingest -## Minimal wiring example (service) +- Run adapter-owned parsers on raw files: + - [`ingestData({ adapter, file })`](packages/vault-core/src/core/vault.ts:284) + - The vault selects the first ingestor that matches and validates the parsed dataset before replacement. -Pseudocode for a Node environment using LibSQL, Git sync, a single Markdown codec, and default conventions: +Runtime traversal -```ts -import { createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; -import { migrate } from "drizzle-orm/libsql/migrator"; -import { VaultService, markdownFormat, defaultConvention, GitSyncEngine } from "@repo/vault-core"; +- Query at runtime and compose cross-adapter views in the app: + - [`getQueryInterface()`](packages/vault-core/src/core/vault.ts:317) returns `{ db, tables }` + - `db`: Drizzle-compatible DB + - `tables`: map of `adapterId -> adapter.schema`, suitable for joins -// importers: an array of Importer instances (each has id, parse, validator, upsert, adapter) -const importers = [redditImporter /*, ...*/]; +## Import/export formats -const client = createClient({ url: "file:/abs/path/to.db" }); -const db = drizzle(client); +Exported paths -const svc = await VaultService.create({ - importers, - database: db, - migrateFunc: migrate, - syncEngine: new GitSyncEngine(process.cwd()), - codec: markdownFormat, - conventions: defaultConvention(), -}); +- Paths follow `adapterId/tableName/pk.json`, for example: + - `reddit/reddit_posts/t3_abc123.json` + - `entity_index/entity_index_entities/entity:subreddit:sveltejs.json` -// Import an export ZIP -await svc.importBlob( - new Blob( - [ - /* bytes */ - ], - { type: "application/zip" } - ), - "reddit" -); +Record content -// Export to files, commit, and push -const store = await svc.gitPull().then(() => svc["syncEngine"]!.getStore()); -await svc.export("reddit", store); -await svc.gitCommit("Export vault"); -await svc.gitPush(); -``` +- JSON records include only table columns (normalized by codec). Primary key values are encoded in the path, not the JSON body. -Notes +Import bundle rules -- You can also use `VaultService`. -- The demo CLI in `apps/demo-mcp` shows how to stand up a tiny importer‑driven pipeline with LibSQL. +- A single bundle can contain files for multiple adapters; `importData` will: + - Skip unknown adapter paths + - Throw on wrong file extensions + - Throw on unknown tables in a known adapter + - Replace tables for each adapter it successfully processes -## Merge‑friendly plaintext by design +## Adapter authoring -- Deterministic serialization (stable key order, consistent newlines). -- YAML frontmatter quotes numeric‑like strings to avoid accidental number/boolean/null coercion on re‑import. -- File paths built from sorted primary keys; nulls omitted to reduce diff noise. -- Import normalization handles `null → undefined` and light primitive coercions for common text fields. +Minimal shape (TypeScript) -## Roadmap / open questions +- Use [`defineAdapter`](packages/vault-core/src/core/adapter.ts:137) to declare: + - `id` (string), `schema` (prefixed Drizzle tables), `versions`, `transforms`, + - Optionally `metadata` for documentation/UI + - Optionally `ingestors` for external inputs + - A Standard Schema `validator` for ingest data +- Prefixing: table names must begin with `adapterId_` (enforced at types); e.g., `example_notes_items` -- Potential future codecs (YAML, TOML, MDX) and richer frontmatter support. -- More sync engines (e.g., cloud/object storage backends). -- Declarative, column‑aware coercion/normalization derived from Drizzle types. -- Formal import/export test suites per Importer. +Validation shape + +- The parsed dataset shape is a de-prefixed object keyed by table names: + - Example: `{ items: Array, note_links?: Array }` +- Standard Schema (arktype) validators should accept this parsed shape and return the same shape; the vault serializes/denormalizes for storage as needed. + +## Server-backed ingestion + +Migrations require DDL, so run vault operations server-side with a DB like Bun SQLite + Drizzle: + +- For a reference implementation, see: + - Vault service singleton: [`apps/vault-demo/src/lib/server/vaultService.ts`](apps/vault-demo/src/lib/server/vaultService.ts + - Endpoints (SvelteKit +server.ts): + - Ingest: [`apps/vault-demo/src/routes/api/vault/ingest/+server.ts`](apps/vault-demo/src/routes/api/vault/ingest/+server.ts + - Import (multi-adapter): [`apps/vault-demo/src/routes/api/vault/import/+server.ts`](apps/vault-demo/src/routes/api/vault/import/+server.ts + - Export: [`apps/vault-demo/src/routes/api/vault/export/+server.ts`](apps/vault-demo/src/routes/api/vault/export/+server.ts + - Counts: [`apps/vault-demo/src/routes/api/vault/tables/+server.ts`](apps/vault-demo/src/routes/api/vault/tables/+server.ts + +## Demo app + +A minimal SvelteKit demo shows: + +- Import/export page calling `importData`/`exportData` via server endpoints: + - [`apps/vault-demo/src/routes/import-export/+page.svelte`](apps/vault-demo/src/routes/import-export/+page.svelte:1) +- Reddit GDPR ingest + entity suggestions → user-curated Entity Index import + - [`apps/vault-demo/src/routes/reddit-upload/+page.svelte`](apps/vault-demo/src/routes/reddit-upload/+page.svelte:1) + - Heuristics for subreddits, users, domains: + - [`apps/vault-demo/src/lib/extract/redditEntities.ts`](apps/vault-demo/src/lib/extract/redditEntities.ts +- Runtime cross-adapter UI (Dashboard, Entities, Notes) using `getQueryInterface()` + +## Notes on the new multi-adapter import + +- One-call, multi-adapter import is now the default +- Import replaces (not merges) the target adapter’s tables +- Unknown adapters and migration metadata files are skipped +- Strict validation is enforced per adapter; failed validation aborts that adapter’s import + +## Limitations and tips + +- Ensure your DB supports DDL; client-only mocks are not compatible with migrations +- The vault’s path convention is authoritative for identifying adapters/tables during import +- Use the adapter’s Standard Schema validator for dataset shapes; do not rely on caller-provided validators diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index dc91446f3b..22424ac711 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -5,10 +5,12 @@ "type": "module", "exports": { ".": "./src/index.ts", + "./codecs": "./src/codecs/index.ts", "./adapters/*": "./src/adapters/*/index.ts", "./utils/*": "./src/utils/*/index.ts" }, "devDependencies": { + "tsx": "^4.20.6", "typescript": "catalog:" }, "scripts": { diff --git a/packages/vault-core/src/adapters/index.ts b/packages/vault-core/src/adapters/index.ts deleted file mode 100644 index 9e653bc150..0000000000 --- a/packages/vault-core/src/adapters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './reddit'; diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index b2cfa2de15..42cc1a7c65 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -7,9 +7,9 @@ import type { import type { CompatibleDB } from './db'; import type { Ingestor } from './ingestor'; import type { + DataTransform, RequiredTransformTags, Tag4, - TransformRegistry, VersionDef, } from './migrations'; @@ -51,7 +51,57 @@ type NullToUndefined = { : Exclude | ([null] extends [T[K]] ? undefined : never); }; -/** Translate schema to object, strip prefix of table names */ +type Simplify = { [K in keyof T]: T[K] } & {}; + +type ColumnHasDefaultValue = TColumn extends { + _: { + hasDefault: infer HasDefault; + hasRuntimeDefault: infer HasRuntimeDefault; + isAutoincrement: infer IsAutoincrement; + }; +} + ? HasDefault extends true + ? true + : HasRuntimeDefault extends true + ? true + : IsAutoincrement extends true + ? true + : false + : false; + +type ColumnKeys = Extract< + keyof TTable['_']['columns'], + string +>; + +type ColumnsWithDefaults = { + [K in ColumnKeys]: ColumnHasDefaultValue< + TTable['_']['columns'][K] + > extends true + ? K + : never; +}[ColumnKeys]; + +type ApplyDefaultableColumns< + TTable extends SQLiteTable, + TRow extends Record, +> = Simplify< + Omit, keyof TRow>> & { + [K in Extract, keyof TRow>]?: + | TRow[K] + | undefined; + } +>; + +// Allow server generated columns (defaults, runtime defaults, autoincrement IDs) to be omitted in validator payloads. +type TableRowShape = NullToUndefined< + InferSelectModel +> extends infer Row + ? Row extends Record + ? ApplyDefaultableColumns + : Row + : never; + /** * Map a prefixed schema record to an object whose keys are the table names with the * adapter prefix removed and whose values are arrays of the inferred row type. @@ -66,7 +116,13 @@ export type SchemaMappedToObject< > = { [K in keyof TObj as K extends `${TID}_${infer Rest}` ? Rest - : K]: NullToUndefined>[]; + : K]: TableRowShape[]; +}; + +type TransformAlignment[]> = { + [K in RequiredTransformTags]: DataTransform; +} & { + [K in Exclude>]?: never; }; /** @@ -79,7 +135,8 @@ export interface Adapter< string, SQLiteTable >, - TVersions extends readonly VersionDef[] = readonly VersionDef[], + TVersions extends + readonly VersionDef[] = readonly VersionDef[], TPreparsed = unknown, TParsed = unknown, > { @@ -124,10 +181,10 @@ export interface Adapter< versions: TVersions; /** - * Transform registry; when provided with 'versions', tag alignment is - * enforced at compile time and verified at runtime. + * Transform registry for forward data migrations. Alignment with versions is enforced + * when adapters are authored through `defineAdapter`. */ - transforms: TransformRegistry>; + transforms: Partial>; } /** @@ -160,20 +217,30 @@ export function defineAdapter< export function defineAdapter unknown>( adapter: F, ): F { - return adapter; + const wrapped = ((...args: Parameters) => { + const instance = adapter(...args); + validateTransformsAgainstVersions(instance as Adapter); + return instance; + }) as unknown as F; + return wrapped; } /** * Compile-time detection of whether a table has a primary key. - * Produces the table name union if any table is missing a primary key; else never. + * Looks for any column with an internal `_.isPrimaryKey === true` flag. + * Produces `true` when at least one PK column exists; otherwise `false`. */ -type TableHasPrimaryKey = { - [K in keyof TColumns]: TColumns[K] extends { _: { isPrimaryKey: true } } - ? K - : never; -}[keyof TColumns] extends never - ? false - : true; +type TableHasPrimaryKey = TTable extends { + _: { columns: infer TCols extends Record }; +} + ? { + [K in keyof TCols]: TCols[K] extends { _: { isPrimaryKey: true } } + ? K + : never; + }[keyof TCols] extends never + ? false + : true + : false; type EnsureSchemaHasPrimaryKeys> = { [K in keyof S]: TableHasPrimaryKey extends false ? never : K & string; }[keyof S]; @@ -189,10 +256,9 @@ type PrefixedAdapter< TVersions extends readonly VersionDef[], TPreparsed, TParsed, -> = Adapter, Schema, TVersions, TPreparsed, TParsed> & - // If any table names are NOT prefixed with `${TID}_`, attach an impossible property - // so TS surfaces a clear, actionable error including the offending keys. - (MissingPrefixedTables extends never +> = Adapter, Schema, TVersions, TPreparsed, TParsed> & { + transforms: TransformAlignment; +} & (MissingPrefixedTables extends never // so TS surfaces a clear, actionable error including the offending keys. // If any table names are NOT prefixed with `${TID}_`, attach an impossible property ? unknown : { __error__schema_table_prefix_mismatch__: `Expected all tables to start with "${TID}_"`; @@ -247,3 +313,24 @@ type NoDuplicateAdapter< */ export type UniqueAdapters = NoDuplicateAdapter extends never ? T : never; + +function validateTransformsAgainstVersions(adapter: Adapter) { + const versions = adapter.versions ?? []; + if (!versions.length) return; + + const declaredTags = versions.map((v) => v.tag); + const required = declaredTags.slice(1); + const transforms = adapter.transforms ?? {}; + const actual = Object.keys(transforms); + + const missing = required.filter((tag) => !actual.includes(tag)); + const extras = actual.filter((tag) => !required.includes(tag)); + + if (missing.length > 0 || extras.length > 0) { + throw new Error( + `defineAdapter: adapter '${adapter.id}' transforms do not match versions. ` + + `required=[${required.join(',')}] actual=[${actual.join(',')}] ` + + `missing=[${missing.join(',')}] extras=[${extras.join(',')}]`, + ); + } +} diff --git a/packages/vault-core/src/core/codec.ts b/packages/vault-core/src/core/codec.ts index 4df2492b13..37b6205bd0 100644 --- a/packages/vault-core/src/core/codec.ts +++ b/packages/vault-core/src/core/codec.ts @@ -21,10 +21,6 @@ export interface Codec { * frontmatter); others may serialize it as a normal field. */ stringify(rec: Record): string; - /** Optional value normalization before writing (e.g., Date -> ISO string) */ - normalize?(value: unknown, columnName: string): unknown; - /** Optional value denormalization after reading (e.g., ISO string -> Date) */ - denormalize?(value: unknown, columnName: string): unknown; } // Runtime view of a Drizzle table @@ -57,7 +53,7 @@ export function listPrimaryKeys(tableName: string, table: SQLiteTable) { const pkCols = []; for (const col of cols) { const [, drizzleCol] = col; - if (drizzleCol._.isPrimaryKey) pkCols.push(col); + if (drizzleCol.primary) pkCols.push(col); } if (pkCols.length === 0) diff --git a/packages/vault-core/src/core/config.ts b/packages/vault-core/src/core/config.ts index 038cbe99b5..0891217d40 100644 --- a/packages/vault-core/src/core/config.ts +++ b/packages/vault-core/src/core/config.ts @@ -1,12 +1,6 @@ import type { Adapter, UniqueAdapters } from './adapter'; import type { Codec, ConventionProfile } from './codec'; import type { DrizzleDb } from './db'; -import type { - DataValidator, - Tag4, - TransformRegistry, - VersionDef, -} from './migrations'; /** Construct a Vault around a Drizzle DB. */ export type CoreOptions = { @@ -31,42 +25,13 @@ export type ExportOptions = { }; /** - * Provide the target Adapter, a files map (path -> contents), - * and the codec used to parse these files. Conventions may be overridden per-call. + * Import options: caller provides files and codec only. + * Adapters, versions, transforms, and validation are determined by adapter definitions. */ -export type ImportOptions = { - /** The target Adapter to use for importing data */ - adapterID: AdapterIDs<[UniqueAdapters[number]]>; +export type ImportOptions = { files: Map; /** Codec (format) to use for imports. Must match the exported format */ codec: Codec; - /** Optional conventions override (otherwise uses built-in default) */ - conventions?: ConventionProfile; - - /** - * Plan A (optional): ordered version definitions used to plan data transforms forward-only. - * When provided together with transforms, core will run the transform chain and then validation before upsert. - * Falls back to adapter-level versions when omitted. - */ - versions?: readonly VersionDef[]; - - /** - * Plan A (optional): registry of data transforms keyed by target version tag. - * Must cover all forward steps when versions are provided. - */ - transforms?: TransformRegistry; - - /** - * Plan A (optional): runtime validator (e.g., drizzle-arktype) invoked after transforms. - * If omitted and versions/transforms are not provided, core falls back to adapter-level validator. - */ - dataValidator?: DataValidator; - - /** - * Plan A (optional): source dataset version tag; used as the starting point for the transform chain. - * If omitted, the host/importer should provide a sensible default (e.g., '0000') or encode version in the bundle metadata. - */ - sourceTag?: string; }; /** IngestOptions variant for one-time single-file ingestors (e.g., ZIP). */ @@ -93,7 +58,7 @@ export type QueryInterface = { */ export type Vault = { exportData(options: ExportOptions): Promise>; - importData(options: ImportOptions): Promise; + importData(options: ImportOptions): Promise; ingestData(options: IngestOptions): Promise; getQueryInterface(): QueryInterface; }; diff --git a/packages/vault-core/src/core/import/importPipeline.ts b/packages/vault-core/src/core/import/importPipeline.ts index b31843fd14..d3140f1c82 100644 --- a/packages/vault-core/src/core/import/importPipeline.ts +++ b/packages/vault-core/src/core/import/importPipeline.ts @@ -33,11 +33,11 @@ import { transformAndValidate } from '../migrations'; export type ImportPipelineInput = { dataset: Record; adapter: Adapter; - transformsOverride?: TransformRegistry; - versionsOverride?: readonly VersionDef[]; - dataValidator?: DataValidator; - sourceTag?: string; - detectedTag?: string; + transformsOverride?: TransformRegistry | undefined; + versionsOverride?: readonly VersionDef[] | undefined; + dataValidator?: DataValidator | undefined; + sourceTag?: string | undefined; + detectedTag?: string | undefined; }; /** diff --git a/packages/vault-core/src/core/import/migrationMetadata.ts b/packages/vault-core/src/core/import/migrationMetadata.ts index f1f1a4ad2e..de07ea3741 100644 --- a/packages/vault-core/src/core/import/migrationMetadata.ts +++ b/packages/vault-core/src/core/import/migrationMetadata.ts @@ -9,6 +9,7 @@ * Host tooling can read this file to pre-populate “source version” selectors, drive * transform planning, or display drift warnings (ledger vs. declared versions). */ +import { jsonFormat } from '../../codecs'; import type { Adapter } from '../adapter'; import type { DrizzleDb } from '../db'; import { ensureVaultLedgerTables, getVaultLedgerTag } from '../migrations'; @@ -26,7 +27,7 @@ export type MigrationMetadata = { ledgerTag: string | null; latestDeclaredTag: string | null; versions: string[]; - exportedAt: string; + exportedAt: Date; }; /** @@ -72,7 +73,7 @@ export async function createMigrationMetadata( ledgerTag: ledgerTag ?? null, latestDeclaredTag: latestDeclaredTag ?? null, versions: declaredTags, - exportedAt: clock().toISOString(), + exportedAt: clock(), }; } @@ -87,7 +88,8 @@ export async function createMigrationMetadataFile( ): Promise<{ path: string; file: File; metadata: MigrationMetadata }> { const metadata = await createMigrationMetadata(adapter, db, clock); const file = new File( - [JSON.stringify(metadata, null, 4)], + // We'll use JSON codec here so that date serialization is consistent + [jsonFormat.stringify(metadata)], MIGRATION_META_FILENAME, { type: 'application/json' }, ); diff --git a/packages/vault-core/src/core/migrations.ts b/packages/vault-core/src/core/migrations.ts index 3220491d5c..0b02e06e70 100644 --- a/packages/vault-core/src/core/migrations.ts +++ b/packages/vault-core/src/core/migrations.ts @@ -11,8 +11,13 @@ */ import type { InferInsertModel } from 'drizzle-orm'; -import { sql } from 'drizzle-orm'; -import type { SQLiteTable } from 'drizzle-orm/sqlite-core'; +import { eq, sql } from 'drizzle-orm'; +import { + integer, + type SQLiteTable, + sqliteTable, + text, +} from 'drizzle-orm/sqlite-core'; import type { DrizzleDb } from './db'; /** Drizzle migration journal entry (parsed from meta/_journal.json by the host). */ @@ -37,7 +42,7 @@ export type MigrationJournal = { */ export type MigrationPlan = { /** Current DB version tag (if known). */ - from?: string; + from: string | undefined; /** Target version tag (must exist in the journal). */ to: string; /** Ordered list of tags to apply to reach the target. */ @@ -56,18 +61,16 @@ export function planToVersion( const order = new Map(journal.entries.map((e, i) => [e.tag, i] as const)); const targetIdx = order.get(targetTag); - if (targetIdx == null) { + if (targetIdx === undefined) throw new Error(`Target migration tag not found in journal: ${targetTag}`); - } const currentIdx = currentTag != null ? (order.get(currentTag) ?? -1) : -1; - if (currentIdx > targetIdx) { - // Downgrade paths are not supported by this planner; hosts can implement if needed. + // Downgrade paths are not supported by this planner; hosts can implement if needed. + if (currentIdx > targetIdx) throw new Error( `Current tag (${currentTag}) is ahead of target tag (${targetTag}); downgrades are not supported in core planner.`, ); - } const forward = journal.entries .slice(currentIdx + 1, targetIdx + 1) @@ -461,25 +464,35 @@ export type MigrationExecutor = ( // ============================== /** Vault-managed migration tables: SQL schema strings hosts can execute. */ -export const VAULT_MIGRATIONS_SQL = ` +const VAULT_MIGRATIONS_TABLE_NAME = 'vault_migrations'; +const VAULT_MIGRATIONS_SQL = ` CREATE TABLE IF NOT EXISTS vault_migrations ( adapter_id TEXT PRIMARY KEY, current_tag TEXT NOT NULL, updated_at INTEGER NOT NULL -); -`; - -export const VAULT_MIGRATION_JOURNAL_SQL = ` +);`; +const VAULT_MIGRATIONS_TABLE = sqliteTable(VAULT_MIGRATIONS_TABLE_NAME, { + adapter_id: text('adapter_id').primaryKey(), + current_tag: text('current_tag').notNull(), + updated_at: integer('updated_at').notNull(), +}); + +const VAULT_MIGRATION_JOURNAL_TABLE_NAME = 'vault_migration_journal'; +const VAULT_MIGRATION_JOURNAL_SQL = ` CREATE TABLE IF NOT EXISTS vault_migration_journal ( adapter_id TEXT NOT NULL, tag TEXT NOT NULL, applied_at INTEGER NOT NULL, PRIMARY KEY (adapter_id, tag) +);`; +const VAULT_MIGRATION_JOURNAL_TABLE = sqliteTable( + VAULT_MIGRATION_JOURNAL_TABLE_NAME, + { + adapter_id: text('adapter_id').notNull(), + tag: text('tag').notNull(), + applied_at: integer('applied_at').notNull(), + }, ); -`; - -const VAULT_MIGRATIONS_TABLE = 'vault_migrations'; -const VAULT_MIGRATION_JOURNAL_TABLE = 'vault_migration_journal'; export async function ensureVaultLedgerTables(db: DrizzleDb): Promise { await db.run(sql.raw(VAULT_MIGRATIONS_SQL)); @@ -491,9 +504,16 @@ export async function getVaultLedgerTag( adapterId: string, ): Promise { await ensureVaultLedgerTables(db); - const row = await db.get<{ current_tag: string | null }>( - sql`SELECT current_tag FROM ${sql.raw(VAULT_MIGRATIONS_TABLE)} WHERE adapter_id = ${adapterId}`, - ); + // const row = await db.get<{ current_tag: string | null }>( + // sql`SELECT current_tag FROM ${sql.raw(VAULT_MIGRATIONS_TABLE_NAME)} WHERE adapter_id = ${adapterId}`, + // ); + const row = await db + .select() + .from(VAULT_MIGRATIONS_TABLE) + .where(eq(VAULT_MIGRATIONS_TABLE.adapter_id, adapterId)) + .limit(1) + .get(); + return row?.current_tag ?? undefined; } @@ -504,11 +524,19 @@ async function setVaultLedgerTag( ): Promise { await ensureVaultLedgerTables(db); const timestamp = Date.now(); - await db.run( - sql`INSERT INTO ${sql.raw(VAULT_MIGRATIONS_TABLE)} (adapter_id, current_tag, updated_at) -VALUES (${adapterId}, ${tag}, ${timestamp}) -ON CONFLICT(adapter_id) DO UPDATE SET current_tag = excluded.current_tag, updated_at = excluded.updated_at`, - ); + + // Upsert semantics: update existing row or insert a new one + await db + .insert(VAULT_MIGRATIONS_TABLE) + .values({ + adapter_id: adapterId, + current_tag: tag, + updated_at: timestamp, + }) + .onConflictDoUpdate({ + target: VAULT_MIGRATIONS_TABLE.adapter_id, + set: { current_tag: tag, updated_at: timestamp }, + }); } async function appendVaultLedgerJournal( @@ -518,49 +546,40 @@ async function appendVaultLedgerJournal( ): Promise { await ensureVaultLedgerTables(db); const timestamp = Date.now(); - await db.run( - sql`INSERT INTO ${sql.raw(VAULT_MIGRATION_JOURNAL_TABLE)} (adapter_id, tag, applied_at) -VALUES (${adapterId}, ${tag}, ${timestamp}) -ON CONFLICT(adapter_id, tag) DO NOTHING`, - ); + await db + .insert(VAULT_MIGRATION_JOURNAL_TABLE) + .values({ + adapter_id: adapterId, + tag, + applied_at: timestamp, + }) + .onConflictDoNothing(); } /** Build a pseudo-journal from a versions tuple to reuse planToVersion. */ export function buildJournalFromVersions< - TVersions extends readonly VersionDef[], + TVersions extends readonly VersionDef[], >(versions: TVersions): MigrationJournal { return { entries: versions.map((v) => ({ tag: v.tag })), }; } -/** Split a monolithic SQL string into executable statements. */ -function splitSqlText(text: string): string[] { - // Prefer explicit drizzle 'statement-breakpoint' markers if present - if (text.includes('--> statement-breakpoint')) { - return text - .split(/-->\s*statement-breakpoint\s*/g) - .map((s) => s.trim()) - .filter((s) => s.length > 0); - } - // Fallback: split on semicolons at end of statements - return text - .split(/;\s*(?:\r?\n|$)/g) - .map((s) => s.trim()) - .filter((s) => s.length > 0) - .map((s) => (s.endsWith(';') ? s : `${s};`)); -} - /** * Startup SQL migration runner for a single adapter. * Forward-only: computes steps from the ledger's current tag to the latest version. */ export async function runStartupSqlMigrations< - TId extends string, - TVersions extends readonly VersionDef[], + TID extends string, + /* + `defineAdapter` discriminates tags, `Adapter` doesn't, so we don't want to constrain TVersions. + Besides, we perform a runtime check on versions, so that is sufficient. + */ + // TVersions extends readonly VersionDef[], >( - adapterId: TId, - versions: TVersions, + adapterId: TID, + // versions: TVersions, + versions: readonly VersionDef[], db: DrizzleDb, reporter?: ProgressReporter, ): Promise<{ applied: string[] }> { @@ -572,18 +591,22 @@ export async function runStartupSqlMigrations< const target = getLatestTag(versions); const current = await getVaultLedgerTag(db, adapterId); + const plan = planToVersion( buildJournalFromVersions(versions), current, target, ); - reporter?.onStart({ type: 'start', totalSteps: plan.tags.length }); + const r = reporter; + r?.onStart({ type: 'start', totalSteps: plan.tags.length }); const applied: string[] = []; for (let i = 0; i < plan.tags.length; i++) { const tag = plan.tags[i]; + if (!tag) throw new Error(`Invalid tag at plan index ${i}`); + const ve = versions.find((v) => v.tag === tag); if (!ve) { const error = new Error(`Version entry not found for tag ${tag}`); @@ -591,10 +614,10 @@ export async function runStartupSqlMigrations< throw error; } - const statements = ve.sql.flatMap((chunk) => splitSqlText(chunk)); + const statements = ve.sql; if (statements.length === 0) { - reporter?.onStep({ + r?.onStep({ type: 'step', index: i, tag, @@ -603,14 +626,22 @@ export async function runStartupSqlMigrations< }); } else { for (const [idx, statement] of statements.entries()) { - reporter?.onStep({ + const preview = statement.replace(/\s+/g, ' ').slice(0, 120); + r?.onStep({ type: 'step', index: i, tag, progress: (idx + 1) / statements.length, - message: `Applying statement ${idx + 1} of ${statements.length}`, + message: `Applying statement ${idx + 1}/${statements.length}: ${preview}...`, }); - await db.run(sql.raw(statement)); + + try { + await db.run(sql.raw(statement)); + } catch (e) { + // Hard failure: bubble up with detailed error + r?.onError({ type: 'error', error: e }); + throw e; + } } } @@ -619,7 +650,7 @@ export async function runStartupSqlMigrations< applied.push(tag); } - reporter?.onComplete({ type: 'complete' }); + r?.onComplete({ type: 'complete' }); return { applied }; } @@ -675,8 +706,13 @@ export function computeForwardTagsFromVersions< sourceTag: string | undefined, targetTag: string, ): string[] { + // If no sourceTag (no metadata provided), treat the baseline (first version) + // as the current tag so we do NOT require a transform for '0000'. + const baseline = versions.length > 0 ? versions[0]?.tag : undefined; + const current = sourceTag ?? baseline; + const j = buildJournalFromVersions(versions); - return planToVersion(j, sourceTag, targetTag).tags; + return planToVersion(j, current, targetTag).tags; } /** @@ -684,8 +720,8 @@ export function computeForwardTagsFromVersions< * The registry must contain a transform for each target tag in the forward plan. */ export async function runDataTransformChain< - TID extends string, - TVersions extends readonly VersionDef[], + TTags extends Tag4, + TVersions extends readonly VersionDef[], TSchema extends Record, >( versions: TVersions, @@ -718,6 +754,8 @@ export async function runDataTransformChain< type RequiredTags = RequiredTransformTags; for (let i = 0; i < plannedTags.length; i++) { const toTag = plannedTags[i]; + if (!toTag) throw new Error(`Invalid planned tag at index ${i}`); + const fn = registry[toTag as RequiredTags]; if (!fn) { const err = new Error(`Missing transform for target tag ${toTag}`); @@ -728,6 +766,13 @@ export async function runDataTransformChain< i === 0 ? (sourceTag ?? previousTagByTarget.get(toTag)) : plannedTags[i - 1]; + if (!fromTag) + throw new Error(`Cannot determine fromTag for target tag ${toTag}`); + if (!sourceTag) + throw new Error( + `Transform chain mismatch: expected fromTag ${fromTag} to match sourceTag ${sourceTag}`, + ); + acc = await fn(acc, { toTag, fromTag, @@ -752,40 +797,44 @@ export async function runDataTransformChain< return acc; } -function getLatestTag[]>( +function getLatestTag[]>( versions: TVersions, ): TVersions[number]['tag'] { - return versions + // Return the numerically greatest tag (e.g., '0003' over '0002'/'0000') + const sorted = versions .map((v) => [v.tag, Number.parseInt(v.tag, 10)] as const) - .sort((a, b) => a[1] - b[1])[0][0]; + .sort((a, b) => a[1] - b[1]); + const result = sorted[sorted.length - 1]?.[0]; + if (!result) + throw new Error('Cannot determine latest tag from versions tuple'); + + return result; } // ============================== // Version tuple type-safety helpers (authoring-time) // ============================== -/** Single decimal digit literal. */ -export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; +type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; /** Four-digit tag, e.g. '0000', '0001'. */ export type Tag4 = `${Digit}${Digit}${Digit}${Digit}`; -/** Version definition for adapter-managed migrations (stricter than runtime). */ -export type VersionDef = { +/** + * Version definition for adapter-managed migrations (stricter than runtime). + * + * We aren't using Tag4 here. This is because `Adapter` itself doesn't discriminate, + * or else it would cause headaches for anything that uses `Adapter` generically. + * `defineAdapter` serves as the dev-time assertion for adapter authors. + */ +export type VersionDef = { /** Four-digit version tag (e.g., '0001'). Must be unique within the tuple. */ tag: TTag; /** Inline array of statements (preferred for environment-agnostic bundles) */ sql: string[]; }; -/** Tuple utilities */ -type LastOfTuple = T extends readonly [ - ...infer _, - infer L, -] - ? L - : never; - +/* Tuple utilities */ type FirstOfTuple = T extends readonly [ infer F, ...unknown[], @@ -794,19 +843,19 @@ type FirstOfTuple = T extends readonly [ : never; /** Extract the union of tags from a version tuple. */ -export type VersionTags[]> = +export type VersionTags[]> = TVersions[number]['tag']; /** First tag from versions tuple. */ -export type FirstTag[]> = - FirstOfTuple extends VersionDef +export type FirstTag[]> = + FirstOfTuple extends VersionDef ? FirstOfTuple['tag'] : never; /** Tag tuple derived from a VersionDef tuple. */ -export type VersionTagTuple[]> = { +export type VersionTagTuple[]> = { [K in keyof TVersions]: TVersions[K] extends VersionDef< - infer TTag extends Tag4 + infer TTag extends string > ? TTag : never; @@ -814,11 +863,11 @@ export type VersionTagTuple[]> = { /** Tuple of required forward transform tags (all tags except the first/baseline). */ export type RequiredTransformTagTuple< - TVersions extends readonly VersionDef[], -> = TVersions extends readonly [VersionDef, ...infer Rest] - ? Rest extends readonly VersionDef[] + TVersions extends readonly VersionDef[], +> = TVersions extends readonly [VersionDef, ...infer Rest] + ? Rest extends readonly VersionDef[] ? { - [K in keyof Rest]: Rest[K] extends VersionDef + [K in keyof Rest]: Rest[K] extends VersionDef ? Tag : never; } @@ -830,7 +879,7 @@ export type RequiredTransformTagTuple< * all version tags except the first (baseline). */ export type RequiredTransformTags< - TVersions extends readonly VersionDef[], + TVersions extends readonly VersionDef[], > = RequiredTransformTagTuple[number]; /** @@ -859,7 +908,6 @@ export type DataValidator = (value: unknown) => unknown | Promise; /** Run chain then validate; returns morphed/validated data if no exception is thrown. */ export async function transformAndValidate< - TID extends string, TVersions extends readonly VersionDef[], TSchema extends Record, >( diff --git a/packages/vault-core/src/core/vault.spec.ts b/packages/vault-core/src/core/vault.spec.ts new file mode 100644 index 0000000000..ced6bdb9cb --- /dev/null +++ b/packages/vault-core/src/core/vault.spec.ts @@ -0,0 +1,159 @@ +import Database from 'bun:sqlite'; +import { test } from 'bun:test'; +import { fail } from 'node:assert'; +import assert from 'node:assert/strict'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { + createTestAdapter, + ingestSchema, + invalidIngestData, + makeImportFiles, + makeIngestFile, + TEST_ADAPTER_ID, + testSchema, + validIngestData, +} from '../../tests/fixtures/testAdapter'; +import { jsonFormat } from '../codecs/json'; +import { getVaultLedgerTag, runStartupSqlMigrations } from './migrations'; +import { createVault } from './vault'; + +function createDatabase() { + const sqlite = new Database(':memory:'); + const db = drizzle(sqlite); + return { sqlite, db }; +} + +function createVaultInstance() { + const { sqlite, db } = createDatabase(); + const adapter = createTestAdapter(); + const vault = createVault({ + database: db, + adapters: [adapter], + }); + return { sqlite, db, adapter, vault }; +} + +test('runStartupSqlMigrations applies schema and updates ledger', async () => { + const { sqlite, db } = createDatabase(); + const adapter = createTestAdapter(); + try { + const result = await runStartupSqlMigrations( + adapter.id, + adapter.versions, + db, + ); + assert.deepEqual(result.applied, ['0000']); + + // Should throw if table is not present + await db.select().from(testSchema.test_items); + + const ledgerTag = await getVaultLedgerTag(db, adapter.id); + assert.equal(ledgerTag, '0000'); + } finally { + sqlite.close(); + } +}); + +test('ingestData stores rows and exposes them via query interface', async () => { + const { sqlite, db, adapter, vault } = createVaultInstance(); + try { + const file = makeIngestFile(validIngestData); + await vault.ingestData({ adapter, file }); + + const rows = await db.select().from(testSchema.test_items); + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data is invalid'); + + assert.equal(rows.length, ingested.items.length); + + const { db: queryDb, tables } = vault.getQueryInterface(); + const qiSchema = tables[TEST_ADAPTER_ID] as typeof testSchema; + const qiRows = await queryDb.select().from(qiSchema.test_items); + assert.equal(qiRows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('ingestData rejects invalid payloads', async () => { + const { sqlite, adapter, vault } = createVaultInstance(); + try { + const invalidFile = makeIngestFile(invalidIngestData, 'invalid.json'); + await assert.rejects( + () => vault.ingestData({ adapter, file: invalidFile }), + /validation/i, + ); + } finally { + sqlite.close(); + } +}); + +test('importData inserts rows when validator succeeds', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) fail('Ingested data is invalid'); + const files = makeImportFiles(ingested); + + await vault.importData({ + files, + codec: jsonFormat, + }); + + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('importData propagates validator errors', async () => { + const { vault } = createVaultInstance(); + assert.rejects(async () => { + const ingested = ingestSchema(invalidIngestData); + // @ts-expect-error invalid data for test + const files = makeImportFiles(ingested); + vault.importData({ + files, + codec: jsonFormat, + }); + }); +}); + +test('smoke: ingest, export, and import round trip', async () => { + const { + sqlite: sourceSqlite, + vault: sourceVault, + adapter: sourceAdapter, + } = createVaultInstance(); + try { + const ingestFile = makeIngestFile(validIngestData); + await sourceVault.ingestData({ adapter: sourceAdapter, file: ingestFile }); + + const exported = await sourceVault.exportData({ codec: jsonFormat }); + const { sqlite: destSqlite, vault: destVault } = createVaultInstance(); + + try { + const importFiles = new Map(); + for (const [path, file] of exported) { + const contents = await file.text(); + importFiles.set(path, { + name: file.name, + type: file.type, + async text() { + return contents; + }, + } as unknown as File); + } + + await destVault.importData({ + files: importFiles, + codec: jsonFormat, + }); + } finally { + destSqlite.close(); + } + } finally { + sourceSqlite.close(); + } +}); diff --git a/packages/vault-core/src/core/vault.ts b/packages/vault-core/src/core/vault.ts index c27d3ba9bd..3c0cf8aaf0 100644 --- a/packages/vault-core/src/core/vault.ts +++ b/packages/vault-core/src/core/vault.ts @@ -1,3 +1,4 @@ +import { createSelectSchema } from 'drizzle-arktype'; import type { Adapter, UniqueAdapters } from './adapter'; import { defaultConvention, @@ -33,8 +34,11 @@ export function createVault( ): Vault { const db = options.database; + // Early validation: enforce adapter transform keys exactly match required version tags + ensureNoDuplicateAdapterIds(options.adapters); + // Ensure migrations have been applied before we touch adapter tables. - async function ensureMigrationsUpToDate(adapter: Adapter) { + async function ensureMigrationsUpToDate(adapter: Adapter, _ctx: string) { const versions = adapter.versions; if (!versions || versions.length === 0) return; await runStartupSqlMigrations(adapter.id, versions, db); @@ -52,13 +56,14 @@ export function createVault( ); const result = await validator['~standard'].validate(value); - if (result.issues) + if (result.issues) { throw new Error( `importData: validation failed for adapter '${adapter.id}': ${result.issues - .map((i) => `${i.path ?? ''} ${i.message ?? ''}`.trim()) + .map((i) => i.message.trim()) .join('; ')}`, ); - return (result as unknown as { value?: unknown }).value ?? value; + } + return 'value' in result ? result.value : value; } /** @@ -78,6 +83,67 @@ export function createVault( } } + /** + * Build a drizzle-arktype based dataset validator for a given adapter's schema. + * Validates the de-prefixed dataset shape: { [unprefixedTable]: Row[] }. + * Throws with aggregated messages on any row failure and returns morphed rows when available. + */ + async function createDrizzleArkTypeValidator(adapter: Adapter) { + // Precompute per-table select schemas indexed by unprefixed table key + const schemas = new Map>(); + for (const [tableName, table] of listTables(adapter.schema)) { + // Expect tableName like "_"; strip "_" + const unprefixed = tableName.startsWith(`${adapter.id}_`) + ? tableName.slice(adapter.id.length + 1) + : tableName; + const t = createSelectSchema(table); + schemas.set(unprefixed, t); + } + + return async (value: unknown) => { + const ds = (value ?? {}) as Record; + const issues: string[] = []; + const out: Record = {}; + + for (const [key, rows] of Object.entries(ds)) { + const typeForTable = schemas.get(key); + if (!typeForTable) { + issues.push( + `unknown table '${key}' for adapter '${adapter.id}' (no schema found)`, + ); + continue; + } + const validator = typeForTable['~standard']; + if (!Array.isArray(rows)) { + issues.push(`table '${key}' expected an array`); + continue; + } + const nextRows: unknown[] = []; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + const res = await validator.validate(row); + if (res.issues) { + const msgs = res.issues + .map((m: { message: string }) => m.message.trim()) + .join('; '); + issues.push(`${key}[${i}]: ${msgs}`); + } else { + const v = res.value ?? row; + nextRows.push(v); + } + } + out[key] = nextRows; + } + + if (issues.length) { + throw new Error( + `importData: drizzle-arktype validation failed for adapter '${adapter.id}': ${issues.join('; ')}`, + ); + } + return out; + }; + } + /** * Drop all rows from a table by name, then insert all provided rows. * @throws if table not found in adapter schema @@ -88,11 +154,22 @@ export function createVault( ) { const { schema } = adapter; for (const [tableName, rows] of Object.entries(dataset)) { - const table = schema[tableName as keyof typeof schema]; - if (!table) + // Try direct lookup first (for prefixed keys like 'test_items') + let table = schema[tableName as keyof typeof schema]; + + // If not found, try adding adapter prefix (for unprefixed keys like 'items') + if (!table) { + const prefixedName = `${adapter.id}_${tableName}`; + table = schema[prefixedName as keyof typeof schema]; + } + + // If still not found, throw with helpful error + if (!table) { + const prefixedName = `${adapter.id}_${tableName}`; throw new Error( - `replaceAdapterTables: unknown table ${tableName} for adapter '${adapter.id}'`, + `replaceAdapterTables: unknown table '${tableName}' for adapter '${adapter.id}'. Tried '${tableName}' and '${prefixedName}'`, ); + } await db.delete(table); for (const row of rows) { @@ -119,7 +196,7 @@ export function createVault( // Iterate over each adapter for (const adapter of adapters) { - await ensureMigrationsUpToDate(adapter); + await ensureMigrationsUpToDate(adapter, 'exportData'); const { schema } = adapter; const adapterId = adapter.id; @@ -136,8 +213,7 @@ export function createVault( const rec: Record = {}; for (const [k, v] of Object.entries(row)) { if (!tableCols.has(k)) continue; - if (v === undefined || v === null) continue; - rec[k] = codec.normalize ? codec.normalize(v, k) : v; + rec[k] = v; } // Compute path using PK values @@ -162,108 +238,134 @@ export function createVault( return files; }, - async importData(opts: ImportOptions) { - const { adapterID, files, codec } = opts; - const adapter = options.adapters.find((a) => a.id === adapterID); - if (!adapter) - throw new Error(`importData: unknown adapter ID '${adapterID}'`); + async importData(opts: ImportOptions) { + const { files, codec } = opts; - await ensureMigrationsUpToDate(adapter); + // Group files by detected adapter id and collect per-adapter detected tags from metadata + type Group = { files: Array<[string, File]>; detectedTag?: string }; + const groups = new Map(); - const { schema, id: adapterId } = adapter; - // Build one huge object with all data, run validator, then upsert in one call - - // Collect rows per dataset key for a single upsert call - const dataset: Record = {}; - let detectedTag: string | undefined; + const knownAdapterIds = new Set(options.adapters.map((a) => a.id)); for (const [path, input] of files) { - const text = await input.text(); - const parts = path.split('/').filter((segment) => segment.length > 0); - const metaIndex = parts.indexOf(MIGRATION_META_DIR); - // TODO clean this up - if (metaIndex !== -1) { + + // Locate any known adapter id segment in the path + const adapterIndex = parts.findIndex((p) => knownAdapterIds.has(p)); + if (adapterIndex === -1) continue; // can't determine adapter; skip + + const adapterIdFromPath = parts[adapterIndex]; + if (!adapterIdFromPath) + throw new Error('unable to determine adapter ID from path'); + + // Migration metadata handling; associate detected tag with this adapter group + if (parts.includes(MIGRATION_META_DIR)) { try { - const parsed = codec.parse(text) as { tag?: string }; - if (typeof parsed?.tag === 'string') detectedTag = parsed.tag; + const text = await input.text(); + const parsed = codec.parse(text); + if (typeof parsed.tag === 'string') { + const group = groups.get(adapterIdFromPath) ?? { files: [] }; + group.detectedTag = parsed.tag; + groups.set(adapterIdFromPath, group); + } } catch { // ignore malformed metadata } continue; } - if (parts.length < 2) continue; // No nested paths supported - - const tableName = parts[0]; + const group = groups.get(adapterIdFromPath) ?? { files: [] }; + group.files.push([path, input]); + groups.set(adapterIdFromPath, group); + } - // Check that file extension matches codec - const file = parts.slice(1).join('/'); - const dot = file.indexOf('.'); - if (dot < 0) - throw new Error(`importData: file ${path} has no extension`); - const ext = file.slice(dot + 1); - if (ext !== codec.fileExtension) - throw new Error( - `importData: file ${path} has wrong extension (expected ${codec.fileExtension})`, - ); + // Process each adapter group independently + for (const [adapterId, group] of groups) { + const adapter = options.adapters.find((a) => a.id === adapterId); + if (!adapter) continue; // unknown adapter in bundle; skip - // Find matching table in schema - const table = schema[tableName as keyof typeof schema]; - if (!table) throw new Error(`importData: unknown table ${tableName}`); - - // Parse file text into a record - const rec = codec.parse(text); - const row: Record = {}; - const tableCols = new Set(listColumns(table).map(([name]) => name)); - for (const [k, v] of Object.entries(rec ?? {})) { - if (!tableCols.has(k)) continue; - row[k] = codec.denormalize ? codec.denormalize(v, k) : v; - } + await ensureMigrationsUpToDate(adapter, 'importData'); - const key = tableName.slice(adapterId.length + 1); + const { schema } = adapter; + const dataset: Record = {}; + + for (const [path, input] of group.files) { + const parts = path.split('/').filter((segment) => segment.length > 0); + + // Recompute indices for this path + const aIdx = parts.indexOf(adapterId); + if (aIdx === -1) continue; + const pathParts = parts.slice(aIdx); + if (pathParts.length < 2) continue; // Need adapter/table structure + + const tableName = pathParts[1]; + if (!tableName) + throw new Error( + 'importData: unable to determine table name from path', + ); + + // Extension check against codec + const dot = path.lastIndexOf('.'); + if (dot < 0) + throw new Error(`importData: file ${path} has no extension`); + const ext = path.slice(dot + 1); + if (ext !== codec.fileExtension) + throw new Error( + `importData: file ${path} has wrong extension (expected ${codec.fileExtension})`, + ); + + // Table lookup + const table = schema[tableName as keyof typeof schema]; + if (!table) throw new Error(`importData: unknown table ${tableName}`); + + // Parse and denormalize row by table columns + const text = await input.text(); + const rec = codec.parse(text); + const row: Record = {}; + const tableCols = new Set(listColumns(table).map(([name]) => name)); + for (const [k, v] of Object.entries(rec ?? {})) { + if (!tableCols.has(k)) continue; + row[k] = v; + } - // Initialize bucket if needed - dataset[key] ??= []; + // Dataset key is unprefixed table name (strip '_') + const key = tableName.slice(adapterId.length + 1); + dataset[key] ??= []; + dataset[key].push(row); + } - const bucket = dataset[key]; - bucket.push(row); + // Build required drizzle-arktype validator bound to this adapter's schema + const dataValidator = await createDrizzleArkTypeValidator(adapter); + + // Run migrations/transforms pipeline with drizzle-arktype validation (sole validator for import) + // We don't want to run the adapter's built-in validator here because it likely won't match the preprocessed shape + const validatedDataset = await runImportPipeline({ + adapter, + dataset, + transformsOverride: undefined, + versionsOverride: undefined, + dataValidator, + sourceTag: undefined, + detectedTag: group.detectedTag, + }); + + // Replace adapter tables with validated dataset + await replaceAdapterTables(adapter, validatedDataset); } - - const pipelineOutput = await runImportPipeline({ - adapter, - dataset, - transformsOverride: opts.transforms, - versionsOverride: opts.versions, - dataValidator: undefined, - sourceTag: opts.sourceTag, - detectedTag, - }); - - const schemaValidator = opts.dataValidator; - if (!schemaValidator) - throw new Error( - `importData: dataValidator (drizzle-arktype) is required for adapter '${adapter.id}'`, - ); - - const validatedDataset = await schemaValidator(pipelineOutput); - await replaceAdapterTables( - adapter, - validatedDataset as Record, - ); }, async ingestData(opts: IngestOptions) { const adapter = opts.adapter; const file = opts.file; ensureNoDuplicateAdapterIds([adapter]); - await ensureMigrationsUpToDate(adapter); + await ensureMigrationsUpToDate(adapter, 'ingestData'); if (!adapter.ingestors || adapter.ingestors.length === 0) throw new Error( `ingestData: adapter '${adapter.id}' has no ingestors configured`, ); + // Catch may be unnecessary, but protects against faulty ingestor implementations const ingestor = adapter.ingestors.find((i) => { try { return i.matches(file); @@ -271,6 +373,7 @@ export function createVault( return false; } }); + // If no ingestor matched, throw if (!ingestor) throw new Error( `ingestData: no ingestor matched file '${file.name}' for adapter '${adapter.id}'`, @@ -280,8 +383,12 @@ export function createVault( // Run validation and use morphed value const validated = await runValidation(adapter, dataset); + + // TODO is this necessary or is there a better way? + // We might be able to do a runtime-based "on-conflict-replace" insert instead await replaceAdapterTables( adapter, + // TODO refine type validated as Record, ); }, diff --git a/packages/vault-core/tests/export-import-roundtrip.spec.ts b/packages/vault-core/tests/export-import-roundtrip.spec.ts new file mode 100644 index 0000000000..7b1c8b8a7f --- /dev/null +++ b/packages/vault-core/tests/export-import-roundtrip.spec.ts @@ -0,0 +1,104 @@ +import Database from 'bun:sqlite'; +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { jsonFormat } from '../src/codecs/json'; +import { defineAdapter } from '../src/core/adapter'; +import { + defineTransformRegistry, + defineVersions, + runStartupSqlMigrations, +} from '../src/core/migrations'; +import { createVault } from '../src/core/vault'; + +function createDatabase() { + const sqlite = new Database(':memory:'); + const db = drizzle(sqlite); + return { sqlite, db }; +} + +const roundtripProfiles = sqliteTable('roundtrip_profiles', { + id: text('id').primaryKey(), + birthdate: integer('birthdate', { mode: 'timestamp' }), + verifiedBirthdate: integer('verified_birthdate', { mode: 'timestamp' }), + verificationState: text('verification_state').notNull().default(''), + verificationMethod: text('verification_method').notNull().default(''), +}); + +const roundtripSchema = { + roundtrip_profiles: roundtripProfiles, +}; + +const roundtripVersions = defineVersions({ + tag: '0000', + sql: [ + `CREATE TABLE IF NOT EXISTS roundtrip_profiles ( + id TEXT PRIMARY KEY, + birthdate INTEGER, + verified_birthdate INTEGER, + verification_state TEXT NOT NULL DEFAULT '', + verification_method TEXT NOT NULL DEFAULT '' + );`, + ], +}); + +const roundtripTransforms = defineTransformRegistry({}); + +const createRoundtripAdapter = defineAdapter(() => ({ + id: 'roundtrip', + schema: roundtripSchema, + versions: roundtripVersions, + transforms: roundtripTransforms, +})); + +test('export/import roundtrip preserves null and Date columns', async () => { + const adapter = createRoundtripAdapter(); + const sampleDate = new Date('2024-04-05T12:00:00Z'); + + const { sqlite: sourceSqlite, db: sourceDb } = createDatabase(); + let exported: Map; + try { + await runStartupSqlMigrations(adapter.id, adapter.versions, sourceDb); + await sourceDb.insert(roundtripProfiles).values({ + id: 'singleton', + birthdate: null, + verifiedBirthdate: sampleDate, + verificationState: '', + verificationMethod: '', + }); + + const sourceVault = createVault({ + database: sourceDb, + adapters: [adapter], + }); + exported = await sourceVault.exportData({ codec: jsonFormat }); + } finally { + sourceSqlite.close(); + } + + const { sqlite: targetSqlite, db: targetDb } = createDatabase(); + try { + const targetVault = createVault({ + database: targetDb, + adapters: [adapter], + }); + + await targetVault.importData({ files: exported, codec: jsonFormat }); + + const rows = await targetDb.select().from(roundtripProfiles); + assert.equal(rows.length, 1); + const row = rows[0]; + assert.equal(row?.id, 'singleton'); + assert.equal(row?.birthdate, null); + assert.ok(row?.verifiedBirthdate instanceof Date); + assert.equal( + row?.verifiedBirthdate?.toISOString(), + sampleDate.toISOString(), + ); + assert.equal(row?.verificationState, ''); + assert.equal(row?.verificationMethod, ''); + } finally { + targetSqlite.close(); + } +}); diff --git a/packages/vault-core/tests/fixtures/testAdapter.ts b/packages/vault-core/tests/fixtures/testAdapter.ts new file mode 100644 index 0000000000..97b877eb35 --- /dev/null +++ b/packages/vault-core/tests/fixtures/testAdapter.ts @@ -0,0 +1,123 @@ +import { type } from 'arktype'; +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { jsonFormat } from '../../src/codecs'; +import { defineAdapter } from '../../src/core/adapter'; +import type { MigrationMetadata } from '../../src/core/import/migrationMetadata'; +import { defineIngestor } from '../../src/core/ingestor'; +import { + defineTransformRegistry, + defineVersions, +} from '../../src/core/migrations'; + +function createMemoryFile( + name: string, + payload: unknown, + type = 'application/json', +): File { + const contents = + typeof payload === 'string' + ? payload + : jsonFormat.stringify(payload as Record); + return new File([new Blob([contents], { type })], name, { type }); +} + +const testItems = sqliteTable('test_items', { + id: integer('id').primaryKey(), + name: text('name').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), +}); + +export const testSchema = { + test_items: testItems, +}; + +export const testVersions = defineVersions({ + tag: '0000', + sql: [ + `CREATE TABLE IF NOT EXISTS test_items ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_at INTEGER NOT NULL + );`, + ], +}); + +export const testTransforms = defineTransformRegistry({}); + +export const ingestSchema = type({ + items: type({ + id: 'number', + name: 'string', + createdAt: type('number').pipe((v) => new Date(v)), + }).array(), +}); + +const jsonIngestor = defineIngestor({ + matches(file: File) { + return file.name.endsWith('.json'); + }, + async parse(file) { + const text = await file.text(); + return JSON.parse(text); + }, +}); + +export const TEST_ADAPTER_ID = 'test'; + +export const createTestAdapter = defineAdapter(() => ({ + id: TEST_ADAPTER_ID, + schema: testSchema, + versions: testVersions, + transforms: testTransforms, + validator: ingestSchema, + ingestors: [jsonIngestor], +})); + +export const validIngestData = { + items: [ + { id: 1, name: 'Alpha', createdAt: 1700000000000 }, + { id: 2, name: 'Beta', createdAt: 1700000000500 }, + ], +} satisfies typeof ingestSchema.inferIn; // This represents the expected input shape + +export const invalidIngestData = { + items: [{ id: 3 } as unknown as (typeof validIngestData)['items'][number]], +}; + +export function makeIngestFile( + data = validIngestData, + name = 'test-data.json', +): File { + const ingestPayload = { + items: data.items.map((item) => ({ ...item })), + }; + return createMemoryFile(name, ingestPayload); +} + +export function makeImportFiles( + data: typeof ingestSchema.inferOut, +): Map { + const files = new Map(); + const records = data.items; + for (const item of records) { + const filename = `${item.id}.json`; + // Use the same path structure as export: vault/adapter/table/filename + files.set( + `vault/test/test_items/${filename}`, + createMemoryFile(filename, item), + ); + } + files.set( + `__meta__/${TEST_ADAPTER_ID}/migration.json`, + createMemoryFile('migration.json', { + tag: testVersions[testVersions.length - 1]?.tag ?? null, + adapterId: TEST_ADAPTER_ID, + source: 'adapter', + ledgerTag: null, + latestDeclaredTag: testVersions[testVersions.length - 1]?.tag ?? null, + versions: testVersions.map((version) => version.tag), + exportedAt: new Date(0), + } satisfies MigrationMetadata), + ); + return files; +} diff --git a/packages/vault-core/tests/import-paths.spec.ts b/packages/vault-core/tests/import-paths.spec.ts new file mode 100644 index 0000000000..49e5e95d9c --- /dev/null +++ b/packages/vault-core/tests/import-paths.spec.ts @@ -0,0 +1,135 @@ +import Database from 'bun:sqlite'; +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { jsonFormat } from '../src/codecs/json'; +import { createVault } from '../src/core/vault'; +import { + createTestAdapter, + ingestSchema, + makeImportFiles, + testSchema, + validIngestData, +} from './fixtures/testAdapter'; + +/** + * Small helper to create an in-memory DB + vault instance (same pattern as vault.spec.ts) + */ +function createDatabase() { + const sqlite = new Database(':memory:'); + const db = drizzle(sqlite); + return { sqlite, db }; +} + +function createVaultInstance() { + const { sqlite, db } = createDatabase(); + const adapter = createTestAdapter(); + const vault = createVault({ + database: db, + adapters: [adapter], + }); + return { sqlite, db, adapter, vault }; +} + +/** + * Utility: transform the keys of a Map by applying a replacer function + */ +function remapFileKeys(files: Map, fn: (k: string) => string) { + const out = new Map(); + for (const [k, v] of files) { + out.set(fn(k), v); + } + return out; +} + +test('import path variants: default path (export shape) works', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data has issues'); + const files = makeImportFiles(ingested); + await vault.importData({ + files, + codec: jsonFormat, + }); + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('import path variants: empty base path (adapter/table/filename) works', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data has issues'); + const files = makeImportFiles(ingested); + const remapped = remapFileKeys(files, (k) => k.replace(/^vault\//, '')); // remove vault/ + await vault.importData({ + files: remapped, + codec: jsonFormat, + }); + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('import path variants: non-default base path (data/vault/...) works', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data has issues'); + const files = makeImportFiles(ingested); + const remapped = remapFileKeys(files, (k) => `data/${k}`); // prepend data/ + await vault.importData({ + files: remapped, + codec: jsonFormat, + }); + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('import path variants: multiple folders before vault works', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data has issues'); + const files = makeImportFiles(ingested); + const remapped = remapFileKeys(files, (k) => `a/b/${k}`); + await vault.importData({ + files: remapped, + codec: jsonFormat, + }); + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); + +test('import path variants: duplicate/trailing slashes are tolerated', async () => { + const { sqlite, db, vault } = createVaultInstance(); + try { + const ingested = ingestSchema(validIngestData); + if ('issues' in ingested) throw new Error('Ingested data has issues'); + const files = makeImportFiles(ingested); + // Inject an extra slash in the base path and in the table segment + const remapped = remapFileKeys(files, (k) => + k.replace('vault/', 'vault//').replace('test_items/', 'test_items//'), + ); + await vault.importData({ + files: remapped, + codec: jsonFormat, + }); + const rows = await db.select().from(testSchema.test_items); + assert.equal(rows.length, validIngestData.items.length); + } finally { + sqlite.close(); + } +}); diff --git a/packages/vault-core/tests/transforms-alignment.spec.ts b/packages/vault-core/tests/transforms-alignment.spec.ts new file mode 100644 index 0000000000..c8eef84402 --- /dev/null +++ b/packages/vault-core/tests/transforms-alignment.spec.ts @@ -0,0 +1,62 @@ +import { test } from 'bun:test'; +import assert from 'node:assert/strict'; +import type { Adapter } from '../src/core/adapter'; +import { defineAdapter } from '../src/core/adapter'; +import { + defineTransformRegistry, + defineVersions, +} from '../src/core/migrations'; +import { testSchema, testVersions } from './fixtures/testAdapter'; + +test('defineAdapter throws when transforms include extra tags not declared in versions', () => { + const transformsWithExtra = defineTransformRegistry({ + '0001': (value) => value, + }); + + const createBadAdapter = defineAdapter( + // @ts-expect-error missing versions, this should fail the type check + (() => + ({ + id: 'test', + schema: testSchema, + versions: testVersions, + transforms: transformsWithExtra, + }) as unknown as Adapter) as () => Adapter, + ); + + assert.throws(() => createBadAdapter(), /transforms do not match versions/i); +}); + +test('defineAdapter throws when transforms are missing required tags from versions', () => { + const versions = defineVersions( + { + tag: '0000', + sql: [ + `CREATE TABLE IF NOT EXISTS test_items ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + created_at INTEGER NOT NULL + );`, + ], + }, + { + tag: '0001', + sql: [], + }, + ); + + const emptyTransforms = defineTransformRegistry({}); + + const createBadAdapter = defineAdapter( + // @ts-expect-error invalid validator, this should fail the type check + (() => + ({ + id: 'test', + schema: testSchema, + versions, + transforms: emptyTransforms, + }) as unknown as Adapter) as () => Adapter, + ); + + assert.throws(() => createBadAdapter(), /transforms do not match versions/i); +}); diff --git a/packages/vault-core/tsconfig.json b/packages/vault-core/tsconfig.json new file mode 100644 index 0000000000..5e515b4388 --- /dev/null +++ b/packages/vault-core/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "esnext", + "target": "esnext", + "types": ["bun-types"], + "moduleResolution": "bundler", + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true + } +} From 6a14f4542f47333d684f958f473302b4e06b7416 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 7 Oct 2025 21:24:59 +0000 Subject: [PATCH 14/21] feat: added migration script for adapters --- packages/vault-core/scripts/migrations-add.ts | 407 ++++++++++++++++++ .../adapters/reddit/migrations/manifest.ts | 1 - 2 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 packages/vault-core/scripts/migrations-add.ts delete mode 100644 packages/vault-core/src/adapters/reddit/migrations/manifest.ts diff --git a/packages/vault-core/scripts/migrations-add.ts b/packages/vault-core/scripts/migrations-add.ts new file mode 100644 index 0000000000..aaadd32865 --- /dev/null +++ b/packages/vault-core/scripts/migrations-add.ts @@ -0,0 +1,407 @@ +#!/usr/bin/env node +/** + * POC migrations add script - moved to packages/vault-core/scripts + * Runs relative to CWD and supports copying drizzle migration files if present. + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +type Args = { + adapter?: string | undefined; + tag?: string | undefined; + sqlId?: string | undefined; + title?: string | undefined; + noTransform?: boolean | undefined; + cwd: string; +}; + +function printUsage() { + console.log( + ` +POC: migrations add + +Required: + --adapter, -a Adapter id (e.g., reddit) + --tag, -t 4-digit version tag (e.g., 0002) + +Optional: + --sql-id, -s Override SQL source id when auto-detection is undesirable + --title Comment header for migration.sql + --no-transform Skip creating versions//transform.ts + +Examples: + # Print-only (recommended): generates migration.sql and prints a ready-to-paste block + node packages/vault-core/scripts/migrations-add.poc.ts -a reddit -t 0002 -s 0002_forward_baseline +`.trim(), + ); +} + +function parseArgs(argv: string[], cwd: string): Args { + const args: Args = { cwd }; + const rest = [...argv]; + for (let i = 0; i < rest.length; i++) { + const cur = rest[i]; + if (cur === undefined) throw new Error('unexpected undefined arg'); + + if (cur === '--adapter' || cur === '-a') { + args.adapter = rest[i + 1]; + i++; + } else if (cur.startsWith('--adapter=')) { + args.adapter = cur.split('=')[1]; + } else if (cur === '--tag' || cur === '-t') { + args.tag = rest[i + 1]; + i++; + } else if (cur.startsWith('--tag=')) { + args.tag = cur.split('=')[1]; + } else if (cur === '--sql-id' || cur === '-s') { + args.sqlId = rest[i + 1]; + i++; + } else if (cur.startsWith('--sql-id=')) { + args.sqlId = cur.split('=')[1]; + } else if (cur === '--title') { + args.title = rest[i + 1]; + i++; + } else if (cur.startsWith('--title=')) { + args.title = cur.split('=')[1]; + } else if (cur === '--no-transform') { + args.noTransform = true; + } else { + // ignore unknown args for POC + } + } + return args; +} + +function assertValid(args: Args) { + const errors: string[] = []; + if (!args.adapter) errors.push('missing --adapter'); + if (!args.tag) errors.push('missing --tag'); + + const tag = args.tag ?? ''; + const sqlId = args.sqlId ?? ''; + + if (!/^\d{4}$/.test(tag)) { + errors.push(`invalid tag '${tag}', expected 4 digits like 0002`); + } + if (sqlId && !/^[A-Za-z0-9_-]+$/.test(sqlId)) { + errors.push( + `invalid sql-id '${sqlId}', allowed [A-Za-z0-9_\\-], e.g. 0002_add_foo`, + ); + } + + if (errors.length) { + for (const e of errors) console.error('•', e); + console.error(); + printUsage(); + process.exit(1); + } +} + +async function pathExists(p: string) { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} + +async function ensureDir(p: string) { + await fs.mkdir(p, { recursive: true }); +} + +async function writeFileIfAbsent(p: string, content: string) { + if (await pathExists(p)) return false; + await fs.writeFile(p, content, 'utf8'); + return true; +} + +function sqlHeader( + adapter: string, + tag: string, + sqlId: string, + title?: string, +) { + const lines = [ + `-- adapter: ${adapter}`, + `-- tag: ${tag}`, + `-- sqlId: ${sqlId}`, + ...(title ? [`-- title: ${title}`] : []), + '--', + '-- Forward-only migration generated by POC scaffolder.', + '-- Add your DDL statements below.', + '', + ]; + return lines.join('\n'); +} + +function transformStub(tag: string) { + return `import type { DataTransform } from '../../../core/migrations'; + +export const transform_${tag}: DataTransform = async (input) => { + // TODO: transform data from previous version to ${tag} + return input; +}; +`; +} + +/** + * Split SQL text into executable statements. + * Mirrors core split logic (drizzle '--> statement-breakpoint' or semicolons). + */ +function splitSqlText(text: string): string[] { + if (text.includes('--> statement-breakpoint')) { + return text + .split(/-->\s*statement-breakpoint\s*/g) + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } + return text + .split(/;\s*(?:\r?\n|$)/g) + .map((s) => s.trim()) + .filter((s) => s.length > 0) + .map((s) => (s.endsWith(';') ? s : `${s};`)); +} + +/** + * Format a versions.ts entry block for copy-paste into an adapter's versions tuple. + * Mirrors the style used in existing adapters (backticked SQL strings, indented). + */ +function formatVersionBlock(tag: string, sqlStatements: string[]): string { + const body = sqlStatements + .map((stmt) => { + // escape backticks and indent multi-line for readability + const escaped = stmt.replace(/`/g, '\\`').replace(/\r?\n/g, '\n\t\t\t'); + return `\t\t\t\`${escaped}\``; + }) + .join(',\n'); + + return [ + '\t{', + `\t\ttag: '${tag}',`, + '\t\t// Generated by migrations-add script', + '\t\tsql: [', + body, + '\t\t],', + '\t},', + ].join('\n'); +} + +/** + * Append a new version object to the adapter's migrations/versions.ts defineVersions() tuple, + * embedding the provided SQL statements inline. + */ +/** + * Try to copy drizzle-produced migration files from common locations under cwd. + * This is a best-effort convenience: hosts may produce drizzle migration outputs in different places. + */ +async function copyDrizzleMigrationFiles( + cwd: string, + tag: string, + destDir: string, + sqlPath: string, +) { + const candidates = [ + 'migrations', + 'drizzle/migrations', + 'migrations/drizzle', + 'drizzle', + ]; + for (const c of candidates) { + const full = path.join(cwd, c); + if (!(await pathExists(full))) continue; + const files = await fs.readdir(full); + const matched: string[] = []; + for (const f of files) { + if (f.includes(tag)) matched.push(f); + } + if (matched.length === 0) return; + + for (const f of matched) { + const src = path.join(full, f); + const dst = path.join(destDir, f); + try { + await fs.copyFile(src, dst); + console.log(` ✓ copied drizzle file ${src} -> ${dst}`); + } catch (err) { + console.warn(` ! failed copying ${src} -> ${dst}: ${err}`); + } + + // If .sql, append its contents into migration.sql + if (path.extname(f).toLowerCase() === '.sql') { + try { + const sqlText = await fs.readFile(src, 'utf8'); + await fs.appendFile( + sqlPath, + `\n-- == Imported from ${src} ==\n${sqlText}\n`, + ); + console.log(` ✓ appended SQL from ${src} into ${sqlPath}`); + } catch (err) { + console.warn( + ` ! failed appending SQL from ${src} into ${sqlPath}: ${err}`, + ); + } + } + } + + return; + } +} + +async function main() { + const argv = process.argv.slice(2); + if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) { + printUsage(); + process.exit(0); + } + + const args = parseArgs(argv, process.cwd()); + assertValid(args); + + const adapter = args.adapter as string; + const tag = args.tag as string; + let sqlId = args.sqlId; + const title = args.title; + + // Resolve key paths (operate on CWD) + const root = args.cwd; + const adapterDir = path.join( + root, + 'packages', + 'vault-core', + 'src', + 'adapters', + adapter, + ); + const versionsFilePath = path.join(adapterDir, 'migrations', 'versions.ts'); + const versionsDir = path.join(adapterDir, 'migrations', 'versions', tag); + const sqlPath = path.join(versionsDir, 'migration.sql'); + const transformPath = path.join(versionsDir, 'transform.ts'); + const migrationsDir = path.join(adapterDir, 'migrations'); + + // Validate adapter and manifest presence + if (!(await pathExists(adapterDir))) { + console.error(`❌ adapter directory not found: ${adapterDir}`); + process.exit(1); + } + if (!(await pathExists(versionsFilePath))) { + console.error( + `❌ versions.ts not found for adapter '${adapter}': ${versionsFilePath}`, + ); + process.exit(1); + } + + let sqlSourcePath: string | undefined; + if (!sqlId) { + if (await pathExists(migrationsDir)) { + const files = await fs.readdir(migrationsDir); + const matches = files.filter( + (f) => f.startsWith(`${tag}_`) && f.toLowerCase().endsWith('.sql'), + ); + if (matches.length === 1) { + const match = matches[0]; + if (match === undefined) throw new Error('unexpected undefined match'); + + sqlId = path.parse(match).name; + sqlSourcePath = path.join(migrationsDir, match); + console.log( + ` ✓ detected sql-id '${sqlId}' from ${path.relative(root, sqlSourcePath)}`, + ); + } else if (matches.length > 1) { + console.error( + `❌ multiple SQL files matched tag '${tag}'. Provide --sql-id to disambiguate:`, + ); + for (const match of matches) { + console.error( + ` ${path.relative(root, path.join(migrationsDir, match))}`, + ); + } + process.exit(1); + } + } + if (!sqlId) { + console.error( + `❌ unable to detect SQL source for tag '${tag}'. Provide --sql-id explicitly.`, + ); + process.exit(1); + } + } else { + const candidate = path.join(migrationsDir, `${sqlId}.sql`); + if (await pathExists(candidate)) { + sqlSourcePath = candidate; + } + } + + const resolvedSqlId = sqlId; + if (!resolvedSqlId) { + throw new Error('failed to resolve sqlId after detection'); + } + + // Create dirs and files + await ensureDir(versionsDir); + + const sqlCreated = await writeFileIfAbsent( + sqlPath, + `${sqlHeader(adapter, tag, resolvedSqlId, title)}`, + ); + if (sqlCreated) { + console.log(` ✓ created ${sqlPath}`); + } else { + console.log(` • exists ${sqlPath}`); + } + + if (sqlCreated && sqlSourcePath) { + const sqlText = await fs.readFile(sqlSourcePath, 'utf8'); + const normalized = sqlText.endsWith('\n') ? sqlText : `${sqlText}\n`; + await fs.appendFile( + sqlPath, + `\n-- == Imported from ${path.relative(root, sqlSourcePath)} ==\n${normalized}`, + ); + console.log( + ` ✓ copied ${path.relative(root, sqlSourcePath)} into ${sqlPath}`, + ); + } else if (sqlCreated && !sqlSourcePath) { + console.log( + ` • no existing SQL artifact found for tag '${tag}' in ${path.relative(root, migrationsDir)}`, + ); + } + + if (!args.noTransform) { + const tCreated = await writeFileIfAbsent(transformPath, transformStub(tag)); + if (tCreated) { + console.log(` ✓ created ${transformPath}`); + } else { + console.log(` • exists ${transformPath}`); + } + } else { + console.log(' • skipped transform stub (--no-transform)'); + } + + // Try to copy drizzle migration files (best-effort) and append .sql to migration.sql + await copyDrizzleMigrationFiles(root, tag, versionsDir, sqlPath); + + // Read migration.sql, split into statements + const finalSqlText = await fs.readFile(sqlPath, 'utf8'); + console.log(sqlPath); + const statements = splitSqlText(finalSqlText); + + // Always print a ready-to-paste block for developer convenience + console.log( + '\n--- Copy & paste into your adapter migrations/versions.ts ---\n', + ); + console.log(formatVersionBlock(tag, statements)); + console.log('\n--- end block ---\n'); + + // Script operates in print-only mode; developer must manually update migrations/versions.ts + console.log( + 'Print-only mode (no file edits). Manually update migrations/versions.ts with the block above.', + ); + + console.log('\nDone.'); +} + +main().catch((err) => { + console.error('❌ Error:', err?.message ?? err); + process.exit(1); +}); diff --git a/packages/vault-core/src/adapters/reddit/migrations/manifest.ts b/packages/vault-core/src/adapters/reddit/migrations/manifest.ts deleted file mode 100644 index 09494eeae9..0000000000 --- a/packages/vault-core/src/adapters/reddit/migrations/manifest.ts +++ /dev/null @@ -1 +0,0 @@ -export { redditVersions as redditMigrations } from './versions'; From 694eaff1d2de2ae8306f7ba2ca0ec610bcbb6a63 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Wed, 8 Oct 2025 14:53:28 +0000 Subject: [PATCH 15/21] feat: strict checking between validator & schema --- packages/vault-core/src/core/adapter.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index 42cc1a7c65..fd19fad818 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -196,7 +196,10 @@ export function defineAdapter< TSchema extends Record, TVersions extends readonly VersionDef[] = readonly VersionDef[], TPreparsed = unknown, - TParsed = SchemaMappedToObject, + TParsed extends SchemaMappedToObject = SchemaMappedToObject< + TID, + TSchema + >, >( adapter: () => PrefixedAdapter, ): () => PrefixedAdapter; @@ -205,7 +208,10 @@ export function defineAdapter< TSchema extends Record, TPreparsed, TVersions extends readonly VersionDef[], - TParsed = SchemaMappedToObject, + TParsed extends SchemaMappedToObject = SchemaMappedToObject< + TID, + TSchema + >, TArgs extends unknown[] = [], >( adapter: ( From d41278fa91e34db7be0699a9587d31ab2962c757 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 21 Oct 2025 17:38:30 +0000 Subject: [PATCH 16/21] feat: added vault-demo --- apps/vault-demo/.gitignore | 23 ++ apps/vault-demo/.npmrc | 1 + apps/vault-demo/README.md | 71 ++++ apps/vault-demo/package.json | 28 ++ apps/vault-demo/src/app.d.ts | 13 + apps/vault-demo/src/app.html | 11 + apps/vault-demo/src/lib/export/index.ts | 15 + .../src/lib/extract/redditEntities.ts | 205 +++++++++++ .../src/lib/remote/entityIndex.remote.ts | 92 +++++ .../vault-demo/src/lib/remote/notes.remote.ts | 138 +++++++ .../vault-demo/src/lib/remote/vault.remote.ts | 43 +++ apps/vault-demo/src/lib/schemas/entities.ts | 22 ++ apps/vault-demo/src/lib/schemas/notes.ts | 50 +++ apps/vault-demo/src/lib/schemas/vault.ts | 13 + .../vault-demo/src/lib/server/vaultService.ts | 64 ++++ apps/vault-demo/src/routes/+layout.svelte | 16 + apps/vault-demo/src/routes/+page.svelte | 76 ++++ .../src/routes/api/vault/counts/+server.ts | 20 + .../src/routes/api/vault/export/+server.ts | 28 ++ .../src/routes/api/vault/import/+server.ts | 43 +++ .../src/routes/api/vault/ingest/+server.ts | 53 +++ .../src/routes/dashboard/+page.svelte | 58 +++ .../src/routes/entities/+page.svelte | 58 +++ .../src/routes/entities/[id]/+page.svelte | 62 ++++ .../src/routes/import-export/+page.svelte | 40 ++ .../routes/import-export/export/+server.ts | 12 + apps/vault-demo/src/routes/notes/+page.svelte | 96 +++++ .../src/routes/notes/[id]/+page.svelte | 84 +++++ .../src/routes/notes/new/+page.svelte | 69 ++++ .../src/routes/reddit-upload/+page.svelte | 348 ++++++++++++++++++ apps/vault-demo/static/robots.txt | 3 + apps/vault-demo/svelte.config.js | 26 ++ apps/vault-demo/tsconfig.json | 22 ++ apps/vault-demo/vite.config.ts | 6 + .../src/adapters/entity-index/index.ts | 1 + .../entity-index/migrations/transforms.ts | 3 + .../entity-index/migrations/versions.ts | 28 ++ .../src/adapters/entity-index/src/adapter.ts | 62 ++++ .../src/adapters/example-notes/index.ts | 1 + .../example-notes/migrations/transforms.ts | 11 + .../example-notes/migrations/versions.ts | 30 ++ .../src/adapters/example-notes/src/adapter.ts | 61 +++ 42 files changed, 2106 insertions(+) create mode 100644 apps/vault-demo/.gitignore create mode 100644 apps/vault-demo/.npmrc create mode 100644 apps/vault-demo/README.md create mode 100644 apps/vault-demo/package.json create mode 100644 apps/vault-demo/src/app.d.ts create mode 100644 apps/vault-demo/src/app.html create mode 100644 apps/vault-demo/src/lib/export/index.ts create mode 100644 apps/vault-demo/src/lib/extract/redditEntities.ts create mode 100644 apps/vault-demo/src/lib/remote/entityIndex.remote.ts create mode 100644 apps/vault-demo/src/lib/remote/notes.remote.ts create mode 100644 apps/vault-demo/src/lib/remote/vault.remote.ts create mode 100644 apps/vault-demo/src/lib/schemas/entities.ts create mode 100644 apps/vault-demo/src/lib/schemas/notes.ts create mode 100644 apps/vault-demo/src/lib/schemas/vault.ts create mode 100644 apps/vault-demo/src/lib/server/vaultService.ts create mode 100644 apps/vault-demo/src/routes/+layout.svelte create mode 100644 apps/vault-demo/src/routes/+page.svelte create mode 100644 apps/vault-demo/src/routes/api/vault/counts/+server.ts create mode 100644 apps/vault-demo/src/routes/api/vault/export/+server.ts create mode 100644 apps/vault-demo/src/routes/api/vault/import/+server.ts create mode 100644 apps/vault-demo/src/routes/api/vault/ingest/+server.ts create mode 100644 apps/vault-demo/src/routes/dashboard/+page.svelte create mode 100644 apps/vault-demo/src/routes/entities/+page.svelte create mode 100644 apps/vault-demo/src/routes/entities/[id]/+page.svelte create mode 100644 apps/vault-demo/src/routes/import-export/+page.svelte create mode 100644 apps/vault-demo/src/routes/import-export/export/+server.ts create mode 100644 apps/vault-demo/src/routes/notes/+page.svelte create mode 100644 apps/vault-demo/src/routes/notes/[id]/+page.svelte create mode 100644 apps/vault-demo/src/routes/notes/new/+page.svelte create mode 100644 apps/vault-demo/src/routes/reddit-upload/+page.svelte create mode 100644 apps/vault-demo/static/robots.txt create mode 100644 apps/vault-demo/svelte.config.js create mode 100644 apps/vault-demo/tsconfig.json create mode 100644 apps/vault-demo/vite.config.ts create mode 100644 packages/vault-core/src/adapters/entity-index/index.ts create mode 100644 packages/vault-core/src/adapters/entity-index/migrations/transforms.ts create mode 100644 packages/vault-core/src/adapters/entity-index/migrations/versions.ts create mode 100644 packages/vault-core/src/adapters/entity-index/src/adapter.ts create mode 100644 packages/vault-core/src/adapters/example-notes/index.ts create mode 100644 packages/vault-core/src/adapters/example-notes/migrations/transforms.ts create mode 100644 packages/vault-core/src/adapters/example-notes/migrations/versions.ts create mode 100644 packages/vault-core/src/adapters/example-notes/src/adapter.ts diff --git a/apps/vault-demo/.gitignore b/apps/vault-demo/.gitignore new file mode 100644 index 0000000000..3b462cb0c4 --- /dev/null +++ b/apps/vault-demo/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/vault-demo/.npmrc b/apps/vault-demo/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/apps/vault-demo/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/vault-demo/README.md b/apps/vault-demo/README.md new file mode 100644 index 0000000000..5a2518ea5e --- /dev/null +++ b/apps/vault-demo/README.md @@ -0,0 +1,71 @@ +# Vault Demo (SvelteKit) + +Overview + +- The demo shows how independent adapters can be composed into one runtime Vault: + - Import/Export of adapter data + - Reddit GDPR upload with entity suggestions → user-curated Entity Index import + - Notes creation with entity linking + - Cross-adapter views: Dashboard, Entities, Notes +- Adapters are independent; the app composes them through runtime joins exposed by [getQueryInterface()](packages/vault-core/src/core/vault.ts:317). + +Quick start (Bun) + +- Prerequisite: Bun installed. +- From repo root: + + ``` + bun install + bun run dev -w apps/vault-demo + ``` + +- Open http://localhost:5173 +- Note: The demo uses an in-memory DB with a vault singleton at [apps/vault-demo/src/lib/vault/singleton.ts](apps/vault-demo/src/lib/vault/singleton.ts) so data persists across routes during a single browser session. + +Key flows + +- Import/Export + - Visit /import-export + - Import: select a folder or multiple files exported by Vault and choose the adapter; the page calls [importData()](packages/vault-core/src/core/vault.ts:176) using [jsonFormat](packages/vault-core/src/codecs/json.ts:3). + - Export: click Export to get a list of files; download per file. The page uses [exportData()](packages/vault-core/src/core/vault.ts:116). + +- Reddit GDPR upload + suggestions → Entity Index import + - Visit /reddit-upload + - Ingest a Reddit file via [ingestData()](packages/vault-core/src/core/vault.ts:284) with [redditAdapter()](packages/vault-core/src/adapters/reddit/src/adapter.ts:12). + - Click “Suggest entities” to scan imported rows using [apps/vault-demo/src/lib/extract/redditEntities.ts](apps/vault-demo/src/lib/extract/redditEntities.ts:1) with heuristics: subreddits r/..., users u/..., URL domains. + - Select entities and import into Entity Index via [importData()](packages/vault-core/src/core/vault.ts:176) using [entityIndexAdapter()](packages/vault-core/src/adapters/entity-index/src/adapter.ts:89) validator. + +- Notes creation + entity linking + - Visit /notes/new + - Create a note with title, body, and pick entities to link; the page writes to Example Notes through [importData()](packages/vault-core/src/core/vault.ts:176) using [exampleNotesAdapter()](packages/vault-core/src/adapters/example-notes/src/adapter.ts:147). + - Visit /entities and click an entity; the detail shows occurrences and “Linked Notes”, parsed from the Notes adapter’s entity_links JSON column (see [packages/vault-core/src/adapters/example-notes/src/adapter.ts](packages/vault-core/src/adapters/example-notes/src/adapter.ts:1)). + +- Dashboard + - Visit /dashboard to see per-adapter table row counts aggregated at runtime via [getQueryInterface()](packages/vault-core/src/core/vault.ts:317). + +Architecture notes + +- Vault wiring is centralized in [apps/vault-demo/src/lib/vault/client.ts](apps/vault-demo/src/lib/vault/client.ts:1) using [createVault()](packages/vault-core/src/core/vault.ts:31). +- The demo uses an in-memory MockDrizzle at [apps/vault-demo/src/lib/vault/mockDrizzle.ts](apps/vault-demo/src/lib/vault/mockDrizzle.ts:1). +- Adapters + - Reddit: [packages/vault-core/src/adapters/reddit/src/adapter.ts](packages/vault-core/src/adapters/reddit/src/adapter.ts:1) + - Entity Index: [packages/vault-core/src/adapters/entity-index/src/adapter.ts](packages/vault-core/src/adapters/entity-index/src/adapter.ts:1) + - Example Notes: [packages/vault-core/src/adapters/example-notes/src/adapter.ts](packages/vault-core/src/adapters/example-notes/src/adapter.ts:1) + +Data model highlights + +- Entity Index stores canonical entities and occurrences; they are user-curated in this demo, not auto-derived. +- Example Notes stores notes with entity_links as a TEXT JSON array; validators (arktype) accept string[] and serialize to DB-ready JSON. +- All export/import uses [jsonFormat](packages/vault-core/src/codecs/json.ts:3). + +Limitations + +- No persistence beyond a browser session; refresh clears data. +- Export is per-file downloads; no archive bundling. +- The Reddit heuristic extractor is intentionally simple. + +Test references + +- [packages/vault-core/tests/fixtures/entity-index-fixture.ts](packages/vault-core/tests/fixtures/entity-index-fixture.ts:1) +- [packages/vault-core/tests/example-notes.spec.ts](packages/vault-core/tests/example-notes.spec.ts:1) +- [packages/vault-core/tests/entity-index.spec.ts](packages/vault-core/tests/entity-index.spec.ts:1) diff --git a/apps/vault-demo/package.json b/apps/vault-demo/package.json new file mode 100644 index 0000000000..cdc129460b --- /dev/null +++ b/apps/vault-demo/package.json @@ -0,0 +1,28 @@ +{ + "name": "vault-demo", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "bun --bun vite dev", + "build": "bun --bun vite build", + "preview": "bun --bun vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "^2.43.2", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "bun-types": "^1.3.0", + "drizzle-orm": "catalog:", + "svelte": "^5.39.5", + "svelte-check": "^4.3.2", + "typescript": "^5.9.2", + "vite": "^7.1.7" + }, + "dependencies": { + "arktype": "catalog:" + } +} diff --git a/apps/vault-demo/src/app.d.ts b/apps/vault-demo/src/app.d.ts new file mode 100644 index 0000000000..da08e6da59 --- /dev/null +++ b/apps/vault-demo/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/vault-demo/src/app.html b/apps/vault-demo/src/app.html new file mode 100644 index 0000000000..f273cc58f7 --- /dev/null +++ b/apps/vault-demo/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/vault-demo/src/lib/export/index.ts b/apps/vault-demo/src/lib/export/index.ts new file mode 100644 index 0000000000..2914483e44 --- /dev/null +++ b/apps/vault-demo/src/lib/export/index.ts @@ -0,0 +1,15 @@ +import { ZIP } from '@repo/vault-core/utils/archive/zip'; +import { getVault, jsonFormat } from '$lib/server/vaultService'; + +export const exportZip = async () => { + const vault = getVault(); + const filesMap = await vault.exportData({ codec: jsonFormat }); + const all = await Promise.all( + filesMap + .entries() + .map(async ([path, file]) => [path, await file.bytes()] as const), + ); + const rec = Object.fromEntries(all); + const zipped = await ZIP.pack(rec); + return zipped; +}; diff --git a/apps/vault-demo/src/lib/extract/redditEntities.ts b/apps/vault-demo/src/lib/extract/redditEntities.ts new file mode 100644 index 0000000000..59a23fa2bc --- /dev/null +++ b/apps/vault-demo/src/lib/extract/redditEntities.ts @@ -0,0 +1,205 @@ +/** + * Small extraction utility: scan Reddit rows to suggest entities and occurrences. + * Heuristics: + * - Subreddits: /\br\/([A-Za-z0-9_]+)\b/ + * - Users: /\bu\/([A-Za-z0-9_-]+)\b/ + * - Domains: any "url" field parsed with new URL().hostname + */ + +export type ExtractedEntity = { + id: string; + name: string; + type: 'subreddit' | 'user' | 'domain'; + description?: string | null; + public_id?: string | null; + created_at: number; +}; + +export type ExtractedOccurrence = { + id: string; + entity_id: string; + source_adapter_id: 'reddit'; + source_table_name: string; + source_pk_json: string; + discovered_at: number; +}; + +type TablesToRows = Record[]>; + +export function extractEntitiesFromReddit(tablesToRows: TablesToRows): { + entities: ExtractedEntity[]; + occurrences: ExtractedOccurrence[]; +} { + const subredditRe = /\br\/([A-Za-z0-9_]+)\b/g; + const userRe = /\bu\/([A-Za-z0-9_-]+)\b/g; + + const entitiesByKey = new Map(); + const occurrenceIds = new Set(); + const occurrences: ExtractedOccurrence[] = []; + const now = Date.now(); + + const ensureEntity = ( + type: ExtractedEntity['type'], + name: string, + ): ExtractedEntity => { + const key = `${type}|${name}`; + let ent = entitiesByKey.get(key); + if (!ent) { + ent = { + id: `${type}:${name}`, + name, + type, + description: null, + public_id: null, + created_at: now, + }; + entitiesByKey.set(key, ent); + } + return ent; + }; + + for (const [tableName, rows] of Object.entries(tablesToRows ?? {})) { + for (const row of rows ?? []) { + // Scan string fields + for (const [field, v] of Object.entries(row)) { + if (typeof v !== 'string' || !v) continue; + + // Domain from URL fields + if (field === 'url') { + const host = safeHostname(v); + if (host) { + const ent = ensureEntity('domain', host); + pushOccurrence( + ent.id, + tableName, + row, + occurrences, + occurrenceIds, + now, + ); + } + } + + // Subreddit mentions + for (const m of v.matchAll(subredditRe)) { + const name = m[1]; + if (!name) continue; + const ent = ensureEntity('subreddit', name); + pushOccurrence( + ent.id, + tableName, + row, + occurrences, + occurrenceIds, + now, + ); + } + + // User mentions + for (const m of v.matchAll(userRe)) { + const name = m[1]; + if (!name) continue; + const ent = ensureEntity('user', name); + pushOccurrence( + ent.id, + tableName, + row, + occurrences, + occurrenceIds, + now, + ); + } + } + } + } + + return { + entities: Array.from(entitiesByKey.values()).sort(byTypeThenName), + occurrences, + }; +} + +function byTypeThenName(a: ExtractedEntity, b: ExtractedEntity): number { + if (a.type !== b.type) return a.type < b.type ? -1 : 1; + return a.name.localeCompare(b.name); +} + +function safeHostname(url: string): string | null { + try { + const u = new URL(url); + return u.hostname || null; + } catch { + return null; + } +} + +function pickStablePk(row: Record): Record { + // Prefer common primary keys if present + const candidates = ['id', 'permalink', 'message_id', 'username']; + for (const k of candidates) { + const v = row[k as keyof typeof row]; + if (typeof v === 'string' && v) return { [k]: v }; + if (typeof v === 'number') return { [k]: v }; + } + // Next, include a small set of stable fields if present + const stable: Record = {}; + const fallbacks = ['url', 'subreddit', 'link', 'post_id', 'thread_id']; + for (const k of fallbacks) { + const v = row[k as keyof typeof row]; + if (typeof v === 'string' && v) stable[k] = v; + if (typeof v === 'number') stable[k] = v; + } + if (Object.keys(stable).length > 0) return stable; + // Final fallback: first two primitive fields in alpha key order + const prims: [string, unknown][] = Object.keys(row) + .sort() + .map((k) => [k, row[k as keyof typeof row] as unknown] as [string, unknown]) + .filter(([, v]) => typeof v === 'string' || typeof v === 'number') + .slice(0, 2); + const out: Record = {}; + for (const [k, v] of prims) out[k] = v; + return out; +} + +function pushOccurrence( + entityId: string, + tableName: string, + row: Record, + out: ExtractedOccurrence[], + seen: Set, + now: number, +) { + const pkObj = pickStablePk(row); + const pkJson = JSON.stringify(pkObj); + const id = makeOccurrenceId(entityId, tableName, pkJson); + if (seen.has(id)) return; + seen.add(id); + out.push({ + id, + entity_id: entityId, + source_adapter_id: 'reddit', + source_table_name: tableName, + source_pk_json: pkJson, + discovered_at: now, + }); +} + +function makeOccurrenceId( + entityId: string, + table: string, + pkJson: string, +): string { + const base = `${entityId}|${table}|${pkJson}`; + const h = hashString(base); + return `occ:${h}`; +} + +function hashString(s: string): string { + // Simple 32-bit FNV-1a + let h = 0x811c9dc5 >>> 0; + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 0x01000193) >>> 0; + } + return h.toString(16); +} diff --git a/apps/vault-demo/src/lib/remote/entityIndex.remote.ts b/apps/vault-demo/src/lib/remote/entityIndex.remote.ts new file mode 100644 index 0000000000..e528ad1d40 --- /dev/null +++ b/apps/vault-demo/src/lib/remote/entityIndex.remote.ts @@ -0,0 +1,92 @@ +import { error } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { command, query } from '$app/server'; +import { InsertEntitiesInputSchema } from '$lib/schemas/entities'; +import { type EntityRow, EntityRowSchema, IdSchema } from '$lib/schemas/notes'; +import { getVault } from '$lib/server/vaultService'; + +/** + * Command: bulk insert into the entity_index adapter tables using the Vault query interface. + */ +export const insertEntities = command( + InsertEntitiesInputSchema, + async (input) => { + const { db, tables } = getVault().getQueryInterface(); + const entitiesTable = tables.entity_index.entity_index_entities; + const occurrencesTable = tables.entity_index.entity_index_occurrences; + + const entityRows = input.entities.map((e) => ({ + id: e.id, + name: e.name ?? null, + type: e.type ?? null, + description: e.description, + public_id: e.public_id, + created_at: new Date(), + })); + + const occurrenceRows = input.occurrences.map((o) => ({ + id: o.id, + entity_id: o.entity_id, + source_adapter_id: o.source_adapter_id, + source_table_name: o.source_table_name, + source_pk_json: o.source_pk_json, + discovered_at: new Date(), + })); + + // Insert entities with conflict-ignore + if (entityRows.length > 0) + await db.insert(entitiesTable).values(entityRows).onConflictDoNothing(); + + // Insert occurrences with conflict-ignore (by PK) when supported + if (occurrenceRows.length > 0) + await db + .insert(occurrencesTable) + .values(occurrenceRows) + .onConflictDoNothing(); + + return { + ok: true as const, + inserted: { + entities: entityRows.length, + occurrences: occurrenceRows.length, + }, + }; + }, +); + +/** + * Query: list entities for selection in the "new note" form. + */ +export const getEntities = query(async () => { + const { db, tables } = getVault().getQueryInterface(); + + const entitiesTable = tables.entity_index.entity_index_entities; + + const rows = await db.select().from(entitiesTable); + + return rows; +}); + +/** + * Query: get a single entity by ID. + */ +export const getEntityById = query(IdSchema, async ({ id }) => { + const { db, tables } = getVault().getQueryInterface(); + + const entitiesTable = tables.entity_index.entity_index_entities; + + const row = await db + .select() + .from(entitiesTable) + .where(eq(entitiesTable.id, id)) + .limit(1) + .get(); + + if (!row) return error(404, 'entity not found'); + + return EntityRowSchema({ + id: row.id, + name: row.name, + type: row.type, + }) as EntityRow; +}); diff --git a/apps/vault-demo/src/lib/remote/notes.remote.ts b/apps/vault-demo/src/lib/remote/notes.remote.ts new file mode 100644 index 0000000000..1ff38522df --- /dev/null +++ b/apps/vault-demo/src/lib/remote/notes.remote.ts @@ -0,0 +1,138 @@ +import { error, redirect } from '@sveltejs/kit'; +import { eq } from 'drizzle-orm'; +import { form, query } from '$app/server'; +import { + CreateNoteInputSchema, + IdSchema, + type NoteView, + NoteViewSchema, + UpdateNoteInputSchema, +} from '$lib/schemas/notes'; +import { getVault } from '$lib/server/vaultService'; + +function parseStringArrayJson(text: unknown): string[] { + if (text == null) return []; + if (typeof text === 'string') { + const v = JSON.parse(text); + return Array.isArray(v) ? v.map(String) : []; + } + if (Array.isArray(text)) return text.map(String); + return []; +} + +function asEpochMs(v: unknown): number { + if (v instanceof Date) return v.getTime(); + if (typeof v === 'number') return v; + const n = Number(v); + return Number.isFinite(n) ? n : Date.now(); +} + +/** + * Query: return latest notes as an array of NoteView (parsed entity_links). + */ +export const getNotes = query(async (): Promise => { + const { db, tables } = getVault().getQueryInterface(); + + const notesTable = tables.example_notes.example_notes_items; + + const rows = await db.select().from(notesTable); + + const notes = rows + .map( + (r) => + NoteViewSchema({ + id: r.id, + title: (r.title ?? '').toString(), + body: r.body == null ? undefined : r.body.toString(), + created_at: asEpochMs(r.created_at), + entity_links: parseStringArrayJson(r.entity_links), + }) as NoteView, + ) + .sort((a, b) => b.created_at - a.created_at); + + return notes; +}); + +/** + * Form: create a new note directly via Drizzle insert. + */ +export const createNote = form(CreateNoteInputSchema, async (input) => { + const { title, body, entity_links } = input; + const trimmed = title.trim(); + if (trimmed.length === 0) throw new Error('title is required'); + + const { db, tables } = getVault().getQueryInterface(); + const notesTable = tables.example_notes.example_notes_items; + + const id = globalThis.crypto?.randomUUID(); + + await db.insert(notesTable).values({ + id, + title: trimmed, + body: body, + // tags: [], // This isn't working, not sure if I need to make sure schema is synced + // Store entity_links as canonical JSON string (TEXT) to match schema/default "[]" + entity_links: JSON.stringify(entity_links), + created_at: new Date(), + public_id: null, + }); + + return redirect(303, `/notes/${id}`); +}); + +/** + * Fetch a single note by id. Returns a NoteView or + */ +export const getNoteById = query(IdSchema, async ({ id }) => { + const vault = getVault(); + const { db, tables } = vault.getQueryInterface(); + const notesTable = tables.example_notes.example_notes_items; + + const rows = await db.select().from(notesTable).where(eq(notesTable.id, id)); + const row = rows?.[0]; + if (!row) return error(404, 'note not found'); + + return NoteViewSchema({ + id: row.id, + title: (row.title ?? '').toString(), + body: row.body == null ? undefined : row.body.toString(), + created_at: asEpochMs(row.created_at), + entity_links: parseStringArrayJson(row.entity_links), + }) as NoteView; +}); + +/** + * Update a note by id. Supports partial updates: title, body, entity_links + */ +export const updateNote = form( + UpdateNoteInputSchema, + async ({ id, title, body, entity_links }) => { + const { db, tables } = getVault().getQueryInterface(); + const notesTable = tables.example_notes.example_notes_items; + + const updates: Record = {}; + if (typeof title === 'string') updates.title = title; + if (typeof body === 'string') updates.body = body; + if (Array.isArray(entity_links)) + updates.entity_links = JSON.stringify(entity_links); + + if (Object.keys(updates).length === 0) return { ok: true }; + + await db.update(notesTable).set(updates).where(eq(notesTable.id, id)); + + return { ok: true }; + }, +); + +/** + * Delete a note by id. + */ +export const deleteNote = form(IdSchema, async ({ id }) => { + const { db, tables } = getVault().getQueryInterface(); + const notesTable = tables.example_notes?.example_notes_items; + if (!notesTable) throw new Error('notes table missing'); + + await db.delete(notesTable).where(eq(notesTable.id, id)); + + return redirect(303, '/notes'); +}); diff --git a/apps/vault-demo/src/lib/remote/vault.remote.ts b/apps/vault-demo/src/lib/remote/vault.remote.ts new file mode 100644 index 0000000000..f751661ee1 --- /dev/null +++ b/apps/vault-demo/src/lib/remote/vault.remote.ts @@ -0,0 +1,43 @@ +import { redditAdapter } from '@repo/vault-core/adapters/reddit'; +import { ZIP } from '@repo/vault-core/utils/archive/zip'; +import { form, query } from '$app/server'; +import { + ImportBundleInputSchema, + IngestFileInputSchema, +} from '$lib/schemas/vault'; +import { getTableCounts, getVault, jsonFormat } from '$lib/server/vaultService'; + +/** + * Query: get per-table row counts grouped by adapter id. + */ +export const getCounts = query(async () => { + return await getTableCounts(); +}); + +/** + * Form: import a bundle of files (multi-adapter). Pairs uploaded files with + * the client-provided paths derived from directory selection. + */ +export const importBundle = form(ImportBundleInputSchema, async (input) => { + const filesInput = input.files; + const files = new Map( + // Here `file.name` should be the relative path within the selected directory + // E.g. `vault-export/vault/entity_index/entity_index_entities/subreddit_todayilearned.json` + Object.values(filesInput).map((file) => [file.name, file]), + ); + await getVault().importData({ files, codec: jsonFormat }); + + return { message: `Imported ${files.size} files` }; +}); + +/** + * Form: ingest a single uploaded file for a specified adapter. + * Validate adapter via simple runtime check; assert File at runtime. + */ +export const ingest = form(IngestFileInputSchema, async (input) => { + const fdFile = input.file; + + await getVault().ingestData({ adapter: redditAdapter(), file: fdFile }); + + return { message: `Ingested file: ${fdFile.name}` }; +}); diff --git a/apps/vault-demo/src/lib/schemas/entities.ts b/apps/vault-demo/src/lib/schemas/entities.ts new file mode 100644 index 0000000000..2ba8c28a43 --- /dev/null +++ b/apps/vault-demo/src/lib/schemas/entities.ts @@ -0,0 +1,22 @@ +import { type } from 'arktype'; + +export const InsertEntitiesInputSchema = type({ + entities: type({ + id: 'string', + name: 'string | null | undefined', + type: 'string | null | undefined', + description: 'string | null | undefined', + public_id: 'string | null | undefined', + created_at: 'string | number | Date | null | undefined', + }).array(), + occurrences: type({ + id: 'string', + entity_id: 'string', + source_adapter_id: 'string', + source_table_name: 'string', + source_pk_json: 'string', + discovered_at: 'string | number | Date | null | undefined', + }).array(), +}); + +export type InsertEntitiesInput = typeof InsertEntitiesInputSchema.infer; diff --git a/apps/vault-demo/src/lib/schemas/notes.ts b/apps/vault-demo/src/lib/schemas/notes.ts new file mode 100644 index 0000000000..9ec5de2187 --- /dev/null +++ b/apps/vault-demo/src/lib/schemas/notes.ts @@ -0,0 +1,50 @@ +// Centralized ArkType schemas and inferred types for the Notes feature. +// Single source of truth for request/response shapes. + +import { type } from 'arktype'; + +// Public view of a note returned to the client/UI +export const NoteViewSchema = type({ + id: 'string', + title: 'string', + body: 'string | undefined', + created_at: 'number', + entity_links: type('string[]'), +}); +export type NoteView = typeof NoteViewSchema.infer; + +// Entity rows (lightweight shape for pickers and linking) +export const EntityRowSchema = type({ + id: 'string', + name: 'string | null | undefined', + type: 'string | null | undefined', +}); +export type EntityRow = typeof EntityRowSchema.infer; + +// Create note input payload +export const CreateNoteInputSchema = type({ + title: 'string', + body: 'string', + 'entity_links?': type('string[]'), +}); +export type CreateNoteInput = typeof CreateNoteInputSchema.infer; + +// I'm not implementing partial updates at the schema level for simplicity +export const UpdateNoteInputSchema = type.and( + CreateNoteInputSchema, + type({ + id: 'string', + }), +); +export type UpdateNoteInput = typeof UpdateNoteInputSchema.infer; + +export const IdSchema = type({ + id: 'string', +}); +export type IdInput = typeof IdSchema.infer; + +// Optional list filter +export const ListNotesInputSchema = type({ + search: 'string | undefined', +}); +export type ListNotesInput = typeof ListNotesInputSchema.infer; diff --git a/apps/vault-demo/src/lib/schemas/vault.ts b/apps/vault-demo/src/lib/schemas/vault.ts new file mode 100644 index 0000000000..4feed1ac2b --- /dev/null +++ b/apps/vault-demo/src/lib/schemas/vault.ts @@ -0,0 +1,13 @@ +import { type } from 'arktype'; + +export const IngestFileInputSchema = type({ + adapter: 'string', + file: 'File', +}); + +export const ImportBundleInputSchema = type({ + files: type('Record'), +}); + +export type IngestFileInput = typeof IngestFileInputSchema.infer; +export type ImportBundleInput = typeof ImportBundleInputSchema.infer; diff --git a/apps/vault-demo/src/lib/server/vaultService.ts b/apps/vault-demo/src/lib/server/vaultService.ts new file mode 100644 index 0000000000..3722b188ac --- /dev/null +++ b/apps/vault-demo/src/lib/server/vaultService.ts @@ -0,0 +1,64 @@ +import { Database } from 'bun:sqlite'; +import fs from 'node:fs'; +import path from 'node:path'; +import { createVault } from '@repo/vault-core'; +import { entityIndexAdapter } from '@repo/vault-core/adapters/entity-index'; +import { exampleNotesAdapter } from '@repo/vault-core/adapters/example-notes'; +import { redditAdapter } from '@repo/vault-core/adapters/reddit'; +import { jsonFormat } from '@repo/vault-core/codecs'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; + +// Ensure data directory and DB path exist (stable across CWDs) +// Priority: +// 1) VAULT_DB_PATH (explicit override) +// 2) If CWD is apps/vault-demo -> use ".data/vault.sqlite" +// 3) Otherwise (running from monorepo root) -> "apps/vault-demo/.data/vault.sqlite" +const VAULT_DB_PATH = process.env.VAULT_DB_PATH; +const cwd = process.cwd().replace(/\\/g, '/'); +const isAppCwd = + cwd.endsWith('/apps/vault-demo') || cwd.includes('/apps/vault-demo/'); +const computedDir = isAppCwd + ? path.resolve(cwd, '.data') + : path.resolve(cwd, 'apps/vault-demo/.data'); +const dataDir = VAULT_DB_PATH ? path.dirname(VAULT_DB_PATH) : computedDir; +fs.mkdirSync(dataDir, { recursive: true }); +const dbPath = VAULT_DB_PATH ?? path.join(dataDir, 'vault.sqlite'); +// Optional debug breadcrumb to confirm which file is used +if ( + (process.env.VAULT_DEBUG ?? '').toLowerCase().includes('migrations') || + ['1', 'true', 'all'].includes((process.env.VAULT_DEBUG ?? '').toLowerCase()) +) { + console.info('[vault-demo:vaultService] using sqlite dbPath=', dbPath); +} + +export function getVault() { + const sqlite = new Database(dbPath, { create: true, readwrite: true }); + const db = drizzle(sqlite); + const v = createVault({ + database: db, + adapters: [redditAdapter(), entityIndexAdapter(), exampleNotesAdapter()], + }); + return v; +} + +// Helper to compute table row counts for each adapter/table +export async function getTableCounts() { + const { db, tables } = getVault().getQueryInterface(); + + const result: Record> = {}; + for (const [adapterId, schema] of Object.entries(tables)) { + result[adapterId] = {}; + for (const [tableName, table] of Object.entries(schema)) { + try { + const rows = await db.select().from(table); + result[adapterId][tableName] = Array.isArray(rows) ? rows.length : 0; + } catch { + // Skip non-table exports if any exist + } + } + } + return result; +} + +// Re-export codec for convenience in remote functions +export { jsonFormat }; diff --git a/apps/vault-demo/src/routes/+layout.svelte b/apps/vault-demo/src/routes/+layout.svelte new file mode 100644 index 0000000000..c735fba1ef --- /dev/null +++ b/apps/vault-demo/src/routes/+layout.svelte @@ -0,0 +1,16 @@ + + +
+ +
+ +
diff --git a/apps/vault-demo/src/routes/+page.svelte b/apps/vault-demo/src/routes/+page.svelte new file mode 100644 index 0000000000..f11d7d55bb --- /dev/null +++ b/apps/vault-demo/src/routes/+page.svelte @@ -0,0 +1,76 @@ + + +

Vault Demo

+

+ This demo shows a server-backed Vault using Bun SQLite (Drizzle) with adapters + for Reddit, Entity Index, and Example Notes. +

+ +

Quickstart

+
    +
  1. + Reddit upload: +
      +
    • Go to Reddit Upload.
    • +
    • Choose your exported Reddit ZIP and click “Ingest”.
    • +
    • Click “Suggest entities” to analyze exported rows.
    • +
    • + Select the entities you want then click “Import selected into Entity + Index”. This writes directly to the Entity Index tables via a server + remote command. +
    • +
    +
  2. +
  3. + Browse entities: +
      +
    • View Entity Index data at Entities.
    • +
    • Click an entity to view its detail page and linked notes.
    • +
    +
  4. +
  5. + Create and edit notes: +
      +
    • + Create a note at New Note then edit it at + Notes. +
    • +
    • + Use the entity picker to link notes to entities (stored as JSON in the + notes table). +
    • +
    +
  6. +
  7. + Import/Export (optional): +
      +
    • + Visit Import/Export to serialize/deserialize + adapter data bundles. +
    • +
    • + Note: The Reddit entities flow now writes directly; Import is reserved + for bundle I/O. +
    • +
    +
  8. +
+ +

Notes

+
    +
  • Server DB file: apps/vault-demo/.data/vault.sqlite (auto-created).
  • +
  • + Row counts for Reddit and Entity Index are visible on the Reddit Upload + page. +
  • +
+ +

Shortcuts

+ diff --git a/apps/vault-demo/src/routes/api/vault/counts/+server.ts b/apps/vault-demo/src/routes/api/vault/counts/+server.ts new file mode 100644 index 0000000000..40a92b465e --- /dev/null +++ b/apps/vault-demo/src/routes/api/vault/counts/+server.ts @@ -0,0 +1,20 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import { getTableCounts } from '$lib/server/vaultService'; + +const json = (data: unknown, init?: ResponseInit) => + new Response(JSON.stringify(data), { + headers: { 'content-type': 'application/json' }, + ...init, + }); + +export const GET: RequestHandler = async () => { + try { + const counts = await getTableCounts(); + return json({ ok: true, counts }); + } catch (err) { + return json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 500 }, + ); + } +}; diff --git a/apps/vault-demo/src/routes/api/vault/export/+server.ts b/apps/vault-demo/src/routes/api/vault/export/+server.ts new file mode 100644 index 0000000000..8640bda2b6 --- /dev/null +++ b/apps/vault-demo/src/routes/api/vault/export/+server.ts @@ -0,0 +1,28 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import { getVault, jsonFormat } from '$lib/server/vaultService'; + +const json = (data: unknown, init?: ResponseInit) => + new Response(JSON.stringify(data), { + headers: { 'content-type': 'application/json' }, + ...init, + }); + +export const GET: RequestHandler = async () => { + try { + const vault = getVault(); + const filesMap = await vault.exportData({ codec: jsonFormat }); + + const files: Array<{ path: string; text: string; mimeType: string }> = []; + for (const [path, file] of filesMap.entries()) { + const text = await file.text(); + files.push({ path, text, mimeType: file.type || 'application/json' }); + } + + return json({ ok: true, files }); + } catch (err) { + return json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 500 }, + ); + } +}; diff --git a/apps/vault-demo/src/routes/api/vault/import/+server.ts b/apps/vault-demo/src/routes/api/vault/import/+server.ts new file mode 100644 index 0000000000..d2c6d42906 --- /dev/null +++ b/apps/vault-demo/src/routes/api/vault/import/+server.ts @@ -0,0 +1,43 @@ +import type { RequestHandler } from '@sveltejs/kit'; +import { getVault, jsonFormat } from '$lib/server/vaultService'; + +const json = (data: unknown, init?: ResponseInit) => + new Response(JSON.stringify(data), { + headers: { 'content-type': 'application/json' }, + ...init, + }); + +type ImportFile = { path: string; text: string; mimeType?: string }; +type ImportBody = { files: ImportFile[] }; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = (await request.json()) as Partial; + const filesInput = body?.files; + if (!Array.isArray(filesInput)) { + return json( + { ok: false, error: 'files array required' }, + { status: 400 }, + ); + } + + const files = new Map(); + for (const f of filesInput) { + if (!f || typeof f.path !== 'string' || typeof f.text !== 'string') + continue; + const filename = f.path.split('/').pop() || 'file.json'; + const file = new File([f.text], filename, { + type: f.mimeType ?? 'application/json', + }); + files.set(f.path, file); + } + + await getVault().importData({ files, codec: jsonFormat }); + return json({ ok: true }); + } catch (err) { + return json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 500 }, + ); + } +}; diff --git a/apps/vault-demo/src/routes/api/vault/ingest/+server.ts b/apps/vault-demo/src/routes/api/vault/ingest/+server.ts new file mode 100644 index 0000000000..615e85a411 --- /dev/null +++ b/apps/vault-demo/src/routes/api/vault/ingest/+server.ts @@ -0,0 +1,53 @@ +import type { Adapter } from '@repo/vault-core'; +import { entityIndexAdapter } from '@repo/vault-core/adapters/entity-index'; +import { exampleNotesAdapter } from '@repo/vault-core/adapters/example-notes'; +import { redditAdapter } from '@repo/vault-core/adapters/reddit'; +import type { RequestHandler } from '@sveltejs/kit'; +import { getTableCounts, getVault } from '$lib/server/vaultService'; + +const json = (data: unknown, init?: ResponseInit) => + new Response(JSON.stringify(data), { + headers: { 'content-type': 'application/json' }, + ...init, + }); + +export const POST: RequestHandler = async ({ request, url }) => { + try { + const adapterId = url.searchParams.get('adapter'); + if (!adapterId) + return json( + { ok: false, error: 'adapter query param required' }, + { status: 400 }, + ); + + const form = await request.formData(); + const file = form.get('file'); + if (!(file instanceof File)) + return json({ ok: false, error: 'file missing' }, { status: 400 }); + + const factories: Record Adapter> = { + reddit: redditAdapter, + entity_index: entityIndexAdapter, + example_notes: exampleNotesAdapter, + }; + const factory = factories[adapterId]; + if (!factory) + return json( + { ok: false, error: `unknown adapter '${adapterId}'` }, + { status: 400 }, + ); + + const adapter = factory(); + + const vault = getVault(); + await vault.ingestData({ adapter, file }); + + const counts = await getTableCounts(); + return json({ ok: true, counts }); + } catch (err) { + return json( + { ok: false, error: err instanceof Error ? err.message : String(err) }, + { status: 500 }, + ); + } +}; diff --git a/apps/vault-demo/src/routes/dashboard/+page.svelte b/apps/vault-demo/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000000..8bb0f58010 --- /dev/null +++ b/apps/vault-demo/src/routes/dashboard/+page.svelte @@ -0,0 +1,58 @@ + + +

Dashboard

+ + + +{#if adapterIds.length === 0} +

No adapters found.

+{:else} + {#each adapterIds as adapterId} +
+

{adapterId}

+ {#if (countsByAdapter[adapterId]?.length ?? 0) === 0} +

No tables.

+ {:else} +
    + {#each countsByAdapter[adapterId] as c} +
  • {c.table}: {c.count}
  • + {/each} +
+ {/if} +
+ {/each} +{/if} + + diff --git a/apps/vault-demo/src/routes/entities/+page.svelte b/apps/vault-demo/src/routes/entities/+page.svelte new file mode 100644 index 0000000000..2f4e1313de --- /dev/null +++ b/apps/vault-demo/src/routes/entities/+page.svelte @@ -0,0 +1,58 @@ + + +

Entities

+ + + +{#if entities.length === 0} +

No entities.

+{:else} +
+ +
+ + + + + + + + + + {#each entities.toSorted( (a, b) => (a.name ?? '').localeCompare(b.name ?? ''), ) as e} + + + + + + + {/each} + +
NameTypeCreatedID
+ {#if e.id} + {e.name ?? '(no name)'} + {:else} + {e.name ?? '(no name)'} + {/if} + {e.type ?? ''}{humanizeCreatedAt((e as any).created_at)}{e.id}
+{/if} + + diff --git a/apps/vault-demo/src/routes/entities/[id]/+page.svelte b/apps/vault-demo/src/routes/entities/[id]/+page.svelte new file mode 100644 index 0000000000..b8dac8f432 --- /dev/null +++ b/apps/vault-demo/src/routes/entities/[id]/+page.svelte @@ -0,0 +1,62 @@ + + +

Entity Detail

+ + + +
+

+ {entity.name ?? '(no name)'} + ({entity.id}) +

+
    +
  • Type: {entity.type ?? ''}
  • +
  • Public ID: {(entity as any).public_id ?? ''}
  • +
  • Created: {humanizeDate((entity as any).created_at)}
  • +
  • Description: {(entity as any).description ?? ''}
  • +
+
+ +
+

Linked Notes

+ {#if linkedNotes.length === 0} +

No linked notes.

+ {:else} + + {/if} +
+ + diff --git a/apps/vault-demo/src/routes/import-export/+page.svelte b/apps/vault-demo/src/routes/import-export/+page.svelte new file mode 100644 index 0000000000..47e8aeb76b --- /dev/null +++ b/apps/vault-demo/src/routes/import-export/+page.svelte @@ -0,0 +1,40 @@ + + +

Import / Export

+ +
+
+

Import

+
+ + +
+
+ +
+ {#each importBundle.fields.allIssues() as issue} +

{issue.message}

+ {/each} + {#if importBundle.result} +

{importBundle.result.message}

+ {/if} +
+
+ +
+ +
+

Export

+ +
+ +

+ Back to Home · + Go to Reddit Upload +

diff --git a/apps/vault-demo/src/routes/import-export/export/+server.ts b/apps/vault-demo/src/routes/import-export/export/+server.ts new file mode 100644 index 0000000000..ecc21b025b --- /dev/null +++ b/apps/vault-demo/src/routes/import-export/export/+server.ts @@ -0,0 +1,12 @@ +import { exportZip } from '$lib/export'; + +export const GET = async () => { + const zipped = await exportZip(); + // @ts-expect-error @types/node, I want to throw you in the sun + return new Response(zipped.buffer, { + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': 'attachment; filename="vault-export.zip"', + }, + }); +}; diff --git a/apps/vault-demo/src/routes/notes/+page.svelte b/apps/vault-demo/src/routes/notes/+page.svelte new file mode 100644 index 0000000000..58d747cf8f --- /dev/null +++ b/apps/vault-demo/src/routes/notes/+page.svelte @@ -0,0 +1,96 @@ + + +

Notes

+ + + +
+ +
+ +{#if !isLoaded} +

Loading…

+{:else if errorText} +

{errorText}

+{:else if notes.length === 0} +

No notes found.

+{:else} +
    + {#each filtered as n} +
  • + + {n.title || '(untitled)'} + + — {humanizeDate(n.created_at)} + {#if n.entity_links.length > 0} +
    + {#each n.entity_links as eid} + + {eid} + + {/each} +
    + {/if} +
  • + {/each} +
+{/if} + + diff --git a/apps/vault-demo/src/routes/notes/[id]/+page.svelte b/apps/vault-demo/src/routes/notes/[id]/+page.svelte new file mode 100644 index 0000000000..d6e26f78ae --- /dev/null +++ b/apps/vault-demo/src/routes/notes/[id]/+page.svelte @@ -0,0 +1,84 @@ + + +

Edit Note

+ + + + +
+ + + + + + +
+ Link to Entities + {#if entities.length === 0} +

No entities available.

+ {:else} +
+ {#each entities as e} + + {/each} +
+ {/if} +
+ +
+ +
+ + {#if updateNote.fields.allIssues()} +

{updateNote.fields.allIssues()?.[0].message}

+ {/if} +
+
+ + +
+ + diff --git a/apps/vault-demo/src/routes/notes/new/+page.svelte b/apps/vault-demo/src/routes/notes/new/+page.svelte new file mode 100644 index 0000000000..50f99c97eb --- /dev/null +++ b/apps/vault-demo/src/routes/notes/new/+page.svelte @@ -0,0 +1,69 @@ + + +

New Note

+ + + +
+ + + + +
+ Link to Entities + {#if entities.length === 0} +

No entities available.

+ {:else} +
+ {#each entities as e} + + {/each} +
+ {/if} +
+ +
+ + Cancel +
+ + {#each createNote.fields.allIssues() as issue} +

{issue.message}

+ {/each} +
+ + diff --git a/apps/vault-demo/src/routes/reddit-upload/+page.svelte b/apps/vault-demo/src/routes/reddit-upload/+page.svelte new file mode 100644 index 0000000000..2090c953a7 --- /dev/null +++ b/apps/vault-demo/src/routes/reddit-upload/+page.svelte @@ -0,0 +1,348 @@ + + +

Reddit Upload

+ +
+ +
+ +
+
+ +
+ + {#if suggestions} + + {/if} +
+ +{#if status.kind === 'success'} +

{status.message}

+{:else if status.kind === 'error'} +

{status.message}

+{/if} + +{#if suggestions} +

Suggested Entities

+ +
+

Subreddits

+
    + {#each suggestions.entities.filter((e) => e.type === 'subreddit') as e} +
  • + +
  • + {/each} +
+
+ +
+

Users

+
    + {#each suggestions.entities.filter((e) => e.type === 'user') as e} +
  • + +
  • + {/each} +
+
+ +
+

Domains

+
    + {#each suggestions.entities.filter((e) => e.type === 'domain') as e} +
  • + +
  • + {/each} +
+
+{/if} + +{#if counts.length > 0} +

Reddit Tables

+
    + {#each counts as c} +
  • {c.table}: {c.count}
  • + {/each} +
+{/if} + +{#if entityIndexCounts.length > 0} +

Entity Index Tables

+
    + {#each entityIndexCounts as c} +
  • {c.table}: {c.count}
  • + {/each} +
+{/if} + +

+ Back to Home · + Go to Import/Export +

diff --git a/apps/vault-demo/static/robots.txt b/apps/vault-demo/static/robots.txt new file mode 100644 index 0000000000..b6dd6670cb --- /dev/null +++ b/apps/vault-demo/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/apps/vault-demo/svelte.config.js b/apps/vault-demo/svelte.config.js new file mode 100644 index 0000000000..07715b514b --- /dev/null +++ b/apps/vault-demo/svelte.config.js @@ -0,0 +1,26 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter(), + experimental: { + remoteFunctions: true, + }, + }, + compilerOptions: { + experimental: { + async: true, + }, + }, +}; + +export default config; diff --git a/apps/vault-demo/tsconfig.json b/apps/vault-demo/tsconfig.json new file mode 100644 index 0000000000..fc66193d83 --- /dev/null +++ b/apps/vault-demo/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "types": [ + "bun-types" + ] + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} \ No newline at end of file diff --git a/apps/vault-demo/vite.config.ts b/apps/vault-demo/vite.config.ts new file mode 100644 index 0000000000..fce9e8704c --- /dev/null +++ b/apps/vault-demo/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], +}); diff --git a/packages/vault-core/src/adapters/entity-index/index.ts b/packages/vault-core/src/adapters/entity-index/index.ts new file mode 100644 index 0000000000..18cb7d8ea5 --- /dev/null +++ b/packages/vault-core/src/adapters/entity-index/index.ts @@ -0,0 +1 @@ +export { entityIndexAdapter } from './src/adapter'; diff --git a/packages/vault-core/src/adapters/entity-index/migrations/transforms.ts b/packages/vault-core/src/adapters/entity-index/migrations/transforms.ts new file mode 100644 index 0000000000..44d1f72340 --- /dev/null +++ b/packages/vault-core/src/adapters/entity-index/migrations/transforms.ts @@ -0,0 +1,3 @@ +import { defineTransformRegistry } from '../../../core/migrations'; + +export const entityIndexTransforms = defineTransformRegistry({}); diff --git a/packages/vault-core/src/adapters/entity-index/migrations/versions.ts b/packages/vault-core/src/adapters/entity-index/migrations/versions.ts new file mode 100644 index 0000000000..4b824bb9c5 --- /dev/null +++ b/packages/vault-core/src/adapters/entity-index/migrations/versions.ts @@ -0,0 +1,28 @@ +/** + * Entity Index adapter migration versions. + * Single baseline version derived from the Drizzle schema in src/adapter.ts. + * SQL is inlined to keep migrations environment-agnostic (mirrors Reddit/Notes). + */ +import { defineVersions } from '../../../core/migrations'; + +export const entityIndexVersions = defineVersions({ + tag: '0000', + sql: [ + `CREATE TABLE \`entity_index_entities\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`name\` text, + \`type\` text, + \`description\` text, + \`public_id\` text, + \`created_at\` integer +);`, + `CREATE TABLE \`entity_index_occurrences\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`entity_id\` text, + \`source_adapter_id\` text, + \`source_table_name\` text, + \`source_pk_json\` text, + \`discovered_at\` integer +);`, + ], +}); diff --git a/packages/vault-core/src/adapters/entity-index/src/adapter.ts b/packages/vault-core/src/adapters/entity-index/src/adapter.ts new file mode 100644 index 0000000000..11b5934eff --- /dev/null +++ b/packages/vault-core/src/adapters/entity-index/src/adapter.ts @@ -0,0 +1,62 @@ +import { defineAdapter } from '@repo/vault-core'; +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import type { AdapterMetadata } from '../../../core/adapter'; +import { entityIndexTransforms } from '../migrations/transforms'; +import { entityIndexVersions } from '../migrations/versions'; + +/** + * Drizzle schema for the entity_index adapter. + * Table names must be prefixed with the adapter id. + */ +export const schema = { + entity_index_entities: sqliteTable('entity_index_entities', { + id: text('id').primaryKey(), + name: text('name'), + type: text('type'), + description: text('description'), + public_id: text('public_id'), + // Epoch milliseconds as integer + created_at: integer('created_at', { mode: 'timestamp' }), + }), + entity_index_occurrences: sqliteTable('entity_index_occurrences', { + id: text('id').primaryKey(), + entity_id: text('entity_id'), + source_adapter_id: text('source_adapter_id'), + source_table_name: text('source_table_name'), + // JSON string (canonical) of the primary key from the source table + source_pk_json: text('source_pk_json'), + // Epoch milliseconds as integer + discovered_at: integer('discovered_at', { mode: 'timestamp' }), + }), +} as const; + +/** Human-friendly column descriptions. */ +export const metadata: AdapterMetadata = { + entity_index_entities: { + id: 'Primary key', + name: 'Entity name', + type: 'Entity type/category', + description: 'Optional description of the entity', + public_id: 'Optional stable public id for cross-adapter linking', + created_at: 'Creation time in epoch milliseconds (stored as INTEGER)', + }, + entity_index_occurrences: { + id: 'Primary key', + entity_id: + 'Logical reference to entity_index_entities.id (no FK enforcement)', + source_adapter_id: 'Adapter id where this occurrence was discovered', + source_table_name: 'Source table name within the adapter', + source_pk_json: + 'Canonical JSON string of the source primary key (e.g., {"id":"t3_abc"})', + discovered_at: 'Discovery time in epoch milliseconds (stored as INTEGER)', + }, +}; + +/** Unified adapter export, no ingestors required for this adapter. */ +export const entityIndexAdapter = defineAdapter(() => ({ + id: 'entity_index', + schema, + metadata, + versions: entityIndexVersions, + transforms: entityIndexTransforms, +})); diff --git a/packages/vault-core/src/adapters/example-notes/index.ts b/packages/vault-core/src/adapters/example-notes/index.ts new file mode 100644 index 0000000000..d89b18e19a --- /dev/null +++ b/packages/vault-core/src/adapters/example-notes/index.ts @@ -0,0 +1 @@ +export { exampleNotesAdapter } from './src/adapter'; diff --git a/packages/vault-core/src/adapters/example-notes/migrations/transforms.ts b/packages/vault-core/src/adapters/example-notes/migrations/transforms.ts new file mode 100644 index 0000000000..c32e662393 --- /dev/null +++ b/packages/vault-core/src/adapters/example-notes/migrations/transforms.ts @@ -0,0 +1,11 @@ +/** + * Transform registry for example_notes adapter. + * + * With versions ['0000', '0001'], provide a no-op transform for target tag '0001' + * to keep registry length aligned with versions (all tags except the first/baseline). + */ +import { defineTransformRegistry } from '../../../core/migrations'; + +export const exampleNotesTransforms = defineTransformRegistry({ + '0001': (input) => input, +}); diff --git a/packages/vault-core/src/adapters/example-notes/migrations/versions.ts b/packages/vault-core/src/adapters/example-notes/migrations/versions.ts new file mode 100644 index 0000000000..e4c1008025 --- /dev/null +++ b/packages/vault-core/src/adapters/example-notes/migrations/versions.ts @@ -0,0 +1,30 @@ +/** + * Migration versions for the example_notes adapter. + * + * Single baseline version that mirrors the Drizzle-generated SQL for the schema + * defined in src/adapter.ts. This follows the Reddit adapter pattern of inlining + * the SQL artifacts for environment-agnostic startup migrations. + */ +import { defineVersions } from '../../../core/migrations'; + +export const exampleNotesVersions = defineVersions( + { + tag: '0000', + sql: [ + `CREATE TABLE \`example_notes_items\` ( + \`id\` text PRIMARY KEY NOT NULL, + \`title\` text, + \`body\` text, + \`tags\` text DEFAULT '[]', + \`created_at\` integer, + \`public_id\` text +);`, + ], + }, + { + tag: '0001', + sql: [ + `ALTER TABLE example_notes_items ADD COLUMN entity_links text NOT NULL DEFAULT '[]';`, + ], + }, +); diff --git a/packages/vault-core/src/adapters/example-notes/src/adapter.ts b/packages/vault-core/src/adapters/example-notes/src/adapter.ts new file mode 100644 index 0000000000..07b63ef6a7 --- /dev/null +++ b/packages/vault-core/src/adapters/example-notes/src/adapter.ts @@ -0,0 +1,61 @@ +/** + * Example Notes adapter refined to mirror Reddit adapter patterns. + * + * - Uses Drizzle schema helpers with intuitive types + * - created_at is an integer (epoch ms) via Drizzle integer timestamp column (typed as number) + * - tags is a TEXT column storing a JSON array string, default "[]" + * - Exposes an arktype-backed StandardSchemaV1-compatible validator + * - Keeps table prefix: example_notes_items + */ + +import { defineAdapter } from '@repo/vault-core'; +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import type { AdapterMetadata } from '../../../core/adapter'; +import { exampleNotesTransforms } from '../migrations/transforms'; +import { exampleNotesVersions } from '../migrations/versions'; + +/** + * Drizzle schema for the example_notes adapter. + * Table names are prefixed with the adapter id: `example_notes_items` + */ +export const schema = { + example_notes_items: sqliteTable('example_notes_items', { + id: text('id').primaryKey(), + title: text('title'), + body: text('body'), + // JSON array string (canonical format) with default [] + tags: text('tags').default('[]').$type(), + // JSON array string (canonical format) of Entity IDs, default [] + entity_links: text('entity_links').notNull().default('[]'), + // Epoch milliseconds as integer; typed as number in TS + created_at: integer('created_at', { mode: 'timestamp' }), + public_id: text('public_id'), + }), +} as const; + +/** + * Human-friendly metadata for adapter tables/columns. + */ +export const metadata = { + example_notes_items: { + id: 'Primary key', + title: 'Note title', + body: 'Note body', + tags: 'JSON array stored as TEXT (default "[]")', + entity_links: + 'JSON array (string[]) of Entity IDs from entity_index_entities, stored as TEXT JSON (default "[]")', + created_at: 'Creation time in epoch milliseconds (SQLite integer)', + public_id: 'Optional stable public id for cross-adapter linking', + }, +} satisfies AdapterMetadata; + +/** + * Export the adapter definition. No ingestors for this example. + */ +export const exampleNotesAdapter = defineAdapter(() => ({ + id: 'example_notes', + schema, + metadata, + versions: exampleNotesVersions, + transforms: exampleNotesTransforms, +})); From 1258ed0d6b4a5375cc368b8c1c9f238ce0c64e9a Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Tue, 21 Oct 2025 20:01:29 +0000 Subject: [PATCH 17/21] ci: fix ci `package.json` scripts for both projects --- apps/vault-demo/package.json | 3 +-- apps/vault-demo/tsconfig.json | 9 +++++---- packages/vault-core/package.json | 5 +---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/vault-demo/package.json b/apps/vault-demo/package.json index cdc129460b..b52a811263 100644 --- a/apps/vault-demo/package.json +++ b/apps/vault-demo/package.json @@ -8,8 +8,7 @@ "build": "bun --bun vite build", "preview": "bun --bun vite preview", "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.1.0", diff --git a/apps/vault-demo/tsconfig.json b/apps/vault-demo/tsconfig.json index fc66193d83..f2fe73d2d0 100644 --- a/apps/vault-demo/tsconfig.json +++ b/apps/vault-demo/tsconfig.json @@ -10,13 +10,14 @@ "sourceMap": true, "strict": true, "moduleResolution": "bundler", - "types": [ - "bun-types" - ] + "module": "esnext", + "target": "esnext", + "lib": ["esnext", "dom"], + "types": ["bun-types"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // // To make changes to top-level options such as include and exclude, we recommend extending // the generated config; see https://svelte.dev/docs/kit/configuration#typescript -} \ No newline at end of file +} diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index 22424ac711..c2b8c661c5 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -14,10 +14,7 @@ "typescript": "catalog:" }, "scripts": { - "format": "echo 'skip format'", - "format:check": "echo 'skip format check'", - "lint": "echo 'skip lint'", - "check": "echo \"TODO add this in a later commit\"" + "check": "tsc --noEmit" }, "dependencies": { "@standard-schema/spec": "^1.0.0", From 502d48cf9939e1d59a84ec689b20c06ec1a2fde4 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Wed, 22 Oct 2025 21:49:59 +0000 Subject: [PATCH 18/21] fix: update demo-mcp & usage --- apps/demo-mcp/src/cli.ts | 282 +++++++++++++++++++-------------------- 1 file changed, 135 insertions(+), 147 deletions(-) diff --git a/apps/demo-mcp/src/cli.ts b/apps/demo-mcp/src/cli.ts index 8b0c17642e..8095f8d127 100644 --- a/apps/demo-mcp/src/cli.ts +++ b/apps/demo-mcp/src/cli.ts @@ -10,7 +10,7 @@ * - serve [--db ] (stub) * * Defaults (if not provided): - * --file ./export_rocket_scientist2_20250811.zip (relative to cwd) + * --file (relative to cwd) * --db ./.data/reddit.db (relative to cwd) * --repo . (current working directory) * @@ -22,15 +22,10 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createClient } from '@libsql/client'; -import type { Importer } from '@repo/vault-core'; -import { - defaultConvention, - LocalFileStore, - markdownFormat, - VaultService, -} from '@repo/vault-core'; +import type { Adapter } from '@repo/vault-core'; +import { createVault, defaultConvention } from '@repo/vault-core'; +import { markdownFormat } from '@repo/vault-core/codecs'; import { drizzle } from 'drizzle-orm/libsql'; -import { migrate } from 'drizzle-orm/libsql/migrator'; // ------------------------------------------------------------- type CLIArgs = { @@ -78,17 +73,17 @@ function getBinPath(): string { function printHelp(): void { const bin = getBinPath(); console.log( - `Usage:\n bun run ${bin} [options]\n\nCommands:\n import Import a Reddit export ZIP into the database\n export-fs Export DB rows to Markdown files under vault//...\n import-fs Import Markdown files from vault//... into the DB\n serve Start stub server (not implemented)\n\nOptions:\n --file Path to Reddit export ZIP (import only)\n --db Path to SQLite DB file (default: ./.data/reddit.db or DATABASE_URL)\n --repo Repo root for plaintext I/O (default: .)\n -h, --help Show this help\n\nNotes:\n - Files are Markdown only, written under vault///.md\n - DATABASE_URL, if set, overrides --db entirely.\n`, + `Usage:\n bun run ${bin} [options]\n\nCommands:\n import Import a Reddit export ZIP into the database\n export-fs Export DB rows to Markdown files under vault//...\n import-fs Import Markdown files from vault//... into the DB\n\nOptions:\n --file Path to Reddit export ZIP (import only)\n --db Path to SQLite DB file (default: ./.data/reddit.db or DATABASE_URL)\n --repo Repo root for plaintext I/O (default: .)\n -h, --help Show this help\n\nNotes:\n - Files are Markdown only, written under vault//
/.md\n - DATABASE_URL, if set, overrides --db entirely.\n`, ); } -function resolveZipPath(p?: string): string { +function resolveZipPath(p: string): string { const candidate = p ?? './export_rocket_scientist2_20250811.zip'; return path.resolve(process.cwd(), candidate); } -function resolveDbFile(p?: string): string { - const candidate = p ?? './.data/reddit.db'; +function resolveDbFile(p: string): string { + const candidate = p; return path.resolve(process.cwd(), candidate); } @@ -116,31 +111,14 @@ function toDbUrl(dbFileAbs: string): string { } // ------------------------------------------------------------- -// Import command +// Helpers to work with new core API // ------------------------------------------------------------- -async function cmdImport(args: CLIArgs, adapterID: string) { - const zipPath = resolveZipPath(args.file); - const dbFile = resolveDbFile(args.db); - const dbUrl = toDbUrl(dbFile); - - // Prepare DB and run migrations - await ensureDirExists(dbFile); - const client = createClient({ url: dbUrl }); - const rawDb = drizzle(client); - // Cast libsql drizzle DB to the generic BaseSQLiteDatabase shape expected by Vault - const db = rawDb; - - // Read input once (adapters may ignore if not applicable) - const data = await fs.readFile(zipPath); - const blob = new Blob([new Uint8Array(data)], { type: 'application/zip' }); - - // Build adapter instances, ensuring migrations path is absolute per adapter package - let importer: Importer | undefined; - - // This is just patch code, don't look too closely! - const keys = await fs.readdir( - path.resolve(repoRoot, 'packages/vault-core/src/adapters'), +async function findAdapter(adapterID: string): Promise { + const adaptersDir = path.resolve( + repoRoot, + 'packages/vault-core/src/adapters', ); + const keys = await fs.readdir(adaptersDir); for (const key of keys) { const modulePath = import.meta.resolve( `../../../packages/vault-core/src/adapters/${key}`, @@ -148,74 +126,138 @@ async function cmdImport(args: CLIArgs, adapterID: string) { const mod = (await import(modulePath)) as Record; for (const func of Object.values(mod)) { if (typeof func !== 'function') continue; - const a = func(); + try { + const a = func(); + if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { + return a as Adapter; + } + } catch { + // ignore factory functions that require params or throw + } + } + } + throw new Error(`Could not find adapter for key ${adapterID}`); +} + +async function writeFilesToRepo( + repoDir: string, + files: Map, +): Promise { + let count = 0; + for (const [relPath, file] of files) { + const absPath = path.resolve(repoDir, relPath); + await ensureDirExists(absPath); + const text = await file.text(); + await fs.writeFile(absPath, text, 'utf8'); + count++; + } + return count; +} - // TODO - if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { - importer = a as Importer; +async function collectFilesFromRepo( + repoDir: string, +): Promise> { + const root = path.resolve(repoDir, 'vault'); + const out = new Map(); + + async function walk(dir: string) { + let entries: Array; + try { + entries = await fs.readdir(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(full); + } else if (entry.isFile()) { + const relFromRepo = path + .relative(repoDir, full) + .split(path.sep) + .join('/'); + const text = await fs.readFile(full, 'utf8'); + const f = new File([text], entry.name, { type: 'text/plain' }); + out.set(relFromRepo, f); } } } - if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); + await walk(root); + return out; +} + +// ------------------------------------------------------------- +// Import command (ZIP ingest via adapter ingestor) +// ------------------------------------------------------------- +async function cmdImport(args: CLIArgs, adapterID: string) { + const { file, db } = args; + if (!file) throw new Error('--file is required for import command'); + if (!db) throw new Error('--db is required for import command'); + + const zipPath = resolveZipPath(file); + const dbFile = resolveDbFile(db); + const dbUrl = toDbUrl(dbFile); - // Initialize VaultService (runs migrations implicitly) - const service = await VaultService.create({ - importers: [importer], - database: db, - migrateFunc: migrate, + // Prepare DB + await ensureDirExists(dbFile); + const client = createClient({ url: dbUrl }); + const rawDb = drizzle(client); + + // Read ZIP and wrap in File for bun runtime + const data = await fs.readFile(zipPath); + const blob = new Blob([new Uint8Array(data)], { type: 'application/zip' }); + const zipFile = new File([blob], path.basename(zipPath), { + type: 'application/zip', + }); + + // Resolve adapter and create vault + const adapter = await findAdapter(adapterID); + const vault = createVault({ + adapters: [adapter], + // @ts-expect-error works but slight type mismatch + database: rawDb, }); - const res = await service.importBlob(blob, adapterID); - const counts = countRecords(res.parsed); - console.log(`\n=== Adapter: ${res.importer} ===`); - printCounts(counts); - console.log(`\nImport complete. DB path: ${dbFile}`); + // Ingest data through adapter's ingestor + await vault.ingestData({ adapter, file: zipFile }); + + console.log( + `\nIngest complete for adapter '${adapterID}'. DB path: ${dbFile}`, + ); } // ------------------------------------------------------------- // Export DB -> Files (Markdown only) // ------------------------------------------------------------- async function cmdExportFs(args: CLIArgs, adapterID: string) { - const dbFile = resolveDbFile(args.db); + const { db } = args; + if (!db) throw new Error('--db is required for export-fs command'); + + const dbFile = resolveDbFile(db); const dbUrl = toDbUrl(dbFile); const repoDir = resolveRepoDir(args.repo); await ensureDirExists(dbFile); const client = createClient({ url: dbUrl }); const rawDb = drizzle(client); - const db = rawDb; - let importer: Importer | undefined; - const keys = await fs.readdir( - path.resolve(repoRoot, 'packages/vault-core/src/adapters'), - ); - for (const key of keys) { - const modulePath = import.meta.resolve( - `../../../packages/vault-core/src/adapters/${key}`, - ); - const mod = (await import(modulePath)) as Record; - for (const func of Object.values(mod)) { - if (typeof func !== 'function') continue; - const a = func(); - if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { - importer = a as Importer; - } - } - } - if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); + // Resolve adapter and create vault + const adapter = await findAdapter(adapterID); + const vault = createVault({ + adapters: [adapter], + // @ts-expect-error works but slight type mismatch + database: rawDb, + }); - const service = await VaultService.create({ - importers: [importer], - database: db, - migrateFunc: migrate, + // Export files as Map using markdown codec and default conventions + const files = await vault.exportData({ + adapterIDs: [adapterID], codec: markdownFormat, conventions: defaultConvention(), }); - const store = new LocalFileStore(repoDir); - const result = await service.export(adapterID, store); - const n = Object.keys(result.files).length; + const n = await writeFilesToRepo(repoDir, files); console.log(`Exported ${n} files to ${repoDir}/vault/${adapterID}`); } @@ -223,88 +265,37 @@ async function cmdExportFs(args: CLIArgs, adapterID: string) { // Import Files -> DB (Markdown only) // ------------------------------------------------------------- async function cmdImportFs(args: CLIArgs, adapterID: string) { - const dbFile = resolveDbFile(args.db); + const { db } = args; + if (!db) throw new Error('--db is required for import-fs command'); + + const dbFile = resolveDbFile(db); const dbUrl = toDbUrl(dbFile); const repoDir = resolveRepoDir(args.repo); await ensureDirExists(dbFile); const client = createClient({ url: dbUrl }); const rawDb = drizzle(client); - const db = rawDb; - let importer: Importer | undefined; - const keys = await fs.readdir( - path.resolve(repoRoot, 'packages/vault-core/src/adapters'), - ); - for (const key of keys) { - const modulePath = import.meta.resolve( - `../../../packages/vault-core/src/adapters/${key}`, - ); - const mod = (await import(modulePath)) as Record; - for (const func of Object.values(mod)) { - if (typeof func !== 'function') continue; - const a = func(); - if (a && typeof a === 'object' && 'id' in a && a.id === adapterID) { - importer = a as Importer; - } - } - } - if (!importer) throw new Error(`Could not find adapter for key ${adapterID}`); + // Resolve adapter and create vault + const adapter = await findAdapter(adapterID); + const vault = createVault({ + adapters: [adapter], + // @ts-expect-error works but slight type mismatch + database: rawDb, + }); - const service = await VaultService.create({ - importers: [importer], - database: db, - migrateFunc: migrate, + // Read files under repoDir/vault and import via markdown codec + const files = await collectFilesFromRepo(repoDir); + await vault.importData({ + files, codec: markdownFormat, - conventions: defaultConvention(), }); - const store = new LocalFileStore(repoDir); - await service.import(adapterID, store); console.log( `Imported files from ${repoDir}/vault/${adapterID} into DB ${dbFile}`, ); } -function printCounts(parsedOrCounts: Record) { - const entries: [string, number][] = Object.entries(parsedOrCounts).map( - ([k, v]) => [ - k, - typeof v === 'number' ? v : Array.isArray(v) ? v.length : 0, - ], - ); - const maxKey = Math.max(...entries.map(([k]) => k.length), 10); - for (const [k, n] of entries.sort((a, b) => a[0].localeCompare(b[0]))) { - console.log(`${k.padEnd(maxKey, ' ')} : ${n}`); - } -} - -function countRecords(parsed: unknown): Record { - const out: Record = {}; - if (parsed && typeof parsed === 'object') { - for (const [k, v] of Object.entries(parsed as Record)) { - out[k] = Array.isArray(v) ? v.length : 0; - } - } - return out; -} - -// ------------------------------------------------------------- -// Serve command (stub) -// ------------------------------------------------------------- -async function cmdServe(args: CLIArgs) { - const dbFile = resolveDbFile(args.db); - const dbUrl = toDbUrl(dbFile); - - console.log('Serve is not implemented in this minimal demo.'); - console.log( - 'Intended behavior: start an MCP server sourced by the adapter and DB.', - ); - console.log(`DB path: ${dbFile}`); - console.log(`DB URL: ${dbUrl}`); - console.log('Exiting.'); -} - // ------------------------------------------------------------- // Entrypoint // ------------------------------------------------------------- @@ -339,9 +330,6 @@ async function main() { await cmdImportFs(args, adapter); } break; - case 'serve': - await cmdServe(args); - break; default: console.error(`Unknown command: ${command}`); printHelp(); From 4d9c3af59070407867aa94d67e0a98287cb2c0e0 Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Wed, 29 Oct 2025 15:18:55 +0000 Subject: [PATCH 19/21] fix: missing/transient deps --- apps/demo-mcp/package.json | 4 +- apps/vault-demo/package.json | 17 ++++----- bun.lock | 65 +++++++++++++++++++++++++++----- packages/vault-core/package.json | 14 ++++--- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/apps/demo-mcp/package.json b/apps/demo-mcp/package.json index 4fe919408d..d061a00377 100644 --- a/apps/demo-mcp/package.json +++ b/apps/demo-mcp/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@libsql/client": "^0.11.0", - "drizzle-orm": "catalog:", - "@repo/vault-core": "workspace:*" + "@repo/vault-core": "workspace:*", + "drizzle-orm": "catalog:" } } diff --git a/apps/vault-demo/package.json b/apps/vault-demo/package.json index b52a811263..d7008aacf5 100644 --- a/apps/vault-demo/package.json +++ b/apps/vault-demo/package.json @@ -11,17 +11,16 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" }, "devDependencies": { + "@repo/vault-core": "workspace:*", "@sveltejs/adapter-auto": "^6.1.0", - "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "catalog:", + "arktype": "catalog:", "bun-types": "^1.3.0", "drizzle-orm": "catalog:", - "svelte": "^5.39.5", - "svelte-check": "^4.3.2", - "typescript": "^5.9.2", - "vite": "^7.1.7" - }, - "dependencies": { - "arktype": "catalog:" + "svelte": "catalog:", + "svelte-check": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" } } diff --git a/bun.lock b/bun.lock index d85cc3c2bf..5966441981 100644 --- a/bun.lock +++ b/bun.lock @@ -142,6 +142,23 @@ "wrangler": "^4.25.0", }, }, + "apps/vault-demo": { + "name": "vault-demo", + "version": "0.0.1", + "devDependencies": { + "@repo/vault-core": "workspace:*", + "@sveltejs/adapter-auto": "^6.1.0", + "@sveltejs/kit": "catalog:", + "@sveltejs/vite-plugin-svelte": "catalog:", + "arktype": "catalog:", + "bun-types": "^1.3.0", + "drizzle-orm": "catalog:", + "svelte": "catalog:", + "svelte-check": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + }, + }, "apps/whispering": { "name": "@repo/whispering", "version": "7.5.5", @@ -313,15 +330,17 @@ "name": "@repo/vault-core", "version": "0.0.0", "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@vanillaes/csv": "^3.0.4", - "drizzle-kit": "catalog:", - "drizzle-orm": "catalog:", + "arktype": "^2.1.25:", + "drizzle-arktype": "catalog:", + "drizzle-orm": "^0.44.7", "fflate": "^0.8.2", "toml": "^3.0.0", "yaml": "^2.8.1", }, "devDependencies": { + "@standard-schema/spec": "^1.0.0", + "bun-types": "^1.3.0", + "drizzle-kit": "catalog:", "tsx": "^4.20.6", "typescript": "catalog:", }, @@ -387,9 +406,9 @@ "@ark/regex": ["@ark/regex@0.0.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg=="], - "@ark/schema": ["@ark/schema@0.50.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ=="], + "@ark/schema": ["@ark/schema@0.53.0", "", { "dependencies": { "@ark/util": "0.53.0" } }, "sha512-1PB7RThUiTlmIu8jbSurPrhHpVixPd4C+xNBUF/HrjIENCeDcAMg36n5mpMzED7OQGDVIzpfXXiMnaTiutjHJw=="], - "@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], + "@ark/util": ["@ark/util@0.53.0", "", {}, "sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.0.5", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.1" } }, "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ=="], @@ -919,6 +938,8 @@ "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.6", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ=="], + "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@6.1.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-cBNt4jgH4KuaNO5gRSB2CZKkGtz+OCZ8lPjRQGjhvVUD4akotnj2weUia6imLl2v07K3IgsQRyM36909miSwoQ=="], + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], "@sveltejs/kit": ["@sveltejs/kit@2.47.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-mbUomaJTiADTrq6GT4ZvQ7v1rs0S+wXGMzrjFwjARAKMEF8FpOUmz2uEJ4M9WMJMQOXCMHpKFzJfdjo9O7M22A=="], @@ -1051,6 +1072,8 @@ "@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="], + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1079,8 +1102,6 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@vanillaes/csv": ["@vanillaes/csv@3.0.4", "", {}, "sha512-cMJ/pAljVGpsHvqgd5N4EpNJOvMjFubg7x+9ehjVgQUFi2h+u4Nc4O4C0ErNLV53rO3rI0W1JnKX3wQz0pWgIA=="], - "@volar/kit": ["@volar/kit@2.4.23", "", { "dependencies": { "@volar/language-service": "2.4.23", "@volar/typescript": "2.4.23", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-YuUIzo9zwC2IkN7FStIcVl1YS9w5vkSFEZfPvnu0IbIMaR9WHhc9ZxvlT+91vrcSoRY469H2jwbrGqpG7m1KaQ=="], "@volar/language-core": ["@volar/language-core@2.4.23", "", { "dependencies": { "@volar/source-map": "2.4.23" } }, "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ=="], @@ -1135,7 +1156,9 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "arktype": ["arktype@2.1.23", "", { "dependencies": { "@ark/regex": "0.0.0", "@ark/schema": "0.50.0", "@ark/util": "0.50.0" } }, "sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ=="], + "arkregex": ["arkregex@0.0.2", "", { "dependencies": { "@ark/util": "0.53.0" } }, "sha512-ttjDUICBVoXD/m8bf7eOjx8XMR6yIT2FmmW9vsN0FCcFOygEZvvIX8zK98tTdXkzi0LkRi5CmadB44jFEIyDNA=="], + + "arktype": ["arktype@2.1.25", "", { "dependencies": { "@ark/schema": "0.53.0", "@ark/util": "0.53.0", "arkregex": "0.0.2" } }, "sha512-fdj10sNlUPeDRg1QUqMbzJ4Q7gutTOWOpLUNdcC4vxeVrN0G+cbDOvLbuxQOFj/NDAode1G7kwFv4yKwQvupJg=="], "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], @@ -1195,6 +1218,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1293,6 +1318,8 @@ "cssstyle": ["cssstyle@5.3.1", "", { "dependencies": { "@asamuzakjp/css-color": "^4.0.3", "@csstools/css-syntax-patches-for-csstree": "^1.0.14", "css-tree": "^3.1.0" } }, "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], @@ -1361,7 +1388,7 @@ "drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="], - "drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="], + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], @@ -2433,6 +2460,8 @@ "vaul-svelte": ["vaul-svelte@1.0.0-next.7", "", { "dependencies": { "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-7zN7Bi3dFQixvvbUJY9uGDe7Ws/dGZeBQR2pXdXmzQiakjrxBvWo0QrmsX3HK+VH+SZOltz378cmgmCS9f9rSg=="], + "vault-demo": ["vault-demo@workspace:apps/vault-demo"], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], @@ -2569,6 +2598,8 @@ "@aptabase/tauri/@tauri-apps/api": ["@tauri-apps/api@1.6.0", "", {}, "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg=="], + "@ark/regex/@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], "@asamuzakjp/dom-selector/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], @@ -2581,6 +2612,10 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@epicenter/cli/arktype": ["arktype@2.1.23", "", { "dependencies": { "@ark/regex": "0.0.0", "@ark/schema": "0.50.0", "@ark/util": "0.50.0" } }, "sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ=="], + + "@epicenter/demo-mcp/drizzle-orm": ["drizzle-orm@0.44.6", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="], + "@epicenter/opencode/hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], "@epicenter/opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], @@ -2641,6 +2676,8 @@ "@repo/constants/@types/node": ["@types/node@20.19.22", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ=="], + "@repo/shared/arktype": ["arktype@2.1.23", "", { "dependencies": { "@ark/regex": "0.0.0", "@ark/schema": "0.50.0", "@ark/util": "0.50.0" } }, "sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ=="], + "@repo/svelte-utils/svelte": ["svelte@5.14.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "acorn-typescript": "^1.4.13", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "esm-env": "^1.2.1", "esrap": "^1.3.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2iR/UHHA2Dsldo4JdXDcdqT+spueuh+uNYw1FoTKBbpnFEECVISeqSo0uubPS4AfBE0xI6u7DGHxcdq3DTDmoQ=="], "@repo/ui/@lucide/svelte": ["@lucide/svelte@0.525.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-dyUxkXzepagLUzL8jHQNdeH286nC66ClLACsg+Neu/bjkRJWPWMzkT+H0DKlE70QdkicGCfs1ZGmXCc351hmZA=="], @@ -2815,6 +2852,10 @@ "@astrojs/svelte/@sveltejs/vite-plugin-svelte/@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + "@epicenter/cli/arktype/@ark/schema": ["@ark/schema@0.50.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ=="], + + "@epicenter/cli/arktype/@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], + "@epicenter/opencode/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "@epicenter/opencode/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -2911,6 +2952,10 @@ "@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.1", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q=="], + "@repo/shared/arktype/@ark/schema": ["@ark/schema@0.50.0", "", { "dependencies": { "@ark/util": "0.50.0" } }, "sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ=="], + + "@repo/shared/arktype/@ark/util": ["@ark/util@0.50.0", "", {}, "sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg=="], + "@repo/svelte-utils/svelte/esrap": ["esrap@1.4.9", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/packages/vault-core/package.json b/packages/vault-core/package.json index c2b8c661c5..dca94455ce 100644 --- a/packages/vault-core/package.json +++ b/packages/vault-core/package.json @@ -9,17 +9,19 @@ "./adapters/*": "./src/adapters/*/index.ts", "./utils/*": "./src/utils/*/index.ts" }, - "devDependencies": { - "tsx": "^4.20.6", - "typescript": "catalog:" - }, "scripts": { "check": "tsc --noEmit" }, - "dependencies": { + "devDependencies": { "@standard-schema/spec": "^1.0.0", - "@vanillaes/csv": "^3.0.4", + "bun-types": "^1.3.0", "drizzle-kit": "catalog:", + "tsx": "^4.20.6", + "typescript": "catalog:" + }, + "dependencies": { + "arktype": "catalog:", + "drizzle-arktype": "catalog:", "drizzle-orm": "catalog:", "fflate": "^0.8.2", "toml": "^3.0.0", From e15d7214837befc869547c95324debb1d39852ed Mon Sep 17 00:00:00 2001 From: Cole Crouter Date: Wed, 29 Oct 2025 16:23:23 +0000 Subject: [PATCH 20/21] fix: vault demo readme instructions Co-Authored-By: Leftium --- apps/vault-demo/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/vault-demo/README.md b/apps/vault-demo/README.md index 5a2518ea5e..12e018eb10 100644 --- a/apps/vault-demo/README.md +++ b/apps/vault-demo/README.md @@ -14,10 +14,10 @@ Quick start (Bun) - Prerequisite: Bun installed. - From repo root: - ``` - bun install - bun run dev -w apps/vault-demo - ``` +```sh +bun install +bun run dev --filter=vault-demo +``` - Open http://localhost:5173 - Note: The demo uses an in-memory DB with a vault singleton at [apps/vault-demo/src/lib/vault/singleton.ts](apps/vault-demo/src/lib/vault/singleton.ts) so data persists across routes during a single browser session. From 69828e9196b98245d00eb94fb3af368dfc51a82e Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:29:08 -0800 Subject: [PATCH 21/21] fix: resolve merge conflicts and update vault-core imports Resolved merge conflicts from merging main into new-adapter-architecture branch. Changes: - Fixed @repo/shared import to use .js extension for ESM compatibility - Updated reddit adapter index.ts to export from ./adapter - Accepted main's changes for config.ts (underscore prefix on unused var) - Kept HEAD's adapter.ts type definitions, removed test code - Removed deleted upsert.ts file - Formatted adapter.ts and db.ts with prettier The API package has pre-existing drizzle-orm version issues that are unrelated to this PR. --- packages/shared/src/index.ts | 2 +- .../src/adapters/reddit/src/index.ts | 39 +------------------ packages/vault-core/src/core/adapter.ts | 20 +++++----- packages/vault-core/src/core/db.ts | 5 +-- 4 files changed, 14 insertions(+), 52 deletions(-) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 4079da7b56..a9c4c30766 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1 +1 @@ -export { safeLookup } from './object'; +export { safeLookup } from './object.js'; diff --git a/packages/vault-core/src/adapters/reddit/src/index.ts b/packages/vault-core/src/adapters/reddit/src/index.ts index cb7df709dc..8d3edf26a3 100644 --- a/packages/vault-core/src/adapters/reddit/src/index.ts +++ b/packages/vault-core/src/adapters/reddit/src/index.ts @@ -1,38 +1 @@ -import { defineAdapter } from '@repo/vault-core'; -import type { RedditAdapterConfig } from './config'; -import drizzleConfig from './drizzle.config'; -import { metadata } from './metadata'; -import { parseRedditExport } from './parse'; -import * as tables from './schema'; -import { upsertRedditData } from './upsert'; -import { parseSchema } from './validation'; - -// Expose all tables from schema module (runtime values only; TS types are erased) -export const schema = tables; -// ArkType infers array schemas like `[ { ... } ]` as a tuple type with one element. -// Convert any such tuple properties into standard `T[]` arrays for our parser/upsert. -type Arrayify = T extends readonly [infer E] ? E[] : T; -type Inferred = (typeof parseSchema)['infer']; -export type ParsedRedditExport = { - [K in keyof Inferred]: Arrayify; -}; -// Back-compat for consumers still importing ParseResult from this module -export type ParseResult = ParsedRedditExport; - -// Adapter export -export const redditAdapter = defineAdapter((args: RedditAdapterConfig) => { - args; // TODO - - const adapter = { - id: 'reddit', - name: 'Reddit Adapter', - schema, - metadata, - validator: parseSchema, - drizzleConfig, - parse: parseRedditExport, - upsert: upsertRedditData, - }; - - return adapter; -}); +export { redditAdapter } from './adapter'; diff --git a/packages/vault-core/src/core/adapter.ts b/packages/vault-core/src/core/adapter.ts index 9b63fe6e03..471a8b1c45 100644 --- a/packages/vault-core/src/core/adapter.ts +++ b/packages/vault-core/src/core/adapter.ts @@ -94,13 +94,12 @@ type ApplyDefaultableColumns< >; // Allow server generated columns (defaults, runtime defaults, autoincrement IDs) to be omitted in validator payloads. -type TableRowShape = NullToUndefined< - InferSelectModel -> extends infer Row - ? Row extends Record - ? ApplyDefaultableColumns - : Row - : never; +type TableRowShape = + NullToUndefined> extends infer Row + ? Row extends Record + ? ApplyDefaultableColumns + : Row + : never; /** * Map a prefixed schema record to an object whose keys are the table names with the @@ -289,9 +288,10 @@ type MissingPrimaryKeyTables> = { type EnsureAllTablesArePrefixedWith< TID extends string, TSchema extends Record, -> = Exclude, `${TID}_${string}`> extends never - ? TSchema - : never; +> = + Exclude, `${TID}_${string}`> extends never + ? TSchema + : never; type SchemaTableNames> = Extract< keyof TSchema, string diff --git a/packages/vault-core/src/core/db.ts b/packages/vault-core/src/core/db.ts index 0f8a01cd61..68522b21bd 100644 --- a/packages/vault-core/src/core/db.ts +++ b/packages/vault-core/src/core/db.ts @@ -3,9 +3,8 @@ import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'; // Shared types -type ExtractedResult = T extends BaseSQLiteDatabase<'async', infer R> - ? R - : never; +type ExtractedResult = + T extends BaseSQLiteDatabase<'async', infer R> ? R : never; type ResultSet = ExtractedResult;