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

View File

@@ -0,0 +1,405 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
isMeilisearchConfigured,
getMeilisearchClient,
isMeilisearchHealthy,
initializeSkillsIndex,
addSkillDocument,
addSkillDocuments,
deleteSkillDocument,
searchSkills,
getAllSkillDocuments,
clearSkillsIndex,
type MeiliSkillDocument,
} from './meilisearch.js';
// Store original env
const originalEnv = { ...process.env };
// Mock MeiliSearch class
vi.mock('meilisearch', () => {
const mockIndex = {
search: vi.fn(),
addDocuments: vi.fn().mockResolvedValue({ taskUid: 1 }),
deleteDocument: vi.fn().mockResolvedValue({ taskUid: 2 }),
deleteAllDocuments: vi.fn().mockResolvedValue({ taskUid: 3 }),
getDocuments: vi.fn().mockResolvedValue({ results: [] }),
getSettings: vi.fn().mockResolvedValue({}),
updateSearchableAttributes: vi.fn().mockResolvedValue({ taskUid: 4 }),
updateFilterableAttributes: vi.fn().mockResolvedValue({ taskUid: 5 }),
updateSortableAttributes: vi.fn().mockResolvedValue({ taskUid: 6 }),
updateRankingRules: vi.fn().mockResolvedValue({ taskUid: 7 }),
};
return {
MeiliSearch: vi.fn().mockImplementation(() => ({
health: vi.fn().mockResolvedValue({ status: 'available' }),
index: vi.fn().mockReturnValue(mockIndex),
createIndex: vi.fn().mockResolvedValue({ taskUid: 0 }),
})),
Index: vi.fn(),
};
});
describe('Meilisearch Configuration', () => {
beforeEach(() => {
// Reset environment
delete process.env.MEILI_URL;
delete process.env.MEILI_MASTER_KEY;
vi.clearAllMocks();
});
afterEach(() => {
// Restore original env
process.env = { ...originalEnv };
});
describe('isMeilisearchConfigured', () => {
it('should return true when MEILI_URL is set', () => {
process.env.MEILI_URL = 'http://localhost:7700';
expect(isMeilisearchConfigured()).toBe(true);
});
it('should return false when MEILI_URL is not set', () => {
delete process.env.MEILI_URL;
expect(isMeilisearchConfigured()).toBe(false);
});
it('should return false when MEILI_URL is empty', () => {
process.env.MEILI_URL = '';
expect(isMeilisearchConfigured()).toBe(false);
});
});
describe('getMeilisearchClient', () => {
it('should return null when not configured', () => {
delete process.env.MEILI_URL;
const client = getMeilisearchClient();
expect(client).toBeNull();
});
it('should return client instance when configured', () => {
process.env.MEILI_URL = 'http://localhost:7700';
process.env.MEILI_MASTER_KEY = 'test-key';
const client = getMeilisearchClient();
expect(client).not.toBeNull();
});
it('should return same instance (singleton)', () => {
process.env.MEILI_URL = 'http://localhost:7700';
const client1 = getMeilisearchClient();
const client2 = getMeilisearchClient();
expect(client1).toBe(client2);
});
});
});
describe('Meilisearch Health', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('isMeilisearchHealthy', () => {
it('should return false when not configured', async () => {
delete process.env.MEILI_URL;
const result = await isMeilisearchHealthy();
expect(result).toBe(false);
});
it('should return true when Meilisearch is healthy', async () => {
process.env.MEILI_URL = 'http://localhost:7700';
const result = await isMeilisearchHealthy();
expect(result).toBe(true);
});
});
});
describe('Meilisearch Index Operations', () => {
beforeEach(() => {
process.env.MEILI_URL = 'http://localhost:7700';
process.env.MEILI_MASTER_KEY = 'test-key';
vi.clearAllMocks();
});
afterEach(() => {
delete process.env.MEILI_URL;
delete process.env.MEILI_MASTER_KEY;
});
describe('initializeSkillsIndex', () => {
it('should create index with correct settings', async () => {
await initializeSkillsIndex();
// The function should complete without errors
expect(true).toBe(true);
});
});
});
describe('Meilisearch Document Operations', () => {
const testSkillDocument: MeiliSkillDocument = {
id: 'test-owner/test-repo/test-skill',
name: 'test-skill',
description: 'A test skill',
githubOwner: 'test-owner',
githubRepo: 'test-repo',
platforms: ['claude', 'codex'],
githubStars: 100,
downloadCount: 50,
rating: 4,
securityScore: 85,
isFeatured: false,
isVerified: true,
indexedAt: new Date().toISOString(),
};
beforeEach(() => {
process.env.MEILI_URL = 'http://localhost:7700';
process.env.MEILI_MASTER_KEY = 'test-key';
vi.clearAllMocks();
});
afterEach(() => {
delete process.env.MEILI_URL;
delete process.env.MEILI_MASTER_KEY;
});
describe('addSkillDocument', () => {
it('should add document to index', async () => {
const result = await addSkillDocument(testSkillDocument);
expect(result).toBe(true);
});
it('should return false when not configured', async () => {
delete process.env.MEILI_URL;
const result = await addSkillDocument(testSkillDocument);
expect(result).toBe(false);
});
});
describe('addSkillDocuments', () => {
it('should add multiple documents', async () => {
const skills = [
testSkillDocument,
{ ...testSkillDocument, id: 'another/skill/test' },
];
const result = await addSkillDocuments(skills);
expect(result).toBe(true);
});
it('should return true for empty array', async () => {
const result = await addSkillDocuments([]);
expect(result).toBe(true);
});
it('should return false when not configured', async () => {
delete process.env.MEILI_URL;
const result = await addSkillDocuments([testSkillDocument]);
expect(result).toBe(false);
});
});
describe('deleteSkillDocument', () => {
it('should delete document from index', async () => {
const result = await deleteSkillDocument('test-owner/test-repo/test-skill');
expect(result).toBe(true);
});
it('should return false when not configured', async () => {
delete process.env.MEILI_URL;
const result = await deleteSkillDocument('test-owner/test-repo/test-skill');
expect(result).toBe(false);
});
});
describe('getAllSkillDocuments', () => {
it('should return all documents', async () => {
const result = await getAllSkillDocuments();
expect(Array.isArray(result)).toBe(true);
});
it('should return empty array when not configured', async () => {
delete process.env.MEILI_URL;
const result = await getAllSkillDocuments();
expect(result).toEqual([]);
});
});
describe('clearSkillsIndex', () => {
it('should clear all documents', async () => {
const result = await clearSkillsIndex();
expect(result).toBe(true);
});
it('should return false when not configured', async () => {
delete process.env.MEILI_URL;
const result = await clearSkillsIndex();
expect(result).toBe(false);
});
});
});
describe('Meilisearch Search', () => {
beforeEach(() => {
process.env.MEILI_URL = 'http://localhost:7700';
process.env.MEILI_MASTER_KEY = 'test-key';
vi.clearAllMocks();
});
afterEach(() => {
delete process.env.MEILI_URL;
delete process.env.MEILI_MASTER_KEY;
});
describe('searchSkills', () => {
it('should search with query', async () => {
// Mock search response
const { MeiliSearch } = await import('meilisearch');
const mockClient = new MeiliSearch({ host: 'http://localhost:7700' });
const mockIndex = mockClient.index('skills');
vi.mocked(mockIndex.search).mockResolvedValue({
hits: [],
estimatedTotalHits: 0,
processingTimeMs: 1,
query: 'test',
limit: 20,
offset: 0,
});
const result = await searchSkills({ query: 'test' });
// Result could be null if index doesn't exist
expect(result === null || typeof result === 'object').toBe(true);
});
it('should return null when not configured', async () => {
delete process.env.MEILI_URL;
const result = await searchSkills({ query: 'test' });
expect(result).toBeNull();
});
it('should apply platform filter', async () => {
const result = await searchSkills({
query: 'test',
filters: { platforms: ['claude'] },
});
// Just verify it doesn't throw
expect(result === null || typeof result === 'object').toBe(true);
});
it('should apply star filter', async () => {
const result = await searchSkills({
query: 'test',
filters: { minStars: 100 },
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should apply verified filter', async () => {
const result = await searchSkills({
query: 'test',
filters: { verified: true },
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should apply featured filter', async () => {
const result = await searchSkills({
query: 'test',
filters: { featured: true },
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should apply security score filter', async () => {
const result = await searchSkills({
query: 'test',
filters: { minSecurity: 80 },
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should sort by stars', async () => {
const result = await searchSkills({
query: 'test',
sort: 'stars',
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should sort by downloads', async () => {
const result = await searchSkills({
query: 'test',
sort: 'downloads',
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should sort by rating', async () => {
const result = await searchSkills({
query: 'test',
sort: 'rating',
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should sort by recent', async () => {
const result = await searchSkills({
query: 'test',
sort: 'recent',
});
expect(result === null || typeof result === 'object').toBe(true);
});
it('should respect pagination', async () => {
const result = await searchSkills({
query: 'test',
limit: 10,
offset: 20,
});
expect(result === null || typeof result === 'object').toBe(true);
});
});
});