From 159c8432a3b981a438f61e44c3646a26d512ec48 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Mon, 19 May 2025 18:35:12 +0530 Subject: [PATCH 1/5] Add `graph node install` command to install local dev node --- packages/cli/package.json | 4 + .../cli/src/command-helpers/local-node.ts | 145 +++++++++++++ packages/cli/src/commands/node.ts | 90 ++++++++ pnpm-lock.yaml | 200 +++++++++++++++++- 4 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/command-helpers/local-node.ts create mode 100644 packages/cli/src/commands/node.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 221015e37..bb0e46d48 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,6 +46,7 @@ "assemblyscript": "0.19.23", "chokidar": "4.0.3", "debug": "4.4.1", + "decompress": "^4.2.1", "docker-compose": "1.2.0", "fs-extra": "11.3.0", "glob": "11.0.2", @@ -57,6 +58,7 @@ "kubo-rpc-client": "^5.0.2", "open": "10.1.2", "prettier": "3.5.3", + "progress": "^2.0.3", "semver": "7.7.2", "tmp-promise": "3.0.3", "undici": "7.9.0", @@ -65,8 +67,10 @@ }, "devDependencies": { "@types/debug": "^4.1.12", + "@types/decompress": "^4.2.7", "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", + "@types/progress": "^2.0.7", "@types/semver": "^7.5.8", "@types/which": "^3.0.4", "copyfiles": "^2.4.1", diff --git a/packages/cli/src/command-helpers/local-node.ts b/packages/cli/src/command-helpers/local-node.ts new file mode 100644 index 000000000..369a1c174 --- /dev/null +++ b/packages/cli/src/command-helpers/local-node.ts @@ -0,0 +1,145 @@ +import * as fs from 'node:fs'; +import { createReadStream, createWriteStream } from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; +import { createGunzip } from 'node:zlib'; +import decompress from 'decompress'; +import fetch from '../fetch.js'; + +function getPlatformBinaryName(): string { + const platform = os.platform(); + const arch = os.arch(); + + if (platform === 'linux' && arch === 'x64') return 'gnd-linux-x86_64.gz'; + if (platform === 'linux' && arch === 'arm64') return 'gnd-linux-aarch64.gz'; + if (platform === 'darwin' && arch === 'x64') return 'gnd-macos-x86_64.gz'; + if (platform === 'darwin' && arch === 'arm64') return 'gnd-windows-x86_64.exe.zip'; //'gnd-macos-aarch64.gz'; + if (platform === 'win32' && arch === 'x64') return 'gnd-windows-x86_64.exe.zip'; + + throw new Error(`Unsupported platform: ${platform} ${arch}`); +} + +export async function getGlobalBinDir(): Promise { + const platform = os.platform(); + let binDir: string; + + if (platform === 'win32') { + // Prefer %USERPROFILE%\gnd\bin + binDir = path.join(process.env.USERPROFILE || os.homedir(), 'gnd', 'bin'); + } else { + binDir = path.join(os.homedir(), '.local', 'bin'); + } + + await fs.promises.mkdir(binDir, { recursive: true }); + return binDir; +} + +async function getLatestGithubRelease(owner: string, repo: string) { + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`); + const data = await res.json(); + return data.tag_name; +} + +export async function getLatestGraphNodeRelease(): Promise { + return getLatestGithubRelease('incrypto32', 'graph-node'); +} + +export async function downloadGraphNodeRelease( + release: string, + outputDir: string, + onProgress?: (downloaded: number, total: number | null) => void, +): Promise { + const fileName = getPlatformBinaryName(); + return downloadGithubRelease( + 'incrypto32', + 'graph-node', + release, + outputDir, + fileName, + onProgress, + ); +} + +async function downloadGithubRelease( + owner: string, + repo: string, + release: string, + outputDir: string, + fileName: string, + onProgress?: (downloaded: number, total: number | null) => void, +): Promise { + const url = `https://github.com/${owner}/${repo}/releases/download/${release}/${fileName}`; + return downloadFile(url, path.join(outputDir, fileName), onProgress); +} + +export async function downloadFile( + url: string, + outputPath: string, + onProgress?: (downloaded: number, total: number | null) => void, +): Promise { + return download(url, outputPath, onProgress); +} + +export async function download( + url: string, + outputPath: string, + onProgress?: (downloaded: number, total: number | null) => void, +): Promise { + const res = await fetch(url); + if (!res.ok || !res.body) { + throw new Error(`Failed to download: ${res.statusText}`); + } + + const totalLength = Number(res.headers.get('content-length')) || null; + let downloaded = 0; + + const fileStream = fs.createWriteStream(outputPath); + const nodeStream = Readable.from(res.body); + + nodeStream.on('data', chunk => { + downloaded += chunk.length; + onProgress?.(downloaded, totalLength); + }); + + nodeStream.pipe(fileStream); + + await new Promise((resolve, reject) => { + nodeStream.on('error', reject); + fileStream.on('finish', resolve); + fileStream.on('error', reject); + }); + + return outputPath; +} + +export async function extractGz(gzPath: string, outputPath?: string): Promise { + const outPath = outputPath || path.join(path.dirname(gzPath), path.basename(gzPath, '.gz')); + + await pipeline(createReadStream(gzPath), createGunzip(), createWriteStream(outPath)); + + return outPath; +} + +export async function extractZipAndGetExe(zipPath: string, outputDir: string): Promise { + const files = await decompress(zipPath, outputDir); + const exe = files.filter(file => file.path.endsWith('.exe')); + + if (exe.length !== 1) { + throw new Error(`Expected 1 executable file in zip, got ${exe.length}`); + } + + return path.join(outputDir, exe[0].path); +} + +export async function moveFileToBinDir(srcPath: string, binDir?: string): Promise { + const targetDir = binDir || (await getGlobalBinDir()); + const destPath = path.join(targetDir, path.basename(srcPath)); + await fs.promises.rename(srcPath, destPath); + return destPath; +} +export async function moveFile(srcPath: string, destPath: string): Promise { + await fs.promises.rename(srcPath, destPath); + return destPath; +} diff --git a/packages/cli/src/commands/node.ts b/packages/cli/src/commands/node.ts new file mode 100644 index 000000000..cc9db5589 --- /dev/null +++ b/packages/cli/src/commands/node.ts @@ -0,0 +1,90 @@ +import * as fs from 'node:fs'; +import { chmod } from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { print } from 'gluegun'; +import ProgressBar from 'progress'; +import { Command, Flags } from '@oclif/core'; +import { + downloadGraphNodeRelease, + extractGz, + extractZipAndGetExe, + getLatestGraphNodeRelease, + moveFileToBinDir, +} from '../command-helpers/local-node.js'; + +export default class NodeCommand extends Command { + static description = 'Manage Graph node related operations'; + + static flags = { + help: Flags.help({ + char: 'h', + }), + }; + + static args = {}; + + static examples = ['$ graph node install']; + + static strict = false; + + async run() { + const { argv } = await this.parse(NodeCommand); + + if (argv.length > 0) { + const subcommand = argv[0]; + + if (subcommand === 'install') { + await installGraphNode(); + } + + // If no valid subcommand is provided, show help + await this.config.runCommand('help', ['node']); + } + } +} + +async function installGraphNode() { + const latestRelease = await getLatestGraphNodeRelease(); + const tmpBase = os.tmpdir(); + const tmpDir = await fs.promises.mkdtemp(path.join(tmpBase, 'graph-node-')); + let progressBar: ProgressBar | undefined; + const downloadPath = await downloadGraphNodeRelease( + latestRelease, + tmpDir, + (downloaded, total) => { + if (!total) return; + + progressBar ||= new ProgressBar(`Downloading ${latestRelease} [:bar] :percent`, { + width: 30, + total, + complete: '=', + incomplete: ' ', + }); + + progressBar.tick(downloaded - (progressBar.curr || 0)); + }, + ); + + let extractedPath: string; + + if (downloadPath.endsWith('.gz')) { + extractedPath = await extractGz(downloadPath); + print.info(`Extracted ${extractedPath}`); + } else if (downloadPath.endsWith('.zip')) { + extractedPath = await extractZipAndGetExe(downloadPath, tmpDir); + print.info(`Extracted ${extractedPath}`); + } else { + throw new Error(`Unsupported file type: ${downloadPath}`); + } + + const movedPath = await moveFileToBinDir(extractedPath); + print.info(`Moved ${extractedPath} to ${movedPath}`); + + if (os.platform() !== 'win32') { + await chmod(movedPath, 0o755); + } + + // Delete the temporary directory + await fs.promises.rm(tmpDir, { recursive: true, force: true }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 474ff0815..1ae5cd71f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -263,6 +263,9 @@ importers: debug: specifier: 4.4.1 version: 4.4.1(supports-color@5.5.0) + decompress: + specifier: ^4.2.1 + version: 4.2.1 docker-compose: specifier: 1.2.0 version: 1.2.0 @@ -296,6 +299,9 @@ importers: prettier: specifier: 3.5.3 version: 3.5.3 + progress: + specifier: ^2.0.3 + version: 2.0.3 semver: specifier: 7.7.2 version: 7.7.2 @@ -315,12 +321,18 @@ importers: '@types/debug': specifier: ^4.1.12 version: 4.1.12 + '@types/decompress': + specifier: ^4.2.7 + version: 4.2.7 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 + '@types/progress': + specifier: ^2.0.7 + version: 2.0.7 '@types/semver': specifier: ^7.5.8 version: 7.7.0 @@ -3407,6 +3419,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/decompress@4.2.7': + resolution: {integrity: sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==} + '@types/dns-packet@5.6.5': resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==} @@ -3503,6 +3518,9 @@ packages: '@types/prettier@2.7.3': resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} + '@types/progress@2.0.7': + resolution: {integrity: sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w==} + '@types/qs@6.9.18': resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} @@ -4299,6 +4317,9 @@ packages: buffer-alloc@1.2.0: resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-fill@1.0.0: resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} @@ -4852,6 +4873,26 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + decompress-tar@4.1.1: + resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} + engines: {node: '>=4'} + + decompress-tarbz2@4.1.1: + resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==} + engines: {node: '>=4'} + + decompress-targz@4.1.1: + resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==} + engines: {node: '>=4'} + + decompress-unzip@4.0.1: + resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==} + engines: {node: '>=4'} + + decompress@4.2.1: + resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==} + engines: {node: '>=4'} + deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} @@ -5604,6 +5645,9 @@ packages: fastq@1.19.0: resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -5627,6 +5671,18 @@ packages: resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} engines: {node: '>=18'} + file-type@3.9.0: + resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} + engines: {node: '>=0.10.0'} + + file-type@5.2.0: + resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==} + engines: {node: '>=4'} + + file-type@6.2.0: + resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==} + engines: {node: '>=4'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -5809,6 +5865,10 @@ packages: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} engines: {node: '>=12'} + get-stream@2.3.1: + resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} + engines: {node: '>=0.10.0'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -6364,6 +6424,9 @@ packages: resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} engines: {node: '>= 0.4'} + is-natural-number@4.0.1: + resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -6918,6 +6981,10 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -7677,6 +7744,9 @@ packages: resolution: {integrity: sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==} engines: {node: '>=18'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -7688,6 +7758,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -7700,6 +7774,14 @@ packages: resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} engines: {node: '>=10'} + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} @@ -7806,6 +7888,10 @@ packages: progress-events@1.0.1: resolution: {integrity: sha512-MOzLIwhpt64KIVN64h1MwdKWiyKFNc/S6BoYKPIVUHFg0/eIEyBulhWCgn678v/4c0ri3FdGuzXymNCv02MUIw==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -8290,6 +8376,10 @@ packages: resolution: {integrity: sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==} engines: {node: '>=18.0.0'} + seek-bzip@1.0.6: + resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} + hasBin: true + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -8637,6 +8727,9 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-dirs@2.1.0: + resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -9024,6 +9117,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -9653,6 +9749,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -13923,6 +14022,10 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/decompress@4.2.7': + dependencies: + '@types/node': 22.15.18 + '@types/dns-packet@5.6.5': dependencies: '@types/node': 22.15.18 @@ -14015,6 +14118,10 @@ snapshots: '@types/prettier@2.7.3': {} + '@types/progress@2.0.7': + dependencies: + '@types/node': 22.15.18 + '@types/qs@6.9.18': {} '@types/react-dom@19.1.1(@types/react@19.1.0)': @@ -14965,7 +15072,7 @@ snapshots: axios@1.9.0: dependencies: - follow-redirects: 1.15.9(debug@4.4.1) + follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.2 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -15310,6 +15417,8 @@ snapshots: buffer-alloc-unsafe: 1.1.0 buffer-fill: 1.0.0 + buffer-crc32@0.2.13: {} + buffer-fill@1.0.0: {} buffer-from@1.1.2: {} @@ -15920,6 +16029,44 @@ snapshots: dependencies: mimic-response: 3.1.0 + decompress-tar@4.1.1: + dependencies: + file-type: 5.2.0 + is-stream: 1.1.0 + tar-stream: 1.6.2 + + decompress-tarbz2@4.1.1: + dependencies: + decompress-tar: 4.1.1 + file-type: 6.2.0 + is-stream: 1.1.0 + seek-bzip: 1.0.6 + unbzip2-stream: 1.4.3 + + decompress-targz@4.1.1: + dependencies: + decompress-tar: 4.1.1 + file-type: 5.2.0 + is-stream: 1.1.0 + + decompress-unzip@4.0.1: + dependencies: + file-type: 3.9.0 + get-stream: 2.3.1 + pify: 2.3.0 + yauzl: 2.10.0 + + decompress@4.2.1: + dependencies: + decompress-tar: 4.1.1 + decompress-tarbz2: 4.1.1 + decompress-targz: 4.1.1 + decompress-unzip: 4.0.1 + graceful-fs: 4.2.11 + make-dir: 1.3.0 + pify: 2.3.0 + strip-dirs: 2.1.0 + deep-eql@4.1.4: dependencies: type-detect: 4.1.0 @@ -16999,6 +17146,10 @@ snapshots: dependencies: reusify: 1.0.4 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -17022,6 +17173,12 @@ snapshots: transitivePeerDependencies: - supports-color + file-type@3.9.0: {} + + file-type@5.2.0: {} + + file-type@6.2.0: {} + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -17229,6 +17386,11 @@ snapshots: get-stdin@9.0.0: {} + get-stream@2.3.1: + dependencies: + object-assign: 4.1.1 + pinkie-promise: 2.0.1 + get-stream@6.0.1: {} get-symbol-description@1.1.0: @@ -17999,6 +18161,8 @@ snapshots: call-bind: 1.0.8 define-properties: 1.2.1 + is-natural-number@4.0.1: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.3 @@ -18538,6 +18702,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@1.3.0: + dependencies: + pify: 3.0.0 + make-error@1.3.6: {} markdown-table@1.1.3: {} @@ -19567,18 +19735,28 @@ snapshots: peek-readable@7.0.0: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.2: {} + pify@2.3.0: {} + pify@3.0.0: {} pify@4.0.1: {} pify@5.0.0: {} + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + pino-abstract-transport@0.5.0: dependencies: duplexify: 4.1.3 @@ -19665,6 +19843,8 @@ snapshots: progress-events@1.0.1: {} + progress@2.0.3: {} + promise-inflight@1.0.1: {} promise-retry@2.0.1: @@ -20231,6 +20411,10 @@ snapshots: node-addon-api: 5.1.0 node-gyp-build: 4.8.4 + seek-bzip@1.0.6: + dependencies: + commander: 2.20.3 + semver@5.7.2: {} semver@6.3.1: {} @@ -20670,6 +20854,10 @@ snapshots: strip-bom@3.0.0: {} + strip-dirs@2.1.0: + dependencies: + is-natural-number: 4.0.1 + strip-final-newline@2.0.0: {} strip-hex-prefix@1.0.0: @@ -21089,6 +21277,11 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + uncrypto@0.1.3: {} undici-types@6.19.8: {} @@ -21779,6 +21972,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yn@3.1.1: {} yocto-queue@0.1.0: {} From 665c5d5abe7260be62dd5ea075c802d56de96d83 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Mon, 19 May 2025 18:38:54 +0530 Subject: [PATCH 2/5] Add changeset --- .changeset/easy-ideas-kick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/easy-ideas-kick.md diff --git a/.changeset/easy-ideas-kick.md b/.changeset/easy-ideas-kick.md new file mode 100644 index 000000000..1c53aff8a --- /dev/null +++ b/.changeset/easy-ideas-kick.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/graph-cli': minor +--- + +Add a new command to install graph-node dev binary (gnd) From 8ca4c8faa5da8d5b307a96f66f45d93a60bdf4b3 Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Tue, 20 May 2025 10:47:40 +0530 Subject: [PATCH 3/5] Better error handling --- .../cli/src/command-helpers/local-node.ts | 27 +++++--- packages/cli/src/commands/node.ts | 62 ++++++++++++------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/command-helpers/local-node.ts b/packages/cli/src/command-helpers/local-node.ts index 369a1c174..c33cf0dce 100644 --- a/packages/cli/src/command-helpers/local-node.ts +++ b/packages/cli/src/command-helpers/local-node.ts @@ -52,14 +52,23 @@ export async function downloadGraphNodeRelease( onProgress?: (downloaded: number, total: number | null) => void, ): Promise { const fileName = getPlatformBinaryName(); - return downloadGithubRelease( - 'incrypto32', - 'graph-node', - release, - outputDir, - fileName, - onProgress, - ); + + try { + return await downloadGithubRelease( + 'incrypto32', + 'graph-node', + release, + outputDir, + fileName, + onProgress, + ); + } catch (e) { + if (e === 404) { + throw `Graph Node release ${release} does not exist, please check the release page for the correct release tag`; + } + + throw `Failed to download: ${release}`; + } } async function downloadGithubRelease( @@ -89,7 +98,7 @@ export async function download( ): Promise { const res = await fetch(url); if (!res.ok || !res.body) { - throw new Error(`Failed to download: ${res.statusText}`); + throw res.status; } const totalLength = Number(res.headers.get('content-length')) || null; diff --git a/packages/cli/src/commands/node.ts b/packages/cli/src/commands/node.ts index cc9db5589..6723e60e1 100644 --- a/packages/cli/src/commands/node.ts +++ b/packages/cli/src/commands/node.ts @@ -4,7 +4,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { print } from 'gluegun'; import ProgressBar from 'progress'; -import { Command, Flags } from '@oclif/core'; +import { Args, Command, Flags } from '@oclif/core'; import { downloadGraphNodeRelease, extractGz, @@ -16,43 +16,51 @@ import { export default class NodeCommand extends Command { static description = 'Manage Graph node related operations'; - static flags = { + static override flags = { help: Flags.help({ char: 'h', }), + tag: Flags.string({ + summary: 'Tag of the Graph Node release to install.', + }), + 'download-dir': Flags.string({ + summary: 'Directory to download the Graph Node release to.', + default: os.tmpdir(), + }), }; - static args = {}; + static override args = { + install: Args.boolean({ + description: 'Install the Graph Node', + }), + }; static examples = ['$ graph node install']; static strict = false; async run() { - const { argv } = await this.parse(NodeCommand); - - if (argv.length > 0) { - const subcommand = argv[0]; - - if (subcommand === 'install') { - await installGraphNode(); - } + const { flags, args } = await this.parse(NodeCommand); - // If no valid subcommand is provided, show help - await this.config.runCommand('help', ['node']); + if (args.install) { + await installGraphNode(flags.tag); + return; } + + // If no valid subcommand is provided, show help + await this.config.runCommand('help', ['node']); } } -async function installGraphNode() { - const latestRelease = await getLatestGraphNodeRelease(); +async function installGraphNode(tag?: string) { + const latestRelease = tag || (await getLatestGraphNodeRelease()); const tmpBase = os.tmpdir(); const tmpDir = await fs.promises.mkdtemp(path.join(tmpBase, 'graph-node-')); let progressBar: ProgressBar | undefined; - const downloadPath = await downloadGraphNodeRelease( - latestRelease, - tmpDir, - (downloaded, total) => { + + let downloadPath: string; + try { + downloadPath = await downloadGraphNodeRelease(latestRelease, tmpDir, (downloaded, total) => { if (!total) return; progressBar ||= new ProgressBar(`Downloading ${latestRelease} [:bar] :percent`, { @@ -63,18 +71,21 @@ async function installGraphNode() { }); progressBar.tick(downloaded - (progressBar.curr || 0)); - }, - ); + }); + } catch (e) { + print.error(e); + throw e; + } let extractedPath: string; + print.info(`Extracting ${downloadPath}`); if (downloadPath.endsWith('.gz')) { extractedPath = await extractGz(downloadPath); - print.info(`Extracted ${extractedPath}`); } else if (downloadPath.endsWith('.zip')) { extractedPath = await extractZipAndGetExe(downloadPath, tmpDir); - print.info(`Extracted ${extractedPath}`); } else { + print.error(`Unsupported file type: ${downloadPath}`); throw new Error(`Unsupported file type: ${downloadPath}`); } @@ -85,6 +96,11 @@ async function installGraphNode() { await chmod(movedPath, 0o755); } + print.info(`Installed Graph Node ${latestRelease}`); + print.info( + `Please add the following to your PATH: ${path.dirname(movedPath)} if it's not already there or if you're using a custom download directory`, + ); + // Delete the temporary directory await fs.promises.rm(tmpDir, { recursive: true, force: true }); } From a7193927ea0f90153024f3555712b1f14fd4a20f Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 12 Jun 2025 12:37:43 +0530 Subject: [PATCH 4/5] Rename binaries approriately --- packages/cli/src/command-helpers/local-node.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/command-helpers/local-node.ts b/packages/cli/src/command-helpers/local-node.ts index c33cf0dce..692723d90 100644 --- a/packages/cli/src/command-helpers/local-node.ts +++ b/packages/cli/src/command-helpers/local-node.ts @@ -15,7 +15,7 @@ function getPlatformBinaryName(): string { if (platform === 'linux' && arch === 'x64') return 'gnd-linux-x86_64.gz'; if (platform === 'linux' && arch === 'arm64') return 'gnd-linux-aarch64.gz'; if (platform === 'darwin' && arch === 'x64') return 'gnd-macos-x86_64.gz'; - if (platform === 'darwin' && arch === 'arm64') return 'gnd-windows-x86_64.exe.zip'; //'gnd-macos-aarch64.gz'; + if (platform === 'darwin' && arch === 'arm64') return 'gnd-macos-aarch64.gz'; if (platform === 'win32' && arch === 'x64') return 'gnd-windows-x86_64.exe.zip'; throw new Error(`Unsupported platform: ${platform} ${arch}`); @@ -144,10 +144,13 @@ export async function extractZipAndGetExe(zipPath: string, outputDir: string): P export async function moveFileToBinDir(srcPath: string, binDir?: string): Promise { const targetDir = binDir || (await getGlobalBinDir()); - const destPath = path.join(targetDir, path.basename(srcPath)); + const platform = os.platform(); + const binaryName = platform === 'win32' ? 'gnd.exe' : 'gnd'; + const destPath = path.join(targetDir, binaryName); await fs.promises.rename(srcPath, destPath); return destPath; } + export async function moveFile(srcPath: string, destPath: string): Promise { await fs.promises.rename(srcPath, destPath); return destPath; From e887768cce48fae988ecc4cfda1bc935e891331e Mon Sep 17 00:00:00 2001 From: incrypto32 Date: Thu, 12 Jun 2025 12:58:31 +0530 Subject: [PATCH 5/5] improve installation UX and add custom bin directory support --- packages/cli/src/commands/node.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/node.ts b/packages/cli/src/commands/node.ts index 6723e60e1..3764a9334 100644 --- a/packages/cli/src/commands/node.ts +++ b/packages/cli/src/commands/node.ts @@ -23,9 +23,8 @@ export default class NodeCommand extends Command { tag: Flags.string({ summary: 'Tag of the Graph Node release to install.', }), - 'download-dir': Flags.string({ - summary: 'Directory to download the Graph Node release to.', - default: os.tmpdir(), + 'bin-dir': Flags.string({ + summary: 'Directory to install the Graph Node binary to.', }), }; @@ -35,7 +34,11 @@ export default class NodeCommand extends Command { }), }; - static examples = ['$ graph node install']; + static examples = [ + '$ graph node install', + '$ graph node install --tag v1.0.0', + '$ graph node install --bin-dir /usr/local/bin', + ]; static strict = false; @@ -43,7 +46,7 @@ export default class NodeCommand extends Command { const { flags, args } = await this.parse(NodeCommand); if (args.install) { - await installGraphNode(flags.tag); + await installGraphNode(flags.tag, flags['bin-dir']); return; } @@ -52,7 +55,7 @@ export default class NodeCommand extends Command { } } -async function installGraphNode(tag?: string) { +async function installGraphNode(tag?: string, binDir?: string) { const latestRelease = tag || (await getLatestGraphNodeRelease()); const tmpBase = os.tmpdir(); const tmpDir = await fs.promises.mkdtemp(path.join(tmpBase, 'graph-node-')); @@ -79,7 +82,7 @@ async function installGraphNode(tag?: string) { let extractedPath: string; - print.info(`Extracting ${downloadPath}`); + print.info(`\nExtracting binary...`); if (downloadPath.endsWith('.gz')) { extractedPath = await extractGz(downloadPath); } else if (downloadPath.endsWith('.zip')) { @@ -89,17 +92,19 @@ async function installGraphNode(tag?: string) { throw new Error(`Unsupported file type: ${downloadPath}`); } - const movedPath = await moveFileToBinDir(extractedPath); - print.info(`Moved ${extractedPath} to ${movedPath}`); + const movedPath = await moveFileToBinDir(extractedPath, binDir); + print.info(`✅ Graph Node ${latestRelease} installed successfully`); + print.info(`Binary location: ${movedPath}`); if (os.platform() !== 'win32') { await chmod(movedPath, 0o755); } - print.info(`Installed Graph Node ${latestRelease}`); - print.info( - `Please add the following to your PATH: ${path.dirname(movedPath)} if it's not already there or if you're using a custom download directory`, - ); + print.info(''); + print.info(`📋 Next steps:`); + print.info(` Add ${path.dirname(movedPath)} to your PATH (if not already)`); + print.info(` Run 'gnd' to start your local Graph Node development environment`); + print.info(''); // Delete the temporary directory await fs.promises.rm(tmpDir, { recursive: true, force: true });