@nospt/plugin-dev-ai-hub-backend 0.1.0 → 0.2.0

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.
@@ -6,6 +6,7 @@ class AiAssetStore {
6
6
  constructor(db) {
7
7
  this.db = db;
8
8
  }
9
+ db;
9
10
  static async create(options) {
10
11
  const db = await options.database.getClient();
11
12
  await db.migrate.latest({
@@ -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 mdFile = fileMap.get(normalizePath(parsed.mdPath));
87
- if (!mdFile) {
88
- logger.warn(
89
- `dev-ai-hub: .md not found for ${filePath} (expected ${parsed.mdPath}), skipping`
90
- );
91
- continue;
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":"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"}
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 mdReference = meta.content ?? `${path__default.default.basename(yamlFilePath, ".yaml")}.md`;
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: parsed.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 /** Resolved path of the .md content file within the repo tree */\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 // Resolve .md path: use the `content` field, or fall back to <same-name>.md\n const mdReference =\n meta.content ?? `${path.basename(yamlFilePath, '.yaml')}.md`;\n const mdPath = path.posix.join(\n path.posix.dirname(yamlFilePath),\n mdReference,\n );\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 */\n static buildAsset(\n parsed: ParsedAssetMeta,\n mdContent: string,\n providerId: string,\n repoUrl: string,\n branch: string,\n yamlFilePath: 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: parsed.mdPath,\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 /** Known top-level directories that contain assets */\n private static readonly ASSET_DIRS = [\n 'instructions/',\n 'agents/',\n 'skills/',\n 'workflows/',\n ];\n\n /** True if the file is in a known asset directory (root or .github/) */\n static isAssetFile(filePath: string): boolean {\n const normalized = filePath.replace(/\\\\/g, '/');\n return AssetParser.ASSET_DIRS.some(\n dir =>\n normalized.startsWith(dir) ||\n normalized.startsWith(`.github/${dir}`),\n );\n }\n}\n"],"names":["yaml","AiAssetFrontmatterSchema","path"],"mappings":"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"}
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.0",
3
+ "version": "0.2.0",
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.0",
58
+ "@nospt/plugin-dev-ai-hub-node": "^0.2.0",
55
59
  "archiver": "^6.0.0",
56
- "express": "^4.18.0",
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
  }
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@nospt/plugin-dev-ai-hub-backend",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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": "^4.18.0",
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
- const mdFile = fileMap.get(normalizePath(parsed.mdPath));
128
- if (!mdFile) {
129
- logger.warn(
130
- `dev-ai-hub: .md not found for ${filePath} (expected ${parsed.mdPath}), skipping`,
131
- );
132
- continue;
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('derives mdPath from the yaml file name by default', () => {
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).toBe('instructions/my-instruction.md');
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
- /** Resolved path of the .md content file within the repo tree */
10
- mdPath: string;
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
- // Resolve .md path: use the `content` field, or fall back to <same-name>.md
38
- const mdReference =
39
- meta.content ?? `${path.basename(yamlFilePath, '.yaml')}.md`;
40
- const mdPath = path.posix.join(
41
- path.posix.dirname(yamlFilePath),
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: parsed.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
  }