@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 CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Nodule — postinstall script.
3
+ * Nodule — postinstall / on-demand binary downloader.
4
4
  *
5
5
  * Downloads the platform-appropriate Nodule binary from the latest GitHub
6
- * Release. Falls back to `go install` if download fails.
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
- const platform = os.platform();
20
- const arch = os.arch();
20
+ var platform = os.platform();
21
+ var arch = os.arch();
21
22
 
22
- let goos, goarch, ext = '';
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
- const assetName = 'nodule-' + goos + '-' + goarch + ext;
44
- return { goos: goos, goarch: goarch, ext: ext, assetName: assetName };
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) return;
91
-
92
- var destPath = path.join(BIN_DIR, 'nodule-' + target.goos + '-' + target.goarch + target.ext);
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
- console.warn('nodule: falling back to go install...');
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
- main();
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
- * This is a thin wrapper: the Go binary handles all MCP protocol logic.
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 getBinaryPath() {
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: throw new Error(`unsupported platform: ${platform}`);
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: throw new Error(`unsupported arch: ${arch}`);
32
+ default: return null;
35
33
  }
36
34
 
37
- const binaryName = `nodule-${goos}-${goarch}${ext}`;
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 main() {
45
+ function ensureBinary() {
42
46
  let binaryPath = getBinaryPath();
43
-
44
- // If the platform binary doesn't exist, try PATH lookup (go install case)
45
- if (!fs.existsSync(binaryPath)) {
47
+ if (!binaryPath) {
48
+ // Unsupported platform try PATH lookup
46
49
  const isWindows = os.platform() === 'win32';
47
- binaryPath = isWindows ? 'nodule.exe' : 'nodule';
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. Reinstall with `npm install nodule` or `go install github.com/redstone-md/nodule/cmd/nodule@latest`\n'
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(`nodule: ${err.message}\n`);
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 ?? 1);
103
+ process.exit(code == null ? 1 : code);
71
104
  }
72
105
  });
73
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redstone-md/nodule",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Local MCP server providing a bounce_idea tool for independent LLM-based code critique (BYOM/BYOK)",
5
5
  "license": "MIT",
6
6
  "repository": {