diff --git a/.agents/skills/manifest-automation/SKILL.md b/.agents/skills/manifest-automation/SKILL.md index 66c418a9..2c30561a 100644 --- a/.agents/skills/manifest-automation/SKILL.md +++ b/.agents/skills/manifest-automation/SKILL.md @@ -42,7 +42,7 @@ The merge helper is advisory. Always inspect its proposed result before applying ## GitHub stars -`data/github-stars.json` tracks `cli`, `desktop`, `extension`, and `ide` entries. Every CLI, desktop, extension, and IDE manifest must have a corresponding entry; use `null` when no official repository or trustworthy count is available. Models, providers, and vendors are not tracked. +`data/github-stars.json` is a repository-keyed snapshot containing only the observation date and raw star counts. Product associations come from each CLI, desktop, extension, or IDE manifest's `githubUrl`; use `sourceCode` when the repository is only a partial source tree or serves as feedback or documentation rather than product source. Use `null` when a tracked repository has no trustworthy count. Models, providers, and vendors are not tracked. ## Validation diff --git a/.agents/skills/manifest-automation/scripts/lib/github-stars-updater.mjs b/.agents/skills/manifest-automation/scripts/lib/github-stars-updater.mjs index f5b62407..73db793f 100644 --- a/.agents/skills/manifest-automation/scripts/lib/github-stars-updater.mjs +++ b/.agents/skills/manifest-automation/scripts/lib/github-stars-updater.mjs @@ -2,7 +2,7 @@ /** * GitHub Stars Updater - * Updates github-stars.json with new manifest entries + * Keeps repository keys in github-stars.json aligned with product manifests. */ import fs from 'node:fs' @@ -11,122 +11,101 @@ import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +const manifestDirectories = { + cli: 'clis', + desktop: 'desktops', + extension: 'extensions', + ide: 'ides', +} -/** - * Get project root directory - */ function getProjectRoot() { return path.resolve(__dirname, '../../../../..') } -/** - * Get the path to github-stars.json - */ function getGithubStarsPath() { return path.join(getProjectRoot(), 'data/github-stars.json') } -/** - * Load github-stars.json - * @returns {Object} The current github-stars data - */ -export function loadGithubStars() { - const filePath = getGithubStarsPath() +function getManifestPath(type, id) { + const directory = manifestDirectories[type] + return directory ? path.join(getProjectRoot(), 'manifests', directory, `${id}.json`) : null +} - if (!fs.existsSync(filePath)) { - throw new Error(`github-stars.json not found at: ${filePath}`) - } +function repositoryIdFromUrl(url) { + const match = url + ?.replace(/\/$/, '') + .replace(/\.git$/, '') + .match(/^https:\/\/github\.com\/(.+\/.+)$/) + return match?.[1] ?? null +} - const content = fs.readFileSync(filePath, 'utf-8') - return JSON.parse(content) +function loadManifestRepository(type, id) { + const manifestPath = getManifestPath(type, id) + if (!manifestPath || !fs.existsSync(manifestPath)) return null + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) + return repositoryIdFromUrl(manifest.githubUrl) } -/** - * Save github-stars.json - * @param {Object} data - The github-stars data to save - */ -export function saveGithubStars(data) { - const filePath = getGithubStarsPath() - const content = `${JSON.stringify(data, null, 2)}\n` - fs.writeFileSync(filePath, content, 'utf-8') +function countRepositoryAssociations(repositoryId, excludedType, excludedId) { + let count = 0 + for (const [type, directory] of Object.entries(manifestDirectories)) { + const directoryPath = path.join(getProjectRoot(), 'manifests', directory) + for (const file of fs.readdirSync(directoryPath).filter(name => name.endsWith('.json'))) { + const id = file.replace(/\.json$/, '') + if (type === excludedType && id === excludedId) continue + const manifest = JSON.parse(fs.readFileSync(path.join(directoryPath, file), 'utf8')) + if (repositoryIdFromUrl(manifest.githubUrl) === repositoryId) count += 1 + } + } + return count } -/** - * Get the tracked category name from manifest type. - * @param {string} type - Manifest type (cli, extension, ide, model) - * @returns {string} Category name for github-stars.json - */ -function getCategoryName(type) { - const mapping = { - cli: 'clis', - extension: 'extensions', - ide: 'ides', - model: 'models', +export function loadGithubStars() { + const filePath = getGithubStarsPath() + if (!fs.existsSync(filePath)) { + throw new Error(`github-stars.json not found at: ${filePath}`) } + return JSON.parse(fs.readFileSync(filePath, 'utf8')) +} - return mapping[type] || null +export function saveGithubStars(data) { + const sortedRepositories = Object.fromEntries( + Object.entries(data.repositories).sort(([left], [right]) => left.localeCompare(right)) + ) + fs.writeFileSync( + getGithubStarsPath(), + `${JSON.stringify({ ...data, repositories: sortedRepositories }, null, 2)}\n`, + 'utf8' + ) } -/** - * Update github-stars.json with a new or updated manifest entry - * @param {string} type - Manifest type (cli, extension, ide, etc.) - * @param {string} id - Manifest id - * @param {Object} options - Options - * @param {boolean} options.isNew - Whether this is a new entry (true) or update (false) - * @returns {Object} Result with status and message - */ export function updateGithubStarsEntry(type, id, options = {}) { const { isNew = false } = options try { - // Load current data - const githubStars = loadGithubStars() - const category = getCategoryName(type) - - if (!category || !Object.hasOwn(githubStars, category)) { + const repositoryId = loadManifestRepository(type, id) + if (!repositoryId) { return { status: 'skipped', - message: `Manifest type "${type}" is not tracked by data/github-stars.json`, + message: `Manifest "${type}:${id}" has no tracked GitHub repository`, } } - // Check if entry already exists - const exists = id in githubStars[category] - + const githubStars = loadGithubStars() + const exists = Object.hasOwn(githubStars.repositories, repositoryId) if (isNew && exists) { return { status: 'skipped', - message: `Entry "${id}" already exists in github-stars.json under "${category}"`, - } - } - - if (!isNew && !exists) { - return { - status: 'skipped', - message: `Entry "${id}" does not exist in github-stars.json under "${category}"; no change made. Verify the official repository, then use the add command.`, + message: `Repository "${repositoryId}" already exists in github-stars.json`, } } - // Add or update entry with null (stars will be fetched later) - githubStars[category][id] = null - - // Sort entries alphabetically within category - const sortedCategory = Object.keys(githubStars[category]) - .sort() - .reduce((acc, key) => { - acc[key] = githubStars[category][key] - return acc - }, {}) - - githubStars[category] = sortedCategory - - // Save updated data + githubStars.repositories[repositoryId] ??= null saveGithubStars(githubStars) - return { status: 'success', - message: `Updated github-stars.json: ${category}["${id}"] = null`, - action: exists ? 'updated' : 'added', + message: `Tracked github-stars.json repository "${repositoryId}"`, + action: exists ? 'unchanged' : 'added', } } catch (error) { return { @@ -137,83 +116,59 @@ export function updateGithubStarsEntry(type, id, options = {}) { } } -/** - * Remove an entry from github-stars.json - * @param {string} type - Manifest type - * @param {string} id - Manifest id - * @returns {Object} Result with status and message - */ export function removeGithubStarsEntry(type, id) { try { - const githubStars = loadGithubStars() - const category = getCategoryName(type) + const repositoryId = loadManifestRepository(type, id) + if (!repositoryId) { + return { + status: 'skipped', + message: `Manifest "${type}:${id}" has no tracked GitHub repository`, + } + } - if (!category || !Object.hasOwn(githubStars, category)) { + if (countRepositoryAssociations(repositoryId, type, id) > 0) { return { status: 'skipped', - message: `Manifest type "${type}" is not tracked by data/github-stars.json`, + message: `Repository "${repositoryId}" is still used by another product surface`, } } - if (!githubStars[category] || !(id in githubStars[category])) { + const githubStars = loadGithubStars() + if (!Object.hasOwn(githubStars.repositories, repositoryId)) { return { status: 'skipped', - message: `Entry "${id}" not found in github-stars.json under "${category}"`, + message: `Repository "${repositoryId}" is not tracked in github-stars.json`, } } - delete githubStars[category][id] + delete githubStars.repositories[repositoryId] saveGithubStars(githubStars) - return { status: 'success', - message: `Removed "${id}" from github-stars.json under "${category}"`, + message: `Removed repository "${repositoryId}" from github-stars.json`, } } catch (error) { return { status: 'error', - message: `Failed to remove entry from github-stars.json: ${error.message}`, + message: `Failed to remove repository from github-stars.json: ${error.message}`, error, } } } -/** - * CLI entry point for testing - */ if (import.meta.url === `file://${process.argv[1]}`) { const [, , command, type, id] = process.argv - - if (!command || !['add', 'update', 'remove'].includes(command)) { + if (!command || !['add', 'update', 'remove'].includes(command) || !type || !id) { console.error('Usage:') - console.error(' node github-stars-updater.mjs add ') - console.error(' node github-stars-updater.mjs update ') - console.error(' node github-stars-updater.mjs remove ') - console.error('') - console.error('Examples:') - console.error(' node github-stars-updater.mjs add cli cursor-cli') - console.error(' node github-stars-updater.mjs update extension claude-code') - console.error(' node github-stars-updater.mjs remove ide windsurf') - process.exit(1) - } - - if (!type || !id) { - console.error('Error: type and id are required') + console.error(' node github-stars-updater.mjs ') process.exit(1) } - let result - - if (command === 'add' || command === 'update') { - result = updateGithubStarsEntry(type, id, { isNew: command === 'add' }) - } else { - result = removeGithubStarsEntry(type, id) - } - + const result = + command === 'remove' + ? removeGithubStarsEntry(type, id) + : updateGithubStarsEntry(type, id, { isNew: command === 'add' }) console.log(`Status: ${result.status}`) console.log(`Message: ${result.message}`) - - if (result.status === 'error') { - process.exit(1) - } + if (result.status === 'error') process.exit(1) } diff --git a/cspell.json b/cspell.json index b1cfa5f7..5a3dd004 100644 --- a/cspell.json +++ b/cspell.json @@ -52,6 +52,7 @@ "продакшену", "confiabilidade", "gözlemlenebilirliğini", + "governança", "modelnya", "observabilitas", "observabilidad", @@ -76,6 +77,7 @@ "ccstatusline", "API'lerle", "acli", + "aaif", "anomalyco", "aracidir", "glab", @@ -142,6 +144,7 @@ "Junie", "Kimi", "Kiro", + "kirodotdev", "Kode", "lmstudio", "multiherramienta", diff --git a/data/data-health.json b/data/data-health.json index a3d01c9c..ed41f9a7 100644 --- a/data/data-health.json +++ b/data/data-health.json @@ -10,18 +10,18 @@ "vendors": 90 }, "summary": { - "totalRecords": 248, - "recordsWithSources": 248, - "verifiedRecords": 248, - "provenanceComplete": 248, + "totalRecords": 249, + "recordsWithSources": 249, + "verifiedRecords": 249, + "provenanceComplete": 249, "staleVerifiedRecords": 0, "translationPlaceholderValues": 300, "danglingRelationships": 0, "modelBenchmarkCoverage": 9.5, - "productsWithPricing": 63, - "productRecords": 63, - "communityUrlsPopulated": 328, - "communityUrlsWithProvenance": 328, + "productsWithPricing": 64, + "productRecords": 64, + "communityUrlsPopulated": 327, + "communityUrlsWithProvenance": 327, "duplicatedVendorCommunityUrls": 0, "errors": 0, "warnings": 0, @@ -35,21 +35,21 @@ "stale": 0 }, "clis": { - "total": 25, - "verified": 25, - "provenanceComplete": 25, + "total": 26, + "verified": 26, + "provenanceComplete": 26, "stale": 0 }, "desktops": { - "total": 11, - "verified": 11, - "provenanceComplete": 11, + "total": 12, + "verified": 12, + "provenanceComplete": 12, "stale": 0 }, "extensions": { - "total": 19, - "verified": 19, - "provenanceComplete": 19, + "total": 18, + "verified": 18, + "provenanceComplete": 18, "stale": 0 }, "models": { @@ -73,57 +73,57 @@ }, "translationsByLocale": { "de": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 39, - "matchingEnglishPercent": 8.4 + "matchingEnglishPercent": 8.3 }, "es": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 25, - "matchingEnglishPercent": 5.4 + "matchingEnglishPercent": 5.3 }, "fr": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 36, "matchingEnglishPercent": 7.7 }, "id": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 30, - "matchingEnglishPercent": 6.5 + "matchingEnglishPercent": 6.4 }, "ja": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 23, "matchingEnglishPercent": 4.9 }, "ko": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 23, "matchingEnglishPercent": 4.9 }, "pt": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 31, - "matchingEnglishPercent": 6.7 + "matchingEnglishPercent": 6.6 }, "ru": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 23, "matchingEnglishPercent": 4.9 }, "tr": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 26, - "matchingEnglishPercent": 5.6 + "matchingEnglishPercent": 5.5 }, "zh-Hans": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 22, "matchingEnglishPercent": 4.7 }, "zh-Hant": { - "totalStrings": 465, + "totalStrings": 470, "matchingEnglish": 22, "matchingEnglishPercent": 4.7 } diff --git a/data/github-stars.json b/data/github-stars.json index f4a396f1..0fb37612 100644 --- a/data/github-stars.json +++ b/data/github-stars.json @@ -1,73 +1,35 @@ { - "extensions": { - "amp": null, - "augment-code": null, - "claude-code": 139.2, - "cline": 65.3, - "codex": 102.6, - "continue": 35.1, - "droid": null, - "gemini-code-assist": null, - "github-copilot": null, - "jetbrains-junie": 0.3, - "kilo-code": 26.5, - "kimi-code": 5.2, - "mistral-vibe": null, - "opencode-extension": 190, - "qoder": null, - "roo-code": 24.4, - "rovo-dev": 0.1, - "tabnine": 10.8, - "verdent": 0 - }, - "clis": { - "amazon-q-developer-cli": 2, - "amp-cli": null, - "antigravity-cli": 1.7, - "auggie-cli": 0.3, - "claude-code-cli": 139.2, - "cline-cli": 65.3, - "codebuddy-cli": null, - "codex-cli": 102.6, - "continue-cli": 35.1, - "cursor-cli": 33.1, - "droid-cli": null, - "gemini-cli": 106.2, - "github-copilot-cli": 11, - "gitlab-duo-cli": null, - "grok-build": 22.8, - "junie-cli": 0.3, - "kilo-code-cli": 26.5, - "kimi-cli": 5.2, - "kiro-cli": 4.1, - "kode": 5.2, - "vibe-cli": 4.8, - "omp": 19.9, - "opencode": 190, - "qoder-cli": null, - "qwen-code": 26.3 - }, - "desktops": { - "air": null, - "claude-code-desktop": 139.2, - "codebuddy": null, - "codex-app": 102.6, - "factory-desktop": null, - "minimax-code": 0, - "opencode-desktop": 190, - "qoder": null, - "stagewise": 6.8, - "verdent-deck": 0, - "zcode": null - }, - "ides": { - "antigravity": null, - "cursor": 33.1, - "intellij-idea": 20.4, - "kiro": 4.1, - "trae": 0.9, - "vscode": 187.9, - "windsurf": null, - "zed": 87.6 + "observedAt": "2026-08-01", + "repositories": { + "aaif-goose/goose": 52043, + "anomalyco/opencode": 190000, + "anthropics/claude-code": 139200, + "augmentcode/auggie": 300, + "aws/amazon-q-developer-cli": 2000, + "can1357/oh-my-pi": 19900, + "cline/cline": 65300, + "codota/TabNine": 10800, + "continuedev/continue": 35100, + "cursor/cursor": 33100, + "github/copilot-cli": 11000, + "google-antigravity/antigravity-cli": 1700, + "google-gemini/gemini-cli": 106200, + "JetBrains/intellij-community": 20400, + "JetBrains/junie": 300, + "Kilo-Org/kilocode": 26500, + "kirodotdev/Kiro": 4100, + "microsoft/vscode": 187900, + "MiniMax-AI/minimax-code": 0, + "mistralai/mistral-vibe": 4800, + "MoonshotAI/kimi-code": 5200, + "openai/codex": 102600, + "QwenLM/qwen-code": 26300, + "RooCodeInc/Roo-Code": 24400, + "shareAI-lab/Kode-CLI": 5200, + "stagewise-io/stagewise": 6800, + "Trae-AI/TRAE": 900, + "verdentAI/docs": 0, + "xai-org/grok-build": 22800, + "zed-industries/zed": 87600 } } diff --git a/data/vendor-company-stages.json b/data/vendor-company-stages.json index 9f3aa86c..1e92f4da 100644 --- a/data/vendor-company-stages.json +++ b/data/vendor-company-stages.json @@ -1,6 +1,6 @@ { "$schema": "./$schemas/vendor-company-stages.schema.json", - "asOf": "2026-07-31", + "asOf": "2026-08-01", "criteria": { "publicCompanyIncludesListedParent": true, "superUnicornMinimumUsd": 10000000000, @@ -34,14 +34,6 @@ "title": "Alibaba Group investor relations" } }, - { - "vendorId": "atlassian", - "stage": "public-company", - "source": { - "url": "https://investors.atlassian.com/", - "title": "Atlassian investor relations" - } - }, { "vendorId": "aws", "stage": "public-company", @@ -266,6 +258,11 @@ "title": "Together AI Series C announcement" } }, + { + "vendorId": "agentic-ai-foundation", + "stage": "startup", + "source": null + }, { "vendorId": "anomaly", "stage": "startup", diff --git a/docs/DATA-HEALTH.md b/docs/DATA-HEALTH.md index d0f84b65..3245bfde 100644 --- a/docs/DATA-HEALTH.md +++ b/docs/DATA-HEALTH.md @@ -6,16 +6,16 @@ Snapshot date: 2026-08-01. Regenerate with `pnpm data-health:report`. | Metric | Value | | --- | ---: | -| Manifest records | 248 | -| Records with structured sources | 248 | -| Verified records | 248 | -| Verified with complete provenance | 248 | +| Manifest records | 249 | +| Records with structured sources | 249 | +| Verified records | 249 | +| Verified with complete provenance | 249 | | Stale verified records | 0 | | Non-English values identical to English | 300 | | Dangling product relationships | 0 | | Model benchmark coverage | 9.5% | -| Products with pricing | 63/63 | -| Community URLs with provenance | 328/328 | +| Products with pricing | 64/64 | +| Community URLs with provenance | 327/327 | | Duplicated vendor community URLs | 0 | | Errors / warnings / info | 0 / 0 / 0 | @@ -24,9 +24,9 @@ Snapshot date: 2026-08-01. Regenerate with `pnpm data-health:report`. | Category | Total | Verified | Provenance complete | Stale | | --- | ---: | ---: | ---: | ---: | | ides | 8 | 8 | 8 | 0 | -| clis | 25 | 25 | 25 | 0 | -| desktops | 11 | 11 | 11 | 0 | -| extensions | 19 | 19 | 19 | 0 | +| clis | 26 | 26 | 26 | 0 | +| desktops | 12 | 12 | 12 | 0 | +| extensions | 18 | 18 | 18 | 0 | | models | 123 | 123 | 123 | 0 | | providers | 17 | 17 | 17 | 0 | | vendors | 45 | 45 | 45 | 0 | @@ -37,17 +37,17 @@ Exact English matches are a triage signal; product names and technical terms can | Locale | Comparable strings | Exact English matches | Match rate | | --- | ---: | ---: | ---: | -| de | 465 | 39 | 8.4% | -| es | 465 | 25 | 5.4% | -| fr | 465 | 36 | 7.7% | -| id | 465 | 30 | 6.5% | -| ja | 465 | 23 | 4.9% | -| ko | 465 | 23 | 4.9% | -| pt | 465 | 31 | 6.7% | -| ru | 465 | 23 | 4.9% | -| tr | 465 | 26 | 5.6% | -| zh-Hans | 465 | 22 | 4.7% | -| zh-Hant | 465 | 22 | 4.7% | +| de | 470 | 39 | 8.3% | +| es | 470 | 25 | 5.3% | +| fr | 470 | 36 | 7.7% | +| id | 470 | 30 | 6.4% | +| ja | 470 | 23 | 4.9% | +| ko | 470 | 23 | 4.9% | +| pt | 470 | 31 | 6.6% | +| ru | 470 | 23 | 4.9% | +| tr | 470 | 26 | 5.5% | +| zh-Hans | 470 | 22 | 4.7% | +| zh-Hant | 470 | 22 | 4.7% | ## Backlog by Issue Type diff --git a/docs/FETCH_GITHUB_STARS.md b/docs/FETCH_GITHUB_STARS.md index c4eae347..9fb8965e 100644 --- a/docs/FETCH_GITHUB_STARS.md +++ b/docs/FETCH_GITHUB_STARS.md @@ -1,6 +1,6 @@ # GitHub Stars Refresh -The stars refresh reads `githubUrl` from IDE, CLI, and extension manifests, queries the GitHub repository API, and writes the centralized cache at `data/github-stars.json`. +The stars refresh reads the repository keys already tracked in `data/github-stars.json`, queries the GitHub repository API, and updates their raw star counts. Product associations are derived separately from the `githubUrl` fields in IDE, CLI, desktop, and extension manifests. ## Run it @@ -24,13 +24,14 @@ The file follows `manifests/$schemas/github-stars.schema.json`: ```ts interface GitHubStarsData { - extensions: Record - clis: Record - ides: Record + observedAt: string + repositories: Record } ``` -Values are stored in thousands with one decimal place, matching the current UI contract; for example, `42.3` means approximately 42,300 stars. A `null` value means the manifest has no usable repository URL or no cached value exists. Transient API failures retain the previous cached value instead of replacing it with `null`. +Repository keys use GitHub's `owner/repository` form and values are raw stargazer counts. The UI converts them to compact thousands when needed. A `null` value means no trustworthy count is currently available. Transient API failures retain the previous cached value instead of replacing it with `null`. + +Product names, product surfaces, repository roles, licenses, and source-code coverage do not belong in the Stars snapshot. They come from product manifests; use the manifest `sourceCode` override when a repository contains only part of the product source or is used only for feedback or documentation. ## Automation @@ -47,7 +48,7 @@ There is only one scheduled owner for this refresh. General scheduled URL checks - `403`: provide `GITHUB_TOKEN` or wait for the API limit to reset. - `404`: check the manifest's `githubUrl` and repository visibility. -- Validation failure: ensure each IDE, CLI, and extension manifest has a matching key and no orphan key remains. +- Validation failure: ensure every non-null product `githubUrl` maps to a repository key and no repository key is orphaned. - No pull request: confirm the workflow checked `data/github-stars.json` and that the refreshed values actually differ. -Last reviewed: 2026-07-18. +Last reviewed: 2026-08-01. diff --git a/manifests/$schemas/github-stars.schema.json b/manifests/$schemas/github-stars.schema.json index c3f4a912..af4cceb6 100644 --- a/manifests/$schemas/github-stars.schema.json +++ b/manifests/$schemas/github-stars.schema.json @@ -1,84 +1,28 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://aicodingstack.io/schemas/github-stars.schema.json", - "title": "GitHub Stars Data", - "description": "Schema for the centralized GitHub stars data file", + "title": "GitHub Repository Stars Data", + "description": "Repository-keyed GitHub star snapshots. Product associations and source-code status are derived from product manifests.", "type": "object", "properties": { - "extensions": { - "type": "object", - "description": "GitHub stars for extensions", - "patternProperties": { - "^[a-z0-9-]+$": { - "oneOf": [ - { - "type": "number", - "minimum": 0, - "description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)" - }, - { - "type": "null", - "description": "No GitHub stars data available" - } - ] - } - }, - "additionalProperties": false - }, - "clis": { - "type": "object", - "description": "GitHub stars for CLI tools", - "patternProperties": { - "^[a-z0-9-]+$": { - "oneOf": [ - { - "type": "number", - "minimum": 0, - "description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)" - }, - { - "type": "null", - "description": "No GitHub stars data available" - } - ] - } - }, - "additionalProperties": false - }, - "desktops": { - "type": "object", - "description": "GitHub stars for desktop coding agents", - "patternProperties": { - "^[a-z0-9-]+$": { - "oneOf": [ - { - "type": "number", - "minimum": 0, - "description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)" - }, - { - "type": "null", - "description": "No GitHub stars data available" - } - ] - } - }, - "additionalProperties": false + "observedAt": { + "type": "string", + "format": "date", + "description": "Date when the repository star counts were observed" }, - "ides": { + "repositories": { "type": "object", - "description": "GitHub stars for IDEs", "patternProperties": { - "^[a-z0-9-]+$": { + "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$": { "oneOf": [ { - "type": "number", + "type": "integer", "minimum": 0, - "description": "Number of GitHub stars in thousands (e.g., 42 = 42k stars)" + "description": "Raw GitHub stargazer count" }, { "type": "null", - "description": "No GitHub stars data available" + "description": "No GitHub star count is currently available" } ] } @@ -86,6 +30,6 @@ "additionalProperties": false } }, - "required": ["extensions", "clis", "desktops", "ides"], + "required": ["observedAt", "repositories"], "additionalProperties": false } diff --git a/manifests/$schemas/ref/product.schema.json b/manifests/$schemas/ref/product.schema.json index 94a4758f..e15041b9 100644 --- a/manifests/$schemas/ref/product.schema.json +++ b/manifests/$schemas/ref/product.schema.json @@ -34,6 +34,10 @@ "description": "Valid SPDX License Identifier or the literal Proprietary", "examples": ["Proprietary", "MIT", "Apache-2.0", "GPL-3.0-only", "GPL-3.0-or-later"] }, + "sourceCode": { + "$ref": "#/$defs/sourceCode", + "description": "Optional override describing how the linked GitHub repository relates to this product surface. When omitted, non-proprietary products are treated as open source and proprietary products as closed source feedback repositories." + }, "pricing": { "type": "array", "description": "Pricing tiers for the product (can be empty array if not applicable, typically found on example.com/pricing or example.com/plans page)", @@ -66,6 +70,28 @@ } ], "$defs": { + "sourceCode": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["open", "partial", "closed"], + "description": "Whether the linked repository contains all, part, or none of this product surface's source code" + }, + "repositoryRole": { + "type": "string", + "enum": ["source", "feedback", "documentation"], + "description": "The linked repository's relationship to this product surface" + }, + "license": { + "type": "string", + "minLength": 1, + "description": "Repository source license when it differs from the product license" + } + }, + "required": ["status", "repositoryRole"], + "additionalProperties": false + }, "releaseTracking": { "oneOf": [ { diff --git a/manifests/clis/codex-cli.json b/manifests/clis/codex-cli.json index 084ddc4d..c61b3ca0 100644 --- a/manifests/clis/codex-cli.json +++ b/manifests/clis/codex-cli.json @@ -53,6 +53,10 @@ }, "githubUrl": "https://github.com/openai/codex", "license": "Apache-2.0", + "sourceCode": { + "status": "open", + "repositoryRole": "source" + }, "pricing": [ { "name": "ChatGPT Free", @@ -139,7 +143,7 @@ { "url": "https://github.com/openai/codex", "title": "Codex CLI github community", - "fields": ["communityUrls.github"] + "fields": ["githubUrl", "license", "sourceCode", "communityUrls.github"] }, { "url": "https://openai.com/news", diff --git a/manifests/clis/goose.json b/manifests/clis/goose.json new file mode 100644 index 00000000..4b6995ad --- /dev/null +++ b/manifests/clis/goose.json @@ -0,0 +1,132 @@ +{ + "$schema": "../$schemas/cli.schema.json", + "id": "goose", + "name": "Goose CLI", + "familyId": "goose", + "description": "Goose CLI is the terminal surface of Goose, an open source local AI agent for coding, research, automation, and other workflows with multiple model providers.", + "translations": { + "de": { + "description": "Goose CLI ist die Terminaloberfläche von Goose, einem quelloffenen lokalen KI-Agenten für Programmierung, Recherche, Automatisierung und weitere Arbeitsabläufe mit mehreren Modellanbietern." + }, + "es": { + "description": "Goose CLI es la interfaz de terminal de Goose, un agente de IA local y de código abierto para programación, investigación, automatización y otros flujos con varios proveedores de modelos." + }, + "fr": { + "description": "Goose CLI est l’interface terminal de Goose, un agent IA local open source destiné au codage, à la recherche, à l’automatisation et à d’autres flux avec plusieurs fournisseurs de modèles." + }, + "id": { + "description": "Goose CLI adalah antarmuka terminal Goose, agen AI lokal sumber terbuka untuk coding, riset, otomatisasi, dan alur kerja lain dengan berbagai penyedia model." + }, + "ja": { + "description": "Goose CLI は、複数のモデルプロバイダーを利用し、コーディング、調査、自動化などのワークフローに対応するオープンソースのローカル AI エージェント Goose のターミナル版です。" + }, + "ko": { + "description": "Goose CLI는 여러 모델 제공자를 활용해 코딩, 조사, 자동화 등의 워크플로를 수행하는 오픈 소스 로컬 AI 에이전트 Goose의 터미널 인터페이스입니다." + }, + "pt": { + "description": "Goose CLI é a interface de terminal do Goose, um agente de IA local e de código aberto para programação, pesquisa, automação e outros fluxos com vários provedores de modelos." + }, + "ru": { + "description": "Goose CLI — терминальный интерфейс Goose, локального ИИ-агента с открытым исходным кодом для программирования, исследований, автоматизации и других процессов с разными поставщиками моделей." + }, + "tr": { + "description": "Goose CLI; kodlama, araştırma, otomasyon ve diğer iş akışları için birden fazla model sağlayıcısını destekleyen açık kaynaklı yerel AI aracısı Goose'un terminal arayüzüdür." + }, + "zh-Hans": { + "description": "Goose CLI 是开源本地 AI Agent Goose 的终端形态,支持多家模型提供商,可用于编码、研究、自动化及其他工作流。" + }, + "zh-Hant": { + "description": "Goose CLI 是開放原始碼本機 AI Agent Goose 的終端機形態,支援多家模型供應商,可用於程式設計、研究、自動化及其他工作流程。" + } + }, + "verified": true, + "lastVerifiedAt": "2026-08-01", + "verifiedBy": "codex-agent", + "confidence": "high", + "websiteUrl": "https://goose-docs.ai", + "docsUrl": "https://goose-docs.ai/docs/getting-started/installation", + "vendor": "Agentic AI Foundation", + "latestVersion": "1.45.0", + "releaseTracking": { + "provider": "github-release", + "identifier": "aaif-goose/goose" + }, + "githubUrl": "https://github.com/aaif-goose/goose", + "license": "Apache-2.0", + "pricing": [ + { + "name": "Open Source", + "value": 0, + "currency": null, + "per": null, + "category": "Individual" + } + ], + "resourceUrls": { + "download": "https://goose-docs.ai/docs/getting-started/installation", + "changelog": "https://github.com/aaif-goose/goose/releases", + "pricing": null, + "issue": "https://github.com/aaif-goose/goose/issues" + }, + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": "https://github.com/aaif-goose/goose", + "youtube": null, + "discord": "https://discord.gg/goose-1268368495196131379", + "reddit": null, + "blog": null + }, + "relatedProducts": [ + { + "type": "desktop", + "productId": "goose" + } + ], + "platforms": [ + { + "os": "macOS", + "installCommand": "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash", + "launchCommand": "goose", + "installPath": null + }, + { + "os": "Windows", + "installCommand": "powershell -ExecutionPolicy Bypass -c \"irm https://github.com/aaif-goose/goose/releases/download/stable/download_cli.ps1 | iex\"", + "launchCommand": "goose", + "installPath": null + }, + { + "os": "Linux", + "installCommand": "curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash", + "launchCommand": "goose", + "installPath": null + } + ], + "sources": [ + { + "url": "https://github.com/aaif-goose/goose", + "title": "Goose official repository", + "fields": [ + "name", + "description", + "websiteUrl", + "githubUrl", + "license", + "platforms", + "communityUrls.github", + "communityUrls.discord" + ] + }, + { + "url": "https://goose-docs.ai/docs/getting-started/installation", + "title": "Goose installation documentation", + "fields": ["docsUrl", "resourceUrls.download", "platforms"] + }, + { + "url": "https://github.com/aaif-goose/goose/releases/tag/v1.45.0", + "title": "Goose v1.45.0 release", + "fields": ["latestVersion", "releaseTracking", "resourceUrls.changelog"] + } + ] +} diff --git a/manifests/desktops/codex-app.json b/manifests/desktops/codex-app.json index bc23394c..6d11e57e 100644 --- a/manifests/desktops/codex-app.json +++ b/manifests/desktops/codex-app.json @@ -48,7 +48,7 @@ { "url": "https://learn.chatgpt.com/docs/open-source", "title": "Codex open-source components", - "fields": ["githubUrl", "license"] + "fields": ["githubUrl", "license", "sourceCode"] }, { "url": "https://github.com/openai/codex", @@ -74,6 +74,10 @@ "latestVersion": "Latest", "githubUrl": "https://github.com/openai/codex", "license": "Proprietary", + "sourceCode": { + "status": "closed", + "repositoryRole": "feedback" + }, "pricing": [ { "name": "ChatGPT Plus", diff --git a/manifests/desktops/goose.json b/manifests/desktops/goose.json new file mode 100644 index 00000000..0cffccb5 --- /dev/null +++ b/manifests/desktops/goose.json @@ -0,0 +1,132 @@ +{ + "$schema": "../$schemas/desktop.schema.json", + "id": "goose", + "name": "Goose Desktop", + "familyId": "goose", + "description": "Goose Desktop is the native graphical surface of Goose, an open source local AI agent for coding, research, automation, and other workflows with multiple model providers.", + "translations": { + "de": { + "description": "Goose Desktop ist die native Oberfläche des lokalen Open-Source-KI-Agenten Goose für Programmierung, Recherche, Automatisierung und weitere Abläufe mit mehreren Modellen." + }, + "es": { + "description": "Goose Desktop es la interfaz gráfica nativa de Goose, un agente de IA local y de código abierto para programación, investigación, automatización y otros flujos con varios proveedores de modelos." + }, + "fr": { + "description": "Goose Desktop est l’interface graphique native de Goose, un agent IA local open source destiné au codage, à la recherche, à l’automatisation et à d’autres flux avec plusieurs fournisseurs de modèles." + }, + "id": { + "description": "Goose Desktop adalah antarmuka grafis native Goose, agen AI lokal sumber terbuka untuk coding, riset, otomatisasi, dan alur kerja lain dengan berbagai penyedia model." + }, + "ja": { + "description": "Goose Desktop は、複数のモデルプロバイダーを利用し、コーディング、調査、自動化などのワークフローに対応するオープンソースのローカル AI エージェント Goose のネイティブ GUI 版です。" + }, + "ko": { + "description": "Goose Desktop은 여러 모델 제공자를 활용해 코딩, 조사, 자동화 등의 워크플로를 수행하는 오픈 소스 로컬 AI 에이전트 Goose의 네이티브 그래픽 인터페이스입니다." + }, + "pt": { + "description": "Goose Desktop é a interface gráfica nativa do Goose, um agente de IA local e de código aberto para programação, pesquisa, automação e outros fluxos com vários provedores de modelos." + }, + "ru": { + "description": "Goose Desktop — графический интерфейс локального ИИ-агента Goose с открытым кодом для программирования, исследований и автоматизации с разными моделями." + }, + "tr": { + "description": "Goose Desktop; kodlama, araştırma, otomasyon ve diğer iş akışları için birden fazla model sağlayıcısını destekleyen açık kaynaklı yerel AI aracısı Goose'un yerel grafik arayüzüdür." + }, + "zh-Hans": { + "description": "Goose Desktop 是开源本地 AI Agent Goose 的原生图形界面,支持多家模型提供商,可用于编码、研究、自动化及其他工作流。" + }, + "zh-Hant": { + "description": "Goose Desktop 是開放原始碼本機 AI Agent Goose 的原生圖形介面,支援多家模型供應商,可用於程式設計、研究、自動化及其他工作流程。" + } + }, + "verified": true, + "lastVerifiedAt": "2026-08-01", + "verifiedBy": "codex-agent", + "confidence": "high", + "websiteUrl": "https://goose-docs.ai", + "docsUrl": "https://goose-docs.ai/docs/getting-started/installation", + "vendor": "Agentic AI Foundation", + "latestVersion": "1.45.0", + "releaseTracking": { + "provider": "github-release", + "identifier": "aaif-goose/goose" + }, + "githubUrl": "https://github.com/aaif-goose/goose", + "license": "Apache-2.0", + "pricing": [ + { + "name": "Open Source", + "value": 0, + "currency": null, + "per": null, + "category": "Individual" + } + ], + "resourceUrls": { + "download": "https://goose-docs.ai/docs/getting-started/installation", + "changelog": "https://github.com/aaif-goose/goose/releases", + "pricing": null, + "issue": "https://github.com/aaif-goose/goose/issues" + }, + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": "https://github.com/aaif-goose/goose", + "youtube": null, + "discord": "https://discord.gg/goose-1268368495196131379", + "reddit": null, + "blog": null + }, + "relatedProducts": [ + { + "type": "cli", + "productId": "goose" + } + ], + "platforms": [ + { + "os": "macOS", + "installPath": "/Applications/Goose.app", + "installCommand": null, + "launchCommand": null + }, + { + "os": "Windows", + "installPath": null, + "installCommand": null, + "launchCommand": null + }, + { + "os": "Linux", + "installPath": null, + "installCommand": null, + "launchCommand": null + } + ], + "sources": [ + { + "url": "https://github.com/aaif-goose/goose", + "title": "Goose official repository", + "fields": [ + "name", + "description", + "websiteUrl", + "githubUrl", + "license", + "platforms", + "communityUrls.github", + "communityUrls.discord" + ] + }, + { + "url": "https://goose-docs.ai/docs/getting-started/installation", + "title": "Goose installation documentation", + "fields": ["docsUrl", "resourceUrls.download", "platforms"] + }, + { + "url": "https://github.com/aaif-goose/goose/releases/tag/v1.45.0", + "title": "Goose v1.45.0 release", + "fields": ["latestVersion", "releaseTracking", "resourceUrls.changelog"] + } + ] +} diff --git a/manifests/desktops/verdent-deck.json b/manifests/desktops/verdent-deck.json index c1066c25..f346f91a 100644 --- a/manifests/desktops/verdent-deck.json +++ b/manifests/desktops/verdent-deck.json @@ -45,6 +45,11 @@ "url": "https://www.verdent.ai/docs/verdent/getting-started/installation", "title": "Verdent installation" }, + { + "url": "https://github.com/verdentAI/docs", + "title": "Verdent documentation repository", + "fields": ["githubUrl", "sourceCode"] + }, { "url": "https://www.verdent.ai/pricing", "title": "Verdent pricing", @@ -64,6 +69,10 @@ "latestVersion": "1.5.0", "githubUrl": "https://github.com/verdentAI/docs", "license": "Proprietary", + "sourceCode": { + "status": "closed", + "repositoryRole": "documentation" + }, "pricing": [ { "name": "Lite", diff --git a/manifests/extensions/codex.json b/manifests/extensions/codex.json index f17cad77..924bac22 100644 --- a/manifests/extensions/codex.json +++ b/manifests/extensions/codex.json @@ -49,7 +49,7 @@ { "url": "https://learn.chatgpt.com/docs/open-source", "title": "Codex open-source components", - "fields": ["githubUrl", "license", "resourceUrls.issue"] + "fields": ["githubUrl", "license", "sourceCode", "resourceUrls.issue"] }, { "url": "https://github.com/openai/codex", @@ -80,6 +80,10 @@ }, "githubUrl": "https://github.com/openai/codex", "license": "Proprietary", + "sourceCode": { + "status": "closed", + "repositoryRole": "feedback" + }, "pricing": [ { "name": "ChatGPT Plus", diff --git a/manifests/extensions/rovo-dev.json b/manifests/extensions/rovo-dev.json deleted file mode 100644 index 1d8f8e36..00000000 --- a/manifests/extensions/rovo-dev.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "$schema": "../$schemas/extension.schema.json", - "id": "rovo-dev", - "name": "Rovo Dev for VS Code", - "familyId": "rovo-dev", - "description": "Rovo Dev for VS Code is Atlassian's editor coding agent for code-aware chat, planning, edits, tests, change review, and access to Jira and Confluence context.", - "translations": { - "de": { - "description": "Rovo Dev für VS Code ist der Editor-Coding-Agent von Atlassian für codebezogenen Chat, Planung, Bearbeitungen, Tests, Änderungsprüfung und Zugriff auf Kontext aus Jira und Confluence." - }, - "es": { - "description": "Rovo Dev para VS Code es el agente de editor de Atlassian, con chat basado en el código, planificación, ediciones, pruebas, revisión de cambios y acceso al contexto de Jira y Confluence." - }, - "fr": { - "description": "Rovo Dev pour VS Code est l’agent de codage pour éditeur d’Atlassian, avec chat contextuel au code, planification, modifications, tests, examen des changements et accès au contexte Jira et Confluence." - }, - "id": { - "description": "Rovo Dev untuk VS Code adalah agen coding editor dari Atlassian untuk chat berbasis kode, perencanaan, pengeditan, pengujian, peninjauan perubahan, serta akses ke konteks Jira dan Confluence." - }, - "ja": { - "description": "Rovo Dev for VS Code は、コードを考慮したチャット、計画、編集、テスト、変更レビュー、Jira と Confluence のコンテキストへのアクセスに対応する Atlassian のエディター向けコーディングエージェントです。" - }, - "ko": { - "description": "Rovo Dev for VS Code는 코드 인식 채팅, 계획, 편집, 테스트, 변경 사항 검토, Jira 및 Confluence 컨텍스트 접근을 위한 Atlassian의 편집기 코딩 에이전트입니다." - }, - "pt": { - "description": "O Rovo Dev para VS Code é o agente da Atlassian para o editor, com chat baseado no código, planejamento, edições, testes, revisão de mudanças e acesso ao contexto do Jira e do Confluence." - }, - "ru": { - "description": "Rovo Dev для VS Code — агент программирования Atlassian для редактора с чатом по коду, планированием, редактированием, тестами, просмотром изменений и доступом к контексту Jira и Confluence." - }, - "tr": { - "description": "Rovo Dev for VS Code, kod odaklı sohbet, planlama, düzenleme, test, değişiklik inceleme ve Jira ile Confluence bağlamına erişim sunan Atlassian editör kodlama aracısıdır." - }, - "zh-Hans": { - "description": "Rovo Dev for VS Code 是 Atlassian 的编辑器编码代理,支持代码感知聊天、规划、编辑、测试、变更审查,以及访问 Jira 和 Confluence 上下文。" - }, - "zh-Hant": { - "description": "Rovo Dev for VS Code 是 Atlassian 的編輯器程式設計代理,支援程式碼感知聊天、規劃、編輯、測試、變更檢查,以及存取 Jira 和 Confluence 內容。" - } - }, - "verified": true, - "sources": [ - { - "url": "https://www.atlassian.com/blog/announcements/rovo-dev-now-generally-available-in-vs-code", - "title": "Rovo Dev is generally available in VS Code" - }, - { - "url": "https://marketplace.visualstudio.com/items?itemName=Atlassian.atlascode", - "title": "Atlassian VS Code extension" - }, - { - "url": "https://www.atlassian.com/software/rovo-dev/pricing", - "title": "Rovo Dev pricing", - "fields": ["pricing", "resourceUrls.pricing"] - }, - { - "url": "https://github.com/atlassian/atlascode", - "title": "Atlassian for VS Code official repository", - "fields": ["githubUrl", "license", "resourceUrls.issue"] - } - ], - "lastVerifiedAt": "2026-07-30", - "verifiedBy": "codex-agent", - "confidence": "high", - "websiteUrl": "https://marketplace.visualstudio.com/items?itemName=Atlassian.atlascode", - "docsUrl": "https://support.atlassian.com/rovo/docs/work-with-rovo-dev-agents/", - "vendor": "Atlassian", - "latestVersion": "4.1.191", - "releaseTracking": { - "provider": "vscode-marketplace", - "identifier": "Atlassian.atlascode" - }, - "githubUrl": "https://github.com/atlassian/atlascode", - "license": "Apache-2.0", - "pricing": [ - { - "name": "Rovo Dev Standard", - "value": 20, - "currency": "USD", - "per": "user/month", - "category": "Business" - } - ], - "resourceUrls": { - "download": "https://marketplace.visualstudio.com/items?itemName=Atlassian.atlascode", - "changelog": "https://marketplace.visualstudio.com/items/Atlassian.atlascode/changelog", - "pricing": "https://www.atlassian.com/software/rovo-dev/pricing", - "issue": "https://github.com/atlassian/atlascode/issues" - }, - "communityUrls": { - "linkedin": null, - "twitter": null, - "github": null, - "youtube": null, - "discord": null, - "reddit": null, - "blog": null - }, - "relatedProducts": [], - "supportedIdes": [ - { - "ideId": "vscode", - "marketplaceUrl": "https://marketplace.visualstudio.com/items?itemName=Atlassian.atlascode", - "installUri": "vscode:extension/Atlassian.atlascode" - }, - { - "ideId": "cursor", - "marketplaceUrl": null, - "installUri": "cursor:extension/Atlassian.atlascode" - }, - { - "ideId": "windsurf", - "marketplaceUrl": null, - "installUri": "windsurf:extension/Atlassian.atlascode" - } - ] -} diff --git a/manifests/extensions/verdent.json b/manifests/extensions/verdent.json index 72f221cf..62424412 100644 --- a/manifests/extensions/verdent.json +++ b/manifests/extensions/verdent.json @@ -49,6 +49,10 @@ "latestVersion": "1.5.5", "githubUrl": "https://github.com/verdentAI/docs", "license": "Proprietary", + "sourceCode": { + "status": "closed", + "repositoryRole": "documentation" + }, "pricing": [ { "name": "Lite", @@ -122,6 +126,11 @@ } ], "sources": [ + { + "url": "https://github.com/verdentAI/docs", + "title": "Verdent documentation repository", + "fields": ["githubUrl", "sourceCode"] + }, { "url": "https://www.linkedin.com/company/verdent", "title": "Verdent linkedin community", diff --git a/manifests/ides/intellij-idea.json b/manifests/ides/intellij-idea.json index e1b72326..ff2a91bc 100644 --- a/manifests/ides/intellij-idea.json +++ b/manifests/ides/intellij-idea.json @@ -48,6 +48,11 @@ "latestVersion": "2024.3", "githubUrl": "https://github.com/JetBrains/intellij-community", "license": "Proprietary", + "sourceCode": { + "status": "partial", + "repositoryRole": "source", + "license": "Apache-2.0" + }, "pricing": [ { "name": "Community", @@ -116,7 +121,7 @@ { "url": "https://github.com/JetBrains/intellij-community", "title": "IntelliJ IDEA github community", - "fields": ["communityUrls.github"] + "fields": ["githubUrl", "sourceCode", "communityUrls.github"] }, { "url": "https://www.reddit.com/r/IntelliJIDEA", diff --git a/manifests/ides/vscode.json b/manifests/ides/vscode.json index bb69d2bc..54d2d757 100644 --- a/manifests/ides/vscode.json +++ b/manifests/ides/vscode.json @@ -48,6 +48,10 @@ "latestVersion": "1.131", "githubUrl": "https://github.com/microsoft/vscode", "license": "Proprietary", + "sourceCode": { + "status": "partial", + "repositoryRole": "source" + }, "pricing": [ { "name": "Free", @@ -127,7 +131,7 @@ { "url": "https://github.com/microsoft/vscode", "title": "Visual Studio Code source repository", - "fields": ["githubUrl", "resourceUrls.issue", "communityUrls.github"] + "fields": ["githubUrl", "sourceCode", "resourceUrls.issue", "communityUrls.github"] }, { "url": "https://x.com/code", diff --git a/manifests/vendors/agentic-ai-foundation.json b/manifests/vendors/agentic-ai-foundation.json new file mode 100644 index 00000000..548d3c19 --- /dev/null +++ b/manifests/vendors/agentic-ai-foundation.json @@ -0,0 +1,68 @@ +{ + "$schema": "../$schemas/vendor.schema.json", + "id": "agentic-ai-foundation", + "name": "Agentic AI Foundation", + "aliases": ["AAIF"], + "description": "The Agentic AI Foundation, hosted by the Linux Foundation, provides vendor-neutral governance for open source agentic AI projects including Goose.", + "translations": { + "de": { + "description": "Die bei der Linux Foundation angesiedelte Agentic AI Foundation bietet anbieterneutrale Governance für quelloffene agentische KI-Projekte wie Goose." + }, + "es": { + "description": "La Agentic AI Foundation, alojada por la Linux Foundation, ofrece gobernanza neutral para proyectos de IA agéntica de código abierto como Goose." + }, + "fr": { + "description": "L’Agentic AI Foundation, hébergée par la Linux Foundation, assure une gouvernance neutre pour des projets d’IA agentique open source tels que Goose." + }, + "id": { + "description": "Agentic AI Foundation, yang dinaungi Linux Foundation, menyediakan tata kelola netral vendor untuk proyek AI agentik sumber terbuka termasuk Goose." + }, + "ja": { + "description": "Linux Foundation がホストする Agentic AI Foundation は、Goose を含むオープンソースのエージェント型 AI プロジェクトにベンダー中立のガバナンスを提供します。" + }, + "ko": { + "description": "Linux Foundation이 주관하는 Agentic AI Foundation은 Goose를 포함한 오픈 소스 에이전틱 AI 프로젝트에 공급자 중립적 거버넌스를 제공합니다." + }, + "pt": { + "description": "A Agentic AI Foundation, hospedada pela Linux Foundation, oferece governança neutra para projetos de IA agêntica de código aberto, incluindo o Goose." + }, + "ru": { + "description": "Agentic AI Foundation под эгидой Linux Foundation обеспечивает нейтральное управление проектами агентного ИИ с открытым исходным кодом, включая Goose." + }, + "tr": { + "description": "Linux Foundation bünyesindeki Agentic AI Foundation, Goose dahil açık kaynaklı aracı AI projeleri için sağlayıcıdan bağımsız yönetişim sunar." + }, + "zh-Hans": { + "description": "Agentic AI Foundation 由 Linux Foundation 托管,为包括 Goose 在内的开源智能体 AI 项目提供厂商中立的治理。" + }, + "zh-Hant": { + "description": "Agentic AI Foundation 由 Linux Foundation 託管,為包括 Goose 在內的開放原始碼智慧代理 AI 專案提供廠商中立的治理。" + } + }, + "verified": true, + "lastVerifiedAt": "2026-08-01", + "verifiedBy": "codex-agent", + "confidence": "high", + "websiteUrl": "https://aaif.io", + "communityUrls": { + "linkedin": null, + "twitter": null, + "github": "https://github.com/aaif-goose", + "youtube": null, + "discord": null, + "reddit": null, + "blog": null + }, + "sources": [ + { + "url": "https://aaif.io", + "title": "Agentic AI Foundation official site", + "fields": ["name", "description", "websiteUrl"] + }, + { + "url": "https://github.com/aaif-goose/goose", + "title": "Goose official repository", + "fields": ["description", "communityUrls.github"] + } + ] +} diff --git a/manifests/vendors/atlassian.json b/manifests/vendors/atlassian.json deleted file mode 100644 index a67c5b02..00000000 --- a/manifests/vendors/atlassian.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$schema": "../$schemas/vendor.schema.json", - "id": "atlassian", - "name": "Atlassian", - "description": "Atlassian develops collaboration and DevOps products including Jira, Confluence, Bitbucket, and the Rovo Dev coding agent.", - "translations": { - "de": { - "description": "Atlassian entwickelt Kollaborations- und DevOps-Produkte, darunter Jira, Confluence, Bitbucket und den Coding-Agenten Rovo Dev." - }, - "es": { - "description": "Atlassian desarrolla productos de colaboración y DevOps, entre ellos Jira, Confluence, Bitbucket y el agente de programación Rovo Dev." - }, - "fr": { - "description": "Atlassian développe des produits de collaboration et de DevOps, notamment Jira, Confluence, Bitbucket et l’agent de codage Rovo Dev." - }, - "id": { - "description": "Atlassian mengembangkan produk kolaborasi dan DevOps, termasuk Jira, Confluence, Bitbucket, dan agen coding Rovo Dev." - }, - "ja": { - "description": "Atlassian は、Jira、Confluence、Bitbucket、コーディングエージェント Rovo Dev などのコラボレーションおよび DevOps 製品を開発しています。" - }, - "ko": { - "description": "Atlassian은 Jira, Confluence, Bitbucket, Rovo Dev 코딩 에이전트를 비롯한 협업 및 DevOps 제품을 개발합니다." - }, - "pt": { - "description": "A Atlassian desenvolve produtos de colaboração e DevOps, incluindo Jira, Confluence, Bitbucket e o agente de programação Rovo Dev." - }, - "ru": { - "description": "Atlassian разрабатывает продукты для совместной работы и DevOps, включая Jira, Confluence, Bitbucket и агент программирования Rovo Dev." - }, - "tr": { - "description": "Atlassian; Jira, Confluence, Bitbucket ve Rovo Dev kodlama aracısı dahil olmak üzere iş birliği ve DevOps ürünleri geliştirir." - }, - "zh-Hans": { - "description": "Atlassian 开发协作和 DevOps 产品,包括 Jira、Confluence、Bitbucket 以及 Rovo Dev 编码代理。" - }, - "zh-Hant": { - "description": "Atlassian 開發協作和 DevOps 產品,包括 Jira、Confluence、Bitbucket 以及 Rovo Dev 程式設計代理。" - } - }, - "verified": true, - "sources": [ - { - "url": "https://www.atlassian.com/company", - "title": "About Atlassian", - "fields": ["name", "description", "websiteUrl"] - }, - { - "url": "https://www.atlassian.com/software/rovo-dev", - "title": "Rovo Dev", - "fields": ["description"] - }, - { - "url": "https://www.linkedin.com/company/atlassian", - "title": "Atlassian linkedin community", - "fields": ["communityUrls.linkedin"] - }, - { - "url": "https://x.com/Atlassian", - "title": "Atlassian twitter community", - "fields": ["communityUrls.twitter"] - }, - { - "url": "https://github.com/atlassian", - "title": "Atlassian github community", - "fields": ["communityUrls.github"] - }, - { - "url": "https://www.youtube.com/@Atlassian", - "title": "Atlassian youtube community", - "fields": ["communityUrls.youtube"] - }, - { - "url": "https://www.reddit.com/r/atlassian", - "title": "Atlassian reddit community", - "fields": ["communityUrls.reddit"] - }, - { - "url": "https://www.atlassian.com/blog", - "title": "Atlassian blog community", - "fields": ["communityUrls.blog"] - } - ], - "lastVerifiedAt": "2026-07-28", - "verifiedBy": "codex-agent", - "confidence": "high", - "websiteUrl": "https://www.atlassian.com", - "communityUrls": { - "linkedin": "https://www.linkedin.com/company/atlassian", - "twitter": "https://x.com/Atlassian", - "github": "https://github.com/atlassian", - "youtube": "https://www.youtube.com/@Atlassian", - "discord": null, - "reddit": "https://www.reddit.com/r/atlassian", - "blog": "https://www.atlassian.com/blog" - } -} diff --git a/scripts/fetch/fetch-github-stars.ts b/scripts/fetch/fetch-github-stars.ts index 58833a6f..a855b089 100644 --- a/scripts/fetch/fetch-github-stars.ts +++ b/scripts/fetch/fetch-github-stars.ts @@ -3,308 +3,82 @@ import https from 'node:https' import path, { dirname } from 'node:path' import { fileURLToPath } from 'node:url' -// Get __dirname equivalent in ES modules const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) - -// GitHub API token (optional but recommended to avoid rate limits) -// Set via environment variable: GITHUB_TOKEN=your_token_here node fetch-github-stars.ts const GITHUB_TOKEN = process.env.GITHUB_TOKEN - -// Path to the centralized GitHub stars data file const GITHUB_STARS_FILE = path.join(__dirname, '..', '..', 'data', 'github-stars.json') -interface DirConfig { - directory: string - category: string -} - -// Directories configuration - mapping manifest directories to categories -const dirsConfig: DirConfig[] = [ - { - directory: 'manifests/extensions', - category: 'extensions', - }, - { - directory: 'manifests/ides', - category: 'ides', - }, - { - directory: 'manifests/clis', - category: 'clis', - }, - { - directory: 'manifests/desktops', - category: 'desktops', - }, -] - -interface GithubRepo { - owner: string - repo: string -} - -interface ProcessResult { - fileId: string - stars: number | null - updated: boolean - skipped: boolean - error: boolean -} - -interface DirectoryResult { - categoryData: Record - stats: { - updated: number - skipped: number - errors: number - } -} - -interface StarsData { - extensions: Record - clis: Record - desktops: Record - ides: Record - [key: string]: Record +type StarsData = { + observedAt: string + repositories: Record } -// Extract owner and repo from GitHub URL -function parseGithubUrl(url: string): GithubRepo | null { - if (!url) return null - const match = url.match(/github\.com\/([^/]+)\/([^/]+)/) - if (!match || match.length < 3) return null - return { - owner: match[1] ?? '', - repo: match[2] ?? '', - } -} - -// Fetch stars from GitHub API -function fetchStars(owner: string, repo: string): Promise { +function fetchStars(repositoryId: string): Promise { return new Promise((resolve, reject) => { const options: https.RequestOptions = { hostname: 'api.github.com', - path: `/repos/${owner}/${repo}`, + path: `/repos/${repositoryId}`, method: 'GET', headers: { - 'User-Agent': 'acs-stars-fetcher', - Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'aicodingstack-stars-fetcher', + Accept: 'application/vnd.github+json', + ...(GITHUB_TOKEN ? { Authorization: `Bearer ${GITHUB_TOKEN}` } : {}), }, } - if (GITHUB_TOKEN) { - options.headers = { - ...options.headers, - Authorization: `token ${GITHUB_TOKEN}`, - } - } - - const req = https.request(options, res => { - let data = '' - - res.on('data', chunk => { - data += chunk + const request = https.request(options, response => { + let body = '' + response.on('data', chunk => { + body += chunk }) - - res.on('end', () => { - if (res.statusCode === 200) { - try { - const json = JSON.parse(data) - const stars = json.stargazers_count - // Convert to k format (1 decimal place) - const starsInK = parseFloat((stars / 1000).toFixed(1)) - resolve(starsInK) - } catch (e) { - reject(new Error(`Failed to parse response: ${(e as Error).message}`)) + response.on('end', () => { + if (response.statusCode !== 200) { + reject(new Error(`GitHub API returned status ${String(response.statusCode)}`)) + return + } + try { + const payload = JSON.parse(body) as { stargazers_count?: unknown } + if (!Number.isInteger(payload.stargazers_count)) { + reject(new Error('GitHub response did not include an integer stargazer count')) + return } - } else if (res.statusCode === 403) { - reject(new Error('Rate limit exceeded. Please set GITHUB_TOKEN environment variable.')) - } else if (res.statusCode === 404) { - reject(new Error('Repository not found')) - } else { - reject(new Error(`GitHub API returned status ${res.statusCode}`)) + resolve(payload.stargazers_count as number) + } catch (error) { + reject(new Error(`Failed to parse GitHub response: ${(error as Error).message}`)) } }) }) - req.on('error', e => { - reject(e) - }) - - req.end() + request.on('error', reject) + request.end() }) } -// Extract file ID from filename (remove .json extension) -function getFileId(fileName: string): string { - return fileName.replace(/\.json$/, '') -} - -// Sleep function to avoid rate limiting -function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) -} - -// Process a single JSON file -// Returns the file ID (from filename) and stars count (or null if no githubUrl or error) -async function processFile(filePath: string, fileName: string): Promise { - const fileId = getFileId(fileName) - const content = fs.readFileSync(filePath, 'utf8') - const item = JSON.parse(content) - - // Get githubUrl field from the item - const githubUrl = item.githubUrl - - // If githubUrl is null, set stars to null in the output - if (!githubUrl || githubUrl === null) { - console.log(` ⏭️ ${fileId}: githubUrl is null, setting stars to null`) - return { fileId, stars: null, updated: false, skipped: true, error: false } - } - - const parsed = parseGithubUrl(githubUrl) - if (!parsed) { - console.log(` ❌ ${fileId}: Failed to parse GitHub URL: ${githubUrl}`) - return { fileId, stars: null, updated: false, skipped: false, error: true } - } - - try { - console.log(` 🔍 ${fileId}: Fetching stars for ${parsed.owner}/${parsed.repo}...`) - const stars = await fetchStars(parsed.owner, parsed.repo) - console.log(` ✅ ${fileId}: ${stars}k stars`) - - // Sleep for 1 second to avoid rate limiting - await sleep(1000) - return { fileId, stars, updated: true, skipped: false, error: false } - } catch (error) { - console.log(` ❌ ${fileId}: Error fetching stars:`, (error as Error).message) - return { fileId, stars: null, updated: false, skipped: false, error: true } - } -} - -// Process all files in a directory -// Maps file names (without .json) to stars data based on githubUrl field -async function processDirectory( - dirConfig: DirConfig, - existingCategoryData: Record -): Promise { - const dirPath = path.join(__dirname, '..', '..', dirConfig.directory) - console.log(`\n📁 Processing ${dirConfig.directory}...`) - - if (!fs.existsSync(dirPath)) { - console.log(` ⚠️ Directory not found: ${dirPath}`) - return { categoryData: {}, stats: { updated: 0, skipped: 0, errors: 0 } } - } - - // Get all JSON files in the directory - const files = fs.readdirSync(dirPath).filter(file => file.endsWith('.json')) - - if (files.length === 0) { - console.log(` ⚠️ No JSON files found in ${dirConfig.directory}`) - return { categoryData: {}, stats: { updated: 0, skipped: 0, errors: 0 } } - } - - let updated = 0 - let skipped = 0 - let errors = 0 - const categoryData: Record = {} - - // Process each file and map by filename (without .json extension) - for (const file of files) { - const filePath = path.join(dirPath, file) - const result = await processFile(filePath, file) - - if (result.updated) updated++ - if (result.skipped) skipped++ - if (result.error) errors++ - - // Map file ID (filename without .json) to stars value (can be null) - categoryData[result.fileId] = result.error - ? (existingCategoryData[result.fileId] ?? null) - : result.stars - } - - console.log( - `\n✨ ${dirConfig.directory} completed: ${updated} updated, ${skipped} skipped, ${errors} errors` - ) - return { categoryData, stats: { updated, skipped, errors } } -} - -// Main function async function main(): Promise { - console.log('🚀 Starting GitHub stars fetcher...\n') - console.log('📝 Note: Updating centralized github-stars.json file\n') - - if (!GITHUB_TOKEN) { - console.log('⚠️ Warning: No GITHUB_TOKEN set. You may hit rate limits (60 requests/hour).') - console.log(' Set it with: GITHUB_TOKEN=your_token node fetch-github-stars.ts\n') - } else { - console.log('✅ Using GitHub token for authentication\n') - } - - // Load existing stars data or create new structure - let starsData: StarsData = { extensions: {}, clis: {}, desktops: {}, ides: {} } - if (fs.existsSync(GITHUB_STARS_FILE)) { - try { - const content = fs.readFileSync(GITHUB_STARS_FILE, 'utf8') - starsData = JSON.parse(content) as StarsData - console.log('📂 Loaded existing github-stars.json\n') - } catch { - console.log('⚠️ Failed to parse existing github-stars.json, creating new one\n') - } - } - - let totalUpdated = 0 - let totalSkipped = 0 - let totalErrors = 0 + const current = JSON.parse(fs.readFileSync(GITHUB_STARS_FILE, 'utf8')) as StarsData + let updated = 0 + let failed = 0 - // Process each directory and collect stars data - // Maps file names to stars based on githubUrl field in each manifest file - for (const dirConfig of dirsConfig) { + for (const repositoryId of Object.keys(current.repositories).sort()) { try { - const { categoryData, stats } = await processDirectory( - dirConfig, - starsData[dirConfig.category] ?? {} - ) - - // Sort the category data by key (alphabetically) - const sortedCategoryData = Object.keys(categoryData) - .sort() - .reduce>((acc, key) => { - acc[key] = categoryData[key] ?? null - return acc - }, {}) - - // Update the stars data for this category - // This will include all files, with null values for items without githubUrl - starsData[dirConfig.category] = sortedCategoryData - - totalUpdated += stats.updated - totalSkipped += stats.skipped - totalErrors += stats.errors + const stars = await fetchStars(repositoryId) + current.repositories[repositoryId] = stars + updated += 1 + console.log(`✓ ${repositoryId}: ${stars.toLocaleString('en-US')}`) } catch (error) { - console.error(`❌ Failed to process ${dirConfig.directory}:`, (error as Error).message) - totalErrors++ + failed += 1 + console.error(`✗ ${repositoryId}: ${(error as Error).message}`) } } - // Write the updated stars data to file - try { - fs.writeFileSync(GITHUB_STARS_FILE, `${JSON.stringify(starsData, null, 2)}\n`, 'utf8') - console.log('\n📝 Successfully updated data/github-stars.json') - } catch (error) { - console.error('\n❌ Failed to write github-stars.json:', (error as Error).message) - process.exit(1) - } + current.observedAt = new Date().toISOString().slice(0, 10) + fs.writeFileSync(GITHUB_STARS_FILE, `${JSON.stringify(current, null, 2)}\n`, 'utf8') + console.log(`Updated ${updated} repositories; ${failed} retained their previous values.`) - console.log(`\n${'='.repeat(50)}`) - console.log('🎉 All directories processed!') - console.log(`📊 Total: ${totalUpdated} updated, ${totalSkipped} skipped, ${totalErrors} errors`) - console.log('='.repeat(50)) + if (failed > 0) process.exitCode = 1 } -// Run the script main().catch(error => { - console.error('Fatal error:', error) + console.error(error) process.exit(1) }) diff --git a/scripts/generate/generate-manifest-indexes.ts b/scripts/generate/generate-manifest-indexes.ts index 10b7b36d..d3015b64 100644 --- a/scripts/generate/generate-manifest-indexes.ts +++ b/scripts/generate/generate-manifest-indexes.ts @@ -207,18 +207,26 @@ function generateGithubStarsFile(): void { import githubStarsJson from '../../../data/github-stars.json' -export type GithubStarsData = Record> +export interface GithubStarsData { + observedAt: string + repositories: Record +} export const githubStarsData = githubStarsJson as GithubStarsData /** * Get GitHub stars for a specific product - * @param category - The product category (extensions, clis, desktops, ides) - * @param id - The product ID + * @param githubUrl - The product's GitHub repository URL * @returns The number of stars (in thousands) or null if not available */ -export function getGithubStars(category: string, id: string): number | null { - return githubStarsData[category]?.[id] ?? null +export function getGithubStars(githubUrl: string | null | undefined): number | null { + if (!githubUrl) return null + const repositoryId = githubUrl + .replace(/\\/$/, '') + .replace(/\\.git$/, '') + .replace(/^https:\\/\\/github\\.com\\//, '') + const stars = githubStarsData.repositories[repositoryId] + return typeof stars === 'number' ? Math.round(stars / 100) / 10 : null } export default githubStarsData @@ -228,9 +236,7 @@ export default githubStarsData fs.writeFileSync(outputPath, content, 'utf8') // Count total entries - const totalEntries = Object.values(starsData).reduce((sum: number, category: unknown) => { - return sum + Object.keys(category as Record).length - }, 0) + const totalEntries = Object.keys(starsData.repositories as Record).length console.log(`✓ Generated github-stars.ts (${totalEntries} entries)`) } diff --git a/scripts/validate/lib/routes.ts b/scripts/validate/lib/routes.ts index fddb8f16..4ca3e8ae 100644 --- a/scripts/validate/lib/routes.ts +++ b/scripts/validate/lib/routes.ts @@ -36,6 +36,7 @@ export function getStaticRoutes(): string[] { '/open-source-rank', '/search', '/clis/comparison', + '/desktops/comparison', '/extensions/comparison', '/ides/comparison', '/models/comparison', diff --git a/src/app/[locale]/clis/[slug]/page.tsx b/src/app/[locale]/clis/[slug]/page.tsx index e0b13298..95c41f0f 100644 --- a/src/app/[locale]/clis/[slug]/page.tsx +++ b/src/app/[locale]/clis/[slug]/page.tsx @@ -118,7 +118,7 @@ export default async function CLIPage({ deprecated={cli.deprecated ?? false} latestVersion={cli.latestVersion} license={cli.license} - githubStars={getGithubStars('clis', cli.id)} + githubStars={getGithubStars(cli.githubUrl)} platforms={cli.platforms?.map(p => p.os)} websiteUrl={websiteUrl} docsUrl={docsUrl} diff --git a/src/app/[locale]/clis/comparison/page.client.tsx b/src/app/[locale]/clis/comparison/page.client.tsx index 75bdf602..09c1a7db 100644 --- a/src/app/[locale]/clis/comparison/page.client.tsx +++ b/src/app/[locale]/clis/comparison/page.client.tsx @@ -98,9 +98,8 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) { key: 'githubStars', label: tShared('terms.stars'), render: (_: unknown, item: Record) => { - const id = item.id as string - const stars = getGithubStars('clis', id) const githubUrl = item.githubUrl as string | null | undefined + const stars = getGithubStars(githubUrl) if (stars === null || stars === undefined) return - diff --git a/src/app/[locale]/desktops/[slug]/page.tsx b/src/app/[locale]/desktops/[slug]/page.tsx index f2712f8b..dfcabdd2 100644 --- a/src/app/[locale]/desktops/[slug]/page.tsx +++ b/src/app/[locale]/desktops/[slug]/page.tsx @@ -97,7 +97,7 @@ export default async function DesktopPage({ verified={desktop.verified ?? false} latestVersion={desktop.latestVersion} license={desktop.license} - githubStars={getGithubStars('desktops', desktop.id)} + githubStars={getGithubStars(desktop.githubUrl)} platforms={desktop.platforms.map(platform => platform.os)} websiteUrl={websiteUrl} docsUrl={desktop.docsUrl || undefined} diff --git a/src/app/[locale]/desktops/comparison/page.client.tsx b/src/app/[locale]/desktops/comparison/page.client.tsx new file mode 100644 index 00000000..6ab72a6e --- /dev/null +++ b/src/app/[locale]/desktops/comparison/page.client.tsx @@ -0,0 +1,307 @@ +'use client' + +import { Download, FileText, Github, Home, Linkedin, Twitter, Youtube } from 'lucide-react' +import { useTranslations } from 'next-intl' +import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/PlatformIcons' +import Footer from '@/components/Footer' +import Header from '@/components/Header' +import { Breadcrumb } from '@/components/navigation/Breadcrumb' +import ComparisonTable, { type ComparisonColumn } from '@/components/product/ComparisonTable' +import { PricingSummaryValue } from '@/components/product/ProductPricing' +import { withVendorCommunityUrlsForCatalog } from '@/lib/community-urls' +import { desktopsData, vendorsData } from '@/lib/generated' +import { getGithubStars } from '@/lib/generated/github-stars' +import { renderLicense } from '@/lib/license' +import type { PricingTier } from '@/lib/pricing' +import type { ManifestDesktop, ManifestVendor } from '@/types/manifests' + +const desktops = withVendorCommunityUrlsForCatalog( + desktopsData as unknown as ManifestDesktop[], + vendorsData as unknown as ManifestVendor[] +) + +type Props = { + locale: string +} + +export default function DesktopComparisonPageClient({ locale: _locale }: Props) { + const tPage = useTranslations('pages.comparison') + const tShared = useTranslations('shared') + const columns: ComparisonColumn[] = [ + { + key: 'vendor', + label: tShared('categories.singular.vendor'), + }, + { + key: 'license', + label: tShared('terms.license'), + render: (value: unknown, item: Record) => + renderLicense(value, item, tShared), + }, + { + key: 'latestVersion', + label: tShared('terms.version'), + }, + { + key: 'platforms', + label: tShared('terms.platforms'), + render: (value: unknown) => { + const platforms = value as Array<{ os: string }> | string[] + if (!platforms || platforms.length === 0) return '-' + + // Handle both old format (string[]) and new format (Array<{ os: string }>) + const platformNames = Array.isArray(platforms) + ? platforms.map(p => (typeof p === 'string' ? p : p.os)) + : [] + + return ( +
+ {platformNames.includes('macOS') && ( + + + + )} + {platformNames.includes('Windows') && ( + + + + )} + {platformNames.includes('Linux') && ( + + + + )} +
+ ) + }, + }, + { + key: 'githubStars', + label: tShared('terms.stars'), + render: (_: unknown, item: Record) => { + const githubUrl = item.githubUrl as string | null | undefined + const stars = getGithubStars(githubUrl) + + if (stars === null || stars === undefined) + return - + + const starsText = `${stars.toFixed(1)}k` + + if (githubUrl) { + return ( + + {starsText} + + ) + } + + return {starsText} + }, + }, + { + key: 'links', + label: tPage('columns.links'), + render: (_: unknown, item: Record) => { + const websiteUrl = item.websiteUrl as string | undefined + const docsUrl = item.docsUrl as string | undefined + const resourceUrls = item.resourceUrls as + | { + download?: string + } + | undefined + const communityUrls = item.communityUrls as + | { + github?: string + twitter?: string + linkedin?: string + youtube?: string + reddit?: string + } + | undefined + + return ( +
+ {websiteUrl ? ( + + + + ) : ( + + + + )} + {resourceUrls?.download ? ( + + + + ) : ( + + + + )} + {docsUrl ? ( + + + + ) : ( + + + + )} + {communityUrls?.github ? ( + + + + ) : ( + + + + )} + {communityUrls?.twitter ? ( + + + + ) : ( + + + + )} + {communityUrls?.linkedin ? ( + + + + ) : ( + + + + )} + {communityUrls?.youtube ? ( + + + + ) : ( + + + + )} +
+ ) + }, + }, + { + key: 'pricing-free', + label: tPage('columns.freePlan'), + render: (_: unknown, item: Record) => { + const pricing = item.pricing as PricingTier[] + if (!pricing || pricing.length === 0) return '-' + const freePlan = pricing.find(p => p.value === 0) + return freePlan ? '✓' : '-' + }, + }, + { + key: 'pricing-min', + label: tPage('columns.startingPrice'), + render: (_: unknown, item: Record) => { + const pricing = item.pricing as PricingTier[] + return + }, + }, + { + key: 'pricing-max', + label: tPage('columns.maxPrice'), + render: (_: unknown, item: Record) => { + const pricing = item.pricing as PricingTier[] + return + }, + }, + ] + + return ( + <> +
+ + + + {/* Page Header */} +
+
+

+ {tPage('desktops.title')} +

+

+ {tPage('desktops.subtitle')} +

+
+
+ + {/* Comparison Table */} +
+
+ []} + columns={columns} + itemLinkPrefix={`/desktops`} + nameColumnLabel={tShared('labels.name')} + caption={tPage('desktops.title')} + scrollHint={tPage('table.scrollHint')} + /> +
+
+ +