diff --git a/README.md b/README.md index 4bae3bb..e727d9e 100644 --- a/README.md +++ b/README.md @@ -108,20 +108,25 @@ npx @shelldandy/grafana-ui-mcp-server grafana-ui-mcp [options] Options: - --github-api-key, -g GitHub Personal Access Token - --help, -h Show help message - --version, -v Show version information + --github-api-key, -g GitHub Personal Access Token + --grafana-repo-path, -l Path to local Grafana repository (takes precedence) + --help, -h Show help message + --version, -v Show version information Environment Variables: - GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token - GITHUB_TOKEN Alternative way to provide GitHub token + GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token + GITHUB_TOKEN Alternative way to provide GitHub token + GRAFANA_REPO_PATH Path to local Grafana repository Examples: npx @shelldandy/grafana-ui-mcp-server --help npx @shelldandy/grafana-ui-mcp-server --version npx @shelldandy/grafana-ui-mcp-server -g ghp_1234567890abcdef + npx @shelldandy/grafana-ui-mcp-server --grafana-repo-path /path/to/grafana + npx @shelldandy/grafana-ui-mcp-server -l /path/to/grafana GITHUB_PERSONAL_ACCESS_TOKEN=ghp_token npx @shelldandy/grafana-ui-mcp-server GITHUB_TOKEN=ghp_token npx @shelldandy/grafana-ui-mcp-server + GRAFANA_REPO_PATH=/path/to/grafana npx @shelldandy/grafana-ui-mcp-server ``` ## ๐Ÿ”‘ GitHub API Token Setup @@ -197,6 +202,75 @@ npx @shelldandy/grafana-ui-mcp-server --github-api-key ghp_your_token --help curl -H "Authorization: token ghp_your_token" https://api.github.com/rate_limit ``` +## ๐Ÿ  Local Development Support + +**NEW**: Work with a local Grafana repository for faster development and access to uncommitted changes! + +### ๐ŸŽฏ Why Use Local Repository? + +- **โšก Faster Access**: Direct filesystem reads, no network latency +- **๐Ÿšซ No Rate Limits**: Unlimited component access +- **๐Ÿ”„ Real-time Updates**: See your local changes immediately +- **๐Ÿ“ก Offline Support**: Works without internet connection +- **๐Ÿงช Development Workflow**: Test with modified/uncommitted components + +### ๐Ÿ”ง Setup with Local Repository + +1. **Clone the Grafana Repository**: + ```bash + git clone https://github.com/grafana/grafana.git + cd grafana + ``` + +2. **Use Local Path** (takes precedence over GitHub API): + ```bash + # Command line option + npx @shelldandy/grafana-ui-mcp-server --grafana-repo-path /path/to/grafana + npx @shelldandy/grafana-ui-mcp-server -l /path/to/grafana + + # Environment variable + export GRAFANA_REPO_PATH=/path/to/grafana + npx @shelldandy/grafana-ui-mcp-server + ``` + +3. **Claude Desktop Configuration**: + ```json + { + "mcpServers": { + "grafana-ui": { + "command": "npx", + "args": ["@shelldandy/grafana-ui-mcp-server"], + "env": { + "GRAFANA_REPO_PATH": "/path/to/your/grafana/repository" + } + } + } + } + ``` + +### ๐Ÿ”„ Configuration Priority + +The server checks sources in this order: + +1. **Local Repository** (`--grafana-repo-path` or `GRAFANA_REPO_PATH`) +2. **GitHub API with Token** (`--github-api-key` or `GITHUB_*_TOKEN`) +3. **GitHub API without Token** (rate limited to 60 requests/hour) + +### ๐Ÿ›ก๏ธ Graceful Fallback + +- If local file doesn't exist โ†’ Falls back to GitHub API automatically +- If local repository is invalid โ†’ Falls back to GitHub API with warning +- Source is indicated in tool responses (`"source": "local"` vs `"source": "github"`) + +### โœ… Verify Local Setup + +```bash +# Test local repository access +npx @shelldandy/grafana-ui-mcp-server --grafana-repo-path /path/to/grafana --help + +# Should show: "Local Grafana repository configured: /path/to/grafana" +``` + ## ๐Ÿ› ๏ธ Tool Usage Examples The MCP server provides the unified `grafana_ui` tool for AI assistants: diff --git a/specs/03-local-repo-support.md b/specs/03-local-repo-support.md new file mode 100644 index 0000000..f7a76ab --- /dev/null +++ b/specs/03-local-repo-support.md @@ -0,0 +1,200 @@ +# Local Grafana Repository Support Specification + +## Overview + +Add a new CLI option `--grafana-repo-path` (and equivalent environment variable `GRAFANA_REPO_PATH`) to allow users to specify a local Grafana repository path. This option takes precedence over GitHub API access, enabling the MCP server to read components directly from a local filesystem. + +## Implementation Plan + +### 1. CLI Interface Updates (`src/index.ts`) + +**Add new CLI option:** +- `--grafana-repo-path ` / `-l ` - Path to local Grafana repository +- Environment variable: `GRAFANA_REPO_PATH` +- Update help text to document the new option +- Precedence: Local repo โ†’ GitHub API key โ†’ Unauthenticated GitHub + +**Configuration logic:** +```typescript +const { githubApiKey, grafanaRepoPath } = await parseArgs(); + +if (grafanaRepoPath) { + axios.setLocalGrafanaRepo(grafanaRepoPath); + console.error("Local Grafana repository configured"); +} else if (githubApiKey) { + axios.setGitHubApiKey(githubApiKey); + console.error("GitHub API key configured"); +} +``` + +### 2. Core Utilities Enhancement (`src/utils/axios.ts`) + +**Add local filesystem support:** +- New function: `setLocalGrafanaRepo(repoPath: string)` +- New internal flag: `localRepoPath: string | null` +- Update all existing functions to check local repo first before GitHub API +- Add filesystem utilities using Node.js `fs` module + +**Function modifications:** +- `getComponentSource()` - Check local filesystem first +- `getComponentDemo()` - Read local `.story.tsx` files +- `getAvailableComponents()` - Use `fs.readdir()` on local components directory +- `getComponentMetadata()` - Parse local directory structure +- `getComponentDocumentation()` - Read local `.mdx` files +- `getComponentTests()` - Read local test files +- `searchComponents()` - Search local filesystem +- `getThemeFiles()` - Read local theme files +- `getComponentDependencies()` - Analyze local files +- `buildDirectoryTree()` - Build tree from local filesystem + +**Path resolution:** +```typescript +const LOCAL_COMPONENTS_PATH = "packages/grafana-ui/src/components"; +const resolveLocalPath = (subPath: string) => + path.join(localRepoPath!, subPath); +``` + +### 3. Error Handling & Validation + +**Validation checks:** +- Verify local path exists and is readable +- Check if path contains expected Grafana structure (`packages/grafana-ui/src/components/`) +- Graceful fallback to GitHub API if local files are missing +- Clear error messages for invalid local repository paths + +**Graceful degradation:** +- If local file doesn't exist, try GitHub API as fallback +- Maintain same error message format for consistency +- Log source (local vs GitHub) for debugging + +### 4. Performance Optimizations + +**Local filesystem advantages:** +- No rate limiting concerns +- Faster file access (no network latency) +- Support for modified/uncommitted components +- Real-time development workflow support + +**Caching strategy:** +- Minimal caching needed for local files +- Optional file modification time checking +- Preserve existing GitHub API caching when used as fallback + +### 5. Documentation Updates + +**Help text updates:** +- Document new `--grafana-repo-path` option +- Explain precedence order (local โ†’ GitHub API โ†’ unauthenticated) +- Add usage examples for local development workflow +- Update environment variable documentation + +**README.md updates:** +- New "Local Development" section +- Examples of local repository setup +- Benefits of local vs GitHub API access +- Troubleshooting section for local path issues + +## Benefits + +1. **Development Workflow**: Developers can work with local, potentially modified components +2. **No Rate Limits**: Unlimited access to components without GitHub API constraints +3. **Faster Access**: Direct filesystem reads are faster than HTTP requests +4. **Offline Support**: Works without internet connection +5. **Real-time Updates**: Reflects local changes immediately +6. **Backward Compatibility**: Existing GitHub API workflow remains unchanged + +## Files to Modify + +1. `specs/03-local-repo-support.md` - **NEW** - This specification document +2. `src/index.ts` - Add CLI argument parsing for `--grafana-repo-path` option +3. `src/utils/axios.ts` - Add filesystem support and local repo precedence logic +4. `README.md` - Document new local repository feature + +## Success Criteria + +- [ ] CLI accepts `--grafana-repo-path` option and `GRAFANA_REPO_PATH` environment variable +- [ ] All 11 MCP tools work with local repository path +- [ ] Graceful fallback to GitHub API when local files missing +- [ ] Path validation with clear error messages +- [ ] Maintains backward compatibility with existing GitHub API workflow +- [ ] Documentation updated with local development examples +- [ ] No breaking changes to existing functionality + +## Implementation Details + +### CLI Argument Parsing + +```typescript +// In parseArgs() function +const grafanaRepoPathIndex = args.findIndex( + (arg) => arg === "--grafana-repo-path" || arg === "-l", +); +let grafanaRepoPath = null; + +if (grafanaRepoPathIndex !== -1 && args[grafanaRepoPathIndex + 1]) { + grafanaRepoPath = args[grafanaRepoPathIndex + 1]; +} else if (process.env.GRAFANA_REPO_PATH) { + grafanaRepoPath = process.env.GRAFANA_REPO_PATH; +} + +return { githubApiKey, grafanaRepoPath }; +``` + +### Filesystem Functions + +```typescript +// New filesystem utilities in axios.ts +import fs from 'fs'; +import path from 'path'; + +let localRepoPath: string | null = null; + +function setLocalGrafanaRepo(repoPath: string): void { + // Validate path exists and has expected structure + const componentsPath = path.join(repoPath, LOCAL_COMPONENTS_PATH); + if (!fs.existsSync(componentsPath)) { + throw new Error(`Invalid Grafana repository path: ${componentsPath} not found`); + } + localRepoPath = repoPath; +} + +async function getComponentSourceLocal(componentName: string): Promise { + if (!localRepoPath) return null; + const componentPath = path.join(localRepoPath, LOCAL_COMPONENTS_PATH, componentName, `${componentName}.tsx`); + + try { + return fs.readFileSync(componentPath, 'utf8'); + } catch (error) { + return null; // Fall back to GitHub API + } +} +``` + +### Help Text Updates + +```text +Options: + --github-api-key, -g GitHub Personal Access Token for API access + --grafana-repo-path, -l Path to local Grafana repository (takes precedence over GitHub API) + --help, -h Show this help message + --version, -v Show version information + +Environment Variables: + GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token + GITHUB_TOKEN Alternative way to provide GitHub token + GRAFANA_REPO_PATH Path to local Grafana repository + +Examples: + npx @shelldandy/grafana-ui-mcp-server + npx @shelldandy/grafana-ui-mcp-server --github-api-key ghp_your_token_here + npx @shelldandy/grafana-ui-mcp-server --grafana-repo-path /path/to/grafana + npx @shelldandy/grafana-ui-mcp-server -l /path/to/grafana +``` + +## Testing Strategy + +1. **Unit Testing**: Test filesystem functions with mock filesystem +2. **Integration Testing**: Test with actual local Grafana repository +3. **Fallback Testing**: Verify GitHub API fallback when local files missing +4. **Error Handling**: Test invalid paths and missing files +5. **Backward Compatibility**: Ensure existing GitHub workflow unaffected \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1af9ede..b147ded 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,21 +30,25 @@ A Model Context Protocol server for Grafana UI components, providing AI assistan with comprehensive access to component source code, documentation, stories, and metadata. Usage: - npx @jpisnice/grafana-ui-mcp-server [options] + npx @shelldandy/grafana-ui-mcp-server [options] Options: - --github-api-key, -g GitHub Personal Access Token for API access - --help, -h Show this help message - --version, -v Show version information + --github-api-key, -g GitHub Personal Access Token for API access + --grafana-repo-path, -l Path to local Grafana repository (takes precedence over GitHub API) + --help, -h Show this help message + --version, -v Show version information Examples: - npx @jpisnice/grafana-ui-mcp-server - npx @jpisnice/grafana-ui-mcp-server --github-api-key ghp_your_token_here - npx @jpisnice/grafana-ui-mcp-server -g ghp_your_token_here + npx @shelldandy/grafana-ui-mcp-server + npx @shelldandy/grafana-ui-mcp-server --github-api-key ghp_your_token_here + npx @shelldandy/grafana-ui-mcp-server -g ghp_your_token_here + npx @shelldandy/grafana-ui-mcp-server --grafana-repo-path /path/to/grafana + npx @shelldandy/grafana-ui-mcp-server -l /path/to/grafana Environment Variables: - GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token - GITHUB_TOKEN Alternative way to provide GitHub token + GITHUB_PERSONAL_ACCESS_TOKEN Alternative way to provide GitHub token + GITHUB_TOKEN Alternative way to provide GitHub token + GRAFANA_REPO_PATH Path to local Grafana repository Available Tool (Unified Interface): Single Tool: grafana_ui @@ -116,7 +120,19 @@ For more information, visit: https://github.com/shelldandy/grafana-ui-mcp-server githubApiKey = process.env.GITHUB_TOKEN; } - return { githubApiKey }; + // Grafana repository path + const grafanaRepoPathIndex = args.findIndex( + (arg) => arg === "--grafana-repo-path" || arg === "-l", + ); + let grafanaRepoPath = null; + + if (grafanaRepoPathIndex !== -1 && args[grafanaRepoPathIndex + 1]) { + grafanaRepoPath = args[grafanaRepoPathIndex + 1]; + } else if (process.env.GRAFANA_REPO_PATH) { + grafanaRepoPath = process.env.GRAFANA_REPO_PATH; + } + + return { githubApiKey, grafanaRepoPath }; } /** @@ -124,18 +140,32 @@ For more information, visit: https://github.com/shelldandy/grafana-ui-mcp-server */ async function main() { try { - const { githubApiKey } = await parseArgs(); - - // Configure GitHub API key if provided - if (githubApiKey) { + const { githubApiKey, grafanaRepoPath } = await parseArgs(); + + // Configure local Grafana repository path (takes precedence over GitHub API) + if (grafanaRepoPath) { + try { + axios.setLocalGrafanaRepo(grafanaRepoPath); + console.error(`Local Grafana repository configured: ${grafanaRepoPath}`); + } catch (error: any) { + console.error(`Error configuring local repository: ${error.message}`); + console.error("Falling back to GitHub API access"); + + // Fall back to GitHub API configuration + if (githubApiKey) { + axios.setGitHubApiKey(githubApiKey); + console.error("GitHub API key configured successfully"); + } + } + } else if (githubApiKey) { axios.setGitHubApiKey(githubApiKey); console.error("GitHub API key configured successfully"); } else { console.error( - "Warning: No GitHub API key provided. Rate limited to 60 requests/hour.", + "Warning: No local repository or GitHub API key provided. Rate limited to 60 requests/hour.", ); console.error( - "Use --github-api-key flag or set GITHUB_PERSONAL_ACCESS_TOKEN or GITHUB_TOKEN environment variable.", + "Use --grafana-repo-path for local access or --github-api-key for GitHub API access.", ); } diff --git a/src/utils/axios.ts b/src/utils/axios.ts index dd32d9f..3dc585f 100644 --- a/src/utils/axios.ts +++ b/src/utils/axios.ts @@ -1,4 +1,6 @@ import { Axios } from "axios"; +import fs from "fs"; +import path from "path"; // Constants for the Grafana UI repository structure const REPO_OWNER = "grafana"; @@ -7,6 +9,9 @@ const REPO_BRANCH = "main"; const GRAFANA_UI_BASE_PATH = "packages/grafana-ui/src"; const COMPONENTS_PATH = `${GRAFANA_UI_BASE_PATH}/components`; +// Local repository configuration +let localRepoPath: string | null = null; + // GitHub API for accessing repository structure and metadata const githubApi = new Axios({ baseURL: "https://api.github.com", @@ -41,12 +46,72 @@ const githubRaw = new Axios({ transformResponse: [(data) => data], // Return raw data }); +/** + * Set local Grafana repository path + * @param repoPath Path to local Grafana repository + */ +function setLocalGrafanaRepo(repoPath: string): void { + // Validate path exists and has expected structure + const componentsPath = path.join(repoPath, COMPONENTS_PATH); + if (!fs.existsSync(componentsPath)) { + throw new Error( + `Invalid Grafana repository path: ${componentsPath} not found. ` + + `Expected Grafana repository structure with ${COMPONENTS_PATH} directory.` + ); + } + + // Additional validation - check for at least one component directory + try { + const componentDirs = fs.readdirSync(componentsPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + if (componentDirs.length === 0) { + throw new Error( + `No component directories found in ${componentsPath}. ` + + `Expected Grafana UI component structure.` + ); + } + } catch (error: any) { + throw new Error( + `Cannot read components directory ${componentsPath}: ${error.message}` + ); + } + + localRepoPath = repoPath; + console.log(`Local Grafana repository configured: ${repoPath}`); +} + +/** + * Get component source from local filesystem + * @param componentName Name of the component + * @returns Promise with component source code or null if not found locally + */ +async function getComponentSourceLocal(componentName: string): Promise { + if (!localRepoPath) return null; + + const componentPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.tsx`); + + try { + return fs.readFileSync(componentPath, 'utf8'); + } catch (error) { + return null; // Fall back to GitHub API + } +} + /** * Fetch component source code from Grafana UI * @param componentName Name of the component (e.g., "Button", "Alert") * @returns Promise with component source code */ async function getComponentSource(componentName: string): Promise { + // Try local filesystem first + const localSource = await getComponentSourceLocal(componentName); + if (localSource !== null) { + return localSource; + } + + // Fall back to GitHub API const componentPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.tsx`; try { @@ -54,17 +119,41 @@ async function getComponentSource(componentName: string): Promise { return response.data; } catch (error) { throw new Error( - `Component "${componentName}" not found in Grafana UI repository`, + `Component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } +/** + * Get component demo from local filesystem + * @param componentName Name of the component + * @returns Promise with component demo code or null if not found locally + */ +async function getComponentDemoLocal(componentName: string): Promise { + if (!localRepoPath) return null; + + const storyPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.story.tsx`); + + try { + return fs.readFileSync(storyPath, 'utf8'); + } catch (error) { + return null; // Fall back to GitHub API + } +} + /** * Fetch component story/example from Grafana UI * @param componentName Name of the component * @returns Promise with component story code */ async function getComponentDemo(componentName: string): Promise { + // Try local filesystem first + const localDemo = await getComponentDemoLocal(componentName); + if (localDemo !== null) { + return localDemo; + } + + // Fall back to GitHub API const storyPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.story.tsx`; try { @@ -72,16 +161,43 @@ async function getComponentDemo(componentName: string): Promise { return response.data; } catch (error) { throw new Error( - `Story for component "${componentName}" not found in Grafana UI repository`, + `Story for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } +/** + * Get available components from local filesystem + * @returns Promise with list of component names or null if not available locally + */ +async function getAvailableComponentsLocal(): Promise { + if (!localRepoPath) return null; + + const componentsPath = path.join(localRepoPath, COMPONENTS_PATH); + + try { + const items = fs.readdirSync(componentsPath, { withFileTypes: true }); + return items + .filter(item => item.isDirectory()) + .map(item => item.name) + .sort(); + } catch (error) { + return null; // Fall back to GitHub API + } +} + /** * Fetch all available components from Grafana UI * @returns Promise with list of component names */ async function getAvailableComponents(): Promise { + // Try local filesystem first + const localComponents = await getAvailableComponentsLocal(); + if (localComponents !== null) { + return localComponents; + } + + // Fall back to GitHub API try { const response = await githubApi.get( `/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}`, @@ -90,7 +206,45 @@ async function getAvailableComponents(): Promise { .filter((item: any) => item.type === "dir") .map((item: any) => item.name); } catch (error) { - throw new Error("Failed to fetch available components from Grafana UI"); + throw new Error( + `Failed to fetch available components from ${localRepoPath ? 'local repository or ' : ''}Grafana UI` + ); + } +} + +/** + * Get component metadata from local filesystem + * @param componentName Name of the component + * @returns Promise with component metadata or null if not available locally + */ +async function getComponentMetadataLocal(componentName: string): Promise { + if (!localRepoPath) return null; + + const componentPath = path.join(localRepoPath, COMPONENTS_PATH, componentName); + + try { + const items = fs.readdirSync(componentPath, { withFileTypes: true }); + const files = items + .filter(item => item.isFile()) + .map(item => item.name); + + // Basic metadata from file structure + return { + name: componentName, + type: "grafana-ui-component", + source: "local", + files: files, + hasImplementation: files.includes(`${componentName}.tsx`), + hasStories: files.some((file) => file.endsWith(".story.tsx")), + hasDocumentation: files.includes(`${componentName}.mdx`), + hasTests: files.some((file) => file.endsWith(".test.tsx")), + hasTypes: files.includes("types.ts"), + hasUtils: files.includes("utils.ts"), + hasStyles: files.includes("styles.ts"), + totalFiles: files.length, + }; + } catch (error) { + return null; // Fall back to GitHub API } } @@ -100,6 +254,13 @@ async function getAvailableComponents(): Promise { * @returns Promise with component metadata */ async function getComponentMetadata(componentName: string): Promise { + // Try local filesystem first + const localMetadata = await getComponentMetadataLocal(componentName); + if (localMetadata !== null) { + return localMetadata; + } + + // Fall back to GitHub API try { // Get the component directory contents const response = await githubApi.get( @@ -116,6 +277,7 @@ async function getComponentMetadata(componentName: string): Promise { return { name: componentName, type: "grafana-ui-component", + source: "github", files: files, hasImplementation: files.includes(`${componentName}.tsx`), hasStories: files.some((file) => file.endsWith(".story.tsx")), @@ -339,6 +501,23 @@ async function buildDirectoryTreeWithFallback( } } +/** + * Get component documentation from local filesystem + * @param componentName Name of the component + * @returns Promise with component documentation or null if not found locally + */ +async function getComponentDocumentationLocal(componentName: string): Promise { + if (!localRepoPath) return null; + + const docPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.mdx`); + + try { + return fs.readFileSync(docPath, 'utf8'); + } catch (error) { + return null; // Fall back to GitHub API + } +} + /** * Fetch component documentation from Grafana UI * @param componentName Name of the component @@ -347,6 +526,13 @@ async function buildDirectoryTreeWithFallback( async function getComponentDocumentation( componentName: string, ): Promise { + // Try local filesystem first + const localDocs = await getComponentDocumentationLocal(componentName); + if (localDocs !== null) { + return localDocs; + } + + // Fall back to GitHub API const docPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.mdx`; try { @@ -354,7 +540,7 @@ async function getComponentDocumentation( return response.data; } catch (error) { throw new Error( - `Documentation for component "${componentName}" not found in Grafana UI repository`, + `Documentation for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } @@ -444,12 +630,36 @@ async function getGitHubRateLimit(): Promise { } } +/** + * Get component tests from local filesystem + * @param componentName Name of the component + * @returns Promise with component test code or null if not found locally + */ +async function getComponentTestsLocal(componentName: string): Promise { + if (!localRepoPath) return null; + + const testPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.test.tsx`); + + try { + return fs.readFileSync(testPath, 'utf8'); + } catch (error) { + return null; // Fall back to GitHub API + } +} + /** * Fetch component test files from Grafana UI * @param componentName Name of the component * @returns Promise with component test code */ async function getComponentTests(componentName: string): Promise { + // Try local filesystem first + const localTests = await getComponentTestsLocal(componentName); + if (localTests !== null) { + return localTests; + } + + // Fall back to GitHub API const testPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.test.tsx`; try { @@ -457,7 +667,7 @@ async function getComponentTests(componentName: string): Promise { return response.data; } catch (error) { throw new Error( - `Tests for component "${componentName}" not found in Grafana UI repository`, + `Tests for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } @@ -530,12 +740,57 @@ async function searchComponents( } } +/** + * Get theme files from local filesystem + * @param category Optional category filter + * @returns Promise with theme files or null if not available locally + */ +async function getThemeFilesLocal(category?: string): Promise { + if (!localRepoPath) return null; + + const themePaths = [ + "packages/grafana-ui/src/themes/light.ts", + "packages/grafana-ui/src/themes/dark.ts", + "packages/grafana-ui/src/themes/base.ts", + "packages/grafana-ui/src/themes/default.ts", + ]; + + const themeFiles: any = { + category: category || "all", + source: "local", + themes: {}, + }; + + let foundAny = false; + + for (const themePath of themePaths) { + try { + const fullPath = path.join(localRepoPath, themePath); + const content = fs.readFileSync(fullPath, 'utf8'); + const themeName = themePath.split("/").pop()?.replace(".ts", "") || "unknown"; + themeFiles.themes[themeName] = content; + foundAny = true; + } catch (error) { + // Theme file doesn't exist locally, skip it + } + } + + return foundAny ? themeFiles : null; +} + /** * Fetch Grafana theme files * @param category Optional category filter (colors, typography, spacing, etc.) * @returns Promise with theme file content */ async function getThemeFiles(category?: string): Promise { + // Try local filesystem first + const localThemes = await getThemeFilesLocal(category); + if (localThemes !== null) { + return localThemes; + } + + // Fall back to GitHub API const themePaths = [ "packages/grafana-ui/src/themes/light.ts", "packages/grafana-ui/src/themes/dark.ts", @@ -545,6 +800,7 @@ async function getThemeFiles(category?: string): Promise { const themeFiles: any = { category: category || "all", + source: "github", themes: {}, }; @@ -658,6 +914,7 @@ export const axios = { getThemeFiles, getComponentDependencies, setGitHubApiKey, + setLocalGrafanaRepo, getGitHubRateLimit, // Path constants for easy access paths: {