@learnrudi/cli 1.6.0 → 1.8.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/dist/index.cjs CHANGED
@@ -186,7 +186,6 @@ __export(src_exports, {
186
186
  PACKAGE_KINDS: () => PACKAGE_KINDS2,
187
187
  RUNTIMES_DOWNLOAD_BASE: () => RUNTIMES_DOWNLOAD_BASE,
188
188
  RUNTIMES_RELEASE_VERSION: () => RUNTIMES_RELEASE_VERSION,
189
- STACKS_RELEASE_VERSION: () => STACKS_RELEASE_VERSION,
190
189
  checkCache: () => checkCache,
191
190
  clearCache: () => clearCache,
192
191
  computeHash: () => computeHash,
@@ -383,7 +382,7 @@ async function downloadPackage(pkg, destPath, options = {}) {
383
382
  }
384
383
  onProgress?.({ phase: "downloading", package: pkg.name || pkg.id });
385
384
  if (pkg.kind === "stack" || registryPath.includes("/stacks/")) {
386
- await downloadStackTarball(pkg, destPath, onProgress);
385
+ await downloadStackFromGitHub(registryPath, destPath, onProgress);
387
386
  return { success: true, path: destPath };
388
387
  }
389
388
  if (registryPath.endsWith(".md")) {
@@ -404,45 +403,129 @@ async function downloadPackage(pkg, destPath, options = {}) {
404
403
  }
405
404
  throw new Error(`Unsupported package type: ${registryPath}`);
406
405
  }
407
- async function downloadStackTarball(pkg, destPath, onProgress) {
408
- const stackName = pkg.id?.replace("stack:", "") || import_path2.default.basename(pkg.path);
409
- const version = pkg.version || "1.0.0";
410
- const tarballName = `${stackName}-${version}.tar.gz`;
411
- const url = `${STACKS_DOWNLOAD_BASE}/${STACKS_RELEASE_VERSION}/${tarballName}`;
412
- onProgress?.({ phase: "downloading", package: stackName, url });
413
- const tempDir = import_path2.default.join(PATHS.cache, "downloads");
414
- if (!import_fs2.default.existsSync(tempDir)) {
415
- import_fs2.default.mkdirSync(tempDir, { recursive: true });
406
+ async function downloadStackFromGitHub(registryPath, destPath, onProgress) {
407
+ const baseUrl = `${GITHUB_RAW_BASE}/${registryPath}`;
408
+ const apiUrl = `https://api.github.com/repos/learn-rudi/registry/contents/${registryPath}`;
409
+ const listResponse = await fetch(apiUrl, {
410
+ headers: {
411
+ "User-Agent": "rudi-cli/2.0",
412
+ "Accept": "application/vnd.github.v3+json"
413
+ }
414
+ });
415
+ if (!listResponse.ok) {
416
+ throw new Error(`Stack not found: ${registryPath}`);
417
+ }
418
+ const contents = await listResponse.json();
419
+ if (!Array.isArray(contents)) {
420
+ throw new Error(`Invalid stack directory: ${registryPath}`);
421
+ }
422
+ const existingItems = /* @__PURE__ */ new Map();
423
+ for (const item of contents) {
424
+ existingItems.set(item.name, item);
425
+ }
426
+ const manifestItem = existingItems.get("manifest.json");
427
+ if (!manifestItem) {
428
+ throw new Error(`Stack missing manifest.json: ${registryPath}`);
416
429
  }
417
- const tempFile = import_path2.default.join(tempDir, tarballName);
430
+ const manifestResponse = await fetch(manifestItem.download_url, {
431
+ headers: { "User-Agent": "rudi-cli/2.0" }
432
+ });
433
+ const manifest = await manifestResponse.json();
434
+ import_fs2.default.writeFileSync(import_path2.default.join(destPath, "manifest.json"), JSON.stringify(manifest, null, 2));
435
+ onProgress?.({ phase: "downloading", file: "manifest.json" });
436
+ const pkgJsonItem = existingItems.get("package.json");
437
+ if (pkgJsonItem) {
438
+ const pkgJsonResponse = await fetch(pkgJsonItem.download_url, {
439
+ headers: { "User-Agent": "rudi-cli/2.0" }
440
+ });
441
+ if (pkgJsonResponse.ok) {
442
+ const pkgJson = await pkgJsonResponse.text();
443
+ import_fs2.default.writeFileSync(import_path2.default.join(destPath, "package.json"), pkgJson);
444
+ onProgress?.({ phase: "downloading", file: "package.json" });
445
+ }
446
+ }
447
+ const envExampleItem = existingItems.get(".env.example");
448
+ if (envExampleItem) {
449
+ const envResponse = await fetch(envExampleItem.download_url, {
450
+ headers: { "User-Agent": "rudi-cli/2.0" }
451
+ });
452
+ if (envResponse.ok) {
453
+ const envContent = await envResponse.text();
454
+ import_fs2.default.writeFileSync(import_path2.default.join(destPath, ".env.example"), envContent);
455
+ }
456
+ }
457
+ const tsconfigItem = existingItems.get("tsconfig.json");
458
+ if (tsconfigItem) {
459
+ const tsconfigResponse = await fetch(tsconfigItem.download_url, {
460
+ headers: { "User-Agent": "rudi-cli/2.0" }
461
+ });
462
+ if (tsconfigResponse.ok) {
463
+ const tsconfig = await tsconfigResponse.text();
464
+ import_fs2.default.writeFileSync(import_path2.default.join(destPath, "tsconfig.json"), tsconfig);
465
+ }
466
+ }
467
+ const requirementsItem = existingItems.get("requirements.txt");
468
+ if (requirementsItem) {
469
+ const reqResponse = await fetch(requirementsItem.download_url, {
470
+ headers: { "User-Agent": "rudi-cli/2.0" }
471
+ });
472
+ if (reqResponse.ok) {
473
+ const requirements = await reqResponse.text();
474
+ import_fs2.default.writeFileSync(import_path2.default.join(destPath, "requirements.txt"), requirements);
475
+ }
476
+ }
477
+ const sourceDirs = ["src", "dist", "node", "python", "lib"];
478
+ for (const dirName of sourceDirs) {
479
+ const dirItem = existingItems.get(dirName);
480
+ if (dirItem && dirItem.type === "dir") {
481
+ onProgress?.({ phase: "downloading", directory: dirName });
482
+ await downloadDirectoryFromGitHub(
483
+ `${baseUrl}/${dirName}`,
484
+ import_path2.default.join(destPath, dirName),
485
+ onProgress
486
+ );
487
+ }
488
+ }
489
+ }
490
+ async function downloadDirectoryFromGitHub(dirUrl, destDir, onProgress) {
491
+ const apiUrl = dirUrl.replace("https://raw.githubusercontent.com/", "https://api.github.com/repos/").replace("/main/", "/contents/");
418
492
  try {
419
- const response = await fetch(url, {
493
+ const response = await fetch(apiUrl, {
420
494
  headers: {
421
495
  "User-Agent": "rudi-cli/2.0",
422
- "Accept": "application/octet-stream"
496
+ "Accept": "application/vnd.github.v3+json"
423
497
  }
424
498
  });
425
499
  if (!response.ok) {
426
- throw new Error(`Failed to download ${stackName}: HTTP ${response.status}`);
500
+ return;
427
501
  }
428
- const buffer = await response.arrayBuffer();
429
- import_fs2.default.writeFileSync(tempFile, Buffer.from(buffer));
430
- onProgress?.({ phase: "extracting", package: stackName });
431
- if (import_fs2.default.existsSync(destPath)) {
432
- import_fs2.default.rmSync(destPath, { recursive: true });
502
+ const contents = await response.json();
503
+ if (!Array.isArray(contents)) {
504
+ return;
433
505
  }
434
- import_fs2.default.mkdirSync(destPath, { recursive: true });
435
- const { execSync: execSync7 } = await import("child_process");
436
- execSync7(`tar -xzf "${tempFile}" -C "${destPath}"`, {
437
- stdio: "pipe"
438
- });
439
- import_fs2.default.unlinkSync(tempFile);
440
- onProgress?.({ phase: "complete", package: stackName, path: destPath });
441
- } catch (error) {
442
- if (import_fs2.default.existsSync(tempFile)) {
443
- import_fs2.default.unlinkSync(tempFile);
506
+ if (!import_fs2.default.existsSync(destDir)) {
507
+ import_fs2.default.mkdirSync(destDir, { recursive: true });
444
508
  }
445
- throw new Error(`Failed to install ${stackName}: ${error.message}`);
509
+ for (const item of contents) {
510
+ if (item.type === "file") {
511
+ const fileResponse = await fetch(item.download_url, {
512
+ headers: { "User-Agent": "rudi-cli/2.0" }
513
+ });
514
+ if (fileResponse.ok) {
515
+ const content = await fileResponse.text();
516
+ import_fs2.default.writeFileSync(import_path2.default.join(destDir, item.name), content);
517
+ onProgress?.({ phase: "downloading", file: item.name });
518
+ }
519
+ } else if (item.type === "dir") {
520
+ await downloadDirectoryFromGitHub(
521
+ item.url.replace("https://api.github.com/repos/", "https://raw.githubusercontent.com/").replace("/contents/", "/main/"),
522
+ import_path2.default.join(destDir, item.name),
523
+ onProgress
524
+ );
525
+ }
526
+ }
527
+ } catch (error) {
528
+ console.error(`Warning: Could not download ${dirUrl}: ${error.message}`);
446
529
  }
447
530
  }
448
531
  async function downloadRuntime(runtime, version, destPath, options = {}) {
@@ -720,7 +803,7 @@ async function computeHash(filePath) {
720
803
  stream.on("error", reject);
721
804
  });
722
805
  }
723
- var import_fs2, import_path2, import_crypto, DEFAULT_REGISTRY_URL, RUNTIMES_DOWNLOAD_BASE, CACHE_TTL, PACKAGE_KINDS2, KIND_PLURALS, GITHUB_RAW_BASE, STACKS_DOWNLOAD_BASE, STACKS_RELEASE_VERSION, RUNTIMES_RELEASE_VERSION;
806
+ var import_fs2, import_path2, import_crypto, DEFAULT_REGISTRY_URL, RUNTIMES_DOWNLOAD_BASE, CACHE_TTL, PACKAGE_KINDS2, KIND_PLURALS, GITHUB_RAW_BASE, RUNTIMES_RELEASE_VERSION;
724
807
  var init_src2 = __esm({
725
808
  "../packages/registry-client/src/index.js"() {
726
809
  import_fs2 = __toESM(require("fs"), 1);
@@ -735,8 +818,6 @@ var init_src2 = __esm({
735
818
  binary: "binaries"
736
819
  };
737
820
  GITHUB_RAW_BASE = "https://raw.githubusercontent.com/learn-rudi/registry/main";
738
- STACKS_DOWNLOAD_BASE = "https://github.com/learn-rudi/registry/releases/download";
739
- STACKS_RELEASE_VERSION = "stacks-v1.0.0";
740
821
  RUNTIMES_RELEASE_VERSION = "v1.0.0";
741
822
  }
742
823
  });
@@ -16303,8 +16384,25 @@ async function loadManifest(installPath) {
16303
16384
  return null;
16304
16385
  }
16305
16386
  }
16387
+ function getBundledBinary(runtime, binary) {
16388
+ const platform = process.platform;
16389
+ const rudiHome = process.env.RUDI_HOME || path8.join(process.env.HOME || process.env.USERPROFILE, ".rudi");
16390
+ if (runtime === "node") {
16391
+ const npmPath = platform === "win32" ? path8.join(rudiHome, "runtimes", "node", "npm.cmd") : path8.join(rudiHome, "runtimes", "node", "bin", "npm");
16392
+ if (require("fs").existsSync(npmPath)) {
16393
+ return npmPath;
16394
+ }
16395
+ }
16396
+ if (runtime === "python") {
16397
+ const pipPath = platform === "win32" ? path8.join(rudiHome, "runtimes", "python", "Scripts", "pip.exe") : path8.join(rudiHome, "runtimes", "python", "bin", "pip3");
16398
+ if (require("fs").existsSync(pipPath)) {
16399
+ return pipPath;
16400
+ }
16401
+ }
16402
+ return binary;
16403
+ }
16306
16404
  async function installDependencies(stackPath, manifest) {
16307
- const runtime = manifest?.runtime || "node";
16405
+ const runtime = manifest?.runtime || manifest?.mcp?.runtime || "node";
16308
16406
  try {
16309
16407
  if (runtime === "node") {
16310
16408
  const packageJsonPath = path8.join(stackPath, "package.json");
@@ -16319,11 +16417,11 @@ async function installDependencies(stackPath, manifest) {
16319
16417
  return { installed: false, reason: "Dependencies already installed" };
16320
16418
  } catch {
16321
16419
  }
16420
+ const npmCmd = getBundledBinary("node", "npm");
16322
16421
  console.log(` Installing npm dependencies...`);
16323
- (0, import_child_process2.execSync)("npm install --production", {
16422
+ (0, import_child_process2.execSync)(`"${npmCmd}" install --production`, {
16324
16423
  cwd: stackPath,
16325
16424
  stdio: "pipe"
16326
- // Suppress output
16327
16425
  });
16328
16426
  return { installed: true };
16329
16427
  } else if (runtime === "python") {
@@ -16333,8 +16431,9 @@ async function installDependencies(stackPath, manifest) {
16333
16431
  } catch {
16334
16432
  return { installed: false, reason: "No requirements.txt" };
16335
16433
  }
16434
+ const pipCmd = getBundledBinary("python", "pip");
16336
16435
  console.log(` Installing pip dependencies...`);
16337
- (0, import_child_process2.execSync)("pip install -r requirements.txt", {
16436
+ (0, import_child_process2.execSync)(`"${pipCmd}" install -r requirements.txt`, {
16338
16437
  cwd: stackPath,
16339
16438
  stdio: "pipe"
16340
16439
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learnrudi/cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.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
+ });