Initial release v1.0.0

Open-source marketplace for AI Agent skills.

Features:
- Next.js 15 web app with i18n (en/fa)
- CLI tool for skill installation (npx skillhub)
- GitHub crawler/indexer with multi-strategy discovery
- Security scanning for all indexed skills
- Self-hostable with Docker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-12 06:08:51 +03:30
commit 97b427831a
227 changed files with 48411 additions and 0 deletions

472
packages/db/src/schema.ts Normal file
View File

@@ -0,0 +1,472 @@
import {
pgTable,
text,
timestamp,
integer,
jsonb,
boolean,
index,
primaryKey,
uniqueIndex,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
/**
* Skills table - main entity storing indexed skills
*/
export const skills = pgTable(
'skills',
{
// Primary key: owner/repo/skill-name
id: text('id').primaryKey(),
name: text('name').notNull(),
description: text('description').notNull(),
// Source information
githubOwner: text('github_owner').notNull(),
githubRepo: text('github_repo').notNull(),
skillPath: text('skill_path').notNull(),
branch: text('branch').default('main'),
commitSha: text('commit_sha'),
// Source format (which platform's instruction file format)
sourceFormat: text('source_format').default('skill.md'),
// Metadata
version: text('version'),
license: text('license'),
author: text('author'),
homepage: text('homepage'),
compatibility: jsonb('compatibility').$type<{
platforms?: string[];
requires?: string[];
minVersion?: string;
}>(),
triggers: jsonb('triggers').$type<{
filePatterns?: string[];
keywords?: string[];
languages?: string[];
}>(),
// Quality signals
githubStars: integer('github_stars').default(0),
githubForks: integer('github_forks').default(0),
downloadCount: integer('download_count').default(0),
viewCount: integer('view_count').default(0),
// Ratings
rating: integer('rating'), // 1-5 average
ratingCount: integer('rating_count').default(0),
ratingSum: integer('rating_sum').default(0),
// Security
securityScore: integer('security_score'), // 0-100 (deprecated, use securityStatus)
securityStatus: text('security_status').$type<'pass' | 'warning' | 'fail'>(), // PASS, WARNING, FAIL
isVerified: boolean('is_verified').default(false),
isFeatured: boolean('is_featured').default(false),
isBlocked: boolean('is_blocked').default(false), // Blocked from re-indexing (owner requested removal)
lastScanned: timestamp('last_scanned'),
// Content (cached)
contentHash: text('content_hash'),
rawContent: text('raw_content'),
// Cached skill files (populated on first download)
cachedFiles: jsonb('cached_files').$type<{
fetchedAt: string; // ISO timestamp when files were fetched
commitSha: string; // Git commit SHA for cache invalidation
totalSize: number; // Total size in bytes
items: Array<{
name: string; // e.g., "SKILL.md", "setup.sh"
path: string; // Relative path from skill root
content: string; // File content (base64 for binary)
size: number; // File size in bytes
isBinary: boolean; // Whether content is base64 encoded
}>;
}>(),
// Timestamps
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
indexedAt: timestamp('indexed_at'),
lastDownloadedAt: timestamp('last_downloaded_at'),
},
(table) => ({
nameIdx: index('idx_skills_name').on(table.name),
ownerIdx: index('idx_skills_owner').on(table.githubOwner),
starsIdx: index('idx_skills_stars').on(table.githubStars),
securityIdx: index('idx_skills_security').on(table.securityScore),
securityStatusIdx: index('idx_skills_security_status').on(table.securityStatus),
verifiedIdx: index('idx_skills_verified').on(table.isVerified),
featuredIdx: index('idx_skills_featured').on(table.isFeatured),
blockedIdx: index('idx_skills_blocked').on(table.isBlocked),
updatedIdx: index('idx_skills_updated').on(table.updatedAt),
sourceFormatIdx: index('idx_skills_source_format').on(table.sourceFormat),
lastDownloadedIdx: index('idx_skills_last_downloaded').on(table.lastDownloadedAt),
})
);
/**
* Categories for organizing skills
*/
export const categories = pgTable(
'categories',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
description: text('description'),
icon: text('icon'),
color: text('color'),
parentId: text('parent_id'),
sortOrder: integer('sort_order').default(0),
skillCount: integer('skill_count').default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
slugIdx: uniqueIndex('idx_categories_slug').on(table.slug),
})
);
/**
* Many-to-many relationship between skills and categories
*/
export const skillCategories = pgTable(
'skill_categories',
{
skillId: text('skill_id')
.references(() => skills.id, { onDelete: 'cascade' })
.notNull(),
categoryId: text('category_id')
.references(() => categories.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.skillId, table.categoryId] }),
skillIdx: index('idx_skill_categories_skill').on(table.skillId),
categoryIdx: index('idx_skill_categories_category').on(table.categoryId),
})
);
/**
* Users (authenticated via GitHub OAuth)
*/
export const users = pgTable(
'users',
{
id: text('id').primaryKey(),
githubId: text('github_id').unique().notNull(),
username: text('username').notNull(),
displayName: text('display_name'),
email: text('email'),
avatarUrl: text('avatar_url'),
bio: text('bio'),
preferredLocale: text('preferred_locale'),
isAdmin: boolean('is_admin').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
lastLoginAt: timestamp('last_login_at'),
},
(table) => ({
githubIdx: uniqueIndex('idx_users_github').on(table.githubId),
usernameIdx: index('idx_users_username').on(table.username),
})
);
/**
* User ratings and reviews for skills
*/
export const ratings = pgTable(
'ratings',
{
id: text('id').primaryKey(),
skillId: text('skill_id')
.references(() => skills.id, { onDelete: 'cascade' })
.notNull(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
rating: integer('rating').notNull(), // 1-5
review: text('review'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
skillIdx: index('idx_ratings_skill').on(table.skillId),
userIdx: index('idx_ratings_user').on(table.userId),
userSkillIdx: uniqueIndex('idx_ratings_user_skill').on(table.userId, table.skillId),
})
);
/**
* Anonymous installation tracking
*/
export const installations = pgTable(
'installations',
{
id: text('id').primaryKey(),
skillId: text('skill_id')
.references(() => skills.id, { onDelete: 'cascade' })
.notNull(),
platform: text('platform').notNull(), // claude, codex, copilot
method: text('method'), // cli, web, desktop
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
skillIdx: index('idx_installations_skill').on(table.skillId),
platformIdx: index('idx_installations_platform').on(table.platform),
createdIdx: index('idx_installations_created').on(table.createdAt),
})
);
/**
* User favorites/bookmarks
*/
export const favorites = pgTable(
'favorites',
{
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
skillId: text('skill_id')
.references(() => skills.id, { onDelete: 'cascade' })
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.userId, table.skillId] }),
userIdx: index('idx_favorites_user').on(table.userId),
skillIdx: index('idx_favorites_skill').on(table.skillId),
})
);
/**
* Indexing job queue status
*/
export const indexingJobs = pgTable(
'indexing_jobs',
{
id: text('id').primaryKey(),
type: text('type').notNull(), // full-crawl, incremental, single-skill
status: text('status').notNull(), // pending, running, completed, failed
skillId: text('skill_id'),
startedAt: timestamp('started_at'),
completedAt: timestamp('completed_at'),
error: text('error'),
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
statusIdx: index('idx_indexing_jobs_status').on(table.status),
typeIdx: index('idx_indexing_jobs_type').on(table.type),
})
);
/**
* Discovered repositories - tracks repos found by various discovery strategies
* Used to queue repos for deep scanning and track discovery sources
*/
export const discoveredRepos = pgTable(
'discovered_repos',
{
id: text('id').primaryKey(), // owner/repo
owner: text('owner').notNull(),
repo: text('repo').notNull(),
discoveredVia: text('discovered_via').notNull(), // 'awesome-list', 'topic-search', 'fork', 'org-scan', 'code-search'
sourceUrl: text('source_url'), // URL of the awesome list or search query that found this repo
lastScanned: timestamp('last_scanned'),
skillCount: integer('skill_count').default(0),
hasSkillMd: boolean('has_skill_md').default(false),
githubStars: integer('github_stars').default(0),
githubForks: integer('github_forks').default(0),
defaultBranch: text('default_branch').default('main'),
isArchived: boolean('is_archived').default(false),
scanError: text('scan_error'), // Last scan error if any
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
ownerIdx: index('idx_discovered_repos_owner').on(table.owner),
discoveredViaIdx: index('idx_discovered_repos_discovered_via').on(table.discoveredVia),
lastScannedIdx: index('idx_discovered_repos_last_scanned').on(table.lastScanned),
skillCountIdx: index('idx_discovered_repos_skill_count').on(table.skillCount),
hasSkillMdIdx: index('idx_discovered_repos_has_skill_md').on(table.hasSkillMd),
})
);
/**
* Awesome lists - tracks curated lists that we crawl for repo discovery
*/
export const awesomeLists = pgTable(
'awesome_lists',
{
id: text('id').primaryKey(), // owner/repo
owner: text('owner').notNull(),
repo: text('repo').notNull(),
name: text('name'),
lastParsed: timestamp('last_parsed'),
repoCount: integer('repo_count').default(0), // Number of repos found in this list
isActive: boolean('is_active').default(true), // Whether to continue crawling this list
createdAt: timestamp('created_at').defaultNow().notNull(),
},
(table) => ({
lastParsedIdx: index('idx_awesome_lists_last_parsed').on(table.lastParsed),
})
);
/**
* Removal requests - allows repo owners to request their skills be removed
*/
export const removalRequests = pgTable(
'removal_requests',
{
id: text('id').primaryKey(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
skillId: text('skill_id').notNull(), // Can reference non-existent skill if already removed
reason: text('reason').notNull(),
status: text('status').notNull().default('pending'), // pending, approved, rejected
verifiedOwner: boolean('verified_owner').default(false), // GitHub API verification result
createdAt: timestamp('created_at').defaultNow().notNull(),
resolvedAt: timestamp('resolved_at'),
resolvedBy: text('resolved_by').references(() => users.id),
resolutionNote: text('resolution_note'),
},
(table) => ({
userIdx: index('idx_removal_requests_user').on(table.userId),
skillIdx: index('idx_removal_requests_skill').on(table.skillId),
statusIdx: index('idx_removal_requests_status').on(table.status),
})
);
/**
* Add requests - allows users to request new skills be indexed
*/
export const addRequests = pgTable(
'add_requests',
{
id: text('id').primaryKey(),
userId: text('user_id')
.references(() => users.id, { onDelete: 'cascade' })
.notNull(),
repositoryUrl: text('repository_url').notNull(), // Full GitHub URL
skillPath: text('skill_path'), // Optional path within repo (for subfolder skills)
reason: text('reason').notNull(), // Why should this skill be added
status: text('status').notNull().default('pending'), // pending, approved, rejected, indexed
validRepo: boolean('valid_repo').default(false), // GitHub API validation result
hasSkillMd: boolean('has_skill_md').default(false), // Whether SKILL.md was found
createdAt: timestamp('created_at').defaultNow().notNull(),
processedAt: timestamp('processed_at'),
indexedSkillId: text('indexed_skill_id'), // Reference to skill if successfully indexed
errorMessage: text('error_message'), // Error if indexing failed
},
(table) => ({
userIdx: index('idx_add_requests_user').on(table.userId),
statusIdx: index('idx_add_requests_status').on(table.status),
repoIdx: index('idx_add_requests_repo').on(table.repositoryUrl),
})
);
// Relations
export const skillsRelations = relations(skills, ({ many }) => ({
categories: many(skillCategories),
ratings: many(ratings),
installations: many(installations),
favorites: many(favorites),
}));
export const categoriesRelations = relations(categories, ({ many, one }) => ({
skills: many(skillCategories),
parent: one(categories, {
fields: [categories.parentId],
references: [categories.id],
}),
}));
export const skillCategoriesRelations = relations(skillCategories, ({ one }) => ({
skill: one(skills, {
fields: [skillCategories.skillId],
references: [skills.id],
}),
category: one(categories, {
fields: [skillCategories.categoryId],
references: [categories.id],
}),
}));
export const usersRelations = relations(users, ({ many }) => ({
ratings: many(ratings),
favorites: many(favorites),
removalRequests: many(removalRequests),
addRequests: many(addRequests),
}));
export const ratingsRelations = relations(ratings, ({ one }) => ({
skill: one(skills, {
fields: [ratings.skillId],
references: [skills.id],
}),
user: one(users, {
fields: [ratings.userId],
references: [users.id],
}),
}));
export const installationsRelations = relations(installations, ({ one }) => ({
skill: one(skills, {
fields: [installations.skillId],
references: [skills.id],
}),
}));
export const favoritesRelations = relations(favorites, ({ one }) => ({
user: one(users, {
fields: [favorites.userId],
references: [users.id],
}),
skill: one(skills, {
fields: [favorites.skillId],
references: [skills.id],
}),
}));
export const removalRequestsRelations = relations(removalRequests, ({ one }) => ({
user: one(users, {
fields: [removalRequests.userId],
references: [users.id],
}),
resolver: one(users, {
fields: [removalRequests.resolvedBy],
references: [users.id],
}),
}));
export const addRequestsRelations = relations(addRequests, ({ one }) => ({
user: one(users, {
fields: [addRequests.userId],
references: [users.id],
}),
}));
/**
* Email subscriptions for newsletter and marketing emails
*/
export const emailSubscriptions = pgTable(
'email_subscriptions',
{
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
source: text('source').notNull(), // 'oauth', 'newsletter', 'claim', 'early-access'
marketingConsent: boolean('marketing_consent').default(false),
consentDate: timestamp('consent_date'),
createdAt: timestamp('created_at').defaultNow().notNull(),
unsubscribedAt: timestamp('unsubscribed_at'),
},
(table) => ({
emailIdx: uniqueIndex('idx_email_subscriptions_email').on(table.email),
sourceIdx: index('idx_email_subscriptions_source').on(table.source),
})
);