Skip to content

Commit 1f62d10

Browse files
committed
Add graph node install command to install local dev node
1 parent 31103e7 commit 1f62d10

File tree

4 files changed

+15283
-8156
lines changed

4 files changed

+15283
-8156
lines changed

packages/cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"assemblyscript": "0.19.23",
4747
"chokidar": "4.0.3",
4848
"debug": "4.4.1",
49+
"decompress": "^4.2.1",
4950
"docker-compose": "1.2.0",
5051
"fs-extra": "11.3.0",
5152
"glob": "11.0.2",
@@ -57,6 +58,7 @@
5758
"kubo-rpc-client": "^5.0.2",
5859
"open": "10.1.2",
5960
"prettier": "3.5.3",
61+
"progress": "^2.0.3",
6062
"semver": "7.7.2",
6163
"tmp-promise": "3.0.3",
6264
"undici": "7.9.0",
@@ -65,8 +67,10 @@
6567
},
6668
"devDependencies": {
6769
"@types/debug": "^4.1.12",
70+
"@types/decompress": "^4.2.7",
6871
"@types/fs-extra": "^11.0.4",
6972
"@types/js-yaml": "^4.0.9",
73+
"@types/progress": "^2.0.7",
7074
"@types/semver": "^7.5.8",
7175
"@types/which": "^3.0.4",
7276
"copyfiles": "^2.4.1",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as fs from 'node:fs';
2+
import { createReadStream, createWriteStream } from 'node:fs';
3+
import * as os from 'node:os';
4+
import * as path from 'node:path';
5+
import { Readable } from 'node:stream';
6+
import { pipeline } from 'node:stream/promises';
7+
import { createGunzip } from 'node:zlib';
8+
import decompress from 'decompress';
9+
import fetch from '../fetch.js';
10+
11+
function getPlatformBinaryName(): string {
12+
const platform = os.platform();
13+
const arch = os.arch();
14+
15+
if (platform === 'linux' && arch === 'x64') return 'gnd-linux-x86_64.gz';
16+
if (platform === 'linux' && arch === 'arm64') return 'gnd-linux-aarch64.gz';
17+
if (platform === 'darwin' && arch === 'x64') return 'gnd-macos-x86_64.gz';
18+
if (platform === 'darwin' && arch === 'arm64') return 'gnd-windows-x86_64.exe.zip'; //'gnd-macos-aarch64.gz';
19+
if (platform === 'win32' && arch === 'x64') return 'gnd-windows-x86_64.exe.zip';
20+
21+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
22+
}
23+
24+
export async function getGlobalBinDir(): Promise<string> {
25+
const platform = os.platform();
26+
let binDir: string;
27+
28+
if (platform === 'win32') {
29+
// Prefer %USERPROFILE%\gnd\bin
30+
binDir = path.join(process.env.USERPROFILE || os.homedir(), 'gnd', 'bin');
31+
} else {
32+
binDir = path.join(os.homedir(), '.local', 'bin');
33+
}
34+
35+
await fs.promises.mkdir(binDir, { recursive: true });
36+
return binDir;
37+
}
38+
39+
async function getLatestGithubRelease(owner: string, repo: string) {
40+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
41+
const data = await res.json();
42+
return data.tag_name;
43+
}
44+
45+
export async function getLatestGraphNodeRelease(): Promise<string> {
46+
return getLatestGithubRelease('incrypto32', 'graph-node');
47+
}
48+
49+
export async function downloadGraphNodeRelease(
50+
release: string,
51+
outputDir: string,
52+
onProgress?: (downloaded: number, total: number | null) => void,
53+
): Promise<string> {
54+
const fileName = getPlatformBinaryName();
55+
return downloadGithubRelease(
56+
'incrypto32',
57+
'graph-node',
58+
release,
59+
outputDir,
60+
fileName,
61+
onProgress,
62+
);
63+
}
64+
65+
async function downloadGithubRelease(
66+
owner: string,
67+
repo: string,
68+
release: string,
69+
outputDir: string,
70+
fileName: string,
71+
onProgress?: (downloaded: number, total: number | null) => void,
72+
): Promise<string> {
73+
const url = `https://github.com/${owner}/${repo}/releases/download/${release}/${fileName}`;
74+
return downloadFile(url, path.join(outputDir, fileName), onProgress);
75+
}
76+
77+
export async function downloadFile(
78+
url: string,
79+
outputPath: string,
80+
onProgress?: (downloaded: number, total: number | null) => void,
81+
): Promise<string> {
82+
return download(url, outputPath, onProgress);
83+
}
84+
85+
export async function download(
86+
url: string,
87+
outputPath: string,
88+
onProgress?: (downloaded: number, total: number | null) => void,
89+
): Promise<string> {
90+
const res = await fetch(url);
91+
if (!res.ok || !res.body) {
92+
throw new Error(`Failed to download: ${res.statusText}`);
93+
}
94+
95+
const totalLength = Number(res.headers.get('content-length')) || null;
96+
let downloaded = 0;
97+
98+
const fileStream = fs.createWriteStream(outputPath);
99+
const nodeStream = Readable.from(res.body);
100+
101+
nodeStream.on('data', chunk => {
102+
downloaded += chunk.length;
103+
onProgress?.(downloaded, totalLength);
104+
});
105+
106+
nodeStream.pipe(fileStream);
107+
108+
await new Promise<void>((resolve, reject) => {
109+
nodeStream.on('error', reject);
110+
fileStream.on('finish', resolve);
111+
fileStream.on('error', reject);
112+
});
113+
114+
return outputPath;
115+
}
116+
117+
export async function extractGz(gzPath: string, outputPath?: string): Promise<string> {
118+
const outPath = outputPath || path.join(path.dirname(gzPath), path.basename(gzPath, '.gz'));
119+
120+
await pipeline(createReadStream(gzPath), createGunzip(), createWriteStream(outPath));
121+
122+
return outPath;
123+
}
124+
125+
export async function extractZipAndGetExe(zipPath: string, outputDir: string): Promise<string> {
126+
const files = await decompress(zipPath, outputDir);
127+
const exe = files.filter(file => file.path.endsWith('.exe'));
128+
129+
if (exe.length !== 1) {
130+
throw new Error(`Expected 1 executable file in zip, got ${exe.length}`);
131+
}
132+
133+
return path.join(outputDir, exe[0].path);
134+
}
135+
136+
export async function moveFileToBinDir(srcPath: string, binDir?: string): Promise<string> {
137+
const targetDir = binDir || (await getGlobalBinDir());
138+
const destPath = path.join(targetDir, path.basename(srcPath));
139+
await fs.promises.rename(srcPath, destPath);
140+
return destPath;
141+
}
142+
export async function moveFile(srcPath: string, destPath: string): Promise<string> {
143+
await fs.promises.rename(srcPath, destPath);
144+
return destPath;
145+
}

packages/cli/src/commands/node.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as fs from 'node:fs';
2+
import { chmod } from 'node:fs/promises';
3+
import * as os from 'node:os';
4+
import * as path from 'node:path';
5+
import { print } from 'gluegun';
6+
import ProgressBar from 'progress';
7+
import { Command, Flags } from '@oclif/core';
8+
import {
9+
downloadGraphNodeRelease,
10+
extractGz,
11+
extractZipAndGetExe,
12+
getLatestGraphNodeRelease,
13+
moveFileToBinDir,
14+
} from '../command-helpers/local-node.js';
15+
16+
export default class NodeCommand extends Command {
17+
static description = 'Manage Graph node related operations';
18+
19+
static flags = {
20+
help: Flags.help({
21+
char: 'h',
22+
}),
23+
};
24+
25+
static args = {};
26+
27+
static examples = ['$ graph node install'];
28+
29+
static strict = false;
30+
31+
async run() {
32+
const { argv } = await this.parse(NodeCommand);
33+
34+
if (argv.length > 0) {
35+
const subcommand = argv[0];
36+
37+
if (subcommand === 'install') {
38+
await installGraphNode();
39+
}
40+
41+
// If no valid subcommand is provided, show help
42+
await this.config.runCommand('help', ['node']);
43+
}
44+
}
45+
}
46+
47+
async function installGraphNode() {
48+
const latestRelease = await getLatestGraphNodeRelease();
49+
const tmpBase = os.tmpdir();
50+
const tmpDir = await fs.promises.mkdtemp(path.join(tmpBase, 'graph-node-'));
51+
let progressBar: ProgressBar | undefined;
52+
const downloadPath = await downloadGraphNodeRelease(
53+
latestRelease,
54+
tmpDir,
55+
(downloaded, total) => {
56+
if (!total) return;
57+
58+
progressBar ||= new ProgressBar(`Downloading ${latestRelease} [:bar] :percent`, {
59+
width: 30,
60+
total,
61+
complete: '=',
62+
incomplete: ' ',
63+
});
64+
65+
progressBar.tick(downloaded - (progressBar.curr || 0));
66+
},
67+
);
68+
69+
let extractedPath: string;
70+
71+
if (downloadPath.endsWith('.gz')) {
72+
extractedPath = await extractGz(downloadPath);
73+
print.info(`Extracted ${extractedPath}`);
74+
} else if (downloadPath.endsWith('.zip')) {
75+
extractedPath = await extractZipAndGetExe(downloadPath, tmpDir);
76+
print.info(`Extracted ${extractedPath}`);
77+
} else {
78+
throw new Error(`Unsupported file type: ${downloadPath}`);
79+
}
80+
81+
const movedPath = await moveFileToBinDir(extractedPath);
82+
print.info(`Moved ${extractedPath} to ${movedPath}`);
83+
84+
if (os.platform() !== 'win32') {
85+
await chmod(movedPath, 0o755);
86+
}
87+
88+
// Delete the temporary directory
89+
await fs.promises.rm(tmpDir, { recursive: true, force: true });
90+
}

0 commit comments

Comments
 (0)