@learnrudi/cli 1.6.0 → 1.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learnrudi/cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "RUDI CLI - Install and manage MCP stacks, runtimes, and AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -9,11 +9,13 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "scripts",
12
13
  "README.md"
13
14
  ],
14
15
  "scripts": {
15
16
  "start": "node src/index.js",
16
17
  "build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3",
18
+ "postinstall": "node scripts/postinstall.js",
17
19
  "prepublishOnly": "npm run build",
18
20
  "test": "node --test src/__tests__/"
19
21
  },
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install script for @learnrudi/cli
4
+ *
5
+ * Fetches runtime manifests from registry and downloads:
6
+ * 1. Node.js runtime → ~/.rudi/runtimes/node/
7
+ * 2. Python runtime → ~/.rudi/runtimes/python/
8
+ * 3. Creates shims → ~/.rudi/shims/
9
+ */
10
+
11
+ import { execSync } from 'child_process';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+
16
+ const RUDI_HOME = path.join(os.homedir(), '.rudi');
17
+ const REGISTRY_BASE = 'https://raw.githubusercontent.com/learn-rudi/registry/main';
18
+
19
+ // Detect platform
20
+ function getPlatformArch() {
21
+ const platform = process.platform;
22
+ const arch = process.arch;
23
+
24
+ if (platform === 'darwin') {
25
+ return arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
26
+ } else if (platform === 'linux') {
27
+ return arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
28
+ } else if (platform === 'win32') {
29
+ return 'win32-x64';
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // Create directory structure
35
+ function ensureDirectories() {
36
+ const dirs = [
37
+ RUDI_HOME,
38
+ path.join(RUDI_HOME, 'runtimes'),
39
+ path.join(RUDI_HOME, 'stacks'),
40
+ path.join(RUDI_HOME, 'tools'),
41
+ path.join(RUDI_HOME, 'shims'),
42
+ path.join(RUDI_HOME, 'cache'),
43
+ ];
44
+
45
+ for (const dir of dirs) {
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ }
49
+ }
50
+ }
51
+
52
+ // Fetch JSON from URL
53
+ async function fetchJson(url) {
54
+ const response = await fetch(url);
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
57
+ }
58
+ return response.json();
59
+ }
60
+
61
+ // Download and extract a tarball
62
+ async function downloadAndExtract(url, destDir, name) {
63
+ const tempFile = path.join(RUDI_HOME, 'cache', `${name}.tar.gz`);
64
+
65
+ console.log(` Downloading ${name}...`);
66
+
67
+ try {
68
+ // Download using curl
69
+ execSync(`curl -fsSL "${url}" -o "${tempFile}"`, { stdio: 'pipe' });
70
+
71
+ // Create dest directory
72
+ if (fs.existsSync(destDir)) {
73
+ fs.rmSync(destDir, { recursive: true });
74
+ }
75
+ fs.mkdirSync(destDir, { recursive: true });
76
+
77
+ // Extract - strip first component to avoid nested dirs
78
+ execSync(`tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`, { stdio: 'pipe' });
79
+
80
+ // Clean up
81
+ fs.unlinkSync(tempFile);
82
+
83
+ console.log(` ✓ ${name} installed`);
84
+ return true;
85
+ } catch (error) {
86
+ console.log(` ⚠ Failed to install ${name}: ${error.message}`);
87
+ if (fs.existsSync(tempFile)) {
88
+ fs.unlinkSync(tempFile);
89
+ }
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // Download runtime from manifest
95
+ async function downloadRuntime(runtimeId, platformArch) {
96
+ const manifestUrl = `${REGISTRY_BASE}/catalog/runtimes/${runtimeId}.json`;
97
+
98
+ try {
99
+ const manifest = await fetchJson(manifestUrl);
100
+ const downloadUrl = manifest.download?.[platformArch];
101
+
102
+ if (!downloadUrl) {
103
+ console.log(` ⚠ No ${runtimeId} available for ${platformArch}`);
104
+ return false;
105
+ }
106
+
107
+ const destDir = path.join(RUDI_HOME, 'runtimes', runtimeId);
108
+ const binaryPath = path.join(destDir, manifest.binary || `bin/${runtimeId}`);
109
+
110
+ // Skip if already installed
111
+ if (fs.existsSync(binaryPath)) {
112
+ console.log(` ✓ ${manifest.name} already installed`);
113
+ return true;
114
+ }
115
+
116
+ return await downloadAndExtract(downloadUrl, destDir, manifest.name);
117
+ } catch (error) {
118
+ console.log(` ⚠ Failed to fetch ${runtimeId} manifest: ${error.message}`);
119
+ return false;
120
+ }
121
+ }
122
+
123
+ // Create the rudi-mcp shim
124
+ function createShim() {
125
+ const shimPath = path.join(RUDI_HOME, 'shims', 'rudi-mcp');
126
+
127
+ const shimContent = `#!/bin/bash
128
+ # RUDI MCP Shim - Routes agent calls to rudi mcp command
129
+ # Usage: rudi-mcp <stack-name>
130
+ exec rudi mcp "$@"
131
+ `;
132
+
133
+ fs.writeFileSync(shimPath, shimContent);
134
+ fs.chmodSync(shimPath, 0o755);
135
+ console.log(` ✓ Created shim at ${shimPath}`);
136
+ }
137
+
138
+ // Initialize secrets file with secure permissions
139
+ function initSecrets() {
140
+ const secretsPath = path.join(RUDI_HOME, 'secrets.json');
141
+
142
+ if (!fs.existsSync(secretsPath)) {
143
+ fs.writeFileSync(secretsPath, '{}', { mode: 0o600 });
144
+ console.log(` ✓ Created secrets store`);
145
+ }
146
+ }
147
+
148
+ // Main setup
149
+ async function setup() {
150
+ console.log('\nSetting up RUDI...\n');
151
+
152
+ const platformArch = getPlatformArch();
153
+ if (!platformArch) {
154
+ console.log('⚠ Unsupported platform. Skipping runtime download.');
155
+ console.log(' You can manually install runtimes later with: rudi install runtime:node\n');
156
+ return;
157
+ }
158
+
159
+ // Create directories
160
+ ensureDirectories();
161
+ console.log('✓ Created ~/.rudi directory structure\n');
162
+
163
+ // Download runtimes from registry manifests
164
+ console.log('Installing runtimes...');
165
+ await downloadRuntime('node', platformArch);
166
+ await downloadRuntime('python', platformArch);
167
+
168
+ // Create shim
169
+ console.log('\nSetting up shims...');
170
+ createShim();
171
+
172
+ // Initialize secrets
173
+ initSecrets();
174
+
175
+ console.log('\n✓ RUDI setup complete!\n');
176
+ console.log('Get started:');
177
+ console.log(' rudi search --all # See available stacks');
178
+ console.log(' rudi install slack # Install a stack');
179
+ console.log(' rudi doctor # Check system health\n');
180
+ }
181
+
182
+ // Run
183
+ setup().catch(err => {
184
+ console.error('Setup error:', err.message);
185
+ // Don't fail npm install - user can run rudi doctor later
186
+ process.exit(0);
187
+ });