Compare commits

...

3 Commits

Author SHA1 Message Date
124705ca84 docs(agents): add IDE diagnostics requirement before linting
Add a new section to AGENTS.md emphasizing the need to check IDE diagnostics first using the `getIdeDiagnostics` tool, resolve all TypeScript errors and warnings, and only then run the Biome linter to ensure type safety before addressing cosmetic issues.
2025-10-01 11:08:53 +00:00
a23b25c953 chore(config): update configs and bump dependencies
Update Biome linter configuration for cleaner formatting.

Format BTS configuration file for consistency.

Bump versions for tailwindcss and tw-animate-css in lockfile.

Update package.json scripts to use @nontara/server.
2025-10-01 11:06:04 +00:00
78f1855ce4 feat(metadata): add credits and provider metadata support
Simplify people roles to CAST/CREW and integrate external metadata providers (TMDB, IMDB, etc.) for enhanced movie and series information. Updated database schemas, models, and services to handle credits, ratings, and provider IDs, enabling richer metadata resolution and storage.

BREAKING CHANGE: PEOPLE_ROLE_TYPES enum simplified from specific roles to "CAST" and "CREW", requiring updates to any code depending on detailed role types.
2025-10-01 11:04:46 +00:00
14 changed files with 481 additions and 75 deletions

View File

@@ -51,6 +51,13 @@
## Code Quality and Linting
### IDE Diagnostics - REQUIRED FIRST STEP
- **MUST check IDE diagnostics BEFORE running the linter**
- Use `getIdeDiagnostics` tool to check for TypeScript errors and warnings
- Resolve ALL diagnostic problems (type errors, unused imports, etc.) first
- Only after fixing IDE diagnostics should you proceed to run the linter
- This prevents linting cosmetic issues while type errors remain unfixed
### Biome Linter - MANDATORY
- **MUST run Biome linter after creating or editing any file**
- Run linter command: `bun run check` (from project root)

View File

@@ -27,26 +27,16 @@ export const STREAM_CODEC_TYPES = [
] as const;
export type StreamCodecType = (typeof STREAM_CODEC_TYPES)[number];
// people roles (actor, director, writer, etc.)
export const PEOPLE_ROLE_TYPES = [
"ACTOR",
"DIRECTOR",
"WRITER",
"PRODUCER",
"EXECUTIVE_PRODUCER",
"CINEMATOGRAPHER",
"EDITOR",
"COMPOSER",
"SOUND_DESIGNER",
"PRODUCTION_DESIGNER",
"COSTUME_DESIGNER",
"MAKEUP_ARTIST",
"VISUAL_EFFECTS_SUPERVISOR",
"CASTING_DIRECTOR",
"STUNT_COORDINATOR",
"NARRATOR",
"GUEST_STAR",
"CREATOR",
"SHOWRUNNER",
] as const;
// people roles (cast or crew)
export const PEOPLE_ROLE_TYPES = ["CAST", "CREW"] as const;
export type PeopleRoleType = (typeof PEOPLE_ROLE_TYPES)[number];
// metadata provider types (external services for metadata)
export const METADATA_PROVIDER_TYPES = [
"TMDB", // The Movie Database
"IMDB", // Internet Movie Database
"OMDB", // Open Movie Database
"TVDB", // The TV Database
"TRAKT", // Trakt
] as const;
export type MetadataProviderType = (typeof METADATA_PROVIDER_TYPES)[number];

View File

@@ -1,4 +1,10 @@
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import {
index,
integer,
sqliteTable,
text,
uniqueIndex,
} from "drizzle-orm/sqlite-core";
import type { PeopleRoleType } from "../enums";
import { movies } from "./movies";
import { people } from "./people";
@@ -16,7 +22,9 @@ export const credits = sqliteTable(
personId: text("person_id")
.notNull()
.references(() => people.id, { onDelete: "cascade" }),
// Reference to content (movie, series, or episode)
// Only one of these should be set per credit
movieId: text("movie_id").references(() => movies.id, {
onDelete: "cascade",
}),
@@ -26,22 +34,60 @@ export const credits = sqliteTable(
episodeId: text("episode_id").references(() => episodes.id, {
onDelete: "cascade",
}),
// Role information
role: text("role").notNull().$type<PeopleRoleType>(),
character: text("character"), // For actors - character name they play
job: text("job"), // For crew - specific job title (e.g., "Director of Photography")
department: text("department"), // e.g., "Acting", "Directing", "Writing", "Production"
order: integer("order"), // For billing order/credits order
// Role type: CAST or CREW
role: text("role").notNull().$type<PeopleRoleType>(), // "CAST" | "CREW"
// CAST-specific fields
character: text("character"), // Character name for cast members
// CREW-specific fields
job: text("job"), // Specific job title (e.g., "Director", "Producer", "Director of Photography")
department: text("department"), // Department (e.g., "Directing", "Writing", "Production", "Camera")
// Display order
order: integer("order"), // Billing/credits display order (lower numbers appear first)
// Timestamps
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.$onUpdate(() => new Date())
.notNull(),
},
(t) => [
// Indexes for efficient lookups
index("idx_credits_person").on(t.personId),
index("idx_credits_movie").on(t.movieId),
index("idx_credits_series").on(t.seriesId),
index("idx_credits_episode").on(t.episodeId),
index("idx_credits_role").on(t.role),
index("idx_credits_department").on(t.department),
index("idx_credits_order").on(t.order),
// Unique constraints to prevent duplicate credits
uniqueIndex("ux_credits_movie_person_role").on(
t.movieId,
t.personId,
t.role,
t.character,
t.job,
),
uniqueIndex("ux_credits_series_person_role").on(
t.seriesId,
t.personId,
t.role,
t.character,
t.job,
),
uniqueIndex("ux_credits_episode_person_role").on(
t.episodeId,
t.personId,
t.role,
t.character,
t.job,
),
],
);

View File

@@ -2,9 +2,11 @@ import {
index,
integer,
primaryKey,
real,
sqliteTable,
text,
} from "drizzle-orm/sqlite-core";
import type { LibraryType, MetadataProviderType } from "../enums";
import { libraryFolders } from "../libraries";
import { genres } from "./genres";
@@ -20,12 +22,33 @@ export const titles = sqliteTable(
libraryFolderId: integer("library_folder_id")
.notNull()
.references(() => libraryFolders.id, { onDelete: "cascade" }),
type: text("type").notNull().$type<LibraryType>(), // "MOVIE" | "SERIES"
// Basic information
title: text("title").notNull(),
originalTitle: text("original_title"),
tagline: text("tagline"),
overview: text("overview"),
// Date information
year: integer("year"),
releaseDate: text("release_date"), // "YYYY-MM-DD"
// Media assets
poster: text("poster"),
backdrop: text("backdrop"),
// Ratings and popularity
voteAverage: real("vote_average"), // e.g., 7.5
voteCount: integer("vote_count"),
popularity: real("popularity"),
// Language
originalLanguage: text("original_language"), // ISO 639-1 code (e.g., "en", "id")
// Additional metadata
homepage: text("homepage"),
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.notNull(),
@@ -36,7 +59,10 @@ export const titles = sqliteTable(
},
(t) => [
index("idx_titles_library").on(t.libraryFolderId),
index("idx_titles_type").on(t.type),
index("idx_titles_title").on(t.title),
index("idx_titles_year").on(t.year),
index("idx_titles_popularity").on(t.popularity),
],
);
@@ -60,3 +86,28 @@ export const titleGenres = sqliteTable(
index("idx_title_genres_genre").on(t.genreId),
],
);
// ----- TITLE_PROVIDERS (junction table for external metadata provider IDs) -----
export const titleProviders = sqliteTable(
"title_providers",
{
titleId: text("title_id")
.notNull()
.references(() => titles.id, { onDelete: "cascade" }),
provider: text("provider").notNull().$type<MetadataProviderType>(), // "TMDB" | "IMDB" | "OMDB" | etc.
providerId: text("provider_id").notNull(), // The ID from the provider (e.g., "tt1234567" for IMDB, "550" for TMDB)
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.$onUpdate(() => new Date())
.notNull(),
},
(t) => [
primaryKey({ columns: [t.titleId, t.provider] }),
index("idx_title_providers_title").on(t.titleId),
index("idx_title_providers_provider").on(t.provider),
index("idx_title_providers_provider_id").on(t.provider, t.providerId),
],
);

View File

@@ -58,3 +58,19 @@ export const ResolvedCreditSchema = z.object({
});
export type ResolvedCredit = z.infer<typeof ResolvedCreditSchema>;
// Info for fetching credits by title
export const TitleCreditsInfoSchema = z.object({
titleType: z.enum(["movie", "series"]),
providerId: z.string(),
});
export type TitleCreditsInfo = z.infer<typeof TitleCreditsInfoSchema>;
// Resolved credits for a title (cast + crew)
export const ResolvedTitleCreditsSchema = z.object({
cast: z.array(CastSchema),
crew: z.array(CrewSchema),
});
export type ResolvedTitleCredits = z.infer<typeof ResolvedTitleCreditsSchema>;

View File

@@ -1,21 +1,28 @@
import { z } from "zod";
// CAST member (actor)
export const Casting = z.object({
id: z.string().optional(),
name: z.string(),
character: z.string(),
profileUrl: z.string(),
order: z.number().optional(), // Billing order
profileUrl: z.string().optional(),
});
export type Casting = z.infer<typeof Casting>;
// CREW member (director, writer, producer, etc.)
export const Crew = z.object({
id: z.string().optional(),
name: z.string(),
role: z.string(),
profileUrl: z.string(),
job: z.string(), // e.g., "Director", "Producer", "Writer"
department: z.string().optional(), // e.g., "Directing", "Production", "Writing"
profileUrl: z.string().optional(),
});
export type Crew = z.infer<typeof Crew>;
// Combined credits
export const Credits = z.object({
cast: Casting.array(),
crew: Crew.array(),

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { METADATA_PROVIDER_TYPES } from "@/db/schema/enums";
import { MetadataLanguage } from "./language";
export const MovieInfo = z.object({
@@ -34,19 +35,33 @@ export const SeriesInfo = z.object({
});
export type SeriesInfo = z.infer<typeof SeriesInfo>;
// Provider ID schema matching titleProviders table
export const ProviderId = z.object({
provider: z.enum(METADATA_PROVIDER_TYPES), // "TMDB" | "IMDB" | "OMDB" | "TVDB" | "TRAKT"
providerId: z.string(), // The ID from the provider
});
export type ProviderId = z.infer<typeof ProviderId>;
export const ResolvedMovieMetadata = z.object({
title: z.string(),
year: z.number(),
description: z.string(),
originalTitle: z.string(),
tagline: z.string().optional(),
releaseDate: z.string(),
runtimeMinutes: z.number(),
genres: z.string().array(),
posterUrl: z.string(),
backdropUrl: z.string(),
rating: z.number(),
voteAverage: z.number().optional(),
voteCount: z.number().optional(),
popularity: z.number().optional(),
language: z.string(),
originalLanguage: z.string().optional(),
countries: z.string().array(),
homepage: z.string().optional(),
providerIds: ProviderId.array().optional(), // Array of external provider IDs
});
export type ResolvedMovieMetadata = z.infer<typeof ResolvedMovieMetadata>;
@@ -54,14 +69,22 @@ export const ResolvedSeriesMetadata = z.object({
title: z.string(),
originalTitle: z.string(),
description: z.string(),
tagline: z.string().optional(),
year: z.number().optional(),
status: z.string().optional(), // "RETURNING" | "ENDED" | ...
genres: z.string().array(),
posterUrl: z.string().optional(),
backdropUrl: z.string().optional(),
rating: z.number().optional(),
voteAverage: z.number().optional(),
voteCount: z.number().optional(),
popularity: z.number().optional(),
language: z.string(),
originalLanguage: z.string().optional(),
countries: z.string().array(),
homepage: z.string().optional(),
firstAirDate: z.string().optional(), // Series first air date
providerIds: ProviderId.array().optional(), // Array of external provider IDs
// Season and episode objects
season: z.object({
number: z.number(),
@@ -72,7 +95,7 @@ export const ResolvedSeriesMetadata = z.object({
title: z.string().optional(),
description: z.string().optional(),
}),
airDate: z.string().optional(),
airDate: z.string().optional(), // Episode air date
runtimeMinutes: z.number().optional(),
});
export type ResolvedSeriesMetadata = z.infer<typeof ResolvedSeriesMetadata>;

View File

@@ -9,7 +9,9 @@ import type {
ResolvedMovieMetadata,
ResolvedPerson,
ResolvedSeriesMetadata,
ResolvedTitleCredits,
SeriesInfo,
TitleCreditsInfo,
} from "@/models";
import { Capabilites } from "@/services/extensions/baseExtension";
import { createLogger } from "@/utils/logger";
@@ -29,5 +31,8 @@ export abstract class MetadataProvider extends Provider {
info: SeriesInfo,
): Promise<ResolvedSeriesMetadata>;
public abstract resolveCredits(info: CreditInfo): Promise<ResolvedCredit>;
public abstract resolveTitleCredits(
info: TitleCreditsInfo,
): Promise<ResolvedTitleCredits>;
public abstract resolvePeople(info: PersonInfo): Promise<ResolvedPerson>;
}

View File

@@ -6,15 +6,18 @@ import { and, eq } from "drizzle-orm";
import ffmpeg from "fluent-ffmpeg";
import { database } from "@/db";
import {
credits,
episodes,
genres,
libraryFolders,
mediaItems,
mediaStreams,
movies,
people,
seasons,
series,
titleGenres,
titleProviders,
titles,
} from "@/db/schema";
import type {
@@ -25,7 +28,9 @@ import type {
import type {
MetadataLanguage,
MovieInfo,
ResolvedMovieMetadata,
ResolvedSeriesMetadata,
ResolvedTitleCredits,
SeriesInfo,
} from "@/models";
import { MovieNamings } from "@/namings/movies";
@@ -356,6 +361,9 @@ export class LibraryScannerService extends BaseService {
return false;
}
// Variable to store movieId outside transaction scope
let createdMovieId: string | null = null;
// Start a transaction to ensure data consistency
await database.transaction(async (tx) => {
// 1. Insert title into titles table
@@ -363,11 +371,20 @@ export class LibraryScannerService extends BaseService {
.insert(titles)
.values({
libraryFolderId: libraryFolderId,
type: "MOVIE" as const,
title: metadata.title,
originalTitle: metadata.originalTitle || null,
tagline: metadata.tagline || null,
overview: metadata.description,
year: metadata.year,
releaseDate: metadata.releaseDate,
poster: metadata.posterUrl,
backdrop: metadata.backdropUrl || null,
voteAverage: metadata.voteAverage || null,
voteCount: metadata.voteCount || null,
popularity: metadata.popularity || null,
originalLanguage: metadata.originalLanguage || null,
homepage: metadata.homepage || null,
})
.returning({ id: titles.id });
@@ -440,6 +457,23 @@ export class LibraryScannerService extends BaseService {
);
}
// Insert external provider IDs (TMDB, IMDB, etc.)
if (metadata.providerIds && metadata.providerIds.length > 0) {
for (const providerId of metadata.providerIds) {
await tx
.insert(titleProviders)
.values({
titleId: titleId,
provider: providerId.provider,
providerId: providerId.providerId,
})
.onConflictDoNothing();
this.logger.debug(
`${indent} → Linked ${providerId.provider} ID: ${providerId.providerId}`,
);
}
}
// 5. Insert stream information (video, audio, subtitle tracks)
if (mediaInfo.streams && mediaInfo.streams.length > 0) {
const streamsToInsert = mediaInfo.streams.map((stream) => {
@@ -455,8 +489,8 @@ export class LibraryScannerService extends BaseService {
let frameRate: number | null = null;
if (stream.r_frame_rate) {
const [num, den] = stream.r_frame_rate.split("/");
if (num && den && Number.parseInt(den) !== 0) {
frameRate = Number.parseInt(num) / Number.parseInt(den);
if (num && den && Number.parseInt(den, 10) !== 0) {
frameRate = Number.parseInt(num, 10) / Number.parseInt(den, 10);
}
}
@@ -495,11 +529,40 @@ export class LibraryScannerService extends BaseService {
);
}
// Store movieId for use after transaction
createdMovieId = movieId;
this.logger.debug(
`${indent} → Successfully inserted movie ID ${movieId} with media item ID ${mediaItemId}`,
);
});
// 6. Resolve and insert credits (cast and crew) after movie is created
if (createdMovieId) {
try {
this.logger.debug(
`${indent} → Fetching credits from metadata provider...`,
);
const creditsData = await this.resolveMovieCredits(
createdMovieId,
metadata,
);
if (creditsData) {
this.logger.debug(`Credits found for file ${filename}`);
await this.insertCreditsForMovie(
createdMovieId,
creditsData,
indent,
);
} else {
this.logger.warn(`Credits not found on file ${filename}`);
}
} catch (error) {
this.logger.warn(`${indent} ⚠ Failed to resolve credits: ${error}`);
}
}
this.logger.success(
`${indent}[${libraryName}] ✓ Added: "${metadata.title}" (${metadata.year}) - ${info.runtimeMinutes}min`,
);
@@ -701,12 +764,20 @@ export class LibraryScannerService extends BaseService {
.insert(titles)
.values({
libraryFolderId: libraryFolderId,
type: "SERIES" as const,
title: seriesTitle,
originalTitle: resolvedMetadata?.originalTitle || parsed.title,
tagline: resolvedMetadata?.tagline || null,
overview: resolvedMetadata?.description || null,
poster: resolvedMetadata?.posterUrl || null,
year: null,
releaseDate: null,
backdrop: resolvedMetadata?.backdropUrl || null,
voteAverage: resolvedMetadata?.voteAverage || null,
voteCount: resolvedMetadata?.voteCount || null,
popularity: resolvedMetadata?.popularity || null,
originalLanguage: resolvedMetadata?.originalLanguage || null,
homepage: resolvedMetadata?.homepage || null,
year: resolvedMetadata?.year || null,
releaseDate: resolvedMetadata?.firstAirDate || null,
})
.returning({ id: titles.id });
titleId = insertedTitle[0].id;
@@ -810,8 +881,8 @@ export class LibraryScannerService extends BaseService {
let frameRate: number | null = null;
if (stream.r_frame_rate) {
const [num, den] = stream.r_frame_rate.split("/");
if (num && den && Number.parseInt(den) !== 0) {
frameRate = Number.parseInt(num) / Number.parseInt(den);
if (num && den && Number.parseInt(den, 10) !== 0) {
frameRate = Number.parseInt(num, 10) / Number.parseInt(den, 10);
}
}
@@ -903,6 +974,26 @@ export class LibraryScannerService extends BaseService {
`${indent} → Inserted ${genresToInsert.length} genres for series`,
);
// 7. Insert external provider IDs (TMDB, IMDB, etc.) for series
if (
resolvedMetadata?.providerIds &&
resolvedMetadata.providerIds.length > 0
) {
for (const providerId of resolvedMetadata.providerIds) {
await tx
.insert(titleProviders)
.values({
titleId: titleId,
provider: providerId.provider,
providerId: providerId.providerId,
})
.onConflictDoNothing();
this.logger.debug(
`${indent} → Linked ${providerId.provider} ID: ${providerId.providerId}`,
);
}
}
this.logger.debug(
`${indent} → Successfully inserted series "${parsed.title}" S${String(parsed.season).padStart(2, "0")}E${String(parsed.episode).padStart(2, "0")}`,
);
@@ -919,4 +1010,125 @@ export class LibraryScannerService extends BaseService {
return false;
}
}
private async resolveMovieCredits(
_movieId: string,
metadata: ResolvedMovieMetadata,
): Promise<ResolvedTitleCredits | null> {
// Try to get credits from metadata providerIds
if (metadata.providerIds && metadata.providerIds.length > 0) {
// Use the first available provider ID
const provider = metadata.providerIds[0];
try {
const credits = await this.metadataService.resolveTitleCredits({
titleType: "movie",
providerId: provider.providerId,
});
return credits;
} catch (error) {
this.logger.warn(
`Failed to resolve credits using ${provider.provider} ID ${provider.providerId}: ${error}`,
);
return null;
}
}
return null;
}
private async insertCreditsForMovie(
movieId: string,
creditsData: ResolvedTitleCredits,
indent: string,
): Promise<void> {
let castInserted = 0;
let crewInserted = 0;
await database.transaction(async (tx) => {
// Insert cast members
for (const castMember of creditsData.cast) {
// 1. Find or create person
let personId: string;
const existingPerson = await tx
.select({ id: people.id })
.from(people)
.where(eq(people.tmdbId, castMember.id))
.limit(1);
if (existingPerson.length > 0) {
personId = existingPerson[0].id;
} else {
// Create new person
const insertedPerson = await tx
.insert(people)
.values({
name: castMember.name,
tmdbId: castMember.id,
profilePicture: castMember.profilePath,
})
.returning({ id: people.id });
personId = insertedPerson[0].id;
}
// 2. Insert credit
await tx
.insert(credits)
.values({
personId: personId,
movieId: movieId,
role: "CAST" as const,
character: castMember.character,
department: "Acting",
order: castMember.order,
})
.onConflictDoNothing();
castInserted++;
}
// Insert crew members
for (const crewMember of creditsData.crew) {
// 1. Find or create person
let personId: string;
const existingPerson = await tx
.select({ id: people.id })
.from(people)
.where(eq(people.tmdbId, crewMember.id))
.limit(1);
if (existingPerson.length > 0) {
personId = existingPerson[0].id;
} else {
// Create new person
const insertedPerson = await tx
.insert(people)
.values({
name: crewMember.name,
tmdbId: crewMember.id,
profilePicture: crewMember.profilePath,
})
.returning({ id: people.id });
personId = insertedPerson[0].id;
}
// 2. Insert credit
await tx
.insert(credits)
.values({
personId: personId,
movieId: movieId,
role: "CREW" as const,
job: crewMember.job,
department: crewMember.department,
order: null,
})
.onConflictDoNothing();
crewInserted++;
}
});
this.logger.debug(
`${indent} → Inserted ${castInserted} cast and ${crewInserted} crew members`,
);
}
}

View File

@@ -1,8 +1,12 @@
import type {
MovieInfo,
PersonInfo,
ResolvedMovieMetadata,
ResolvedPerson,
ResolvedSeriesMetadata,
ResolvedTitleCredits,
SeriesInfo,
TitleCreditsInfo,
} from "@/models";
import type { MetadataProvider } from "@/providers/metadata";
import { BaseService, ServiceError } from "../BaseService";
@@ -82,6 +86,60 @@ export class MetadataService extends BaseService {
return null;
}
public async resolveCredits(
movieId: number,
seriesId?: number,
): Promise<null> {
for (const provider of this.providers) {
try {
// For now, we'll fetch credits by movie/series ID
// Providers should implement this based on their API
await provider.resolveCredits({
creditId: String(movieId || seriesId),
});
// Transform single credit to Credits structure if needed
// This is a placeholder - actual implementation depends on provider
return null;
} catch (error) {
this.logger.warn(
`Provider ${provider.name} failed to resolve credits.`,
error,
);
}
}
return null;
}
public async resolveTitleCredits(
info: TitleCreditsInfo,
): Promise<ResolvedTitleCredits | null> {
for (const provider of this.providers) {
try {
return await provider.resolveTitleCredits(info);
} catch (error) {
this.logger.warn(
`Provider ${provider.name} failed to resolve title credits.`,
error,
);
}
}
return null;
}
public async resolvePerson(info: PersonInfo): Promise<ResolvedPerson | null> {
for (const provider of this.providers) {
try {
return await provider.resolvePeople(info);
} catch (error) {
this.logger.warn(
`Provider ${provider.name} failed to resolve person details.`,
error,
);
}
}
return null;
}
private refreshProviders(): void {
this.providers = this.extensions.getByCapability<MetadataProvider>(
Capabilites.MetadataProvider,

View File

@@ -44,11 +44,7 @@
"level": "warn",
"fix": "safe",
"options": {
"functions": [
"clsx",
"cva",
"cn"
]
"functions": ["clsx", "cva", "cn"]
}
}
},
@@ -71,4 +67,4 @@
"quoteStyle": "double"
}
}
}
}

View File

@@ -2,25 +2,20 @@
// safe to delete
{
"$schema": "https://r2.better-t-stack.dev/schema.json",
"version": "2.46.0",
"createdAt": "2025-09-16T19:14:46.247Z",
"database": "sqlite",
"orm": "drizzle",
"backend": "hono",
"runtime": "bun",
"frontend": [
"tanstack-start"
],
"addons": [
"biome",
"turborepo"
],
"examples": [],
"auth": "better-auth",
"packageManager": "bun",
"dbSetup": "none",
"api": "trpc",
"webDeploy": "none",
"serverDeploy": "none"
}
"$schema": "https://r2.better-t-stack.dev/schema.json",
"version": "2.46.0",
"createdAt": "2025-09-16T19:14:46.247Z",
"database": "sqlite",
"orm": "drizzle",
"backend": "hono",
"runtime": "bun",
"frontend": ["tanstack-start"],
"addons": ["biome", "turborepo"],
"examples": [],
"auth": "better-auth",
"packageManager": "bun",
"dbSetup": "none",
"api": "trpc",
"webDeploy": "none",
"serverDeploy": "none"
}

View File

@@ -67,8 +67,8 @@
"react-dom": "19.1.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.3",
"tw-animate-css": "^1.2.5",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^5.1.4",
"zod": "^4.0.2",
},
@@ -1830,7 +1830,7 @@
"turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="],
"tw-animate-css": ["tw-animate-css@1.3.8", "", {}, "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw=="],
"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=="],

View File

@@ -14,11 +14,11 @@
"check-types": "turbo check-types",
"dev:native": "turbo -F native dev",
"dev:web": "turbo -F web dev",
"dev:server": "turbo -F server dev",
"db:push": "turbo -F server db:push",
"db:studio": "turbo -F server db:studio",
"db:generate": "turbo -F server db:generate",
"db:migrate": "turbo -F server db:migrate"
"dev:server": "turbo -F @nontara/server dev",
"db:push": "turbo -F @nontara/server db:push",
"db:studio": "turbo -F @nontara/server db:studio",
"db:generate": "turbo -F @nontara/server db:generate",
"db:migrate": "turbo -F @nontara/server db:migrate"
},
"dependencies": {},
"devDependencies": {