From ba3e529b649a214228a6b1ead4699b4d29aa856f Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Tue, 11 Aug 2026 20:43:29 +0200 Subject: [PATCH 1/2] feat: replace grafanactl+logcli installers with unified gcx installer Add lib/installers/gcx.js (binary tarball install from grafana/gcx + browser-OAuth login), remove the grafanactl and logcli installers, and rewire exports, CLI menu/flags (--gcx), LLM docs, and README. Bump to 1.12.0 since 1.11.0 is already published without these changes. --- README.md | 79 +++--- bin/cli.js | 69 ++--- lib/index.js | 37 +-- lib/installers/gcx.js | 331 ++++++++++++++++++++++ lib/installers/grafanactl.js | 517 ----------------------------------- lib/installers/index.js | 8 +- lib/installers/logcli.js | 183 ------------- lib/llm/index.js | 43 ++- package.json | 4 +- 9 files changed, 438 insertions(+), 833 deletions(-) create mode 100644 lib/installers/gcx.js delete mode 100644 lib/installers/grafanactl.js delete mode 100644 lib/installers/logcli.js diff --git a/README.md b/README.md index c2347a5..94761ec 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,22 @@ A CLI tool to set up developer tools and teach your AI coding assistants how to ## What This Does -1. **Installs CLI tools**: sqlcmd, GitHub CLI (gh), Atlassian CLI (atl), n8nctl, grafanactl, logcli, m365, esq, discordctl, playwright, hcloud +1. **Installs CLI tools**: sqlcmd, GitHub CLI (gh), Atlassian CLI (atl), n8nctl, gcx, m365, esq, discordctl, playwright, hcloud 2. **Configures sqlcmd contexts**: Named contexts for switching between database environments 3. **Teaches your AI assistants**: Injects CLI documentation into Claude Code, Antigravity, and Codex configs -| Tool | Purpose | -| -------------- | -------------------------------------------------------------- | -| **sqlcmd** | Microsoft SQL Server command-line tool with context management | -| **gh** | GitHub CLI for PR/issue management | -| **atl** | Atlassian CLI for Jira/Confluence | -| **n8nctl** | n8n workflow automation CLI | -| **grafanactl** | Grafana CLI for dashboard/resource management | -| **logcli** | Loki CLI for querying Grafana Loki logs | -| **m365** | Microsoft 365 CLI for SharePoint/Teams/OneDrive management | -| **esq** | Elasticsearch Query CLI for cross-environment cluster queries | -| **discordctl** | Discord CLI for REST API interactions (messages, reactions) | -| **playwright** | Browser automation for screenshots, PDFs, and web testing | -| **hcloud** | Hetzner Cloud CLI for servers, networks, volumes, firewalls | +| Tool | Purpose | +| -------------- | ----------------------------------------------------------------------------------- | +| **sqlcmd** | Microsoft SQL Server command-line tool with context management | +| **gh** | GitHub CLI for PR/issue management | +| **atl** | Atlassian CLI for Jira/Confluence | +| **n8nctl** | n8n workflow automation CLI | +| **gcx** | Unified Grafana Cloud CLI (dashboards, resources, metrics, logs, traces, SLOs, IRM) | +| **m365** | Microsoft 365 CLI for SharePoint/Teams/OneDrive management | +| **esq** | Elasticsearch Query CLI for cross-environment cluster queries | +| **discordctl** | Discord CLI for REST API interactions (messages, reactions) | +| **playwright** | Browser automation for screenshots, PDFs, and web testing | +| **hcloud** | Hetzner Cloud CLI for servers, networks, volumes, firewalls | ## Repository Overrides @@ -34,6 +33,8 @@ export DISCORD_CLI_REPO="your-org/discordctl" # Default: enthus-appdev/discor export DISCORD_CLI_DIR="/path/to/discordctl" # Default: ~/dev/discordctl ``` +gcx installs from [grafana/gcx](https://github.com/grafana/gcx) GitHub releases by default. Override with `GCX_REPO` if needed. It is installed via a prebuilt binary download (no Go toolchain required). + The Go-based tools (atl, n8nctl, esq) are installed via `go install`. discordctl is installed via `npm link` from a local clone. ## Quick Start @@ -63,8 +64,7 @@ llm-cli-setup --sql # Configure SQL tools only llm-cli-setup --gh # Configure GitHub CLI only llm-cli-setup --atl # Configure Atlassian CLI only llm-cli-setup --n8n # Configure n8n CLI only -llm-cli-setup --grafana # Configure Grafana CLI only -llm-cli-setup --logcli # Configure Loki CLI only +llm-cli-setup --gcx # Configure Grafana Cloud CLI (gcx) only llm-cli-setup --m365 # Configure Microsoft 365 CLI only llm-cli-setup --esq # Configure Elasticsearch CLI only llm-cli-setup --discord # Configure Discord CLI only @@ -131,31 +131,19 @@ n8nctl variable list # List variables n8nctl variable create k --value 'v' # Create (--value avoids shell expansion) ``` -### Grafana CLI (grafanactl) +### Grafana Cloud CLI (gcx) -Requires Grafana 12+. Installs via `go install` or binary download. +Installs the latest [gcx](https://github.com/grafana/gcx) release binary from GitHub (no Go toolchain required). gcx is the unified Grafana Cloud CLI and replaces the old `grafanactl` (Grafana) and `logcli` (Loki) CLIs. ```bash -grafanactl config check # Verify configuration -grafanactl config use-context prod # Switch context -grafanactl resources list # List resource types -grafanactl resources get dashboards # Get all dashboards -grafanactl resources pull dashboards -o ./dashboards/ # Backup -grafanactl resources push ./dashboards/ # Restore -``` - -### Loki CLI (logcli) - -Installs from [grafana/loki](https://github.com/grafana/loki) GitHub releases. - -```bash -# Set up environment variables -export LOKI_ADDR="/api/datasources/proxy/" -export LOKI_BEARER_TOKEN="" - -# Query logs -logcli labels k8s_deployment_name -logcli query '{k8s_deployment_name="myapp"}' --limit=20 --since=1h +gcx --version # Show version +gcx login prod --server # Log in (browser OAuth) & create context +gcx config check # Verify configuration +gcx config use-context prod # Switch context +gcx resources list-types # List resource types +gcx logs query '{service_name="myapp"}' --since=1h # Query Loki logs +gcx metrics query 'rate(http_requests_total[5m])' # Query Prometheus +gcx dashboards list # List dashboards ``` ### Microsoft 365 CLI (m365) @@ -211,7 +199,7 @@ This tool teaches your AI coding assistants how to use these CLI tools by inject The documentation includes: -- Command syntax and examples for sqlcmd, gh, atl, n8nctl, grafanactl, logcli, m365, esq, discordctl, playwright, and hcloud +- Command syntax and examples for sqlcmd, gh, atl, n8nctl, gcx, m365, esq, discordctl, playwright, and hcloud - Safety guidelines (e.g., confirm before SQL writes) - Formatting guides (Jira wiki markup, Confluence HTML) @@ -237,8 +225,7 @@ llm-cli-setup/ │ │ ├── gh.js # GitHub CLI setup │ │ ├── atl.js # Atlassian CLI setup │ │ ├── n8n.js # n8n CLI setup -│ │ ├── grafanactl.js # Grafana CLI setup -│ │ ├── logcli.js # Loki CLI setup +│ │ ├── gcx.js # Grafana Cloud CLI setup │ │ ├── m365.js # Microsoft 365 CLI setup │ │ ├── esq.js # Elasticsearch Query CLI setup │ │ └── discord.js # Discord CLI setup @@ -269,9 +256,15 @@ npm start # Run the CLI First-time users must run `atl auth setup` before `atl auth login`. This creates the OAuth app configuration. -### Go binaries not in PATH +### gcx binary not in PATH + +gcx installs to `~/.local/bin`. Add to your shell profile if not already on PATH: + +```bash +export PATH="$PATH:$HOME/.local/bin" +``` -All Go-based tools (atl, n8nctl, grafanactl) install to `~/go/bin`. Add to your shell profile: +The Go-based tools (atl, n8nctl, esq) install to `~/go/bin`: ```bash export PATH="$PATH:$HOME/go/bin" diff --git a/bin/cli.js b/bin/cli.js index 4c84e46..417c04a 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -8,8 +8,7 @@ import { installSqlcmd, configureSqlEnv } from '../lib/installers/sqlcmd.js'; import { configureGitHubCli } from '../lib/installers/gh.js'; import { configureAtlassianCli } from '../lib/installers/atl.js'; import { configureN8nCli } from '../lib/installers/n8n.js'; -import { configureGrafanaCli } from '../lib/installers/grafanactl.js'; -import { configureLogcli } from '../lib/installers/logcli.js'; +import { configureGcx } from '../lib/installers/gcx.js'; import { configureM365Cli } from '../lib/installers/m365.js'; import { configureEsqCli } from '../lib/installers/esq.js'; import { configureDiscordCli } from '../lib/installers/discord.js'; @@ -59,52 +58,48 @@ const runFullSetup = async () => { console.log(chalk.gray('This will configure all CLI tools and LLM integrations.\n')); // Step 1: sqlcmd - console.log(chalk.cyan.bold('\n[1/12] SQL Server Tools')); + console.log(chalk.cyan.bold('\n[1/11] SQL Server Tools')); await installSqlcmd(); await configureSqlEnv(); // Step 2: GitHub CLI - console.log(chalk.cyan.bold('\n[2/12] GitHub CLI')); + console.log(chalk.cyan.bold('\n[2/11] GitHub CLI')); await configureGitHubCli(); // Step 3: Atlassian CLI - console.log(chalk.cyan.bold('\n[3/12] Atlassian CLI')); + console.log(chalk.cyan.bold('\n[3/11] Atlassian CLI')); await configureAtlassianCli(); // Step 4: n8n CLI - console.log(chalk.cyan.bold('\n[4/12] n8n CLI')); + console.log(chalk.cyan.bold('\n[4/11] n8n CLI')); await configureN8nCli(); - // Step 5: Grafana CLI - console.log(chalk.cyan.bold('\n[5/12] Grafana CLI')); - await configureGrafanaCli(); + // Step 5: Grafana Cloud CLI + console.log(chalk.cyan.bold('\n[5/11] Grafana Cloud CLI (gcx)')); + await configureGcx(); - // Step 6: Loki CLI - console.log(chalk.cyan.bold('\n[6/12] Loki CLI')); - await configureLogcli(); - - // Step 7: Microsoft 365 CLI - console.log(chalk.cyan.bold('\n[7/12] Microsoft 365 CLI')); + // Step 6: Microsoft 365 CLI + console.log(chalk.cyan.bold('\n[6/11] Microsoft 365 CLI')); await configureM365Cli(); - // Step 8: Elasticsearch Query CLI - console.log(chalk.cyan.bold('\n[8/12] Elasticsearch Query CLI')); + // Step 7: Elasticsearch Query CLI + console.log(chalk.cyan.bold('\n[7/11] Elasticsearch Query CLI')); await configureEsqCli(); - // Step 9: Discord CLI - console.log(chalk.cyan.bold('\n[9/12] Discord CLI')); + // Step 8: Discord CLI + console.log(chalk.cyan.bold('\n[8/11] Discord CLI')); await configureDiscordCli(); - // Step 10: Playwright - console.log(chalk.cyan.bold('\n[10/12] Playwright')); + // Step 9: Playwright + console.log(chalk.cyan.bold('\n[9/11] Playwright')); await configurePlaywright(); - // Step 11: Hetzner Cloud CLI - console.log(chalk.cyan.bold('\n[11/12] Hetzner Cloud CLI')); + // Step 10: Hetzner Cloud CLI + console.log(chalk.cyan.bold('\n[10/11] Hetzner Cloud CLI')); await configureHcloudCli(); - // Step 12: LLM Configuration - console.log(chalk.cyan.bold('\n[12/12] LLM Configuration')); + // Step 11: LLM Configuration + console.log(chalk.cyan.bold('\n[11/11] LLM Configuration')); await configureLlmTools(); // Final summary @@ -127,8 +122,7 @@ const runMenu = async () => { { name: '🐙 Configure GitHub CLI', value: 'gh' }, { name: '📋 Configure Atlassian CLI', value: 'atl' }, { name: '🔄 Configure n8n CLI', value: 'n8n' }, - { name: '📊 Configure Grafana CLI', value: 'grafana' }, - { name: '📜 Configure Loki CLI (logcli)', value: 'logcli' }, + { name: '📊 Configure Grafana Cloud CLI (gcx)', value: 'gcx' }, { name: '☁️ Configure Microsoft 365 CLI', value: 'm365' }, { name: '🔍 Configure Elasticsearch CLI (esq)', value: 'esq' }, { name: '💬 Configure Discord CLI', value: 'discord' }, @@ -159,11 +153,8 @@ const runMenu = async () => { case 'n8n': await configureN8nCli(); break; - case 'grafana': - await configureGrafanaCli(); - break; - case 'logcli': - await configureLogcli(); + case 'gcx': + await configureGcx(); break; case 'm365': await configureM365Cli(); @@ -238,9 +229,8 @@ ${chalk.bold('Usage:')} llm-cli-setup --gh Configure GitHub CLI only llm-cli-setup --atl Configure Atlassian CLI only llm-cli-setup --n8n Configure n8n CLI only - llm-cli-setup --grafana Configure Grafana CLI only - llm-cli-setup --logcli Configure Loki CLI only - llm-cli-setup --m365 Configure Microsoft 365 CLI only + llm-cli-setup --gcx Configure Grafana Cloud CLI (gcx) only + llm-cli-setup --m365 Configure Microsoft 365 CLI only llm-cli-setup --esq Configure Elasticsearch CLI only llm-cli-setup --discord Configure Discord CLI only llm-cli-setup --playwright Configure Playwright only @@ -265,8 +255,7 @@ ${chalk.bold('Options:')} gh: args.includes('--gh'), atl: args.includes('--atl'), n8n: args.includes('--n8n'), - grafana: args.includes('--grafana'), - logcli: args.includes('--logcli'), + gcx: args.includes('--gcx'), m365: args.includes('--m365'), esq: args.includes('--esq'), discord: args.includes('--discord'), @@ -297,10 +286,8 @@ const main = async () => { await configureAtlassianCli(); } else if (args.n8n) { await configureN8nCli(); - } else if (args.grafana) { - await configureGrafanaCli(); - } else if (args.logcli) { - await configureLogcli(); + } else if (args.gcx) { + await configureGcx(); } else if (args.m365) { await configureM365Cli(); } else if (args.esq) { diff --git a/lib/index.js b/lib/index.js index ed7b884..d8666ea 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,12 +15,10 @@ export { isN8nInstalled, isN8nConfigured, N8N_CLI_REPO, - configureGrafanaCli, - isGrafanactlInstalled, - isGrafanactlConfigured, - getCurrentContext as getGrafanaContext, - configureLogcli, - isLogcliInstalled, + configureGcx, + isGcxInstalled, + isGcxConfigured, + getCurrentContext as getGcxContext, configureM365Cli, isM365Installed, isM365Authenticated, @@ -95,12 +93,10 @@ import { isN8nInstalled, isN8nConfigured, getCurrentInstance, - configureGrafanaCli, - isGrafanactlInstalled, - isGrafanactlConfigured, + configureGcx, + isGcxInstalled, + isGcxConfigured, getCurrentContext, - configureLogcli, - isLogcliInstalled, configureM365Cli, isM365Installed, isM365Authenticated, @@ -178,25 +174,16 @@ const CLI_TOOLS = [ }, }, { - id: 'grafana', - name: 'Grafana CLI (grafanactl)', - configure: configureGrafanaCli, + id: 'gcx', + name: 'Grafana Cloud CLI (gcx)', + configure: configureGcx, getStatus: () => { - if (!isGrafanactlInstalled()) return chalk.red('not installed'); - if (!isGrafanactlConfigured()) return chalk.yellow('not configured'); + if (!isGcxInstalled()) return chalk.red('not installed'); + if (!isGcxConfigured()) return chalk.yellow('not configured'); const ctx = getCurrentContext(); return chalk.green(`configured (${ctx || 'default'})`); }, }, - { - id: 'logcli', - name: 'Loki CLI (logcli)', - configure: configureLogcli, - getStatus: () => { - if (!isLogcliInstalled()) return chalk.red('not installed'); - return chalk.green('installed'); - }, - }, { id: 'm365', name: 'Microsoft 365 CLI (m365)', diff --git a/lib/installers/gcx.js b/lib/installers/gcx.js new file mode 100644 index 0000000..a23115e --- /dev/null +++ b/lib/installers/gcx.js @@ -0,0 +1,331 @@ +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { execFileSync, spawnSync } from 'child_process'; +import { commandExists, getPlatformInfo } from '../utils/platform.js'; + +// Repo that provides gcx release binaries (override via env for testing). +export const GCX_REPO = process.env.GCX_REPO || 'grafana/gcx'; + +// Common locations where the gcx binary may live (in PATH first). +const BINARY_LOCATIONS = [ + path.join(os.homedir(), 'go', 'bin', 'gcx'), + path.join(os.homedir(), '.local', 'bin', 'gcx'), + '/usr/local/bin/gcx', +]; + +/** + * Find the gcx binary (in PATH or common locations) + */ +const findGcxBinary = () => { + if (commandExists('gcx')) { + return 'gcx'; + } + + for (const loc of BINARY_LOCATIONS) { + if (fs.existsSync(loc)) { + return loc; + } + } + + return null; +}; + +/** + * Check if gcx is installed + */ +export const isGcxInstalled = () => findGcxBinary() !== null; + +/** + * Read gcx's current context name. gcx outputs plain text in a normal + * terminal but JSON ({"current-context":""}) in agent mode, so parse both. + */ +const readCurrentContext = (binary) => { + const result = execFileSync(binary, ['config', 'current-context'], { + encoding: 'utf8', + stdio: 'pipe', + }).trim(); + if (!result) return null; + try { + const parsed = JSON.parse(result); + return parsed['current-context'] || null; + } catch { + return result; + } +}; + +/** + * Check if gcx has a current context configured + */ +export const isGcxConfigured = () => { + const binary = findGcxBinary(); + if (!binary) return false; + + try { + return readCurrentContext(binary) !== null; + } catch { + return false; + } +}; + +/** + * Get the current gcx context name + */ +export const getCurrentContext = () => { + const binary = findGcxBinary(); + if (!binary) return null; + + try { + return readCurrentContext(binary); + } catch { + return null; + } +}; + +/** + * Install gcx by downloading a prebuilt tarball from GitHub releases. + * Matches the logcli installer pattern (no Go toolchain required). + */ +const installViaBinary = async () => { + const { platform, arch } = getPlatformInfo(); + + // Map to gcx release naming: gcx___.tar.gz + let osName = platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : null; + let archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : null; + + if (!osName || !archName) { + console.log(chalk.yellow(`Unsupported platform: ${platform}/${arch}`)); + return { success: false, binaryPath: null }; + } + + // Required tools + if (!commandExists('curl') || !commandExists('tar')) { + console.log(chalk.red('✗ curl and tar are required but not installed')); + return { success: false, binaryPath: null }; + } + + console.log(chalk.blue('Installing gcx from GitHub releases...')); + console.log(chalk.gray(`Downloading latest release from ${GCX_REPO}...\n`)); + + const installDir = path.join(os.homedir(), '.local', 'bin'); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gcx-install-')); + + try { + // Get latest version + const releaseInfo = execFileSync( + 'curl', + ['-s', `https://api.github.com/repos/${GCX_REPO}/releases/latest`], + { encoding: 'utf8' } + ); + const release = JSON.parse(releaseInfo); + const version = release.tag_name; + + // Validate version format (anchored to prevent injection via suffix) + if (!/^v?\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) { + throw new Error(`Invalid version format: ${version}`); + } + + // Download and install to ~/.local/bin (no sudo required) + const tag = release.tag_name; + const filename = `gcx_${version.replace(/^v/, '')}_${osName}_${archName}.tar.gz`; + const downloadUrl = `https://github.com/${GCX_REPO}/releases/download/${tag}/${filename}`; + const tarballPath = path.join(tmpDir, 'gcx.tar.gz'); + const targetBinary = path.join(installDir, 'gcx'); + + fs.mkdirSync(installDir, { recursive: true }); + execFileSync('curl', ['-sL', downloadUrl, '-o', tarballPath]); + execFileSync('tar', ['xzf', tarballPath, '-C', tmpDir]); + + // The tarball contains the `gcx` binary (plus README/LICENSE/CHANGELOG). + const extractedBinary = path.join(tmpDir, 'gcx'); + if (!fs.existsSync(extractedBinary)) { + throw new Error('gcx binary not found in the release archive'); + } + + fs.renameSync(extractedBinary, targetBinary); + fs.chmodSync(targetBinary, 0o755); + + console.log(chalk.green(`✓ gcx ${version} installed successfully`)); + + // Warn if not in PATH + if (!commandExists('gcx')) { + console.log(chalk.yellow(`\n⚠ gcx installed to ${targetBinary}`)); + console.log(chalk.yellow("But it's not in your PATH. Add it with:")); + console.log(chalk.gray(` export PATH="$PATH:${installDir}"`)); + console.log(chalk.gray('\nOr add this line to your shell profile (~/.zshrc or ~/.bashrc)')); + } + + return { success: true, binaryPath: targetBinary }; + } catch (error) { + console.error(chalk.red(`✗ Failed to install: ${error.message}`)); + console.log(chalk.gray(`Manual installation: https://github.com/${GCX_REPO}/releases`)); + return { success: false, binaryPath: null }; + } finally { + // Clean up temp directory + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + } +}; + +/** + * Run `gcx login` (browser OAuth, the recommended gcx flow) for a context. + */ +const runLogin = async (binary) => { + const { contextName } = await inquirer.prompt([ + { + type: 'input', + name: 'contextName', + message: 'Context name (e.g., prod, default, staging):', + default: 'default', + }, + ]); + + const { serverUrl } = await inquirer.prompt([ + { + type: 'input', + name: 'serverUrl', + message: 'Grafana server URL:', + default: 'http://localhost:3000', + }, + ]); + + console.log(chalk.gray('\nOpening browser for OAuth approval...')); + console.log(chalk.gray('Follow the prompts in your browser, then return here.\n')); + + try { + spawnSync(binary, ['login', contextName, '--server', serverUrl, '--oauth'], { + stdio: 'inherit', + }); + const current = readCurrentContext(binary); + if (current === contextName) { + console.log(chalk.green(`\n✓ Context '${contextName}' configured`)); + console.log(chalk.gray(`Verify anytime with: ${binary} config check`)); + return true; + } + console.log( + chalk.yellow('\n! Could not confirm the new context. Verify with `gcx config check`.') + ); + return false; + } catch (error) { + console.error(chalk.red(`\n✗ Login failed: ${error.message}`)); + console.log( + chalk.gray(`Retry later with: ${binary} login ${contextName} --server ${serverUrl}`) + ); + return false; + } +}; + +/** + * Configure gcx + */ +export const configureGcx = async () => { + console.log(chalk.cyan('\n=== Grafana Cloud CLI (gcx) Configuration ===\n')); + console.log( + chalk.gray( + 'gcx is the unified Grafana Cloud CLI (dashboards, resources, metrics, logs, traces, SLOs, IRM, ...).\n' + ) + ); + + // Check installation + let binary = findGcxBinary(); + + if (!binary) { + console.log(chalk.yellow('! gcx is not installed')); + + const { install } = await inquirer.prompt([ + { + type: 'confirm', + name: 'install', + message: 'Install gcx?', + default: true, + }, + ]); + + if (install) { + const result = await installViaBinary(); + if (!result.success) { + return false; + } + binary = result.binaryPath || findGcxBinary(); + if (!binary) { + console.log(chalk.red('✗ Could not find gcx after installation')); + return false; + } + } else { + console.log(chalk.gray('Skipping gcx setup.')); + return false; + } + } else { + console.log(chalk.green('✓ gcx is installed')); + + // Show path if not in PATH + if (!commandExists('gcx')) { + console.log(chalk.yellow(` Location: ${binary}`)); + console.log(chalk.yellow(' Note: Not in PATH. Add to your shell profile:')); + console.log(chalk.gray(` export PATH="$PATH:${path.dirname(binary)}"`)); + } + + // Check version + try { + const version = execFileSync(binary, ['--version'], { encoding: 'utf8' }).trim(); + console.log(chalk.gray(` Version: ${version}`)); + } catch { + // Version check failed, continue anyway + } + } + + // Check configuration + const currentContext = getCurrentContext(); + + if (currentContext) { + console.log(chalk.green(`✓ Active context: ${chalk.white(currentContext)}`)); + + const { action } = await inquirer.prompt([ + { + type: 'select', + name: 'action', + message: 'What would you like to do?', + choices: [ + { name: 'Re-authenticate / add a context', value: 'login' }, + { name: 'View current configuration', value: 'view' }, + { name: 'Done', value: 'done' }, + ], + }, + ]); + + if (action === 'login') { + await runLogin(binary); + } else if (action === 'view') { + console.log(); + execFileSync(binary, ['config', 'view'], { stdio: 'inherit' }); + } + } else { + // No context configured + const { configure } = await inquirer.prompt([ + { + type: 'confirm', + name: 'configure', + message: 'Set up gcx now (browser OAuth)?', + default: true, + }, + ]); + + if (configure) { + await runLogin(binary); + } else { + console.log(chalk.gray('\nSkipping gcx setup.')); + console.log(chalk.gray(`Login later with: gcx login --server `)); + console.log( + chalk.gray('Or use a service-account token: gcx login --yes --token glsa_xxx') + ); + } + } + + console.log(chalk.green('\n✓ gcx configuration complete')); + return true; +}; diff --git a/lib/installers/grafanactl.js b/lib/installers/grafanactl.js deleted file mode 100644 index 6a924d0..0000000 --- a/lib/installers/grafanactl.js +++ /dev/null @@ -1,517 +0,0 @@ -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import { execSync, spawnSync } from 'child_process'; -import { commandExists, getPlatformInfo } from '../utils/platform.js'; - -// Keys whose following arg is a secret (e.g. contexts.X.grafana.password). -const SENSITIVE_KEY_PATTERN = /\.(token|password|secret|api[_-]?key)$/i; - -/** - * Redact values in args that follow a sensitive key so secrets don't leak - * into error messages or logs. - */ -const redactSensitiveArgs = (args) => - args.map((arg, i) => { - const prev = args[i - 1]; - if (typeof prev === 'string' && SENSITIVE_KEY_PATTERN.test(prev)) { - return ''; - } - return arg; - }); - -/** - * Run a grafanactl config command, throwing on failure - */ -const runConfig = (binary, args) => { - const result = spawnSync(binary, args, { stdio: 'pipe', encoding: 'utf8' }); - if (result.status !== 0) { - const safeArgs = redactSensitiveArgs(args); - throw new Error(`grafanactl ${safeArgs.join(' ')} failed: ${result.stderr || 'unknown error'}`); - } - return result; -}; - -/** - * Find grafanactl binary (in PATH or common locations) - */ -const findGrafanactlBinary = () => { - // First check if it's in PATH - if (commandExists('grafanactl')) { - return 'grafanactl'; - } - - // Check common installation locations - const locations = [ - path.join(os.homedir(), 'go', 'bin', 'grafanactl'), - path.join(os.homedir(), '.local', 'bin', 'grafanactl'), - '/usr/local/bin/grafanactl', - ]; - - for (const loc of locations) { - if (fs.existsSync(loc)) { - return loc; - } - } - - return null; -}; - -/** - * Check if grafanactl is installed - */ -export const isGrafanactlInstalled = () => findGrafanactlBinary() !== null; - -/** - * Check if grafanactl is configured (has at least one context) - */ -export const isGrafanactlConfigured = () => { - const binary = findGrafanactlBinary(); - if (!binary) return false; - - try { - const result = execSync(`${binary} config check`, { stdio: 'pipe', encoding: 'utf8' }); - return !result.includes('no contexts configured'); - } catch { - return false; - } -}; - -/** - * Get current grafanactl context - */ -export const getCurrentContext = () => { - const binary = findGrafanactlBinary(); - if (!binary) return null; - - try { - return execSync(`${binary} config current-context`, { stdio: 'pipe', encoding: 'utf8' }).trim(); - } catch { - return null; - } -}; - -/** - * Install grafanactl via go install - */ -const installViaGo = async () => { - if (!commandExists('go')) { - console.log(chalk.yellow('Go is required but not installed.')); - console.log(chalk.gray('Install Go 1.24+ from: https://go.dev/dl/')); - return { success: false, binaryPath: null }; - } - - // Check Go version (requires 1.24+) - try { - const version = execSync('go version', { encoding: 'utf8' }); - const match = version.match(/go(\d+)\.(\d+)/); - if (match) { - const major = parseInt(match[1], 10); - const minor = parseInt(match[2], 10); - if (major < 1 || (major === 1 && minor < 24)) { - console.log(chalk.yellow(`Go 1.24+ required, found ${match[0]}`)); - return { success: false, binaryPath: null }; - } - } - } catch { - console.log(chalk.yellow('Could not verify Go version')); - } - - console.log(chalk.blue('Installing grafanactl via go install...')); - try { - execSync('go install github.com/grafana/grafanactl/cmd/grafanactl@latest', { - stdio: 'inherit', - shell: true, - }); - - // Find the installed binary - const binaryPath = findGrafanactlBinary(); - if (!binaryPath) { - console.error(chalk.red('✗ Install completed but grafanactl not found')); - console.log(chalk.gray('Check ~/go/bin, ~/.local/bin, or /usr/local/bin')); - return { success: false, binaryPath: null }; - } - - console.log(chalk.green('✓ grafanactl installed successfully')); - - // Warn if not in PATH - if (!commandExists('grafanactl')) { - console.log(chalk.yellow(`\n⚠ grafanactl installed to ${binaryPath}`)); - console.log(chalk.yellow("But it's not in your PATH. Add it with:")); - console.log(chalk.gray(` export PATH="$PATH:${path.dirname(binaryPath)}"`)); - console.log(chalk.gray('\nOr add this line to your shell profile (~/.zshrc or ~/.bashrc)')); - } - - return { success: true, binaryPath }; - } catch (error) { - console.error(chalk.red(`✗ Failed to install: ${error.message}`)); - return { success: false, binaryPath: null }; - } -}; - -/** - * Install grafanactl via binary download - */ -const installViaBinary = async () => { - const { platform, arch } = getPlatformInfo(); - - // Map to GitHub release naming - let osName = platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : null; - let archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : null; - - if (!osName || !archName) { - console.log(chalk.yellow(`Unsupported platform: ${platform}/${arch}`)); - return { success: false, binaryPath: null }; - } - - console.log(chalk.blue('Installing grafanactl from binary...')); - console.log(chalk.gray('Downloading latest release from GitHub...\n')); - - try { - // Get latest version - const releaseInfo = execSync( - 'curl -s https://api.github.com/repos/grafana/grafanactl/releases/latest', - { - encoding: 'utf8', - } - ); - const release = JSON.parse(releaseInfo); - const version = release.tag_name.replace('v', ''); - - // Download and install to ~/.local/bin (no sudo required) - const filename = `grafanactl_${version}_${osName}_${archName}.tar.gz`; - const downloadUrl = `https://github.com/grafana/grafanactl/releases/download/${release.tag_name}/${filename}`; - - const installDir = path.join(os.homedir(), '.local', 'bin'); - const tmpDir = '/tmp/grafanactl-install'; - - execSync(`mkdir -p ${installDir}`, { shell: true }); - execSync(`mkdir -p ${tmpDir}`, { shell: true }); - execSync(`curl -sL "${downloadUrl}" | tar xz -C ${tmpDir}`, { shell: true, stdio: 'inherit' }); - execSync(`mv ${tmpDir}/grafanactl ${installDir}/`, { shell: true, stdio: 'inherit' }); - execSync(`rm -rf ${tmpDir}`, { shell: true }); - - const binaryPath = path.join(installDir, 'grafanactl'); - console.log(chalk.green('✓ grafanactl installed successfully')); - return { success: true, binaryPath }; - } catch (error) { - console.error(chalk.red(`✗ Failed to install: ${error.message}`)); - console.log(chalk.gray('Manual installation: https://github.com/grafana/grafanactl/releases')); - return { success: false, binaryPath: null }; - } -}; - -/** - * Install grafanactl - */ -const installGrafanactl = async () => { - const { platform } = getPlatformInfo(); - - if (platform !== 'darwin' && platform !== 'linux') { - console.log(chalk.yellow('grafanactl installation is only supported on macOS and Linux.')); - console.log( - chalk.gray('Please install manually: https://github.com/grafana/grafanactl/releases') - ); - return { success: false, binaryPath: null }; - } - - // Offer installation methods - const hasGo = commandExists('go'); - - const choices = []; - if (hasGo) { - choices.push({ name: 'go install (recommended if Go 1.24+ is installed)', value: 'go' }); - } - choices.push({ name: 'Download binary from GitHub releases', value: 'binary' }); - choices.push({ name: 'Skip installation', value: 'skip' }); - - const { method } = await inquirer.prompt([ - { - type: 'select', - name: 'method', - message: 'How would you like to install grafanactl?', - choices, - }, - ]); - - switch (method) { - case 'go': - return await installViaGo(); - case 'binary': - return await installViaBinary(); - default: - console.log(chalk.gray('Skipping grafanactl installation.')); - return { success: false, binaryPath: null }; - } -}; - -/** - * Configure grafanactl context - */ -const configureContext = async (binary) => { - console.log(chalk.blue('\nConfiguring Grafana context...')); - console.log(chalk.gray('A context defines how to connect to a Grafana instance.\n')); - - const { contextName } = await inquirer.prompt([ - { - type: 'input', - name: 'contextName', - message: 'Context name (e.g., default, staging, prod):', - default: 'default', - }, - ]); - - const { serverUrl } = await inquirer.prompt([ - { - type: 'input', - name: 'serverUrl', - message: 'Grafana server URL:', - default: 'http://localhost:3000', - }, - ]); - - const { orgId } = await inquirer.prompt([ - { - type: 'input', - name: 'orgId', - message: 'Organization ID:', - default: '1', - }, - ]); - - const { authMethod } = await inquirer.prompt([ - { - type: 'select', - name: 'authMethod', - message: 'Authentication method:', - choices: [ - { name: 'Service Account Token (recommended)', value: 'token' }, - { name: 'Username/Password', value: 'basic' }, - { name: 'Skip authentication setup', value: 'skip' }, - ], - }, - ]); - - try { - // Set server and org - runConfig(binary, ['config', 'set', `contexts.${contextName}.grafana.server`, serverUrl]); - runConfig(binary, ['config', 'set', `contexts.${contextName}.grafana.org-id`, orgId]); - - if (authMethod === 'token') { - console.log(chalk.gray('\nCreate a Service Account Token in Grafana:')); - console.log(chalk.gray(' Administration → Service Accounts → Create token\n')); - - const { token } = await inquirer.prompt([ - { - type: 'password', - name: 'token', - message: 'Service Account Token:', - mask: '*', - }, - ]); - - if (token) { - runConfig(binary, ['config', 'set', `contexts.${contextName}.grafana.token`, token]); - } - } else if (authMethod === 'basic') { - const { username } = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'Username:', - default: 'admin', - }, - ]); - - const { password } = await inquirer.prompt([ - { - type: 'password', - name: 'password', - message: 'Password:', - mask: '*', - }, - ]); - - runConfig(binary, ['config', 'set', `contexts.${contextName}.grafana.user`, username]); - runConfig(binary, ['config', 'set', `contexts.${contextName}.grafana.password`, password]); - } - - // Set as current context if it's the first one - runConfig(binary, ['config', 'use-context', contextName]); - - console.log(chalk.green(`\n✓ Context '${contextName}' configured`)); - - // Verify connection for this specific context - console.log(chalk.blue('\nVerifying connection...')); - const checkResult = spawnSync(binary, ['config', 'check', '--context', contextName], { - stdio: 'inherit', - }); - if (checkResult.status === 0) { - console.log(chalk.green('✓ Connection verified')); - } else { - console.log(chalk.yellow('! Could not verify connection (check credentials)')); - } - - return true; - } catch (error) { - console.error(chalk.red(`\n✗ Configuration failed: ${error.message}`)); - return false; - } -}; - -/** - * Configure Grafana CLI - */ -export const configureGrafanaCli = async () => { - console.log(chalk.cyan('\n=== Grafana CLI Configuration ===\n')); - console.log(chalk.gray('grafanactl requires Grafana 12+ and is in public preview.\n')); - - // Check installation - let binary = findGrafanactlBinary(); - - if (!binary) { - console.log(chalk.yellow('! Grafana CLI (grafanactl) is not installed')); - - const { install } = await inquirer.prompt([ - { - type: 'confirm', - name: 'install', - message: 'Install grafanactl?', - default: true, - }, - ]); - - if (install) { - const result = await installGrafanactl(); - if (!result.success) { - return false; - } - binary = result.binaryPath || findGrafanactlBinary(); - if (!binary) { - console.log(chalk.red('✗ Could not find grafanactl after installation')); - return false; - } - } else { - console.log(chalk.gray('Skipping Grafana CLI setup.')); - return false; - } - } else { - console.log(chalk.green('✓ Grafana CLI is installed')); - - // Show path if not in PATH - if (!commandExists('grafanactl')) { - console.log(chalk.yellow(` Location: ${binary}`)); - console.log(chalk.yellow(' Note: Not in PATH. Add to your shell profile:')); - console.log(chalk.gray(` export PATH="$PATH:${path.dirname(binary)}"`)); - } - - // Check version - try { - const version = execSync(`${binary} --version`, { encoding: 'utf8' }).trim(); - console.log(chalk.gray(` Version: ${version}`)); - } catch { - // Version check failed, continue anyway - } - } - - // Check configuration - const currentContext = getCurrentContext(); - - if (currentContext) { - console.log(chalk.green(`✓ Active context: ${chalk.white(currentContext)}`)); - - // List all contexts - let contexts = []; - try { - const output = execSync(`${binary} config list-contexts`, { - encoding: 'utf8', - stdio: 'pipe', - }); - contexts = output - .split('\n') - .map((line) => line.replace(/^\*?\s*/, '').trim()) - .filter(Boolean); - } catch { - // Ignore errors - } - - const { action } = await inquirer.prompt([ - { - type: 'select', - name: 'action', - message: 'What would you like to do?', - choices: [ - { name: 'Add another context', value: 'add' }, - { name: 'Remove a context', value: 'remove' }, - { name: 'View current configuration', value: 'view' }, - { name: 'Done', value: 'done' }, - ], - }, - ]); - - if (action === 'add') { - await configureContext(binary); - } else if (action === 'remove') { - if (contexts.length === 0) { - console.log(chalk.yellow('No contexts to remove.')); - } else { - const { contextToRemove } = await inquirer.prompt([ - { - type: 'select', - name: 'contextToRemove', - message: 'Select context to remove:', - choices: contexts.map((ctx) => ({ - name: ctx === currentContext ? `${ctx} (current)` : ctx, - value: ctx, - })), - }, - ]); - - try { - runConfig(binary, ['config', 'unset', `contexts.${contextToRemove}`]); - console.log(chalk.green(`✓ Context '${contextToRemove}' removed`)); - - // If we removed the current context, switch to another if available - if (contextToRemove === currentContext) { - const remaining = contexts.filter((c) => c !== contextToRemove); - if (remaining.length > 0) { - runConfig(binary, ['config', 'use-context', remaining[0]]); - console.log(chalk.gray(` Switched to context '${remaining[0]}'`)); - } - } - } catch (error) { - console.log(chalk.red(`✗ Failed to remove context: ${error.message}`)); - } - } - } else if (action === 'view') { - console.log(); - execSync(`${binary} config view`, { stdio: 'inherit' }); - } - } else { - // No contexts configured - const { configure } = await inquirer.prompt([ - { - type: 'confirm', - name: 'configure', - message: 'Set up a Grafana context now?', - default: true, - }, - ]); - - if (configure) { - await configureContext(binary); - } else { - console.log(chalk.gray('\nSkipping context setup.')); - console.log( - chalk.gray( - `Configure later with: ${binary} config set contexts..grafana.server ` - ) - ); - } - } - - console.log(chalk.green('\n✓ Grafana CLI configuration complete')); - return true; -}; diff --git a/lib/installers/index.js b/lib/installers/index.js index f333cd1..04d3a65 100644 --- a/lib/installers/index.js +++ b/lib/installers/index.js @@ -8,13 +8,7 @@ export { getCurrentInstance, N8N_CLI_REPO, } from './n8n.js'; -export { - configureGrafanaCli, - isGrafanactlInstalled, - isGrafanactlConfigured, - getCurrentContext, -} from './grafanactl.js'; -export { configureLogcli, isLogcliInstalled } from './logcli.js'; +export { configureGcx, isGcxInstalled, isGcxConfigured, getCurrentContext } from './gcx.js'; export { configureM365Cli, isM365Installed, isM365Authenticated, getM365User } from './m365.js'; export { configureEsqCli, diff --git a/lib/installers/logcli.js b/lib/installers/logcli.js deleted file mode 100644 index eaaedee..0000000 --- a/lib/installers/logcli.js +++ /dev/null @@ -1,183 +0,0 @@ -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import fs from 'fs'; -import os from 'os'; -import path from 'path'; -import { execFileSync } from 'child_process'; -import { commandExists, getPlatformInfo } from '../utils/platform.js'; - -/** - * Find logcli binary (in PATH or common locations) - */ -const findLogcliBinary = () => { - // First check if it's in PATH - if (commandExists('logcli')) { - return 'logcli'; - } - - // Check common installation locations - const locations = [ - path.join(os.homedir(), 'bin', 'logcli'), - path.join(os.homedir(), '.local', 'bin', 'logcli'), - '/usr/local/bin/logcli', - ]; - - for (const loc of locations) { - if (fs.existsSync(loc)) { - return loc; - } - } - - return null; -}; - -/** - * Check if logcli is installed - */ -export const isLogcliInstalled = () => findLogcliBinary() !== null; - -/** - * Install logcli via binary download from GitHub releases - */ -const installViaBinary = async () => { - const { platform, arch } = getPlatformInfo(); - - // Map to GitHub release naming - let osName = platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : null; - let archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : null; - - if (!osName || !archName) { - console.log(chalk.yellow(`Unsupported platform: ${platform}/${arch}`)); - return { success: false, binaryPath: null }; - } - - // Check for required tools - if (!commandExists('curl')) { - console.log(chalk.red('✗ curl is required but not installed')); - return { success: false, binaryPath: null }; - } - - if (!commandExists('unzip')) { - console.log(chalk.red('✗ unzip is required but not installed')); - return { success: false, binaryPath: null }; - } - - console.log(chalk.blue('Installing logcli from GitHub releases...')); - console.log(chalk.gray('Downloading latest release from grafana/loki...\n')); - - const installDir = path.join(os.homedir(), '.local', 'bin'); - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'logcli-install-')); - - try { - // Get latest version - const releaseInfo = execFileSync( - 'curl', - ['-s', 'https://api.github.com/repos/grafana/loki/releases/latest'], - { encoding: 'utf8' } - ); - const release = JSON.parse(releaseInfo); - const version = release.tag_name; - - // Validate version format (anchored to prevent injection via suffix) - if (!/^v?\d+\.\d+\.\d+(-[\w.]+)?$/.test(version)) { - throw new Error(`Invalid version format: ${version}`); - } - - // Download and install to ~/.local/bin (no sudo required) - const filename = `logcli-${osName}-${archName}.zip`; - const downloadUrl = `https://github.com/grafana/loki/releases/download/${version}/${filename}`; - const zipPath = path.join(tmpDir, 'logcli.zip'); - const extractedBinary = path.join(tmpDir, `logcli-${osName}-${archName}`); - const targetBinary = path.join(installDir, 'logcli'); - - fs.mkdirSync(installDir, { recursive: true }); - execFileSync('curl', ['-sL', downloadUrl, '-o', zipPath]); - execFileSync('unzip', ['-o', zipPath, '-d', tmpDir], { stdio: 'pipe' }); - fs.renameSync(extractedBinary, targetBinary); - fs.chmodSync(targetBinary, 0o755); - - const binaryPath = path.join(installDir, 'logcli'); - console.log(chalk.green(`✓ logcli ${version} installed successfully`)); - return { success: true, binaryPath }; - } catch (error) { - console.error(chalk.red(`✗ Failed to install: ${error.message}`)); - console.log(chalk.gray('Manual installation: https://github.com/grafana/loki/releases')); - return { success: false, binaryPath: null }; - } finally { - // Clean up temp directory - try { - fs.rmSync(tmpDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - } -}; - -/** - * Configure logcli - */ -export const configureLogcli = async () => { - console.log(chalk.cyan('\n=== Loki CLI (logcli) Configuration ===\n')); - console.log(chalk.gray('logcli is the command-line tool for querying Grafana Loki.\n')); - - // Check installation - let binary = findLogcliBinary(); - - if (!binary) { - console.log(chalk.yellow('! logcli is not installed')); - - const { install } = await inquirer.prompt([ - { - type: 'confirm', - name: 'install', - message: 'Install logcli?', - default: true, - }, - ]); - - if (install) { - const result = await installViaBinary(); - if (!result.success) { - return false; - } - binary = result.binaryPath || findLogcliBinary(); - if (!binary) { - console.log(chalk.red('✗ Could not find logcli after installation')); - return false; - } - } else { - console.log(chalk.gray('Skipping logcli setup.')); - return false; - } - } else { - console.log(chalk.green('✓ logcli is installed')); - - // Show path if not in PATH - if (!commandExists('logcli')) { - console.log(chalk.yellow(` Location: ${binary}`)); - console.log(chalk.yellow(' Note: Not in PATH. Add to your shell profile:')); - console.log(chalk.gray(` export PATH="$PATH:${path.dirname(binary)}"`)); - } - - // Check version - try { - const version = execFileSync(binary, ['--version'], { encoding: 'utf8' }).trim(); - console.log(chalk.gray(` Version: ${version}`)); - } catch { - // Version check failed, continue anyway - } - } - - // Show configuration instructions - console.log(chalk.blue('\nConfiguration:')); - console.log(chalk.gray('logcli uses environment variables for authentication:\n')); - console.log( - chalk.white(' export LOKI_ADDR="/api/datasources/proxy/"') - ); - console.log(chalk.white(' export LOKI_BEARER_TOKEN=""\n')); - console.log(chalk.gray('Get these values from your Grafana instance.')); - console.log(chalk.gray('The datasource ID can be found in the Loki datasource URL.\n')); - - console.log(chalk.green('✓ logcli configuration complete')); - return true; -}; diff --git a/lib/llm/index.js b/lib/llm/index.js index 50e8c33..bf15c45 100644 --- a/lib/llm/index.js +++ b/lib/llm/index.js @@ -614,35 +614,48 @@ n8nctl execution delete # Delete execution grafana: { filename: 'cli-grafana.md', name: 'grafana', - purpose: 'Grafana logs (logcli) and traces via API', - content: `## Grafana + purpose: 'Grafana Cloud queries (logs, metrics, traces, resources) via gcx', + content: `## Grafana Cloud (gcx) -**Config**: \`~/.grafanactl/config.yaml\` +**Config**: \`~/.config/gcx/config.yaml\` (contexts + stack). -### Logs (logcli) +\`gcx\` is the unified Grafana Cloud CLI. It replaces the old \`logcli\` and \`grafanactl\` CLIs. + +### Setup / verify \`\`\`bash -export LOKI_ADDR="/api/datasources/proxy/" -export LOKI_BEARER_TOKEN="" +gcx config check # Verify active context & connectivity +gcx login --server # Log in and create/use a context +\`\`\` + +### Logs (Loki) -logcli labels k8s_deployment_name -logcli query '{k8s_deployment_name=""}' --limit=20 --since=1h +\`\`\`bash +gcx logs query '{service_name=""}' --since=1h +gcx logs query '{deployment_environment="production"}' --since=24h --limit=200 \`\`\` -**Note**: Get server/token from \`~/.grafanactl/config.yaml\`. Datasource ID varies by instance. +### Metrics (Prometheus) -### Traces (API) +\`\`\`bash +gcx metrics query 'rate(http_requests_total[5m])' +\`\`\` -Must use \`queryType: "traceId"\`: +### Traces (Tempo) \`\`\`bash -curl -s "$GRAFANA_URL/api/ds/query" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \\ - -d '{"queries":[{"datasource":{"uid":"grafanacloud-traces"},"query":"","queryType":"traceId"}],"from":"now-1h","to":"now"}' +gcx traces get # Retrieve a trace by ID \`\`\` -### Log→Trace +### Resources (dashboards, datasources, ...) + +\`\`\`bash +gcx resources list-types # List resource types +gcx dashboards list # List dashboards +gcx datasources list # List datasources +\`\`\` -Logs have \`trace_id\` label. Not all traces stored (sampling). +**Notes**: Logs are queried by resource-attribute labels (\`service_name\`, \`deployment_environment\`, ...), not \`k8s_*\` labels. Log→trace correlation uses the \`trace_id\` label; not all traces are stored (sampling). `, }, m365: { diff --git a/package.json b/package.json index f3777d0..cba242b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@enthus-appdev/llm-cli-setup", - "version": "1.11.0", - "description": "CLI tools setup with LLM integration - installs and configures sqlcmd, gh, atl, n8nctl, grafanactl, logcli, m365, esq, discordctl, playwright, and hcloud with Claude Code, Antigravity, and Codex support", + "version": "1.12.0", + "description": "CLI tools setup with LLM integration - installs and configures sqlcmd, gh, atl, n8nctl, gcx, m365, esq, discordctl, playwright, and hcloud with Claude Code, Antigravity, and Codex support", "type": "module", "main": "./lib/index.js", "exports": { From 75fef0e6ed132157c96f973f227609537c0f331e Mon Sep 17 00:00:00 2001 From: Florian Kinder Date: Tue, 11 Aug 2026 20:49:27 +0200 Subject: [PATCH 2/2] fix: harden gcx installer (curl -f, cross-device copy, safe config view) Address PR review feedback: - curl -sLf so HTTP errors fail the download instead of writing an error page - fs.copyFileSync + chmod instead of renameSync to avoid EXDEV across filesystems - wrap 'gcx config view' in try-catch so a bad config can't crash the CLI --- lib/installers/gcx.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/installers/gcx.js b/lib/installers/gcx.js index a23115e..1373c38 100644 --- a/lib/installers/gcx.js +++ b/lib/installers/gcx.js @@ -135,7 +135,7 @@ const installViaBinary = async () => { const targetBinary = path.join(installDir, 'gcx'); fs.mkdirSync(installDir, { recursive: true }); - execFileSync('curl', ['-sL', downloadUrl, '-o', tarballPath]); + execFileSync('curl', ['-sLf', downloadUrl, '-o', tarballPath]); execFileSync('tar', ['xzf', tarballPath, '-C', tmpDir]); // The tarball contains the `gcx` binary (plus README/LICENSE/CHANGELOG). @@ -144,7 +144,8 @@ const installViaBinary = async () => { throw new Error('gcx binary not found in the release archive'); } - fs.renameSync(extractedBinary, targetBinary); + // Copy (not rename) so it works across filesystems (e.g. /tmp → ~/.local/bin). + fs.copyFileSync(extractedBinary, targetBinary); fs.chmodSync(targetBinary, 0o755); console.log(chalk.green(`✓ gcx ${version} installed successfully`)); @@ -302,7 +303,11 @@ export const configureGcx = async () => { await runLogin(binary); } else if (action === 'view') { console.log(); - execFileSync(binary, ['config', 'view'], { stdio: 'inherit' }); + try { + execFileSync(binary, ['config', 'view'], { stdio: 'inherit' }); + } catch (error) { + console.error(chalk.red(`Failed to view configuration: ${error.message}`)); + } } } else { // No context configured