-
Notifications
You must be signed in to change notification settings - Fork 228
Add graph node install
command to install local dev node
#2041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
2799515
Add `graph node install` command to install local dev node
incrypto32 575eac3
Add changeset
incrypto32 a97d857
Better error handling
incrypto32 f199ab7
Rename binaries approriately
incrypto32 b1f77f6
improve installation UX and add custom bin directory support
incrypto32 07c5fe3
Use the right graph protocol repo
incrypto32 b393a6a
catch exceptions
incrypto32 aa55bbb
Make the github org and repo an env var
incrypto32 e287b3b
chore(dependencies): updated changesets for modified dependencies
pinax-bot d06dea9
Use Error constructor
incrypto32 0e7b441
fix lint
YaroShkvorets File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@graphprotocol/graph-cli": patch | ||
--- | ||
dependencies updates: | ||
- Added dependency [`decompress@^4.2.1` ↗︎](https://www.npmjs.com/package/decompress/v/4.2.1) (to `dependencies`) | ||
- Added dependency [`progress@^2.0.3` ↗︎](https://www.npmjs.com/package/progress/v/2.0.3) (to `dependencies`) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@graphprotocol/graph-cli': minor | ||
--- | ||
|
||
Add a new command to install graph-node dev binary (gnd) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
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'; | ||
|
||
// Add GitHub repository configuration via environment variables with defaults | ||
const GRAPH_NODE_GITHUB_OWNER = process.env.GRAPH_NODE_GITHUB_OWNER || 'graphprotocol'; | ||
const GRAPH_NODE_GITHUB_REPO = process.env.GRAPH_NODE_GITHUB_REPO || 'graph-node'; | ||
|
||
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-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<string> { | ||
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<string> { | ||
return getLatestGithubRelease(GRAPH_NODE_GITHUB_OWNER, GRAPH_NODE_GITHUB_REPO); | ||
} | ||
|
||
export async function downloadGraphNodeRelease( | ||
release: string, | ||
outputDir: string, | ||
onProgress?: (downloaded: number, total: number | null) => void, | ||
): Promise<string> { | ||
const fileName = getPlatformBinaryName(); | ||
|
||
try { | ||
return await downloadGithubRelease( | ||
GRAPH_NODE_GITHUB_OWNER, | ||
GRAPH_NODE_GITHUB_REPO, | ||
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( | ||
owner: string, | ||
repo: string, | ||
release: string, | ||
outputDir: string, | ||
fileName: string, | ||
onProgress?: (downloaded: number, total: number | null) => void, | ||
): Promise<string> { | ||
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<string> { | ||
return download(url, outputPath, onProgress); | ||
} | ||
|
||
export async function download( | ||
url: string, | ||
outputPath: string, | ||
onProgress?: (downloaded: number, total: number | null) => void, | ||
): Promise<string> { | ||
const res = await fetch(url); | ||
if (!res.ok || !res.body) { | ||
throw res.status; | ||
} | ||
|
||
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<void>((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<string> { | ||
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<string> { | ||
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<string> { | ||
const targetDir = binDir || (await getGlobalBinDir()); | ||
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<string> { | ||
await fs.promises.rename(srcPath, destPath); | ||
return destPath; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
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 { Args, 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 override flags = { | ||
help: Flags.help({ | ||
char: 'h', | ||
}), | ||
tag: Flags.string({ | ||
summary: 'Tag of the Graph Node release to install.', | ||
}), | ||
'bin-dir': Flags.string({ | ||
summary: 'Directory to install the Graph Node binary to.', | ||
}), | ||
}; | ||
|
||
static override args = { | ||
install: Args.boolean({ | ||
description: 'Install the Graph Node', | ||
}), | ||
}; | ||
|
||
static examples = [ | ||
'$ graph node install', | ||
'$ graph node install --tag v1.0.0', | ||
'$ graph node install --bin-dir /usr/local/bin', | ||
]; | ||
|
||
static strict = false; | ||
|
||
async run() { | ||
const { flags, args } = await this.parse(NodeCommand); | ||
|
||
if (args.install) { | ||
try { | ||
await installGraphNode(flags.tag, flags['bin-dir']); | ||
} catch (e) { | ||
this.error(`Failed to install: ${e.message}`, { exit: 1 }); | ||
} | ||
return; | ||
} | ||
|
||
// If no valid subcommand is provided, show help | ||
await this.config.runCommand('help', ['node']); | ||
} | ||
} | ||
|
||
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-')); | ||
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; | ||
|
||
print.info(`\nExtracting binary...`); | ||
if (downloadPath.endsWith('.gz')) { | ||
extractedPath = await extractGz(downloadPath); | ||
} else if (downloadPath.endsWith('.zip')) { | ||
extractedPath = await extractZipAndGetExe(downloadPath, tmpDir); | ||
} else { | ||
print.error(`Unsupported file type: ${downloadPath}`); | ||
throw new Error(`Unsupported file type: ${downloadPath}`); | ||
} | ||
|
||
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(''); | ||
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 }); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.