@nospt/plugin-dev-ai-hub-backend 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/AiAssetStore.cjs.js +1 -0
- package/dist/database/AiAssetStore.cjs.js.map +1 -1
- package/dist/service/AiAssetSyncService.cjs.js +33 -8
- package/dist/service/AiAssetSyncService.cjs.js.map +1 -1
- package/dist/service/AssetParser.cjs.js +4 -21
- package/dist/service/AssetParser.cjs.js.map +1 -1
- package/package.json +17 -10
- package/package.json-prepack +17 -10
- package/src/router.smoke.test.ts +262 -0
- package/src/service/AiAssetSyncService.ts +39 -8
- package/src/service/AssetParser.test.ts +106 -38
- package/src/service/AssetParser.ts +13 -27
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiAssetStore.cjs.js","sources":["../../src/database/AiAssetStore.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport { resolvePackagePath, type DatabaseService } from '@backstage/backend-plugin-api';\nimport type { AiAsset, AiAssetSummary, AiHubStats, AssetListFilter, AssetType } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetInput, SyncStatus } from '../types';\n\nexport class AiAssetStore {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: { database: DatabaseService }): Promise<AiAssetStore> {\n const db = await options.database.getClient();\n await db.migrate.latest({\n directory: resolvePackagePath('@nospt/plugin-dev-ai-hub-backend', 'migrations'),\n loadExtensions: ['.js'],\n });\n return new AiAssetStore(db);\n }\n\n async upsertAsset(input: AiAssetInput): Promise<void> {\n const now = new Date().toISOString();\n const row = {\n id: input.id,\n provider_id: input.providerId,\n name: input.name,\n label: input.label ?? null,\n description: input.description,\n type: input.type,\n tools: JSON.stringify(input.tools),\n tags: JSON.stringify(input.tags),\n author: input.author,\n icon: input.icon ?? null,\n version: input.version,\n install_path: input.installPath ?? null,\n install_paths: input.installPaths ? JSON.stringify(input.installPaths) : null,\n content: input.content,\n yaml_raw: input.yamlRaw,\n metadata: input.metadata ? JSON.stringify(input.metadata) : null,\n resources_content: input.resourcesContent ? JSON.stringify(input.resourcesContent) : null,\n yaml_path: input.yamlPath,\n md_path: input.mdPath,\n repo_url: input.repoUrl,\n branch: input.branch,\n commit_sha: input.commitSha ?? null,\n synced_at: now,\n updated_at: now,\n };\n\n await this.db('ai_assets')\n .insert({ ...row, created_at: now })\n .onConflict('id')\n .merge(Object.keys(row) as (keyof typeof row)[]);\n }\n\n async listAssets(\n filter: AssetListFilter,\n ): Promise<{ items: AiAssetSummary[]; totalCount: number }> {\n let query = this.db<Record<string, unknown>>('ai_assets');\n\n if (filter.type) {\n query = query.where('type', filter.type);\n }\n if (filter.tool) {\n query = query.where(function toolFilter() {\n this.where('tools', 'like', `%\"${filter.tool}\"%`)\n .orWhere('tools', 'like', `%\"all\"%`);\n });\n }\n if (filter.providerId) {\n query = query.where('provider_id', filter.providerId);\n }\n if (filter.search) {\n // Split into words so \"Python linting\" matches items containing both words\n const words = filter.search.trim().split(/\\s+/).filter(Boolean);\n for (const word of words) {\n const term = `%${word}%`;\n query = query.where(function searchFilter() {\n this.where('name', 'like', term)\n .orWhere('label', 'like', term)\n .orWhere('description', 'like', term)\n .orWhere('tags', 'like', term)\n .orWhere('content', 'like', term);\n });\n }\n }\n if (filter.tags && filter.tags.length > 0) {\n for (const tag of filter.tags) {\n query = query.where('tags', 'like', `%\"${tag}\"%`);\n }\n }\n\n const countResult = await query.clone().count('id as count').first();\n const totalCount = Number(countResult?.count ?? 0);\n\n const page = Math.max(1, filter.page ?? 1);\n const pageSize = Math.min(100, Math.max(1, filter.pageSize ?? 20));\n\n const rows = await query\n .select('id', 'provider_id', 'name', 'label', 'description', 'type', 'tools', 'tags',\n 'author', 'icon', 'version', 'install_count', 'synced_at', 'created_at', 'updated_at')\n .orderBy('name', 'asc')\n .limit(pageSize)\n .offset((page - 1) * pageSize);\n\n return { items: rows.map(r => this.rowToAssetSummary(r)), totalCount };\n }\n\n async getAsset(id: string): Promise<AiAsset | null> {\n const row = await this.db('ai_assets').where('id', id).first();\n return row ? this.rowToAsset(row) : null;\n }\n\n async incrementInstallCount(id: string): Promise<void> {\n await this.db('ai_assets').where('id', id).increment('install_count', 1);\n }\n\n async deleteAssetsNotIn(providerId: string, ids: string[]): Promise<void> {\n let query = this.db('ai_assets').where('provider_id', providerId);\n if (ids.length > 0) {\n query = query.whereNotIn('id', ids);\n }\n await query.delete();\n }\n\n async upsertSyncStatus(status: SyncStatus): Promise<void> {\n const row = {\n provider_id: status.providerId,\n last_sync: status.lastSync ?? null,\n last_commit: status.lastCommit ?? null,\n status: status.status,\n error: status.error ?? null,\n asset_count: status.assetCount,\n };\n await this.db('ai_asset_sync_status')\n .insert(row)\n .onConflict('provider_id')\n .merge(Object.keys(row) as (keyof typeof row)[]);\n }\n\n async getSyncStatus(providerId: string): Promise<SyncStatus | null> {\n const row = await this.db('ai_asset_sync_status')\n .where('provider_id', providerId)\n .first();\n if (!row) return null;\n return {\n providerId: row.provider_id,\n lastSync: row.last_sync ?? undefined,\n lastCommit: row.last_commit ?? undefined,\n status: row.status,\n error: row.error ?? undefined,\n assetCount: row.asset_count ?? 0,\n };\n }\n\n async getAllSyncStatuses(): Promise<SyncStatus[]> {\n const rows = await this.db('ai_asset_sync_status').select('*');\n return rows.map(row => ({\n providerId: row.provider_id,\n lastSync: row.last_sync ?? undefined,\n lastCommit: row.last_commit ?? undefined,\n status: row.status,\n error: row.error ?? undefined,\n assetCount: row.asset_count ?? 0,\n }));\n }\n\n async getStats(): Promise<AiHubStats> {\n const rows = await this.db('ai_assets').select('type', 'tools', 'provider_id');\n\n const byType: Record<AssetType, number> = {\n instruction: 0,\n agent: 0,\n skill: 0,\n workflow: 0,\n };\n const byTool: Record<string, number> = {};\n const byProvider: Record<string, number> = {};\n\n for (const row of rows) {\n byType[row.type as AssetType] = (byType[row.type as AssetType] ?? 0) + 1;\n byProvider[row.provider_id] = (byProvider[row.provider_id] ?? 0) + 1;\n\n const tools: string[] = JSON.parse(row.tools || '[]');\n for (const tool of tools) {\n byTool[tool] = (byTool[tool] ?? 0) + 1;\n }\n }\n\n const lastSyncRow = await this.db('ai_asset_sync_status')\n .select(this.db.raw('max(last_sync) as last_sync'))\n .first();\n\n return {\n totalAssets: rows.length,\n byType,\n byTool,\n byProvider,\n lastSync: (lastSyncRow?.last_sync as string | null) ?? undefined,\n };\n }\n\n private rowToAssetSummary(row: Record<string, unknown>): AiAssetSummary {\n return {\n id: row.id as string,\n providerId: row.provider_id as string,\n name: row.name as string,\n label: (row.label as string | null) ?? undefined,\n description: row.description as string,\n type: row.type as AssetType,\n tools: JSON.parse((row.tools as string) || '[]'),\n tags: JSON.parse((row.tags as string) || '[]'),\n author: row.author as string,\n icon: (row.icon as string | null) ?? undefined,\n version: row.version as string,\n installCount: (row.install_count as number) ?? 0,\n syncedAt: row.synced_at as string,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n\n private rowToAsset(row: Record<string, unknown>): AiAsset {\n return {\n id: row.id as string,\n providerId: row.provider_id as string,\n name: row.name as string,\n label: (row.label as string | null) ?? undefined,\n description: row.description as string,\n type: row.type as AssetType,\n tools: JSON.parse((row.tools as string) || '[]'),\n tags: JSON.parse((row.tags as string) || '[]'),\n author: row.author as string,\n icon: (row.icon as string | null) ?? undefined,\n version: row.version as string,\n installPath: (row.install_path as string | null) ?? undefined,\n installPaths: row.install_paths\n ? JSON.parse(row.install_paths as string)\n : undefined,\n content: row.content as string,\n yamlRaw: row.yaml_raw as string,\n metadata: row.metadata\n ? JSON.parse(row.metadata as string)\n : undefined,\n resourcesContent: row.resources_content\n ? JSON.parse(row.resources_content as string)\n : undefined,\n yamlPath: row.yaml_path as string,\n mdPath: row.md_path as string,\n repoUrl: row.repo_url as string,\n branch: row.branch as string,\n commitSha: (row.commit_sha as string | null) ?? undefined,\n installCount: (row.install_count as number) ?? 0,\n syncedAt: row.synced_at as string,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n"],"names":["resolvePackagePath"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiAssetStore.cjs.js","sources":["../../src/database/AiAssetStore.ts"],"sourcesContent":["import type { Knex } from 'knex';\nimport { resolvePackagePath, type DatabaseService } from '@backstage/backend-plugin-api';\nimport type { AiAsset, AiAssetSummary, AiHubStats, AssetListFilter, AssetType } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetInput, SyncStatus } from '../types';\n\nexport class AiAssetStore {\n private constructor(private readonly db: Knex) {}\n\n static async create(options: { database: DatabaseService }): Promise<AiAssetStore> {\n const db = await options.database.getClient();\n await db.migrate.latest({\n directory: resolvePackagePath('@nospt/plugin-dev-ai-hub-backend', 'migrations'),\n loadExtensions: ['.js'],\n });\n return new AiAssetStore(db);\n }\n\n async upsertAsset(input: AiAssetInput): Promise<void> {\n const now = new Date().toISOString();\n const row = {\n id: input.id,\n provider_id: input.providerId,\n name: input.name,\n label: input.label ?? null,\n description: input.description,\n type: input.type,\n tools: JSON.stringify(input.tools),\n tags: JSON.stringify(input.tags),\n author: input.author,\n icon: input.icon ?? null,\n version: input.version,\n install_path: input.installPath ?? null,\n install_paths: input.installPaths ? JSON.stringify(input.installPaths) : null,\n content: input.content,\n yaml_raw: input.yamlRaw,\n metadata: input.metadata ? JSON.stringify(input.metadata) : null,\n resources_content: input.resourcesContent ? JSON.stringify(input.resourcesContent) : null,\n yaml_path: input.yamlPath,\n md_path: input.mdPath,\n repo_url: input.repoUrl,\n branch: input.branch,\n commit_sha: input.commitSha ?? null,\n synced_at: now,\n updated_at: now,\n };\n\n await this.db('ai_assets')\n .insert({ ...row, created_at: now })\n .onConflict('id')\n .merge(Object.keys(row) as (keyof typeof row)[]);\n }\n\n async listAssets(\n filter: AssetListFilter,\n ): Promise<{ items: AiAssetSummary[]; totalCount: number }> {\n let query = this.db<Record<string, unknown>>('ai_assets');\n\n if (filter.type) {\n query = query.where('type', filter.type);\n }\n if (filter.tool) {\n query = query.where(function toolFilter() {\n this.where('tools', 'like', `%\"${filter.tool}\"%`)\n .orWhere('tools', 'like', `%\"all\"%`);\n });\n }\n if (filter.providerId) {\n query = query.where('provider_id', filter.providerId);\n }\n if (filter.search) {\n // Split into words so \"Python linting\" matches items containing both words\n const words = filter.search.trim().split(/\\s+/).filter(Boolean);\n for (const word of words) {\n const term = `%${word}%`;\n query = query.where(function searchFilter() {\n this.where('name', 'like', term)\n .orWhere('label', 'like', term)\n .orWhere('description', 'like', term)\n .orWhere('tags', 'like', term)\n .orWhere('content', 'like', term);\n });\n }\n }\n if (filter.tags && filter.tags.length > 0) {\n for (const tag of filter.tags) {\n query = query.where('tags', 'like', `%\"${tag}\"%`);\n }\n }\n\n const countResult = await query.clone().count('id as count').first();\n const totalCount = Number(countResult?.count ?? 0);\n\n const page = Math.max(1, filter.page ?? 1);\n const pageSize = Math.min(100, Math.max(1, filter.pageSize ?? 20));\n\n const rows = await query\n .select('id', 'provider_id', 'name', 'label', 'description', 'type', 'tools', 'tags',\n 'author', 'icon', 'version', 'install_count', 'synced_at', 'created_at', 'updated_at')\n .orderBy('name', 'asc')\n .limit(pageSize)\n .offset((page - 1) * pageSize);\n\n return { items: rows.map(r => this.rowToAssetSummary(r)), totalCount };\n }\n\n async getAsset(id: string): Promise<AiAsset | null> {\n const row = await this.db('ai_assets').where('id', id).first();\n return row ? this.rowToAsset(row) : null;\n }\n\n async incrementInstallCount(id: string): Promise<void> {\n await this.db('ai_assets').where('id', id).increment('install_count', 1);\n }\n\n async deleteAssetsNotIn(providerId: string, ids: string[]): Promise<void> {\n let query = this.db('ai_assets').where('provider_id', providerId);\n if (ids.length > 0) {\n query = query.whereNotIn('id', ids);\n }\n await query.delete();\n }\n\n async upsertSyncStatus(status: SyncStatus): Promise<void> {\n const row = {\n provider_id: status.providerId,\n last_sync: status.lastSync ?? null,\n last_commit: status.lastCommit ?? null,\n status: status.status,\n error: status.error ?? null,\n asset_count: status.assetCount,\n };\n await this.db('ai_asset_sync_status')\n .insert(row)\n .onConflict('provider_id')\n .merge(Object.keys(row) as (keyof typeof row)[]);\n }\n\n async getSyncStatus(providerId: string): Promise<SyncStatus | null> {\n const row = await this.db('ai_asset_sync_status')\n .where('provider_id', providerId)\n .first();\n if (!row) return null;\n return {\n providerId: row.provider_id,\n lastSync: row.last_sync ?? undefined,\n lastCommit: row.last_commit ?? undefined,\n status: row.status,\n error: row.error ?? undefined,\n assetCount: row.asset_count ?? 0,\n };\n }\n\n async getAllSyncStatuses(): Promise<SyncStatus[]> {\n const rows = await this.db('ai_asset_sync_status').select('*');\n return rows.map(row => ({\n providerId: row.provider_id,\n lastSync: row.last_sync ?? undefined,\n lastCommit: row.last_commit ?? undefined,\n status: row.status,\n error: row.error ?? undefined,\n assetCount: row.asset_count ?? 0,\n }));\n }\n\n async getStats(): Promise<AiHubStats> {\n const rows = await this.db('ai_assets').select('type', 'tools', 'provider_id');\n\n const byType: Record<AssetType, number> = {\n instruction: 0,\n agent: 0,\n skill: 0,\n workflow: 0,\n };\n const byTool: Record<string, number> = {};\n const byProvider: Record<string, number> = {};\n\n for (const row of rows) {\n byType[row.type as AssetType] = (byType[row.type as AssetType] ?? 0) + 1;\n byProvider[row.provider_id] = (byProvider[row.provider_id] ?? 0) + 1;\n\n const tools: string[] = JSON.parse(row.tools || '[]');\n for (const tool of tools) {\n byTool[tool] = (byTool[tool] ?? 0) + 1;\n }\n }\n\n const lastSyncRow = await this.db('ai_asset_sync_status')\n .select(this.db.raw('max(last_sync) as last_sync'))\n .first();\n\n return {\n totalAssets: rows.length,\n byType,\n byTool,\n byProvider,\n lastSync: (lastSyncRow?.last_sync as string | null) ?? undefined,\n };\n }\n\n private rowToAssetSummary(row: Record<string, unknown>): AiAssetSummary {\n return {\n id: row.id as string,\n providerId: row.provider_id as string,\n name: row.name as string,\n label: (row.label as string | null) ?? undefined,\n description: row.description as string,\n type: row.type as AssetType,\n tools: JSON.parse((row.tools as string) || '[]'),\n tags: JSON.parse((row.tags as string) || '[]'),\n author: row.author as string,\n icon: (row.icon as string | null) ?? undefined,\n version: row.version as string,\n installCount: (row.install_count as number) ?? 0,\n syncedAt: row.synced_at as string,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n\n private rowToAsset(row: Record<string, unknown>): AiAsset {\n return {\n id: row.id as string,\n providerId: row.provider_id as string,\n name: row.name as string,\n label: (row.label as string | null) ?? undefined,\n description: row.description as string,\n type: row.type as AssetType,\n tools: JSON.parse((row.tools as string) || '[]'),\n tags: JSON.parse((row.tags as string) || '[]'),\n author: row.author as string,\n icon: (row.icon as string | null) ?? undefined,\n version: row.version as string,\n installPath: (row.install_path as string | null) ?? undefined,\n installPaths: row.install_paths\n ? JSON.parse(row.install_paths as string)\n : undefined,\n content: row.content as string,\n yamlRaw: row.yaml_raw as string,\n metadata: row.metadata\n ? JSON.parse(row.metadata as string)\n : undefined,\n resourcesContent: row.resources_content\n ? JSON.parse(row.resources_content as string)\n : undefined,\n yamlPath: row.yaml_path as string,\n mdPath: row.md_path as string,\n repoUrl: row.repo_url as string,\n branch: row.branch as string,\n commitSha: (row.commit_sha as string | null) ?? undefined,\n installCount: (row.install_count as number) ?? 0,\n syncedAt: row.synced_at as string,\n createdAt: row.created_at as string,\n updatedAt: row.updated_at as string,\n };\n }\n}\n"],"names":["resolvePackagePath"],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ class AiAssetSyncService {
|
|
|
6
6
|
constructor(options) {
|
|
7
7
|
this.options = options;
|
|
8
8
|
}
|
|
9
|
+
options;
|
|
9
10
|
async start() {
|
|
10
11
|
const { scheduler, logger, providers, externalProviders } = this.options;
|
|
11
12
|
if (providers.length === 0 && externalProviders.length === 0) {
|
|
@@ -65,7 +66,6 @@ class AiAssetSyncService {
|
|
|
65
66
|
const syncedIds = [];
|
|
66
67
|
for (const [filePath, file] of fileMap) {
|
|
67
68
|
if (!filePath.endsWith(".yaml")) continue;
|
|
68
|
-
if (!AssetParser.AssetParser.isAssetFile(filePath)) continue;
|
|
69
69
|
const yamlContent = (await file.content()).toString("utf-8");
|
|
70
70
|
const parsed = AssetParser.AssetParser.parseYaml(yamlContent, filePath);
|
|
71
71
|
if (!parsed) {
|
|
@@ -83,19 +83,43 @@ class AiAssetSyncService {
|
|
|
83
83
|
);
|
|
84
84
|
if (!hasMatchingTool) continue;
|
|
85
85
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
|
|
86
|
+
const yamlDir = filePath.includes("/") ? filePath.split("/").slice(0, -1).join("/") : "";
|
|
87
|
+
let resolvedMdPath;
|
|
88
|
+
let mdFile;
|
|
89
|
+
if (parsed.mdPath) {
|
|
90
|
+
resolvedMdPath = normalizePath(parsed.mdPath);
|
|
91
|
+
mdFile = fileMap.get(resolvedMdPath);
|
|
92
|
+
if (!mdFile) {
|
|
93
|
+
logger.warn(
|
|
94
|
+
`dev-ai-hub: content file "${parsed.mdPath}" not found for ${filePath}, skipping`
|
|
95
|
+
);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
const mdCandidates = [...fileMap.keys()].filter((p) => {
|
|
100
|
+
const dir = p.includes("/") ? p.split("/").slice(0, -1).join("/") : "";
|
|
101
|
+
return dir === yamlDir && p.endsWith(".md");
|
|
102
|
+
});
|
|
103
|
+
if (mdCandidates.length === 1) {
|
|
104
|
+
resolvedMdPath = mdCandidates[0];
|
|
105
|
+
mdFile = fileMap.get(resolvedMdPath);
|
|
106
|
+
} else if (mdCandidates.length > 1) {
|
|
107
|
+
logger.warn(
|
|
108
|
+
`dev-ai-hub: multiple .md files in "${yamlDir || "."}" for ${filePath} \u2014 add a 'content' field to the YAML to specify which one, skipping`
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
} else {
|
|
112
|
+
logger.warn(
|
|
113
|
+
`dev-ai-hub: no .md file found for ${filePath}, skipping`
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
92
117
|
}
|
|
93
118
|
const mdContent = (await mdFile.content()).toString("utf-8");
|
|
94
119
|
let resourcesContent;
|
|
95
120
|
const resourcePaths = parsed.meta.resources;
|
|
96
121
|
if (resourcePaths && resourcePaths.length > 0) {
|
|
97
122
|
resourcesContent = {};
|
|
98
|
-
const yamlDir = filePath.split("/").slice(0, -1).join("/");
|
|
99
123
|
for (const resourcePath of resourcePaths) {
|
|
100
124
|
const fullPath = normalizePath(
|
|
101
125
|
yamlDir ? `${yamlDir}/${resourcePath}` : resourcePath
|
|
@@ -118,6 +142,7 @@ class AiAssetSyncService {
|
|
|
118
142
|
provider.target,
|
|
119
143
|
provider.branch,
|
|
120
144
|
filePath,
|
|
145
|
+
resolvedMdPath,
|
|
121
146
|
resourcesContent
|
|
122
147
|
);
|
|
123
148
|
await store.upsertAsset(asset);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiAssetSyncService.cjs.js","sources":["../../src/service/AiAssetSyncService.ts"],"sourcesContent":["import type {\n LoggerService,\n SchedulerService,\n UrlReaderService,\n} from '@backstage/backend-plugin-api';\nimport type { AiAssetStore } from '../database/AiAssetStore';\nimport type { ProviderConfig } from '../types';\nimport type { AiAssetProvider } from '@nospt/plugin-dev-ai-hub-node';\nimport { AssetParser } from './AssetParser';\n\ninterface Options {\n logger: LoggerService;\n store: AiAssetStore;\n scheduler: SchedulerService;\n urlReader: UrlReaderService;\n providers: ProviderConfig[];\n externalProviders: AiAssetProvider[];\n}\n\nexport class AiAssetSyncService {\n constructor(private readonly options: Options) {}\n\n async start(): Promise<void> {\n const { scheduler, logger, providers, externalProviders } = this.options;\n\n if (providers.length === 0 && externalProviders.length === 0) {\n logger.warn(\n 'dev-ai-hub: no providers configured under devAiHub.providers',\n );\n return;\n }\n\n for (const provider of providers) {\n const { frequency, timeout } = provider.schedule;\n const frequencyDuration = frequency.minutes\n ? { minutes: frequency.minutes }\n : { hours: frequency.hours ?? 1 };\n const timeoutDuration = timeout.minutes\n ? { minutes: timeout.minutes }\n : { hours: timeout.hours ?? 1 };\n\n await scheduler.scheduleTask({\n id: `dev-ai-hub-sync-${provider.id}`,\n frequency: frequencyDuration,\n timeout: timeoutDuration,\n initialDelay: { seconds: 15 },\n fn: async () => {\n await this.syncProvider(provider);\n },\n });\n\n logger.info(\n `dev-ai-hub: scheduled sync for provider \"${provider.id}\" every ${JSON.stringify(frequencyDuration)}`,\n );\n }\n\n for (const extProvider of externalProviders) {\n await scheduler.scheduleTask({\n id: `dev-ai-hub-sync-external-${extProvider.id}`,\n frequency: { minutes: 60 },\n timeout: { minutes: 5 },\n initialDelay: { seconds: 15 },\n fn: async () => {\n await this.syncExternalProvider(extProvider);\n },\n });\n\n logger.info(\n `dev-ai-hub: scheduled sync for external provider \"${extProvider.id}\"`,\n );\n }\n }\n\n async syncProvider(provider: ProviderConfig): Promise<void> {\n const { logger, urlReader, store } = this.options;\n\n logger.info(`dev-ai-hub: starting sync for provider \"${provider.id}\"`);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'syncing',\n assetCount: 0,\n });\n\n try {\n const treeUrl = buildTreeUrl(provider);\n const tree = await urlReader.readTree(treeUrl);\n const files = await tree.files();\n\n const fileMap = new Map<\n string,\n { content(): Promise<Buffer>; path: string }\n >();\n for (const file of files) {\n fileMap.set(normalizePath(file.path), file);\n }\n\n const syncedIds: string[] = [];\n\n for (const [filePath, file] of fileMap) {\n if (!filePath.endsWith('.yaml')) continue;\n if (!AssetParser.isAssetFile(filePath)) continue;\n\n const yamlContent = (await file.content()).toString('utf-8');\n const parsed = AssetParser.parseYaml(yamlContent, filePath);\n\n if (!parsed) {\n logger.warn(\n `dev-ai-hub: skipping invalid YAML at ${filePath} (provider ${provider.id})`,\n );\n continue;\n }\n\n if (\n provider.filters?.types &&\n !provider.filters.types.includes(parsed.meta.type)\n ) {\n continue;\n }\n if (provider.filters?.tools) {\n const hasMatchingTool = parsed.meta.tools.some(t =>\n provider.filters!.tools!.includes(t),\n );\n if (!hasMatchingTool) continue;\n }\n\n const mdFile = fileMap.get(normalizePath(parsed.mdPath));\n if (!mdFile) {\n logger.warn(\n `dev-ai-hub: .md not found for ${filePath} (expected ${parsed.mdPath}), skipping`,\n );\n continue;\n }\n\n const mdContent = (await mdFile.content()).toString('utf-8');\n\n // For skills: read bundled resource files and store their content\n let resourcesContent: Record<string, string> | undefined;\n const resourcePaths = parsed.meta.resources;\n if (resourcePaths && resourcePaths.length > 0) {\n resourcesContent = {};\n const yamlDir = filePath.split('/').slice(0, -1).join('/');\n for (const resourcePath of resourcePaths) {\n const fullPath = normalizePath(\n yamlDir ? `${yamlDir}/${resourcePath}` : resourcePath,\n );\n const resourceFile = fileMap.get(fullPath);\n if (resourceFile) {\n resourcesContent[resourcePath] = (await resourceFile.content()).toString('utf-8');\n } else {\n logger.warn(\n `dev-ai-hub: resource file not found: ${fullPath} (asset ${filePath})`,\n );\n }\n }\n if (Object.keys(resourcesContent).length === 0) resourcesContent = undefined;\n }\n\n const asset = AssetParser.buildAsset(\n parsed,\n mdContent,\n provider.id,\n provider.target,\n provider.branch,\n filePath,\n resourcesContent,\n );\n\n await store.upsertAsset(asset);\n syncedIds.push(asset.id);\n }\n\n await store.deleteAssetsNotIn(provider.id, syncedIds);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n lastSync: new Date().toISOString(),\n status: 'idle',\n assetCount: syncedIds.length,\n });\n\n logger.info(\n `dev-ai-hub: sync complete for provider \"${provider.id}\" — ${syncedIds.length} assets`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.options.logger.error(\n `dev-ai-hub: sync failed for provider \"${provider.id}\": ${message}`,\n );\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'error',\n error: message,\n assetCount: 0,\n });\n }\n }\n\n private async syncExternalProvider(provider: AiAssetProvider): Promise<void> {\n const { logger, store } = this.options;\n\n logger.info(`dev-ai-hub: syncing external provider \"${provider.id}\"`);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'syncing',\n assetCount: 0,\n });\n\n try {\n const assets = await provider.getAssets();\n const syncedIds: string[] = [];\n\n for (const assetData of assets) {\n const id = AssetParser.buildId(provider.id, assetData.yamlPath);\n await store.upsertAsset({ ...assetData, id });\n syncedIds.push(id);\n }\n\n await store.deleteAssetsNotIn(provider.id, syncedIds);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n lastSync: new Date().toISOString(),\n status: 'idle',\n assetCount: syncedIds.length,\n });\n\n logger.info(\n `dev-ai-hub: external sync complete for \"${provider.id}\" — ${syncedIds.length} assets`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(\n `dev-ai-hub: external sync failed for \"${provider.id}\": ${message}`,\n );\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'error',\n error: message,\n assetCount: 0,\n });\n }\n }\n}\n\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\//, '');\n}\n\nfunction buildTreeUrl(provider: ProviderConfig): string {\n const base = provider.target.replace(/\\.git$/, '');\n const { branch, type } = provider;\n\n switch (type) {\n case 'gitlab':\n return `${base}/-/tree/${branch}/`;\n case 'bitbucket':\n return `${base}/src/${branch}/`;\n case 'azure-devops':\n return `${base}?version=GB${branch}`;\n case 'github':\n case 'git':\n default:\n return `${base}/tree/${branch}/`;\n }\n}\n"],"names":["AssetParser"],"mappings":";;;;AAmBO,MAAM,kBAAA,CAAmB;AAAA,EAC9B,YAA6B,OAAA,EAAkB;AAAlB,IAAA,IAAA,CAAA,OAAA,GAAA,OAAA;AAAA,EAAmB;AAAA,EAEhD,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,EAAE,SAAA,EAAW,MAAA,EAAQ,SAAA,EAAW,iBAAA,KAAsB,IAAA,CAAK,OAAA;AAEjE,IAAA,IAAI,SAAA,CAAU,MAAA,KAAW,CAAA,IAAK,iBAAA,CAAkB,WAAW,CAAA,EAAG;AAC5D,MAAA,MAAA,CAAO,IAAA;AAAA,QACL;AAAA,OACF;AACA,MAAA;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,MAAM,EAAE,SAAA,EAAW,OAAA,EAAQ,GAAI,QAAA,CAAS,QAAA;AACxC,MAAA,MAAM,iBAAA,GAAoB,SAAA,CAAU,OAAA,GAChC,EAAE,OAAA,EAAS,SAAA,CAAU,OAAA,EAAQ,GAC7B,EAAE,KAAA,EAAO,SAAA,CAAU,KAAA,IAAS,CAAA,EAAE;AAClC,MAAA,MAAM,eAAA,GAAkB,OAAA,CAAQ,OAAA,GAC5B,EAAE,OAAA,EAAS,OAAA,CAAQ,OAAA,EAAQ,GAC3B,EAAE,KAAA,EAAO,OAAA,CAAQ,KAAA,IAAS,CAAA,EAAE;AAEhC,MAAA,MAAM,UAAU,YAAA,CAAa;AAAA,QAC3B,EAAA,EAAI,CAAA,gBAAA,EAAmB,QAAA,CAAS,EAAE,CAAA,CAAA;AAAA,QAClC,SAAA,EAAW,iBAAA;AAAA,QACX,OAAA,EAAS,eAAA;AAAA,QACT,YAAA,EAAc,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,QAC5B,IAAI,YAAY;AACd,UAAA,MAAM,IAAA,CAAK,aAAa,QAAQ,CAAA;AAAA,QAClC;AAAA,OACD,CAAA;AAED,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,4CAA4C,QAAA,CAAS,EAAE,WAAW,IAAA,CAAK,SAAA,CAAU,iBAAiB,CAAC,CAAA;AAAA,OACrG;AAAA,IACF;AAEA,IAAA,KAAA,MAAW,eAAe,iBAAA,EAAmB;AAC3C,MAAA,MAAM,UAAU,YAAA,CAAa;AAAA,QAC3B,EAAA,EAAI,CAAA,yBAAA,EAA4B,WAAA,CAAY,EAAE,CAAA,CAAA;AAAA,QAC9C,SAAA,EAAW,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,QACzB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,QACtB,YAAA,EAAc,EAAE,OAAA,EAAS,EAAA,EAAG;AAAA,QAC5B,IAAI,YAAY;AACd,UAAA,MAAM,IAAA,CAAK,qBAAqB,WAAW,CAAA;AAAA,QAC7C;AAAA,OACD,CAAA;AAED,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,CAAA,kDAAA,EAAqD,YAAY,EAAE,CAAA,CAAA;AAAA,OACrE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,QAAA,EAAyC;AAC1D,IAAA,MAAM,EAAE,MAAA,EAAQ,SAAA,EAAW,KAAA,KAAU,IAAA,CAAK,OAAA;AAE1C,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,wCAAA,EAA2C,QAAA,CAAS,EAAE,CAAA,CAAA,CAAG,CAAA;AAErE,IAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,MAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,MACrB,MAAA,EAAQ,SAAA;AAAA,MACR,UAAA,EAAY;AAAA,KACb,CAAA;AAED,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,aAAa,QAAQ,CAAA;AACrC,MAAA,MAAM,IAAA,GAAO,MAAM,SAAA,CAAU,QAAA,CAAS,OAAO,CAAA;AAC7C,MAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,KAAA,EAAM;AAE/B,MAAA,MAAM,OAAA,uBAAc,GAAA,EAGlB;AACF,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,OAAA,CAAQ,GAAA,CAAI,aAAA,CAAc,IAAA,CAAK,IAAI,GAAG,IAAI,CAAA;AAAA,MAC5C;AAEA,MAAA,MAAM,YAAsB,EAAC;AAE7B,MAAA,KAAA,MAAW,CAAC,QAAA,EAAU,IAAI,CAAA,IAAK,OAAA,EAAS;AACtC,QAAA,IAAI,CAAC,QAAA,CAAS,QAAA,CAAS,OAAO,CAAA,EAAG;AACjC,QAAA,IAAI,CAACA,uBAAA,CAAY,WAAA,CAAY,QAAQ,CAAA,EAAG;AAExC,QAAA,MAAM,eAAe,MAAM,IAAA,CAAK,OAAA,EAAQ,EAAG,SAAS,OAAO,CAAA;AAC3D,QAAA,MAAM,MAAA,GAASA,uBAAA,CAAY,SAAA,CAAU,WAAA,EAAa,QAAQ,CAAA;AAE1D,QAAA,IAAI,CAAC,MAAA,EAAQ;AACX,UAAA,MAAA,CAAO,IAAA;AAAA,YACL,CAAA,qCAAA,EAAwC,QAAQ,CAAA,WAAA,EAAc,QAAA,CAAS,EAAE,CAAA,CAAA;AAAA,WAC3E;AACA,UAAA;AAAA,QACF;AAEA,QAAA,IACE,QAAA,CAAS,OAAA,EAAS,KAAA,IAClB,CAAC,QAAA,CAAS,OAAA,CAAQ,KAAA,CAAM,QAAA,CAAS,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,EACjD;AACA,UAAA;AAAA,QACF;AACA,QAAA,IAAI,QAAA,CAAS,SAAS,KAAA,EAAO;AAC3B,UAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,IAAA,CAAK,KAAA,CAAM,IAAA;AAAA,YAAK,CAAA,CAAA,KAC7C,QAAA,CAAS,OAAA,CAAS,KAAA,CAAO,SAAS,CAAC;AAAA,WACrC;AACA,UAAA,IAAI,CAAC,eAAA,EAAiB;AAAA,QACxB;AAEA,QAAA,MAAM,SAAS,OAAA,CAAQ,GAAA,CAAI,aAAA,CAAc,MAAA,CAAO,MAAM,CAAC,CAAA;AACvD,QAAA,IAAI,CAAC,MAAA,EAAQ;AACX,UAAA,MAAA,CAAO,IAAA;AAAA,YACL,CAAA,8BAAA,EAAiC,QAAQ,CAAA,WAAA,EAAc,MAAA,CAAO,MAAM,CAAA,WAAA;AAAA,WACtE;AACA,UAAA;AAAA,QACF;AAEA,QAAA,MAAM,aAAa,MAAM,MAAA,CAAO,OAAA,EAAQ,EAAG,SAAS,OAAO,CAAA;AAG3D,QAAA,IAAI,gBAAA;AACJ,QAAA,MAAM,aAAA,GAAgB,OAAO,IAAA,CAAK,SAAA;AAClC,QAAA,IAAI,aAAA,IAAiB,aAAA,CAAc,MAAA,GAAS,CAAA,EAAG;AAC7C,UAAA,gBAAA,GAAmB,EAAC;AACpB,UAAA,MAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,MAAM,CAAA,EAAG,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AACzD,UAAA,KAAA,MAAW,gBAAgB,aAAA,EAAe;AACxC,YAAA,MAAM,QAAA,GAAW,aAAA;AAAA,cACf,OAAA,GAAU,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,YAAY,CAAA,CAAA,GAAK;AAAA,aAC3C;AACA,YAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACzC,YAAA,IAAI,YAAA,EAAc;AAChB,cAAA,gBAAA,CAAiB,YAAY,CAAA,GAAA,CAAK,MAAM,aAAa,OAAA,EAAQ,EAAG,SAAS,OAAO,CAAA;AAAA,YAClF,CAAA,MAAO;AACL,cAAA,MAAA,CAAO,IAAA;AAAA,gBACL,CAAA,qCAAA,EAAwC,QAAQ,CAAA,QAAA,EAAW,QAAQ,CAAA,CAAA;AAAA,eACrE;AAAA,YACF;AAAA,UACF;AACA,UAAA,IAAI,OAAO,IAAA,CAAK,gBAAgB,CAAA,CAAE,MAAA,KAAW,GAAG,gBAAA,GAAmB,KAAA,CAAA;AAAA,QACrE;AAEA,QAAA,MAAM,QAAQA,uBAAA,CAAY,UAAA;AAAA,UACxB,MAAA;AAAA,UACA,SAAA;AAAA,UACA,QAAA,CAAS,EAAA;AAAA,UACT,QAAA,CAAS,MAAA;AAAA,UACT,QAAA,CAAS,MAAA;AAAA,UACT,QAAA;AAAA,UACA;AAAA,SACF;AAEA,QAAA,MAAM,KAAA,CAAM,YAAY,KAAK,CAAA;AAC7B,QAAA,SAAA,CAAU,IAAA,CAAK,MAAM,EAAE,CAAA;AAAA,MACzB;AAEA,MAAA,MAAM,KAAA,CAAM,iBAAA,CAAkB,QAAA,CAAS,EAAA,EAAI,SAAS,CAAA;AAEpD,MAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,QAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,QACrB,QAAA,EAAA,iBAAU,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAAA,QACjC,MAAA,EAAQ,MAAA;AAAA,QACR,YAAY,SAAA,CAAU;AAAA,OACvB,CAAA;AAED,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,CAAA,wCAAA,EAA2C,QAAA,CAAS,EAAE,CAAA,SAAA,EAAO,UAAU,MAAM,CAAA,OAAA;AAAA,OAC/E;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,MAAA,IAAA,CAAK,QAAQ,MAAA,CAAO,KAAA;AAAA,QAClB,CAAA,sCAAA,EAAyC,QAAA,CAAS,EAAE,CAAA,GAAA,EAAM,OAAO,CAAA;AAAA,OACnE;AACA,MAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,QAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,QACrB,MAAA,EAAQ,OAAA;AAAA,QACR,KAAA,EAAO,OAAA;AAAA,QACP,UAAA,EAAY;AAAA,OACb,CAAA;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,qBAAqB,QAAA,EAA0C;AAC3E,IAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAM,GAAI,IAAA,CAAK,OAAA;AAE/B,IAAA,MAAA,CAAO,IAAA,CAAK,CAAA,uCAAA,EAA0C,QAAA,CAAS,EAAE,CAAA,CAAA,CAAG,CAAA;AAEpE,IAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,MAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,MACrB,MAAA,EAAQ,SAAA;AAAA,MACR,UAAA,EAAY;AAAA,KACb,CAAA;AAED,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AACxC,MAAA,MAAM,YAAsB,EAAC;AAE7B,MAAA,KAAA,MAAW,aAAa,MAAA,EAAQ;AAC9B,QAAA,MAAM,KAAKA,uBAAA,CAAY,OAAA,CAAQ,QAAA,CAAS,EAAA,EAAI,UAAU,QAAQ,CAAA;AAC9D,QAAA,MAAM,MAAM,WAAA,CAAY,EAAE,GAAG,SAAA,EAAW,IAAI,CAAA;AAC5C,QAAA,SAAA,CAAU,KAAK,EAAE,CAAA;AAAA,MACnB;AAEA,MAAA,MAAM,KAAA,CAAM,iBAAA,CAAkB,QAAA,CAAS,EAAA,EAAI,SAAS,CAAA;AAEpD,MAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,QAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,QACrB,QAAA,EAAA,iBAAU,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAAA,QACjC,MAAA,EAAQ,MAAA;AAAA,QACR,YAAY,SAAA,CAAU;AAAA,OACvB,CAAA;AAED,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,CAAA,wCAAA,EAA2C,QAAA,CAAS,EAAE,CAAA,SAAA,EAAO,UAAU,MAAM,CAAA,OAAA;AAAA,OAC/E;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,MAAA,MAAA,CAAO,KAAA;AAAA,QACL,CAAA,sCAAA,EAAyC,QAAA,CAAS,EAAE,CAAA,GAAA,EAAM,OAAO,CAAA;AAAA,OACnE;AACA,MAAA,MAAM,MAAM,gBAAA,CAAiB;AAAA,QAC3B,YAAY,QAAA,CAAS,EAAA;AAAA,QACrB,MAAA,EAAQ,OAAA;AAAA,QACR,KAAA,EAAO,OAAA;AAAA,QACP,UAAA,EAAY;AAAA,OACb,CAAA;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAAS,cAAc,CAAA,EAAmB;AACxC,EAAA,OAAO,EAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD;AAEA,SAAS,aAAa,QAAA,EAAkC;AACtD,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,MAAA,CAAO,OAAA,CAAQ,UAAU,EAAE,CAAA;AACjD,EAAA,MAAM,EAAE,MAAA,EAAQ,IAAA,EAAK,GAAI,QAAA;AAEzB,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,QAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,QAAA,EAAW,MAAM,CAAA,CAAA,CAAA;AAAA,IACjC,KAAK,WAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,KAAA,EAAQ,MAAM,CAAA,CAAA,CAAA;AAAA,IAC9B,KAAK,cAAA;AACH,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,WAAA,EAAc,MAAM,CAAA,CAAA;AAAA,IACpC,KAAK,QAAA;AAAA,IACL,KAAK,KAAA;AAAA,IACL;AACE,MAAA,OAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,MAAM,CAAA,CAAA,CAAA;AAAA;AAEnC;;;;"}
|
|
1
|
+
{"version":3,"file":"AiAssetSyncService.cjs.js","sources":["../../src/service/AiAssetSyncService.ts"],"sourcesContent":["import type {\n LoggerService,\n SchedulerService,\n UrlReaderService,\n} from '@backstage/backend-plugin-api';\nimport type { AiAssetStore } from '../database/AiAssetStore';\nimport type { ProviderConfig } from '../types';\nimport type { AiAssetProvider } from '@nospt/plugin-dev-ai-hub-node';\nimport { AssetParser } from './AssetParser';\n\ninterface Options {\n logger: LoggerService;\n store: AiAssetStore;\n scheduler: SchedulerService;\n urlReader: UrlReaderService;\n providers: ProviderConfig[];\n externalProviders: AiAssetProvider[];\n}\n\nexport class AiAssetSyncService {\n constructor(private readonly options: Options) {}\n\n async start(): Promise<void> {\n const { scheduler, logger, providers, externalProviders } = this.options;\n\n if (providers.length === 0 && externalProviders.length === 0) {\n logger.warn(\n 'dev-ai-hub: no providers configured under devAiHub.providers',\n );\n return;\n }\n\n for (const provider of providers) {\n const { frequency, timeout } = provider.schedule;\n const frequencyDuration = frequency.minutes\n ? { minutes: frequency.minutes }\n : { hours: frequency.hours ?? 1 };\n const timeoutDuration = timeout.minutes\n ? { minutes: timeout.minutes }\n : { hours: timeout.hours ?? 1 };\n\n await scheduler.scheduleTask({\n id: `dev-ai-hub-sync-${provider.id}`,\n frequency: frequencyDuration,\n timeout: timeoutDuration,\n initialDelay: { seconds: 15 },\n fn: async () => {\n await this.syncProvider(provider);\n },\n });\n\n logger.info(\n `dev-ai-hub: scheduled sync for provider \"${provider.id}\" every ${JSON.stringify(frequencyDuration)}`,\n );\n }\n\n for (const extProvider of externalProviders) {\n await scheduler.scheduleTask({\n id: `dev-ai-hub-sync-external-${extProvider.id}`,\n frequency: { minutes: 60 },\n timeout: { minutes: 5 },\n initialDelay: { seconds: 15 },\n fn: async () => {\n await this.syncExternalProvider(extProvider);\n },\n });\n\n logger.info(\n `dev-ai-hub: scheduled sync for external provider \"${extProvider.id}\"`,\n );\n }\n }\n\n async syncProvider(provider: ProviderConfig): Promise<void> {\n const { logger, urlReader, store } = this.options;\n\n logger.info(`dev-ai-hub: starting sync for provider \"${provider.id}\"`);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'syncing',\n assetCount: 0,\n });\n\n try {\n const treeUrl = buildTreeUrl(provider);\n const tree = await urlReader.readTree(treeUrl);\n const files = await tree.files();\n\n const fileMap = new Map<\n string,\n { content(): Promise<Buffer>; path: string }\n >();\n for (const file of files) {\n fileMap.set(normalizePath(file.path), file);\n }\n\n const syncedIds: string[] = [];\n\n for (const [filePath, file] of fileMap) {\n if (!filePath.endsWith('.yaml')) continue;\n\n const yamlContent = (await file.content()).toString('utf-8');\n const parsed = AssetParser.parseYaml(yamlContent, filePath);\n\n if (!parsed) {\n logger.warn(\n `dev-ai-hub: skipping invalid YAML at ${filePath} (provider ${provider.id})`,\n );\n continue;\n }\n\n if (\n provider.filters?.types &&\n !provider.filters.types.includes(parsed.meta.type)\n ) {\n continue;\n }\n if (provider.filters?.tools) {\n const hasMatchingTool = parsed.meta.tools.some(t =>\n provider.filters!.tools!.includes(t),\n );\n if (!hasMatchingTool) continue;\n }\n\n // Resolve the .md file:\n // - explicit: yaml has a content: field → use that exact path\n // - junction: find the single .md co-located in the same directory\n const yamlDir = filePath.includes('/')\n ? filePath.split('/').slice(0, -1).join('/')\n : '';\n\n let resolvedMdPath: string;\n let mdFile: { content(): Promise<Buffer>; path: string } | undefined;\n\n if (parsed.mdPath) {\n resolvedMdPath = normalizePath(parsed.mdPath);\n mdFile = fileMap.get(resolvedMdPath);\n if (!mdFile) {\n logger.warn(\n `dev-ai-hub: content file \"${parsed.mdPath}\" not found for ${filePath}, skipping`,\n );\n continue;\n }\n } else {\n const mdCandidates = [...fileMap.keys()].filter(p => {\n const dir = p.includes('/') ? p.split('/').slice(0, -1).join('/') : '';\n return dir === yamlDir && p.endsWith('.md');\n });\n if (mdCandidates.length === 1) {\n resolvedMdPath = mdCandidates[0];\n mdFile = fileMap.get(resolvedMdPath)!;\n } else if (mdCandidates.length > 1) {\n logger.warn(\n `dev-ai-hub: multiple .md files in \"${yamlDir || '.'}\" for ${filePath} — add a 'content' field to the YAML to specify which one, skipping`,\n );\n continue;\n } else {\n logger.warn(\n `dev-ai-hub: no .md file found for ${filePath}, skipping`,\n );\n continue;\n }\n }\n\n const mdContent = (await mdFile.content()).toString('utf-8');\n\n // For skills: read bundled resource files and store their content\n let resourcesContent: Record<string, string> | undefined;\n const resourcePaths = parsed.meta.resources;\n if (resourcePaths && resourcePaths.length > 0) {\n resourcesContent = {};\n for (const resourcePath of resourcePaths) {\n const fullPath = normalizePath(\n yamlDir ? `${yamlDir}/${resourcePath}` : resourcePath,\n );\n const resourceFile = fileMap.get(fullPath);\n if (resourceFile) {\n resourcesContent[resourcePath] = (await resourceFile.content()).toString('utf-8');\n } else {\n logger.warn(\n `dev-ai-hub: resource file not found: ${fullPath} (asset ${filePath})`,\n );\n }\n }\n if (Object.keys(resourcesContent).length === 0) resourcesContent = undefined;\n }\n\n const asset = AssetParser.buildAsset(\n parsed,\n mdContent,\n provider.id,\n provider.target,\n provider.branch,\n filePath,\n resolvedMdPath,\n resourcesContent,\n );\n\n await store.upsertAsset(asset);\n syncedIds.push(asset.id);\n }\n\n await store.deleteAssetsNotIn(provider.id, syncedIds);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n lastSync: new Date().toISOString(),\n status: 'idle',\n assetCount: syncedIds.length,\n });\n\n logger.info(\n `dev-ai-hub: sync complete for provider \"${provider.id}\" — ${syncedIds.length} assets`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.options.logger.error(\n `dev-ai-hub: sync failed for provider \"${provider.id}\": ${message}`,\n );\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'error',\n error: message,\n assetCount: 0,\n });\n }\n }\n\n private async syncExternalProvider(provider: AiAssetProvider): Promise<void> {\n const { logger, store } = this.options;\n\n logger.info(`dev-ai-hub: syncing external provider \"${provider.id}\"`);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'syncing',\n assetCount: 0,\n });\n\n try {\n const assets = await provider.getAssets();\n const syncedIds: string[] = [];\n\n for (const assetData of assets) {\n const id = AssetParser.buildId(provider.id, assetData.yamlPath);\n await store.upsertAsset({ ...assetData, id });\n syncedIds.push(id);\n }\n\n await store.deleteAssetsNotIn(provider.id, syncedIds);\n\n await store.upsertSyncStatus({\n providerId: provider.id,\n lastSync: new Date().toISOString(),\n status: 'idle',\n assetCount: syncedIds.length,\n });\n\n logger.info(\n `dev-ai-hub: external sync complete for \"${provider.id}\" — ${syncedIds.length} assets`,\n );\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(\n `dev-ai-hub: external sync failed for \"${provider.id}\": ${message}`,\n );\n await store.upsertSyncStatus({\n providerId: provider.id,\n status: 'error',\n error: message,\n assetCount: 0,\n });\n }\n }\n}\n\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\//, '');\n}\n\nfunction buildTreeUrl(provider: ProviderConfig): string {\n const base = provider.target.replace(/\\.git$/, '');\n const { branch, type } = provider;\n\n switch (type) {\n case 'gitlab':\n return `${base}/-/tree/${branch}/`;\n case 'bitbucket':\n return `${base}/src/${branch}/`;\n case 'azure-devops':\n return `${base}?version=GB${branch}`;\n case 'github':\n case 'git':\n default:\n return `${base}/tree/${branch}/`;\n }\n}\n"],"names":["AssetParser"],"mappings":"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"}
|
|
@@ -26,18 +26,15 @@ class AssetParser {
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
const meta = result.data;
|
|
29
|
-
const
|
|
30
|
-
const mdPath = path__default.default.posix.join(
|
|
31
|
-
path__default.default.posix.dirname(yamlFilePath),
|
|
32
|
-
mdReference
|
|
33
|
-
);
|
|
29
|
+
const mdPath = meta.content ? path__default.default.posix.join(path__default.default.posix.dirname(yamlFilePath), meta.content) : void 0;
|
|
34
30
|
return { meta, mdPath, yamlRaw: yamlContent };
|
|
35
31
|
}
|
|
36
32
|
/**
|
|
37
33
|
* Build the full AiAssetInput from parsed metadata + raw markdown content.
|
|
38
34
|
* The mdContent is stored verbatim — never modified.
|
|
35
|
+
* actualMdPath is the resolved path of the md file as found by the sync service.
|
|
39
36
|
*/
|
|
40
|
-
static buildAsset(parsed, mdContent, providerId, repoUrl, branch, yamlFilePath, resourcesContent) {
|
|
37
|
+
static buildAsset(parsed, mdContent, providerId, repoUrl, branch, yamlFilePath, actualMdPath, resourcesContent) {
|
|
41
38
|
const { meta } = parsed;
|
|
42
39
|
const metadata = {};
|
|
43
40
|
if (meta.resources) metadata.resources = meta.resources;
|
|
@@ -62,7 +59,7 @@ class AssetParser {
|
|
|
62
59
|
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
63
60
|
resourcesContent,
|
|
64
61
|
yamlPath: yamlFilePath,
|
|
65
|
-
mdPath:
|
|
62
|
+
mdPath: actualMdPath,
|
|
66
63
|
repoUrl,
|
|
67
64
|
branch
|
|
68
65
|
};
|
|
@@ -71,20 +68,6 @@ class AssetParser {
|
|
|
71
68
|
const normalized = yamlPath.replace(/\\/g, "/").replace(/^\//, "");
|
|
72
69
|
return Buffer.from(`${providerId}:${normalized}`).toString("base64url");
|
|
73
70
|
}
|
|
74
|
-
/** Known top-level directories that contain assets */
|
|
75
|
-
static ASSET_DIRS = [
|
|
76
|
-
"instructions/",
|
|
77
|
-
"agents/",
|
|
78
|
-
"skills/",
|
|
79
|
-
"workflows/"
|
|
80
|
-
];
|
|
81
|
-
/** True if the file is in a known asset directory (root or .github/) */
|
|
82
|
-
static isAssetFile(filePath) {
|
|
83
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
84
|
-
return AssetParser.ASSET_DIRS.some(
|
|
85
|
-
(dir) => normalized.startsWith(dir) || normalized.startsWith(`.github/${dir}`)
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
71
|
}
|
|
89
72
|
|
|
90
73
|
exports.AssetParser = AssetParser;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetParser.cjs.js","sources":["../../src/service/AssetParser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport path from 'path';\nimport { AiAssetFrontmatterSchema } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetFrontmatter } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetInput } from '../types';\n\nexport interface ParsedAssetMeta {\n meta: AiAssetFrontmatter;\n
|
|
1
|
+
{"version":3,"file":"AssetParser.cjs.js","sources":["../../src/service/AssetParser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport path from 'path';\nimport { AiAssetFrontmatterSchema } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetFrontmatter } from '@nospt/plugin-dev-ai-hub-common';\nimport type { AiAssetInput } from '../types';\n\nexport interface ParsedAssetMeta {\n meta: AiAssetFrontmatter;\n /**\n * Resolved explicit .md path — only set when the YAML has a `content:` field.\n * When absent, the sync service resolves the md file by directory junction.\n */\n mdPath?: string;\n yamlRaw: string;\n}\n\nexport class AssetParser {\n /**\n * Parse the YAML metadata file (the envelope).\n * Returns null when the YAML is invalid or missing required fields.\n */\n static parseYaml(\n yamlContent: string,\n yamlFilePath: string,\n ): ParsedAssetMeta | null {\n let data: unknown;\n try {\n data = yaml.load(yamlContent);\n } catch {\n return null;\n }\n\n const result = AiAssetFrontmatterSchema.safeParse(data);\n if (!result.success) {\n return null;\n }\n\n const meta = result.data;\n\n // Only resolve mdPath when content: is explicitly set in the YAML.\n // Otherwise the sync service pairs yaml + md by directory junction.\n const mdPath = meta.content\n ? path.posix.join(path.posix.dirname(yamlFilePath), meta.content)\n : undefined;\n\n return { meta, mdPath, yamlRaw: yamlContent };\n }\n\n /**\n * Build the full AiAssetInput from parsed metadata + raw markdown content.\n * The mdContent is stored verbatim — never modified.\n * actualMdPath is the resolved path of the md file as found by the sync service.\n */\n static buildAsset(\n parsed: ParsedAssetMeta,\n mdContent: string,\n providerId: string,\n repoUrl: string,\n branch: string,\n yamlFilePath: string,\n actualMdPath: string,\n resourcesContent?: Record<string, string>,\n ): AiAssetInput {\n const { meta } = parsed;\n\n // Store extra fields in metadata (used for display/reference)\n const metadata: Record<string, unknown> = {};\n if (meta.resources) metadata.resources = meta.resources;\n if ((meta as any).mcpServers) metadata.mcpServers = (meta as any).mcpServers;\n if ((meta as any).steps) metadata.steps = (meta as any).steps;\n\n return {\n id: AssetParser.buildId(providerId, yamlFilePath),\n providerId,\n name: meta.name,\n label: meta.label,\n description: meta.description,\n type: meta.type,\n tools: meta.tools,\n tags: meta.tags ?? [],\n author: meta.author ?? 'Unknown',\n icon: meta.icon,\n version: meta.version ?? '1.0.0',\n installPath: meta.installPath,\n installPaths: meta.installPaths,\n content: mdContent,\n yamlRaw: parsed.yamlRaw,\n metadata: Object.keys(metadata).length > 0 ? metadata : undefined,\n resourcesContent,\n yamlPath: yamlFilePath,\n mdPath: actualMdPath,\n repoUrl,\n branch,\n };\n }\n\n static buildId(providerId: string, yamlPath: string): string {\n // Simple deterministic ID: provider + normalized path\n const normalized = yamlPath.replace(/\\\\/g, '/').replace(/^\\//, '');\n return Buffer.from(`${providerId}:${normalized}`).toString('base64url');\n }\n\n}\n"],"names":["yaml","AiAssetFrontmatterSchema","path"],"mappings":";;;;;;;;;;;AAgBO,MAAM,WAAA,CAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAKvB,OAAO,SAAA,CACL,WAAA,EACA,YAAA,EACwB;AACxB,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAOA,qBAAA,CAAK,KAAK,WAAW,CAAA;AAAA,IAC9B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,MAAA,GAASC,6CAAA,CAAyB,SAAA,CAAU,IAAI,CAAA;AACtD,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,OAAO,MAAA,CAAO,IAAA;AAIpB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,GAChBC,qBAAA,CAAK,KAAA,CAAM,IAAA,CAAKA,qBAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,YAAY,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,GAC9D,MAAA;AAEJ,IAAA,OAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,WAAA,EAAY;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,WACL,MAAA,EACA,SAAA,EACA,YACA,OAAA,EACA,MAAA,EACA,YAAA,EACA,YAAA,EACA,gBAAA,EACc;AACd,IAAA,MAAM,EAAE,MAAK,GAAI,MAAA;AAGjB,IAAA,MAAM,WAAoC,EAAC;AAC3C,IAAA,IAAI,IAAA,CAAK,SAAA,EAAW,QAAA,CAAS,SAAA,GAAY,IAAA,CAAK,SAAA;AAC9C,IAAA,IAAK,IAAA,CAAa,UAAA,EAAY,QAAA,CAAS,UAAA,GAAc,IAAA,CAAa,UAAA;AAClE,IAAA,IAAK,IAAA,CAAa,KAAA,EAAO,QAAA,CAAS,KAAA,GAAS,IAAA,CAAa,KAAA;AAExD,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,WAAA,CAAY,OAAA,CAAQ,UAAA,EAAY,YAAY,CAAA;AAAA,MAChD,UAAA;AAAA,MACA,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,aAAa,IAAA,CAAK,WAAA;AAAA,MAClB,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,IAAA,EAAM,IAAA,CAAK,IAAA,IAAQ,EAAC;AAAA,MACpB,MAAA,EAAQ,KAAK,MAAA,IAAU,SAAA;AAAA,MACvB,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAA,EAAS,KAAK,OAAA,IAAW,OAAA;AAAA,MACzB,aAAa,IAAA,CAAK,WAAA;AAAA,MAClB,cAAc,IAAA,CAAK,YAAA;AAAA,MACnB,OAAA,EAAS,SAAA;AAAA,MACT,SAAS,MAAA,CAAO,OAAA;AAAA,MAChB,UAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,CAAE,MAAA,GAAS,IAAI,QAAA,GAAW,MAAA;AAAA,MACxD,gBAAA;AAAA,MACA,QAAA,EAAU,YAAA;AAAA,MACV,MAAA,EAAQ,YAAA;AAAA,MACR,OAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA,EAEA,OAAO,OAAA,CAAQ,UAAA,EAAoB,QAAA,EAA0B;AAE3D,IAAA,MAAM,UAAA,GAAa,SAAS,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AACjE,IAAA,OAAO,MAAA,CAAO,KAAK,CAAA,EAAG,UAAU,IAAI,UAAU,CAAA,CAAE,CAAA,CAAE,QAAA,CAAS,WAAW,CAAA;AAAA,EACxE;AAEF;;;;"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nospt/plugin-dev-ai-hub-backend",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Backend plugin for Dev AI Hub",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.cjs.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/nosportugal/backstage-plugin-dev-ai-hub"
|
|
11
|
+
},
|
|
8
12
|
"publishConfig": {
|
|
9
13
|
"access": "public"
|
|
10
14
|
},
|
|
@@ -44,29 +48,32 @@
|
|
|
44
48
|
"lint": "backstage-cli package lint",
|
|
45
49
|
"prepack": "backstage-cli package prepack",
|
|
46
50
|
"postpack": "backstage-cli package postpack",
|
|
47
|
-
"test": "backstage-cli package test --passWithNoTests",
|
|
51
|
+
"test": "backstage-cli package test --passWithNoTests --watchAll=false",
|
|
48
52
|
"tsc": "tsc --noEmit"
|
|
49
53
|
},
|
|
50
54
|
"dependencies": {
|
|
51
|
-
"@backstage/backend-plugin-api": "
|
|
55
|
+
"@backstage/backend-plugin-api": "^1.7.0",
|
|
52
56
|
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
53
|
-
"@nospt/plugin-dev-ai-hub-common": "
|
|
54
|
-
"@nospt/plugin-dev-ai-hub-node": "
|
|
57
|
+
"@nospt/plugin-dev-ai-hub-common": "^0.2.1",
|
|
58
|
+
"@nospt/plugin-dev-ai-hub-node": "^0.2.1",
|
|
55
59
|
"archiver": "^6.0.0",
|
|
56
|
-
"express": "^
|
|
60
|
+
"express": "^5.2.1",
|
|
57
61
|
"js-yaml": "^4.1.0",
|
|
58
62
|
"zod": "^3.22.0"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
61
|
-
"@backstage/backend-defaults": "
|
|
62
|
-
"@backstage/backend-test-utils": "
|
|
63
|
-
"@backstage/cli": "
|
|
65
|
+
"@backstage/backend-defaults": "^0.17.0",
|
|
66
|
+
"@backstage/backend-test-utils": "^1.11.0",
|
|
67
|
+
"@backstage/cli": "^0.36.1",
|
|
64
68
|
"@types/archiver": "^6.0.0",
|
|
65
69
|
"@types/express": "^4.17.21",
|
|
66
70
|
"@types/jest": "^29.0.0",
|
|
67
71
|
"@types/js-yaml": "^4.0.9",
|
|
72
|
+
"@types/supertest": "^6.0.0",
|
|
73
|
+
"better-sqlite3": "^12.0.0",
|
|
68
74
|
"jest": "^29.0.0",
|
|
69
75
|
"jest-environment-jsdom": "^29.0.0",
|
|
70
|
-
"knex": "^3.1.0"
|
|
76
|
+
"knex": "^3.1.0",
|
|
77
|
+
"supertest": "^7.0.0"
|
|
71
78
|
}
|
|
72
79
|
}
|
package/package.json-prepack
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nospt/plugin-dev-ai-hub-backend",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Backend plugin for Dev AI Hub",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/nosportugal/backstage-plugin-dev-ai-hub"
|
|
11
|
+
},
|
|
8
12
|
"publishConfig": {
|
|
9
13
|
"access": "public"
|
|
10
14
|
},
|
|
@@ -36,29 +40,32 @@
|
|
|
36
40
|
"lint": "backstage-cli package lint",
|
|
37
41
|
"prepack": "backstage-cli package prepack",
|
|
38
42
|
"postpack": "backstage-cli package postpack",
|
|
39
|
-
"test": "backstage-cli package test --passWithNoTests",
|
|
43
|
+
"test": "backstage-cli package test --passWithNoTests --watchAll=false",
|
|
40
44
|
"tsc": "tsc --noEmit"
|
|
41
45
|
},
|
|
42
46
|
"dependencies": {
|
|
43
|
-
"@backstage/backend-plugin-api": "
|
|
47
|
+
"@backstage/backend-plugin-api": "^1.7.0",
|
|
44
48
|
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
45
|
-
"@nospt/plugin-dev-ai-hub-common": "
|
|
46
|
-
"@nospt/plugin-dev-ai-hub-node": "
|
|
49
|
+
"@nospt/plugin-dev-ai-hub-common": "workspace:^",
|
|
50
|
+
"@nospt/plugin-dev-ai-hub-node": "workspace:^",
|
|
47
51
|
"archiver": "^6.0.0",
|
|
48
|
-
"express": "^
|
|
52
|
+
"express": "^5.2.1",
|
|
49
53
|
"js-yaml": "^4.1.0",
|
|
50
54
|
"zod": "^3.22.0"
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
53
|
-
"@backstage/backend-defaults": "
|
|
54
|
-
"@backstage/backend-test-utils": "
|
|
55
|
-
"@backstage/cli": "
|
|
57
|
+
"@backstage/backend-defaults": "^0.17.0",
|
|
58
|
+
"@backstage/backend-test-utils": "^1.11.0",
|
|
59
|
+
"@backstage/cli": "^0.36.1",
|
|
56
60
|
"@types/archiver": "^6.0.0",
|
|
57
61
|
"@types/express": "^4.17.21",
|
|
58
62
|
"@types/jest": "^29.0.0",
|
|
59
63
|
"@types/js-yaml": "^4.0.9",
|
|
64
|
+
"@types/supertest": "^6.0.0",
|
|
65
|
+
"better-sqlite3": "^12.0.0",
|
|
60
66
|
"jest": "^29.0.0",
|
|
61
67
|
"jest-environment-jsdom": "^29.0.0",
|
|
62
|
-
"knex": "^3.1.0"
|
|
68
|
+
"knex": "^3.1.0",
|
|
69
|
+
"supertest": "^7.0.0"
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for the Express router created by `createRouter`.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that all route patterns are correctly resolved by
|
|
5
|
+
* path-to-regexp (via Express), that :id parameters are captured, and that the
|
|
6
|
+
* right status codes are returned — without starting a real Backstage backend.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import request from 'supertest';
|
|
11
|
+
import { createRouter } from './router';
|
|
12
|
+
import type { AiAssetStore } from './database/AiAssetStore';
|
|
13
|
+
import type { AiAssetSyncService } from './service/AiAssetSyncService';
|
|
14
|
+
import type { LoggerService } from '@backstage/backend-plugin-api';
|
|
15
|
+
import type { ProviderConfig } from './types';
|
|
16
|
+
|
|
17
|
+
// ── Fixtures ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const ASSET_SUMMARY = {
|
|
20
|
+
id: 'asset-123',
|
|
21
|
+
providerId: 'provider-1',
|
|
22
|
+
name: 'Test Instruction',
|
|
23
|
+
description: 'A test asset',
|
|
24
|
+
type: 'instruction',
|
|
25
|
+
tools: ['github-copilot'],
|
|
26
|
+
tags: ['test'],
|
|
27
|
+
author: 'tester',
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
installCount: 5,
|
|
30
|
+
syncedAt: '2026-01-01T00:00:00Z',
|
|
31
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
32
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
const ASSET_FULL = {
|
|
36
|
+
...ASSET_SUMMARY,
|
|
37
|
+
content: '# Test Instruction\n\nSome content.',
|
|
38
|
+
yamlRaw: 'name: test',
|
|
39
|
+
yamlPath: 'instructions/test.yaml',
|
|
40
|
+
mdPath: 'instructions/test.md',
|
|
41
|
+
repoUrl: 'https://github.com/org/repo',
|
|
42
|
+
branch: 'main',
|
|
43
|
+
resourcesContent: null,
|
|
44
|
+
installPaths: null,
|
|
45
|
+
installPath: null,
|
|
46
|
+
icon: null,
|
|
47
|
+
label: null,
|
|
48
|
+
metadata: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const PROVIDER: ProviderConfig = {
|
|
52
|
+
id: 'provider-1',
|
|
53
|
+
type: 'github',
|
|
54
|
+
target: 'https://github.com/org/repo',
|
|
55
|
+
branch: 'main',
|
|
56
|
+
schedule: {
|
|
57
|
+
frequency: { hours: 1 },
|
|
58
|
+
timeout: { minutes: 5 },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function makeStoreMock(overrides: Partial<AiAssetStore> = {}): AiAssetStore {
|
|
65
|
+
return {
|
|
66
|
+
listAssets: jest.fn().mockResolvedValue({ items: [ASSET_SUMMARY], totalCount: 1 }),
|
|
67
|
+
getAsset: jest.fn().mockImplementation(async (id: string) =>
|
|
68
|
+
id === 'asset-123' ? ASSET_FULL : null,
|
|
69
|
+
),
|
|
70
|
+
incrementInstallCount: jest.fn().mockResolvedValue(undefined),
|
|
71
|
+
getAllSyncStatuses: jest.fn().mockResolvedValue([
|
|
72
|
+
{ providerId: 'provider-1', status: 'idle', assetCount: 1 },
|
|
73
|
+
]),
|
|
74
|
+
getSyncStatus: jest.fn().mockImplementation(async (id: string) =>
|
|
75
|
+
id === 'provider-1' ? { providerId: 'provider-1', status: 'idle', assetCount: 1 } : null,
|
|
76
|
+
),
|
|
77
|
+
getStats: jest.fn().mockResolvedValue({
|
|
78
|
+
totalAssets: 1,
|
|
79
|
+
byType: { instruction: 1, agent: 0, skill: 0, workflow: 0 },
|
|
80
|
+
byTool: { 'github-copilot': 1 },
|
|
81
|
+
byProvider: { 'provider-1': 1 },
|
|
82
|
+
}),
|
|
83
|
+
// remaining methods not needed by router
|
|
84
|
+
upsertAsset: jest.fn(),
|
|
85
|
+
deleteAssetsNotIn: jest.fn(),
|
|
86
|
+
upsertSyncStatus: jest.fn(),
|
|
87
|
+
...overrides,
|
|
88
|
+
} as unknown as AiAssetStore;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeSyncServiceMock(): AiAssetSyncService {
|
|
92
|
+
return {
|
|
93
|
+
syncProvider: jest.fn().mockResolvedValue(undefined),
|
|
94
|
+
start: jest.fn().mockResolvedValue(undefined),
|
|
95
|
+
} as unknown as AiAssetSyncService;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const noopLogger: LoggerService = {
|
|
99
|
+
error: jest.fn(),
|
|
100
|
+
warn: jest.fn(),
|
|
101
|
+
info: jest.fn(),
|
|
102
|
+
debug: jest.fn(),
|
|
103
|
+
child: jest.fn().mockReturnThis(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Test app factory ──────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function makeApp(storeOverrides: Partial<AiAssetStore> = {}) {
|
|
109
|
+
const store = makeStoreMock(storeOverrides);
|
|
110
|
+
const syncService = makeSyncServiceMock();
|
|
111
|
+
|
|
112
|
+
const router = createRouter({
|
|
113
|
+
logger: noopLogger,
|
|
114
|
+
store,
|
|
115
|
+
syncService,
|
|
116
|
+
providers: [PROVIDER],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const app = express();
|
|
120
|
+
app.use(express.json());
|
|
121
|
+
app.use(router);
|
|
122
|
+
return { app, store, syncService };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
describe('router smoke tests (path-to-regexp route matching)', () => {
|
|
128
|
+
// ── /assets ────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe('GET /assets', () => {
|
|
131
|
+
it('returns 200 with paginated asset list', async () => {
|
|
132
|
+
const { app } = makeApp();
|
|
133
|
+
const res = await request(app).get('/assets');
|
|
134
|
+
expect(res.status).toBe(200);
|
|
135
|
+
expect(res.body).toMatchObject({ items: expect.any(Array), totalCount: 1 });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('forwards query parameters to the store', async () => {
|
|
139
|
+
const { app, store } = makeApp();
|
|
140
|
+
await request(app).get('/assets?type=instruction&search=test&page=2&pageSize=5');
|
|
141
|
+
expect(store.listAssets).toHaveBeenCalledWith(
|
|
142
|
+
expect.objectContaining({ type: 'instruction', search: 'test', page: 2, pageSize: 5 }),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('GET /assets/:id', () => {
|
|
148
|
+
it('returns 200 for a known asset and captures the :id param', async () => {
|
|
149
|
+
const { app, store } = makeApp();
|
|
150
|
+
const res = await request(app).get('/assets/asset-123');
|
|
151
|
+
expect(res.status).toBe(200);
|
|
152
|
+
expect(store.getAsset).toHaveBeenCalledWith('asset-123');
|
|
153
|
+
expect(res.body.id).toBe('asset-123');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns 404 for an unknown asset', async () => {
|
|
157
|
+
const { app } = makeApp();
|
|
158
|
+
const res = await request(app).get('/assets/does-not-exist');
|
|
159
|
+
expect(res.status).toBe(404);
|
|
160
|
+
expect(res.body.error).toMatch(/not found/i);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('GET /assets/:id/raw', () => {
|
|
165
|
+
it('returns 200 with text/markdown content type', async () => {
|
|
166
|
+
const { app } = makeApp();
|
|
167
|
+
const res = await request(app).get('/assets/asset-123/raw');
|
|
168
|
+
expect(res.status).toBe(200);
|
|
169
|
+
expect(res.headers['content-type']).toMatch(/text\/markdown/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('returns 404 when asset does not exist', async () => {
|
|
173
|
+
const { app } = makeApp();
|
|
174
|
+
const res = await request(app).get('/assets/missing/raw');
|
|
175
|
+
expect(res.status).toBe(404);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('GET /assets/:id/download', () => {
|
|
180
|
+
it('returns 200 with Content-Disposition header for non-skill assets', async () => {
|
|
181
|
+
const { app } = makeApp();
|
|
182
|
+
const res = await request(app).get('/assets/asset-123/download');
|
|
183
|
+
expect(res.status).toBe(200);
|
|
184
|
+
expect(res.headers['content-disposition']).toMatch(/attachment/);
|
|
185
|
+
expect(res.headers['content-disposition']).toMatch(/\.md/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns 404 when asset does not exist', async () => {
|
|
189
|
+
const { app } = makeApp();
|
|
190
|
+
const res = await request(app).get('/assets/missing/download');
|
|
191
|
+
expect(res.status).toBe(404);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('POST /assets/:id/track-install', () => {
|
|
196
|
+
it('returns 204 and increments count for known asset', async () => {
|
|
197
|
+
const { app, store } = makeApp();
|
|
198
|
+
const res = await request(app).post('/assets/asset-123/track-install');
|
|
199
|
+
expect(res.status).toBe(204);
|
|
200
|
+
expect(store.incrementInstallCount).toHaveBeenCalledWith('asset-123');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns 404 for unknown asset', async () => {
|
|
204
|
+
const { app } = makeApp();
|
|
205
|
+
const res = await request(app).post('/assets/missing/track-install');
|
|
206
|
+
expect(res.status).toBe(404);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── /providers ─────────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe('GET /providers', () => {
|
|
213
|
+
it('returns 200 with provider list', async () => {
|
|
214
|
+
const { app } = makeApp();
|
|
215
|
+
const res = await request(app).get('/providers');
|
|
216
|
+
expect(res.status).toBe(200);
|
|
217
|
+
expect(Array.isArray(res.body)).toBe(true);
|
|
218
|
+
expect(res.body[0]).toMatchObject({ id: 'provider-1' });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('GET /providers/:id/status', () => {
|
|
223
|
+
it('returns 200 for a known provider and captures :id param', async () => {
|
|
224
|
+
const { app, store } = makeApp();
|
|
225
|
+
const res = await request(app).get('/providers/provider-1/status');
|
|
226
|
+
expect(res.status).toBe(200);
|
|
227
|
+
expect(store.getSyncStatus).toHaveBeenCalledWith('provider-1');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns 404 for an unknown provider', async () => {
|
|
231
|
+
const { app } = makeApp();
|
|
232
|
+
const res = await request(app).get('/providers/unknown-provider/status');
|
|
233
|
+
expect(res.status).toBe(404);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('POST /providers/:id/sync', () => {
|
|
238
|
+
it('returns 200 and fires background sync for known provider', async () => {
|
|
239
|
+
const { app } = makeApp();
|
|
240
|
+
const res = await request(app).post('/providers/provider-1/sync');
|
|
241
|
+
expect(res.status).toBe(200);
|
|
242
|
+
expect(res.body.providerId).toBe('provider-1');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('returns 404 for an unknown provider', async () => {
|
|
246
|
+
const { app } = makeApp();
|
|
247
|
+
const res = await request(app).post('/providers/unknown-provider/sync');
|
|
248
|
+
expect(res.status).toBe(404);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── /stats ─────────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe('GET /stats', () => {
|
|
255
|
+
it('returns 200 with stats object', async () => {
|
|
256
|
+
const { app } = makeApp();
|
|
257
|
+
const res = await request(app).get('/stats');
|
|
258
|
+
expect(res.status).toBe(200);
|
|
259
|
+
expect(res.body).toMatchObject({ totalAssets: 1 });
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -99,7 +99,6 @@ export class AiAssetSyncService {
|
|
|
99
99
|
|
|
100
100
|
for (const [filePath, file] of fileMap) {
|
|
101
101
|
if (!filePath.endsWith('.yaml')) continue;
|
|
102
|
-
if (!AssetParser.isAssetFile(filePath)) continue;
|
|
103
102
|
|
|
104
103
|
const yamlContent = (await file.content()).toString('utf-8');
|
|
105
104
|
const parsed = AssetParser.parseYaml(yamlContent, filePath);
|
|
@@ -124,12 +123,44 @@ export class AiAssetSyncService {
|
|
|
124
123
|
if (!hasMatchingTool) continue;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
|
|
126
|
+
// Resolve the .md file:
|
|
127
|
+
// - explicit: yaml has a content: field → use that exact path
|
|
128
|
+
// - junction: find the single .md co-located in the same directory
|
|
129
|
+
const yamlDir = filePath.includes('/')
|
|
130
|
+
? filePath.split('/').slice(0, -1).join('/')
|
|
131
|
+
: '';
|
|
132
|
+
|
|
133
|
+
let resolvedMdPath: string;
|
|
134
|
+
let mdFile: { content(): Promise<Buffer>; path: string } | undefined;
|
|
135
|
+
|
|
136
|
+
if (parsed.mdPath) {
|
|
137
|
+
resolvedMdPath = normalizePath(parsed.mdPath);
|
|
138
|
+
mdFile = fileMap.get(resolvedMdPath);
|
|
139
|
+
if (!mdFile) {
|
|
140
|
+
logger.warn(
|
|
141
|
+
`dev-ai-hub: content file "${parsed.mdPath}" not found for ${filePath}, skipping`,
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
const mdCandidates = [...fileMap.keys()].filter(p => {
|
|
147
|
+
const dir = p.includes('/') ? p.split('/').slice(0, -1).join('/') : '';
|
|
148
|
+
return dir === yamlDir && p.endsWith('.md');
|
|
149
|
+
});
|
|
150
|
+
if (mdCandidates.length === 1) {
|
|
151
|
+
resolvedMdPath = mdCandidates[0];
|
|
152
|
+
mdFile = fileMap.get(resolvedMdPath)!;
|
|
153
|
+
} else if (mdCandidates.length > 1) {
|
|
154
|
+
logger.warn(
|
|
155
|
+
`dev-ai-hub: multiple .md files in "${yamlDir || '.'}" for ${filePath} — add a 'content' field to the YAML to specify which one, skipping`,
|
|
156
|
+
);
|
|
157
|
+
continue;
|
|
158
|
+
} else {
|
|
159
|
+
logger.warn(
|
|
160
|
+
`dev-ai-hub: no .md file found for ${filePath}, skipping`,
|
|
161
|
+
);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
const mdContent = (await mdFile.content()).toString('utf-8');
|
|
@@ -139,7 +170,6 @@ export class AiAssetSyncService {
|
|
|
139
170
|
const resourcePaths = parsed.meta.resources;
|
|
140
171
|
if (resourcePaths && resourcePaths.length > 0) {
|
|
141
172
|
resourcesContent = {};
|
|
142
|
-
const yamlDir = filePath.split('/').slice(0, -1).join('/');
|
|
143
173
|
for (const resourcePath of resourcePaths) {
|
|
144
174
|
const fullPath = normalizePath(
|
|
145
175
|
yamlDir ? `${yamlDir}/${resourcePath}` : resourcePath,
|
|
@@ -163,6 +193,7 @@ export class AiAssetSyncService {
|
|
|
163
193
|
provider.target,
|
|
164
194
|
provider.branch,
|
|
165
195
|
filePath,
|
|
196
|
+
resolvedMdPath,
|
|
166
197
|
resourcesContent,
|
|
167
198
|
);
|
|
168
199
|
|
|
@@ -26,9 +26,9 @@ describe('AssetParser.parseYaml', () => {
|
|
|
26
26
|
expect(result!.yamlRaw).toBe(VALID_YAML);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('
|
|
29
|
+
it('leaves mdPath undefined when no content field is set', () => {
|
|
30
30
|
const result = AssetParser.parseYaml(VALID_YAML, 'instructions/my-instruction.yaml');
|
|
31
|
-
expect(result!.mdPath).
|
|
31
|
+
expect(result!.mdPath).toBeUndefined();
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('uses the content field to resolve mdPath when present', () => {
|
|
@@ -146,38 +146,6 @@ describe('AssetParser.buildId', () => {
|
|
|
146
146
|
});
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
describe('AssetParser.isAssetFile', () => {
|
|
150
|
-
it.each([
|
|
151
|
-
'instructions/my-rule.yaml',
|
|
152
|
-
'agents/my-agent.yaml',
|
|
153
|
-
'skills/my-skill.yaml',
|
|
154
|
-
'workflows/my-workflow.yaml',
|
|
155
|
-
'.github/instructions/my-rule.yaml',
|
|
156
|
-
'.github/agents/my-agent.yaml',
|
|
157
|
-
'.github/skills/my-skill.yaml',
|
|
158
|
-
'.github/workflows/my-workflow.yaml',
|
|
159
|
-
])('returns true for %s', filePath => {
|
|
160
|
-
expect(AssetParser.isAssetFile(filePath)).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it.each([
|
|
164
|
-
'random/file.yaml',
|
|
165
|
-
'README.md',
|
|
166
|
-
'instruction.yaml',
|
|
167
|
-
'some/instructions/nested.yaml',
|
|
168
|
-
'',
|
|
169
|
-
])('returns false for %s', filePath => {
|
|
170
|
-
expect(AssetParser.isAssetFile(filePath)).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('normalises backslashes (Windows paths)', () => {
|
|
174
|
-
expect(AssetParser.isAssetFile('instructions\\my-rule.yaml')).toBe(true);
|
|
175
|
-
expect(AssetParser.isAssetFile('agents\\my-agent.yaml')).toBe(true);
|
|
176
|
-
expect(AssetParser.isAssetFile('.github\\instructions\\my-rule.yaml')).toBe(true);
|
|
177
|
-
expect(AssetParser.isAssetFile('.github\\agents\\my-agent.yaml')).toBe(true);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
149
|
describe('AssetParser.buildAsset', () => {
|
|
182
150
|
const parsed = AssetParser.parseYaml(VALID_YAML, 'instructions/my-instruction.yaml')!;
|
|
183
151
|
|
|
@@ -189,6 +157,7 @@ describe('AssetParser.buildAsset', () => {
|
|
|
189
157
|
'https://github.com/org/repo',
|
|
190
158
|
'main',
|
|
191
159
|
'instructions/my-instruction.yaml',
|
|
160
|
+
'instructions/my-instruction.md',
|
|
192
161
|
);
|
|
193
162
|
|
|
194
163
|
expect(asset.id).toBe(AssetParser.buildId('my-provider', 'instructions/my-instruction.yaml'));
|
|
@@ -210,14 +179,14 @@ describe('AssetParser.buildAsset', () => {
|
|
|
210
179
|
it('sets metadata.mcpServers when mcpServers is present in the YAML', () => {
|
|
211
180
|
const yaml = `${VALID_YAML}mcpServers:\n my-server:\n url: http://localhost\n`;
|
|
212
181
|
const parsedWithMcp = AssetParser.parseYaml(yaml, 'agents/agent.yaml')!;
|
|
213
|
-
const asset = AssetParser.buildAsset(parsedWithMcp, '', 'p', 'url', 'main', 'agents/agent.yaml');
|
|
182
|
+
const asset = AssetParser.buildAsset(parsedWithMcp, '', 'p', 'url', 'main', 'agents/agent.yaml', 'agents/agent.md');
|
|
214
183
|
expect(asset.metadata?.mcpServers).toEqual({ 'my-server': { url: 'http://localhost' } });
|
|
215
184
|
});
|
|
216
185
|
|
|
217
186
|
it('sets metadata.steps when steps is present in the YAML', () => {
|
|
218
187
|
const yaml = `${VALID_YAML}steps:\n - name: step1\n action: do-something\n`;
|
|
219
188
|
const parsedWithSteps = AssetParser.parseYaml(yaml, 'workflows/w.yaml')!;
|
|
220
|
-
const asset = AssetParser.buildAsset(parsedWithSteps, '', 'p', 'url', 'main', 'workflows/w.yaml');
|
|
189
|
+
const asset = AssetParser.buildAsset(parsedWithSteps, '', 'p', 'url', 'main', 'workflows/w.yaml', 'workflows/w.md');
|
|
221
190
|
expect(asset.metadata?.steps).toEqual([{ name: 'step1', action: 'do-something' }]);
|
|
222
191
|
});
|
|
223
192
|
|
|
@@ -229,6 +198,7 @@ describe('AssetParser.buildAsset', () => {
|
|
|
229
198
|
'url',
|
|
230
199
|
'main',
|
|
231
200
|
'instructions/my-instruction.yaml',
|
|
201
|
+
'instructions/my-instruction.md',
|
|
232
202
|
);
|
|
233
203
|
expect(asset.metadata).toBeUndefined();
|
|
234
204
|
});
|
|
@@ -242,7 +212,7 @@ tools:
|
|
|
242
212
|
- claude-code
|
|
243
213
|
`;
|
|
244
214
|
const parsedNoTags = AssetParser.parseYaml(yaml, 'instructions/no-tags.yaml')!;
|
|
245
|
-
const asset = AssetParser.buildAsset(parsedNoTags, '', 'p', 'url', 'main', 'instructions/no-tags.yaml');
|
|
215
|
+
const asset = AssetParser.buildAsset(parsedNoTags, '', 'p', 'url', 'main', 'instructions/no-tags.yaml', 'instructions/no-tags.md');
|
|
246
216
|
expect(asset.tags).toEqual([]);
|
|
247
217
|
});
|
|
248
218
|
|
|
@@ -255,7 +225,105 @@ tools:
|
|
|
255
225
|
- claude-code
|
|
256
226
|
`;
|
|
257
227
|
const parsedNoAuthor = AssetParser.parseYaml(yaml, 'instructions/no-author.yaml')!;
|
|
258
|
-
const asset = AssetParser.buildAsset(parsedNoAuthor, '', 'p', 'url', 'main', 'instructions/no-author.yaml');
|
|
228
|
+
const asset = AssetParser.buildAsset(parsedNoAuthor, '', 'p', 'url', 'main', 'instructions/no-author.yaml', 'instructions/no-author.md');
|
|
259
229
|
expect(asset.author).toBe('Unknown');
|
|
260
230
|
});
|
|
261
231
|
});
|
|
232
|
+
|
|
233
|
+
describe('AssetParser.parseYaml — schema-as-gatekeeper (arbitrary path discovery)', () => {
|
|
234
|
+
it('discovers a valid asset at an arbitrary custom directory', () => {
|
|
235
|
+
const result = AssetParser.parseYaml(VALID_YAML, 'ai-artifacts/my-agent.yaml');
|
|
236
|
+
expect(result).not.toBeNull();
|
|
237
|
+
expect(result!.meta.name).toBe('My Instruction');
|
|
238
|
+
expect(result!.mdPath).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('discovers a valid asset at a deeply nested path', () => {
|
|
242
|
+
const result = AssetParser.parseYaml(VALID_YAML, 'team/ai-assets/copilot/my-agent.yaml');
|
|
243
|
+
expect(result).not.toBeNull();
|
|
244
|
+
expect(result!.mdPath).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('discovers a valid asset at the repo root (no subdirectory)', () => {
|
|
248
|
+
const result = AssetParser.parseYaml(VALID_YAML, 'my-instruction.yaml');
|
|
249
|
+
expect(result).not.toBeNull();
|
|
250
|
+
expect(result!.mdPath).toBeUndefined();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('rejects a GitHub Actions workflow YAML (missing required fields)', () => {
|
|
254
|
+
const ghActionsYaml = `
|
|
255
|
+
on:
|
|
256
|
+
push:
|
|
257
|
+
branches: [main]
|
|
258
|
+
jobs:
|
|
259
|
+
build:
|
|
260
|
+
runs-on: ubuntu-latest
|
|
261
|
+
steps:
|
|
262
|
+
- uses: actions/checkout@v4
|
|
263
|
+
`;
|
|
264
|
+
expect(AssetParser.parseYaml(ghActionsYaml, '.github/workflows/ci.yaml')).toBeNull();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('rejects a Helm values YAML (no name/type/tools)', () => {
|
|
268
|
+
const helmYaml = `
|
|
269
|
+
replicaCount: 2
|
|
270
|
+
image:
|
|
271
|
+
repository: nginx
|
|
272
|
+
tag: latest
|
|
273
|
+
service:
|
|
274
|
+
type: ClusterIP
|
|
275
|
+
port: 80
|
|
276
|
+
`;
|
|
277
|
+
expect(AssetParser.parseYaml(helmYaml, 'helm/values.yaml')).toBeNull();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('rejects a Kubernetes manifest YAML (apiVersion/kind fields, no asset fields)', () => {
|
|
281
|
+
const k8sYaml = `
|
|
282
|
+
apiVersion: apps/v1
|
|
283
|
+
kind: Deployment
|
|
284
|
+
metadata:
|
|
285
|
+
name: my-app
|
|
286
|
+
spec:
|
|
287
|
+
replicas: 1
|
|
288
|
+
`;
|
|
289
|
+
expect(AssetParser.parseYaml(k8sYaml, 'k8s/deployment.yaml')).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('rejects a YAML with a valid-looking name but invalid type enum', () => {
|
|
293
|
+
const yaml = `
|
|
294
|
+
name: CI Pipeline
|
|
295
|
+
description: Continuous integration pipeline config
|
|
296
|
+
type: pipeline
|
|
297
|
+
tools:
|
|
298
|
+
- claude-code
|
|
299
|
+
`;
|
|
300
|
+
expect(AssetParser.parseYaml(yaml, 'ai-artifacts/pipeline.yaml')).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('rejects a YAML with a valid-looking type but unrecognised tool', () => {
|
|
304
|
+
const yaml = `
|
|
305
|
+
name: My Agent
|
|
306
|
+
description: An agent config
|
|
307
|
+
type: agent
|
|
308
|
+
tools:
|
|
309
|
+
- unknown-tool
|
|
310
|
+
`;
|
|
311
|
+
expect(AssetParser.parseYaml(yaml, 'ai-artifacts/agent.yaml')).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('buildAsset correctly sets yamlPath and mdPath for an arbitrary directory', () => {
|
|
315
|
+
const parsed = AssetParser.parseYaml(VALID_YAML, 'ai-artifacts/my-agent.yaml')!;
|
|
316
|
+
const asset = AssetParser.buildAsset(
|
|
317
|
+
parsed,
|
|
318
|
+
'# Content',
|
|
319
|
+
'my-provider',
|
|
320
|
+
'https://github.com/org/repo',
|
|
321
|
+
'main',
|
|
322
|
+
'ai-artifacts/my-agent.yaml',
|
|
323
|
+
'ai-artifacts/SKILL.md',
|
|
324
|
+
);
|
|
325
|
+
expect(asset.yamlPath).toBe('ai-artifacts/my-agent.yaml');
|
|
326
|
+
expect(asset.mdPath).toBe('ai-artifacts/SKILL.md');
|
|
327
|
+
expect(asset.id).toBe(AssetParser.buildId('my-provider', 'ai-artifacts/my-agent.yaml'));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -6,8 +6,11 @@ import type { AiAssetInput } from '../types';
|
|
|
6
6
|
|
|
7
7
|
export interface ParsedAssetMeta {
|
|
8
8
|
meta: AiAssetFrontmatter;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Resolved explicit .md path — only set when the YAML has a `content:` field.
|
|
11
|
+
* When absent, the sync service resolves the md file by directory junction.
|
|
12
|
+
*/
|
|
13
|
+
mdPath?: string;
|
|
11
14
|
yamlRaw: string;
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -34,13 +37,11 @@ export class AssetParser {
|
|
|
34
37
|
|
|
35
38
|
const meta = result.data;
|
|
36
39
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
mdReference,
|
|
43
|
-
);
|
|
40
|
+
// Only resolve mdPath when content: is explicitly set in the YAML.
|
|
41
|
+
// Otherwise the sync service pairs yaml + md by directory junction.
|
|
42
|
+
const mdPath = meta.content
|
|
43
|
+
? path.posix.join(path.posix.dirname(yamlFilePath), meta.content)
|
|
44
|
+
: undefined;
|
|
44
45
|
|
|
45
46
|
return { meta, mdPath, yamlRaw: yamlContent };
|
|
46
47
|
}
|
|
@@ -48,6 +49,7 @@ export class AssetParser {
|
|
|
48
49
|
/**
|
|
49
50
|
* Build the full AiAssetInput from parsed metadata + raw markdown content.
|
|
50
51
|
* The mdContent is stored verbatim — never modified.
|
|
52
|
+
* actualMdPath is the resolved path of the md file as found by the sync service.
|
|
51
53
|
*/
|
|
52
54
|
static buildAsset(
|
|
53
55
|
parsed: ParsedAssetMeta,
|
|
@@ -56,6 +58,7 @@ export class AssetParser {
|
|
|
56
58
|
repoUrl: string,
|
|
57
59
|
branch: string,
|
|
58
60
|
yamlFilePath: string,
|
|
61
|
+
actualMdPath: string,
|
|
59
62
|
resourcesContent?: Record<string, string>,
|
|
60
63
|
): AiAssetInput {
|
|
61
64
|
const { meta } = parsed;
|
|
@@ -85,7 +88,7 @@ export class AssetParser {
|
|
|
85
88
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
86
89
|
resourcesContent,
|
|
87
90
|
yamlPath: yamlFilePath,
|
|
88
|
-
mdPath:
|
|
91
|
+
mdPath: actualMdPath,
|
|
89
92
|
repoUrl,
|
|
90
93
|
branch,
|
|
91
94
|
};
|
|
@@ -97,21 +100,4 @@ export class AssetParser {
|
|
|
97
100
|
return Buffer.from(`${providerId}:${normalized}`).toString('base64url');
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
/** Known top-level directories that contain assets */
|
|
101
|
-
private static readonly ASSET_DIRS = [
|
|
102
|
-
'instructions/',
|
|
103
|
-
'agents/',
|
|
104
|
-
'skills/',
|
|
105
|
-
'workflows/',
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
/** True if the file is in a known asset directory (root or .github/) */
|
|
109
|
-
static isAssetFile(filePath: string): boolean {
|
|
110
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
111
|
-
return AssetParser.ASSET_DIRS.some(
|
|
112
|
-
dir =>
|
|
113
|
-
normalized.startsWith(dir) ||
|
|
114
|
-
normalized.startsWith(`.github/${dir}`),
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
103
|
}
|