@redstone-md/nodule 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.js +108 -30
- package/bin/nodule.js +50 -17
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Nodule — postinstall
|
|
3
|
+
* Nodule — postinstall / on-demand binary downloader.
|
|
4
4
|
*
|
|
5
5
|
* Downloads the platform-appropriate Nodule binary from the latest GitHub
|
|
6
|
-
* Release.
|
|
6
|
+
* Release. Supports both async (postinstall) and sync (first-run from nodule.js)
|
|
7
|
+
* modes. Falls back to `go install` if download fails.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const https = require('https');
|
|
@@ -16,34 +17,114 @@ const GITHUB_REPO = 'redstone-md/nodule';
|
|
|
16
17
|
const BIN_DIR = __dirname;
|
|
17
18
|
|
|
18
19
|
function getTarget() {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
var platform = os.platform();
|
|
21
|
+
var arch = os.arch();
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
var goos, goarch, ext = '';
|
|
23
24
|
|
|
24
25
|
switch (platform) {
|
|
25
26
|
case 'linux': goos = 'linux'; break;
|
|
26
27
|
case 'darwin': goos = 'darwin'; break;
|
|
27
28
|
case 'win32': goos = 'windows'; ext = '.exe'; break;
|
|
28
|
-
default:
|
|
29
|
-
console.warn('nodule: unsupported platform ' + platform + ', skipping download');
|
|
30
|
-
console.warn('nodule: install manually: go install github.com/redstone-md/nodule/cmd/nodule@latest');
|
|
31
|
-
return null;
|
|
29
|
+
default: return null;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
switch (arch) {
|
|
35
33
|
case 'x64': goarch = 'amd64'; break;
|
|
36
34
|
case 'arm64': goarch = 'arm64'; break;
|
|
37
35
|
case 'ia32': goarch = '386'; break;
|
|
38
|
-
default:
|
|
39
|
-
console.warn('nodule: unsupported arch ' + arch + ', skipping download');
|
|
40
|
-
return null;
|
|
36
|
+
default: return null;
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
return {
|
|
40
|
+
goos: goos,
|
|
41
|
+
goarch: goarch,
|
|
42
|
+
ext: ext,
|
|
43
|
+
assetName: 'nodule-' + goos + '-' + goarch + ext
|
|
44
|
+
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function getDestPath(target) {
|
|
48
|
+
return path.join(BIN_DIR, 'nodule-' + target.goos + '-' + target.goarch + target.ext);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Synchronous download — used by nodule.js at first run.
|
|
53
|
+
* Returns true if binary was installed successfully.
|
|
54
|
+
*/
|
|
55
|
+
function installSync() {
|
|
56
|
+
var target = getTarget();
|
|
57
|
+
if (!target) return false;
|
|
58
|
+
|
|
59
|
+
var destPath = getDestPath(target);
|
|
60
|
+
if (fs.existsSync(destPath)) return true;
|
|
61
|
+
|
|
62
|
+
process.stderr.write('nodule: downloading binary for ' + target.goos + '/' + target.goarch + '...\n');
|
|
63
|
+
|
|
64
|
+
// Fetch latest release URL synchronously via curl or Invoke-WebRequest
|
|
65
|
+
var curlResult = spawnSync('curl', [
|
|
66
|
+
'-s', '-L', '-H', 'User-Agent: nodule-installer',
|
|
67
|
+
'https://api.github.com/repos/' + GITHUB_REPO + '/releases/latest'
|
|
68
|
+
], { encoding: 'utf8', timeout: 30000 });
|
|
69
|
+
|
|
70
|
+
if (curlResult.status !== 0 || !curlResult.stdout) {
|
|
71
|
+
tryGoInstall();
|
|
72
|
+
return fs.existsSync(destPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var release;
|
|
76
|
+
try { release = JSON.parse(curlResult.stdout); }
|
|
77
|
+
catch (e) {
|
|
78
|
+
tryGoInstall();
|
|
79
|
+
return fs.existsSync(destPath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var asset = (release.assets || []).find(function(a) { return a.name === target.assetName; });
|
|
83
|
+
if (!asset) {
|
|
84
|
+
process.stderr.write('nodule: asset ' + target.assetName + ' not found in ' + release.tag_name + '\n');
|
|
85
|
+
tryGoInstall();
|
|
86
|
+
return fs.existsSync(destPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Download binary via curl
|
|
90
|
+
var dlResult = spawnSync('curl', [
|
|
91
|
+
'-s', '-L', '-o', destPath,
|
|
92
|
+
'-H', 'User-Agent: nodule-installer',
|
|
93
|
+
'-H', 'Accept: application/octet-stream',
|
|
94
|
+
asset.browser_download_url
|
|
95
|
+
], { timeout: 60000 });
|
|
96
|
+
|
|
97
|
+
if (dlResult.status !== 0) {
|
|
98
|
+
process.stderr.write('nodule: download failed\n');
|
|
99
|
+
try { fs.unlinkSync(destPath); } catch (e) {}
|
|
100
|
+
tryGoInstall();
|
|
101
|
+
return fs.existsSync(destPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (target.goos !== 'windows') {
|
|
105
|
+
try { fs.chmodSync(destPath, 0o755); } catch (e) {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.stderr.write('nodule: installed ' + release.tag_name + '\n');
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function tryGoInstall() {
|
|
113
|
+
process.stderr.write('nodule: falling back to go install...\n');
|
|
114
|
+
var result = spawnSync('go', [
|
|
115
|
+
'install', 'github.com/' + GITHUB_REPO + '/cmd/nodule@latest'
|
|
116
|
+
], { stdio: 'inherit', timeout: 120000 });
|
|
117
|
+
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
process.stderr.write('nodule: go install failed. Install manually:\n');
|
|
120
|
+
process.stderr.write(' go install github.com/redstone-md/nodule/cmd/nodule@latest\n');
|
|
121
|
+
} else {
|
|
122
|
+
process.stderr.write('nodule: installed via go install\n');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Async mode (postinstall) ---
|
|
127
|
+
|
|
47
128
|
function fetchLatestReleaseTag() {
|
|
48
129
|
return new Promise(function(resolve, reject) {
|
|
49
130
|
var url = 'https://api.github.com/repos/' + GITHUB_REPO + '/releases/latest';
|
|
@@ -87,10 +168,12 @@ function downloadAsset(url, destPath) {
|
|
|
87
168
|
|
|
88
169
|
async function main() {
|
|
89
170
|
var target = getTarget();
|
|
90
|
-
if (!target)
|
|
91
|
-
|
|
92
|
-
|
|
171
|
+
if (!target) {
|
|
172
|
+
process.stderr.write('nodule: unsupported platform, skipping download\n');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
93
175
|
|
|
176
|
+
var destPath = getDestPath(target);
|
|
94
177
|
if (fs.existsSync(destPath)) {
|
|
95
178
|
console.log('nodule: binary already present, skipping download');
|
|
96
179
|
return;
|
|
@@ -114,19 +197,14 @@ async function main() {
|
|
|
114
197
|
console.log('nodule: installed ' + release.tag_name);
|
|
115
198
|
} catch (err) {
|
|
116
199
|
console.warn('nodule: could not download binary (' + err.message + ')');
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
var result = spawnSync('go', [
|
|
120
|
-
'install', 'github.com/' + GITHUB_REPO + '/cmd/nodule@latest'
|
|
121
|
-
], { stdio: 'inherit' });
|
|
122
|
-
|
|
123
|
-
if (result.status !== 0) {
|
|
124
|
-
console.warn('nodule: go install failed. Install manually:');
|
|
125
|
-
console.warn(' go install github.com/redstone-md/nodule/cmd/nodule@latest');
|
|
126
|
-
} else {
|
|
127
|
-
console.log('nodule: installed via go install (ensure $GOPATH/bin is in PATH)');
|
|
128
|
-
}
|
|
200
|
+
tryGoInstall();
|
|
129
201
|
}
|
|
130
202
|
}
|
|
131
203
|
|
|
132
|
-
|
|
204
|
+
// Export sync installer for nodule.js, run async on postinstall
|
|
205
|
+
module.exports = { installSync: installSync };
|
|
206
|
+
|
|
207
|
+
// When run directly (postinstall), execute async download
|
|
208
|
+
if (require.main === module) {
|
|
209
|
+
main();
|
|
210
|
+
}
|
package/bin/nodule.js
CHANGED
|
@@ -3,18 +3,16 @@
|
|
|
3
3
|
* Nodule — binary launcher.
|
|
4
4
|
*
|
|
5
5
|
* Runs the platform-specific Nodule MCP server binary with stdio passthrough.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* All environment variables (NODULE_LLM_PROVIDER, NODULE_API_KEY, etc.)
|
|
9
|
-
* are inherited from the parent process — Nodule is fully BYOM/BYOK.
|
|
6
|
+
* If the binary is not present (first run, no postinstall), downloads it
|
|
7
|
+
* synchronously before launching. All environment variables are inherited.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
const { spawn } = require('child_process');
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
13
11
|
const path = require('path');
|
|
14
12
|
const os = require('os');
|
|
15
13
|
const fs = require('fs');
|
|
16
14
|
|
|
17
|
-
function
|
|
15
|
+
function getTarget() {
|
|
18
16
|
const platform = os.platform();
|
|
19
17
|
const arch = os.arch();
|
|
20
18
|
|
|
@@ -23,7 +21,7 @@ function getBinaryPath() {
|
|
|
23
21
|
case 'linux': goos = 'linux'; break;
|
|
24
22
|
case 'darwin': goos = 'darwin'; break;
|
|
25
23
|
case 'win32': goos = 'windows'; ext = '.exe'; break;
|
|
26
|
-
default:
|
|
24
|
+
default: return null;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
let goarch;
|
|
@@ -31,22 +29,54 @@ function getBinaryPath() {
|
|
|
31
29
|
case 'x64': goarch = 'amd64'; break;
|
|
32
30
|
case 'arm64': goarch = 'arm64'; break;
|
|
33
31
|
case 'ia32': goarch = '386'; break;
|
|
34
|
-
default:
|
|
32
|
+
default: return null;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
return { goos: goos, goarch: goarch, ext: ext };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getBinaryPath() {
|
|
39
|
+
const target = getTarget();
|
|
40
|
+
if (!target) return null;
|
|
41
|
+
const binaryName = 'nodule-' + target.goos + '-' + target.goarch + target.ext;
|
|
38
42
|
return path.join(__dirname, binaryName);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
45
|
+
function ensureBinary() {
|
|
42
46
|
let binaryPath = getBinaryPath();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (!fs.existsSync(binaryPath)) {
|
|
47
|
+
if (!binaryPath) {
|
|
48
|
+
// Unsupported platform — try PATH lookup
|
|
46
49
|
const isWindows = os.platform() === 'win32';
|
|
47
|
-
|
|
50
|
+
return isWindows ? 'nodule.exe' : 'nodule';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Binary already present
|
|
54
|
+
if (fs.existsSync(binaryPath)) {
|
|
55
|
+
return binaryPath;
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
// Binary missing — download synchronously before launching
|
|
59
|
+
process.stderr.write('nodule: first run, downloading binary...\n');
|
|
60
|
+
try {
|
|
61
|
+
var installer = require('./install.js');
|
|
62
|
+
installer.installSync();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Re-check after install attempt
|
|
68
|
+
if (fs.existsSync(binaryPath)) {
|
|
69
|
+
return binaryPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Final fallback: PATH lookup (go install case)
|
|
73
|
+
const isWindows = os.platform() === 'win32';
|
|
74
|
+
return isWindows ? 'nodule.exe' : 'nodule';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
const binaryPath = ensureBinary();
|
|
79
|
+
|
|
50
80
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
51
81
|
stdio: 'inherit',
|
|
52
82
|
env: process.env,
|
|
@@ -55,10 +85,13 @@ function main() {
|
|
|
55
85
|
child.on('error', (err) => {
|
|
56
86
|
if (err.code === 'ENOENT') {
|
|
57
87
|
process.stderr.write(
|
|
58
|
-
'nodule: binary not found.
|
|
88
|
+
'nodule: binary not found. Install with:\n' +
|
|
89
|
+
' npm install @redstone-md/nodule\n' +
|
|
90
|
+
'or:\n' +
|
|
91
|
+
' go install github.com/redstone-md/nodule/cmd/nodule@latest\n'
|
|
59
92
|
);
|
|
60
93
|
} else {
|
|
61
|
-
process.stderr.write(
|
|
94
|
+
process.stderr.write('nodule: ' + err.message + '\n');
|
|
62
95
|
}
|
|
63
96
|
process.exit(1);
|
|
64
97
|
});
|
|
@@ -67,7 +100,7 @@ function main() {
|
|
|
67
100
|
if (signal) {
|
|
68
101
|
process.kill(process.pid, signal);
|
|
69
102
|
} else {
|
|
70
|
-
process.exit(code
|
|
103
|
+
process.exit(code == null ? 1 : code);
|
|
71
104
|
}
|
|
72
105
|
});
|
|
73
106
|
}
|