1+ #!/usr/bin/env node
2+
3+ import { spawn } from 'node:child_process' ;
4+ import { createHash } from 'node:crypto' ;
5+ import { createWriteStream , existsSync , mkdirSync , readFileSync , writeFileSync , chmodSync } from 'node:fs' ;
6+ import { tmpdir } from 'node:os' ;
7+ import { join } from 'node:path' ;
8+ import process from 'node:process' ;
9+ import { pipeline } from 'node:stream/promises' ;
10+
11+ const PLUGIN_ROOT = process . env . CLAUDE_PLUGIN_ROOT ;
12+ if ( ! PLUGIN_ROOT ) {
13+ console . error ( 'Error: CLAUDE_PLUGIN_ROOT environment variable not set' ) ;
14+ process . exit ( 1 ) ;
15+ }
16+
17+ // Detect OS and architecture
18+ const platform = process . platform ;
19+ const arch = process . arch ;
20+
21+ let OS , ARCH , EXT , BINARY_NAME , BINARY_PATH ;
22+
23+ // Map Node.js arch to release naming
24+ switch ( arch ) {
25+ case 'x64' :
26+ ARCH = 'x86_64' ;
27+ break ;
28+ case 'arm64' :
29+ ARCH = 'arm64' ;
30+ break ;
31+ case 'ia32' :
32+ ARCH = 'i386' ;
33+ break ;
34+ default :
35+ console . error ( `Unsupported architecture: ${ arch } ` ) ;
36+ process . exit ( 1 ) ;
37+ }
38+
39+ // Map Node.js platform to release naming
40+ switch ( platform ) {
41+ case 'darwin' :
42+ OS = 'Darwin' ;
43+ EXT = 'tar.gz' ;
44+ BINARY_NAME = 'mcp-grafana' ;
45+ break ;
46+ case 'linux' :
47+ OS = 'Linux' ;
48+ EXT = 'tar.gz' ;
49+ BINARY_NAME = 'mcp-grafana' ;
50+ break ;
51+ case 'win32' :
52+ OS = 'Windows' ;
53+ EXT = 'zip' ;
54+ BINARY_NAME = 'mcp-grafana.exe' ;
55+ break ;
56+ default :
57+ console . error ( `Unsupported OS: ${ platform } ` ) ;
58+ process . exit ( 1 ) ;
59+ }
60+
61+ BINARY_PATH = join ( PLUGIN_ROOT , BINARY_NAME ) ;
62+
63+ // Fetch latest version from GitHub API
64+ async function getLatestVersion ( ) {
65+ const headers = { } ;
66+ // Use GitHub token if available (for CI environments)
67+ if ( process . env . GITHUB_TOKEN ) {
68+ headers [ 'Authorization' ] = `Bearer ${ process . env . GITHUB_TOKEN } ` ;
69+ }
70+
71+ const response = await fetch ( 'https://api.github.com/repos/grafana/mcp-grafana/releases/latest' , {
72+ headers
73+ } ) ;
74+ if ( ! response . ok ) {
75+ throw new Error ( `Failed to fetch latest version: ${ response . statusText } ` ) ;
76+ }
77+ const data = await response . json ( ) ;
78+ return data . tag_name ;
79+ }
80+
81+ // Download file from URL
82+ async function downloadFile ( url , destPath ) {
83+ const response = await fetch ( url ) ;
84+ if ( ! response . ok ) {
85+ throw new Error ( `Failed to download ${ url } : ${ response . statusText } ` ) ;
86+ }
87+ await pipeline ( response . body , createWriteStream ( destPath ) ) ;
88+ }
89+
90+ // Verify SHA256 checksum
91+ async function verifyChecksum ( filePath , checksumsContent , archiveName ) {
92+ const fileBuffer = readFileSync ( filePath ) ;
93+ const hash = createHash ( 'sha256' ) . update ( fileBuffer ) . digest ( 'hex' ) ;
94+
95+ const lines = checksumsContent . split ( '\n' ) ;
96+ for ( const line of lines ) {
97+ if ( line . includes ( archiveName ) ) {
98+ const [ expectedHash ] = line . split ( / \s + / ) ;
99+ if ( hash === expectedHash ) {
100+ console . error ( `✓ Checksum verified` ) ;
101+ return true ;
102+ } else {
103+ throw new Error ( `Checksum mismatch for ${ archiveName } ` ) ;
104+ }
105+ }
106+ }
107+ throw new Error ( `No checksum found for ${ archiveName } ` ) ;
108+ }
109+
110+ // Extract tar.gz archive using system tar command
111+ function extractTarGz ( archivePath , destDir ) {
112+ return new Promise ( ( resolve , reject ) => {
113+ const tar = spawn ( 'tar' , [ '-xzf' , archivePath , '-C' , destDir ] ) ;
114+ tar . on ( 'close' , ( code ) => {
115+ if ( code === 0 ) resolve ( ) ;
116+ else reject ( new Error ( `tar extraction failed with code ${ code } ` ) ) ;
117+ } ) ;
118+ } ) ;
119+ }
120+
121+ // Extract zip archive using system command
122+ function extractZip ( archivePath , destDir ) {
123+ return new Promise ( ( resolve , reject ) => {
124+ // Use PowerShell on Windows
125+ const unzip = spawn ( 'powershell' , [ '-Command' , `Expand-Archive -Path "${ archivePath } " -DestinationPath "${ destDir } " -Force` ] ) ;
126+ unzip . on ( 'close' , ( code ) => {
127+ if ( code === 0 ) resolve ( ) ;
128+ else reject ( new Error ( `zip extraction failed with code ${ code } ` ) ) ;
129+ } ) ;
130+ } ) ;
131+ }
132+
133+ async function main ( ) {
134+ try {
135+ // Get latest version
136+ console . error ( 'Fetching latest version...' ) ;
137+ const VERSION = await getLatestVersion ( ) ;
138+
139+ const ARCHIVE_NAME = `mcp-grafana_${ OS } _${ ARCH } .${ EXT } ` ;
140+ const VERSION_FILE = join ( PLUGIN_ROOT , '.mcp-grafana-version' ) ;
141+
142+ // Check if binary exists and version matches
143+ const needsInstall = ! existsSync ( BINARY_PATH ) ||
144+ ! existsSync ( VERSION_FILE ) ||
145+ readFileSync ( VERSION_FILE , 'utf8' ) . trim ( ) !== VERSION ;
146+
147+ if ( ! needsInstall ) {
148+ // Binary is up to date, just execute it
149+ const child = spawn ( BINARY_PATH , process . argv . slice ( 2 ) , { stdio : 'inherit' } ) ;
150+ child . on ( 'exit' , ( code ) => process . exit ( code || 0 ) ) ;
151+ return ;
152+ }
153+
154+ console . error ( `Downloading mcp-grafana ${ VERSION } for ${ OS } -${ ARCH } ...` ) ;
155+
156+ // Create temp directory
157+ const TEMP_DIR = join ( tmpdir ( ) , `mcp-grafana-${ Date . now ( ) } ` ) ;
158+ mkdirSync ( TEMP_DIR , { recursive : true } ) ;
159+
160+ try {
161+ const ARCHIVE_PATH = join ( TEMP_DIR , ARCHIVE_NAME ) ;
162+ const DOWNLOAD_URL = `https://github.com/grafana/mcp-grafana/releases/latest/download/${ ARCHIVE_NAME } ` ;
163+
164+ // Download archive
165+ await downloadFile ( DOWNLOAD_URL , ARCHIVE_PATH ) ;
166+
167+ // Download and verify checksums
168+ console . error ( 'Verifying checksum...' ) ;
169+ const VERSION_NUMBER = VERSION . replace ( / ^ v / , '' ) ; // Remove 'v' prefix
170+ const CHECKSUMS_URL = `https://github.com/grafana/mcp-grafana/releases/download/${ VERSION } /mcp-grafana_${ VERSION_NUMBER } _checksums.txt` ;
171+ const checksumResponse = await fetch ( CHECKSUMS_URL ) ;
172+ if ( ! checksumResponse . ok ) {
173+ throw new Error ( `Failed to download checksums: ${ checksumResponse . statusText } ` ) ;
174+ }
175+ const checksumsContent = await checksumResponse . text ( ) ;
176+ await verifyChecksum ( ARCHIVE_PATH , checksumsContent , ARCHIVE_NAME ) ;
177+
178+ // Extract archive
179+ console . error ( 'Extracting archive...' ) ;
180+ if ( EXT === 'tar.gz' ) {
181+ await extractTarGz ( ARCHIVE_PATH , TEMP_DIR ) ;
182+ } else {
183+ await extractZip ( ARCHIVE_PATH , TEMP_DIR ) ;
184+ }
185+
186+ // Move binary to plugin root
187+ const extractedBinary = join ( TEMP_DIR , BINARY_NAME ) ;
188+ if ( ! existsSync ( extractedBinary ) ) {
189+ throw new Error ( `Binary not found after extraction: ${ extractedBinary } ` ) ;
190+ }
191+
192+ mkdirSync ( PLUGIN_ROOT , { recursive : true } ) ;
193+ const binaryContent = readFileSync ( extractedBinary ) ;
194+ writeFileSync ( BINARY_PATH , binaryContent ) ;
195+
196+ if ( platform !== 'win32' ) {
197+ chmodSync ( BINARY_PATH , 0o755 ) ;
198+ }
199+
200+ writeFileSync ( VERSION_FILE , VERSION ) ;
201+
202+ console . error ( `Successfully installed mcp-grafana ${ VERSION } ` ) ;
203+ } finally {
204+ // Cleanup temp directory
205+ try {
206+ const { rmSync } = await import ( 'fs' ) ;
207+ rmSync ( TEMP_DIR , { recursive : true , force : true } ) ;
208+ } catch ( e ) {
209+ // Ignore cleanup errors
210+ }
211+ }
212+
213+ // Execute the binary
214+ const child = spawn ( BINARY_PATH , process . argv . slice ( 2 ) , { stdio : 'inherit' } ) ;
215+ child . on ( 'exit' , ( code ) => process . exit ( code || 0 ) ) ;
216+
217+ } catch ( error ) {
218+ console . error ( `Error: ${ error . message } ` ) ;
219+ process . exit ( 1 ) ;
220+ }
221+ }
222+
223+ main ( ) ;
0 commit comments