@learnrudi/cli 1.9.7 → 1.9.8

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.
@@ -0,0 +1,189 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "generated": "2026-01-09T15:24:13.250Z",
4
+ "packages": {
5
+ "runtimes": [
6
+ {
7
+ "id": "bun",
8
+ "name": "Bun",
9
+ "kind": "runtime",
10
+ "installDir": "bun",
11
+ "basePath": "runtimes",
12
+ "installType": "binary",
13
+ "commands": [
14
+ {
15
+ "name": "bun",
16
+ "bin": "bun",
17
+ "args": null
18
+ },
19
+ {
20
+ "name": "bunx",
21
+ "bin": "bun",
22
+ "args": [
23
+ "x"
24
+ ]
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "id": "deno",
30
+ "name": "Deno",
31
+ "kind": "runtime",
32
+ "installDir": "deno",
33
+ "basePath": "runtimes",
34
+ "installType": "binary",
35
+ "commands": [
36
+ {
37
+ "name": "deno",
38
+ "bin": "deno",
39
+ "args": null
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ "id": "node",
45
+ "name": "Node.js",
46
+ "kind": "runtime",
47
+ "installDir": "node",
48
+ "basePath": "runtimes",
49
+ "installType": "binary",
50
+ "commands": [
51
+ {
52
+ "name": "node",
53
+ "bin": "bin/node",
54
+ "args": null
55
+ },
56
+ {
57
+ "name": "npm",
58
+ "bin": "bin/npm",
59
+ "args": null
60
+ },
61
+ {
62
+ "name": "npx",
63
+ "bin": "bin/npx",
64
+ "args": null
65
+ }
66
+ ]
67
+ },
68
+ {
69
+ "id": "python",
70
+ "name": "Python",
71
+ "kind": "runtime",
72
+ "installDir": "python",
73
+ "basePath": "runtimes",
74
+ "installType": "binary",
75
+ "commands": [
76
+ {
77
+ "name": "python",
78
+ "bin": "bin/python3",
79
+ "args": null
80
+ },
81
+ {
82
+ "name": "python3",
83
+ "bin": "bin/python3",
84
+ "args": null
85
+ },
86
+ {
87
+ "name": "pip",
88
+ "bin": "bin/python3",
89
+ "args": [
90
+ "-m",
91
+ "pip"
92
+ ]
93
+ },
94
+ {
95
+ "name": "pip3",
96
+ "bin": "bin/python3",
97
+ "args": [
98
+ "-m",
99
+ "pip"
100
+ ]
101
+ }
102
+ ]
103
+ }
104
+ ],
105
+ "agents": [
106
+ {
107
+ "id": "claude",
108
+ "name": "Claude Code",
109
+ "kind": "agent",
110
+ "installDir": "claude",
111
+ "basePath": "agents",
112
+ "installType": "npm",
113
+ "commands": [
114
+ {
115
+ "name": "claude",
116
+ "bin": "node_modules/.bin/claude",
117
+ "args": null
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ "id": "codex",
123
+ "name": "OpenAI Codex",
124
+ "kind": "agent",
125
+ "installDir": "codex",
126
+ "basePath": "agents",
127
+ "installType": "npm",
128
+ "commands": [
129
+ {
130
+ "name": "codex",
131
+ "bin": "node_modules/.bin/codex",
132
+ "args": null
133
+ }
134
+ ]
135
+ },
136
+ {
137
+ "id": "copilot",
138
+ "name": "GitHub Copilot",
139
+ "kind": "agent",
140
+ "installDir": "copilot",
141
+ "basePath": "agents",
142
+ "installType": "npm",
143
+ "commands": [
144
+ {
145
+ "name": "copilot",
146
+ "bin": "node_modules/.bin/github-copilot-cli",
147
+ "args": null
148
+ },
149
+ {
150
+ "name": "github-copilot-cli",
151
+ "bin": "node_modules/.bin/github-copilot-cli",
152
+ "args": null
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ "id": "gemini",
158
+ "name": "Gemini CLI",
159
+ "kind": "agent",
160
+ "installDir": "gemini",
161
+ "basePath": "agents",
162
+ "installType": "npm",
163
+ "commands": [
164
+ {
165
+ "name": "gemini",
166
+ "bin": "node_modules/.bin/gemini",
167
+ "args": null
168
+ }
169
+ ]
170
+ },
171
+ {
172
+ "id": "ollama",
173
+ "name": "Ollama",
174
+ "kind": "agent",
175
+ "installDir": "ollama",
176
+ "basePath": "agents",
177
+ "installType": "binary",
178
+ "commands": [
179
+ {
180
+ "name": "ollama",
181
+ "bin": "ollama",
182
+ "args": null
183
+ }
184
+ ]
185
+ }
186
+ ],
187
+ "binaries": []
188
+ }
189
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learnrudi/cli",
3
- "version": "1.9.7",
3
+ "version": "1.9.8",
4
4
  "description": "RUDI CLI - Install and manage MCP stacks, runtimes, and AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "start": "node src/index.js",
17
- "build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 --external:@learnrudi/core --external:@learnrudi/db --external:@learnrudi/env --external:@learnrudi/manifest --external:@learnrudi/mcp --external:@learnrudi/registry-client --external:@learnrudi/runner --external:@learnrudi/secrets --external:@learnrudi/utils && cp src/router-mcp.js dist/router-mcp.js",
17
+ "prebuild": "node scripts/generate-manifest.js",
18
+ "build": "esbuild src/index.js --bundle --platform=node --format=cjs --outfile=dist/index.cjs --external:better-sqlite3 && cp src/router-mcp.js dist/router-mcp.js && cp src/packages-manifest.json dist/packages-manifest.json",
18
19
  "postinstall": "node scripts/postinstall.js",
19
20
  "prepublishOnly": "npm run build",
20
21
  "test": "node --test src/__tests__/"
@@ -28,7 +29,8 @@
28
29
  "@learnrudi/registry-client": "^1.0.0",
29
30
  "@learnrudi/runner": "^1.0.0",
30
31
  "@learnrudi/secrets": "^1.0.0",
31
- "@learnrudi/utils": "^1.0.0"
32
+ "@learnrudi/utils": "^1.0.0",
33
+ "better-sqlite3": "^12.5.0"
32
34
  },
33
35
  "devDependencies": {
34
36
  "esbuild": "^0.27.2"
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate packages-manifest.json from registry catalog
4
+ *
5
+ * This script reads all package definitions from the registry catalog
6
+ * and generates a unified manifest for shim generation.
7
+ *
8
+ * Run at build time: node scripts/generate-manifest.js
9
+ * Output: src/packages-manifest.json (bundled with CLI)
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ // Registry catalog paths - relative to CLI or absolute
19
+ const REGISTRY_PATHS = [
20
+ path.resolve(__dirname, '../../registry/catalog'), // Monorepo structure
21
+ path.resolve(__dirname, '../../../registry/catalog'), // Alternative layout
22
+ ];
23
+
24
+ function findRegistryPath() {
25
+ for (const p of REGISTRY_PATHS) {
26
+ if (fs.existsSync(p)) {
27
+ return p;
28
+ }
29
+ }
30
+ throw new Error(`Registry catalog not found. Tried: ${REGISTRY_PATHS.join(', ')}`);
31
+ }
32
+
33
+ /**
34
+ * Read all JSON files from a directory
35
+ */
36
+ function readCatalogDir(dirPath) {
37
+ if (!fs.existsSync(dirPath)) {
38
+ return [];
39
+ }
40
+
41
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
42
+ return files.map(f => {
43
+ const content = fs.readFileSync(path.join(dirPath, f), 'utf-8');
44
+ return JSON.parse(content);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Extract commands from a package definition
50
+ * Handles multiple formats:
51
+ * - commands: [{ name, bin, args? }] - new explicit format
52
+ * - binary: "name" - single binary
53
+ * - binaries: ["a", "b"] - multiple binaries with same name
54
+ */
55
+ function extractCommands(pkg, kind) {
56
+ // New format: explicit commands array
57
+ if (pkg.commands && Array.isArray(pkg.commands)) {
58
+ return pkg.commands.map(cmd => ({
59
+ name: cmd.name,
60
+ bin: cmd.bin,
61
+ args: cmd.args || null,
62
+ }));
63
+ }
64
+
65
+ // Legacy format: binaries array (ffmpeg has ["ffmpeg", "ffprobe"])
66
+ if (pkg.binaries && Array.isArray(pkg.binaries)) {
67
+ return pkg.binaries.map(name => ({
68
+ name,
69
+ bin: name,
70
+ args: null,
71
+ }));
72
+ }
73
+
74
+ // Legacy format: single binary field
75
+ if (pkg.binary) {
76
+ const id = pkg.id.replace(/^(runtime|binary|agent):/, '');
77
+ return [{
78
+ name: id,
79
+ bin: pkg.binary,
80
+ args: null,
81
+ }];
82
+ }
83
+
84
+ // Fallback: use id as command name
85
+ const id = pkg.id.replace(/^(runtime|binary|agent):/, '');
86
+ return [{
87
+ name: id,
88
+ bin: id,
89
+ args: null,
90
+ }];
91
+ }
92
+
93
+ /**
94
+ * Get install directory for a package
95
+ */
96
+ function getInstallDir(pkg, kind) {
97
+ if (pkg.installDir) {
98
+ return pkg.installDir;
99
+ }
100
+ const id = pkg.id.replace(/^(runtime|binary|agent):/, '');
101
+ return id;
102
+ }
103
+
104
+ /**
105
+ * Get the base path where this kind of package is installed
106
+ */
107
+ function getKindBasePath(kind) {
108
+ switch (kind) {
109
+ case 'runtime': return 'runtimes';
110
+ case 'agent': return 'agents';
111
+ case 'binary': return 'binaries';
112
+ default: return kind + 's';
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Process packages from a catalog directory
118
+ */
119
+ function processPackages(catalogPath, kind) {
120
+ const dirPath = path.join(catalogPath, kind + 's'); // runtimes, agents, binaries
121
+ const packages = readCatalogDir(dirPath);
122
+
123
+ return packages.map(pkg => {
124
+ const id = pkg.id.replace(/^(runtime|binary|agent):/, '');
125
+ const installDir = getInstallDir(pkg, kind);
126
+ const basePath = getKindBasePath(kind);
127
+
128
+ return {
129
+ id,
130
+ name: pkg.name,
131
+ kind,
132
+ installDir,
133
+ basePath,
134
+ installType: pkg.installType || 'binary',
135
+ commands: extractCommands(pkg, kind),
136
+ };
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Generate the manifest
142
+ */
143
+ function generateManifest() {
144
+ const catalogPath = findRegistryPath();
145
+ console.log(`Reading catalog from: ${catalogPath}`);
146
+
147
+ const manifest = {
148
+ version: '1.0.0',
149
+ generated: new Date().toISOString(),
150
+ packages: {
151
+ runtimes: processPackages(catalogPath, 'runtime'),
152
+ agents: processPackages(catalogPath, 'agent'),
153
+ binaries: processPackages(catalogPath, 'binary'),
154
+ },
155
+ };
156
+
157
+ // Summary
158
+ const counts = {
159
+ runtimes: manifest.packages.runtimes.length,
160
+ agents: manifest.packages.agents.length,
161
+ binaries: manifest.packages.binaries.length,
162
+ };
163
+ console.log(`Found: ${counts.runtimes} runtimes, ${counts.agents} agents, ${counts.binaries} binaries`);
164
+
165
+ // Count total commands
166
+ let totalCommands = 0;
167
+ for (const kind of Object.values(manifest.packages)) {
168
+ for (const pkg of kind) {
169
+ totalCommands += pkg.commands.length;
170
+ }
171
+ }
172
+ console.log(`Total commands: ${totalCommands}`);
173
+
174
+ return manifest;
175
+ }
176
+
177
+ /**
178
+ * Main
179
+ */
180
+ function main() {
181
+ try {
182
+ const manifest = generateManifest();
183
+
184
+ // Write to src directory (will be bundled with CLI)
185
+ const outputPath = path.resolve(__dirname, '../src/packages-manifest.json');
186
+ fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
187
+ console.log(`\nWrote manifest to: ${outputPath}`);
188
+
189
+ } catch (err) {
190
+ console.error('Error generating manifest:', err.message);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ main();
@@ -19,6 +19,9 @@ const RUDI_JSON_PATH = path.join(RUDI_HOME, 'rudi.json');
19
19
  const RUDI_JSON_TMP = path.join(RUDI_HOME, 'rudi.json.tmp');
20
20
  const REGISTRY_BASE = 'https://raw.githubusercontent.com/learn-rudi/registry/main';
21
21
 
22
+ // Use 'bins' (consistent with @learnrudi/env) not 'shims'
23
+ const BINS_DIR = path.join(RUDI_HOME, 'bins');
24
+
22
25
  // =============================================================================
23
26
  // RUDI.JSON CONFIG MANAGEMENT
24
27
  // =============================================================================
@@ -162,8 +165,10 @@ function ensureDirectories() {
162
165
  path.join(RUDI_HOME, 'runtimes'),
163
166
  path.join(RUDI_HOME, 'stacks'),
164
167
  path.join(RUDI_HOME, 'binaries'),
165
- path.join(RUDI_HOME, 'shims'),
168
+ path.join(RUDI_HOME, 'agents'),
169
+ BINS_DIR, // Use bins/ (consistent with @learnrudi/env)
166
170
  path.join(RUDI_HOME, 'cache'),
171
+ path.join(RUDI_HOME, 'router'),
167
172
  ];
168
173
 
169
174
  for (const dir of dirs) {
@@ -251,46 +256,171 @@ async function downloadRuntime(runtimeId, platformArch) {
251
256
  }
252
257
  }
253
258
 
254
- // Create all shims
259
+ // =============================================================================
260
+ // SHIM GENERATION
261
+ // =============================================================================
262
+
263
+ /**
264
+ * Load packages manifest (generated at build time from registry catalog)
265
+ * Falls back to empty manifest if not found
266
+ */
267
+ function loadPackagesManifest() {
268
+ const possiblePaths = [
269
+ // When running from npm install (dist directory)
270
+ path.join(path.dirname(process.argv[1]), '..', 'dist', 'packages-manifest.json'),
271
+ // When running from source/dev
272
+ path.join(path.dirname(process.argv[1]), '..', 'src', 'packages-manifest.json'),
273
+ ];
274
+
275
+ for (const manifestPath of possiblePaths) {
276
+ try {
277
+ if (fs.existsSync(manifestPath)) {
278
+ const content = fs.readFileSync(manifestPath, 'utf-8');
279
+ return JSON.parse(content);
280
+ }
281
+ } catch {
282
+ // Try next path
283
+ }
284
+ }
285
+
286
+ console.log(' ⚠ packages-manifest.json not found, using minimal shims');
287
+ return { packages: { runtimes: [], agents: [], binaries: [] } };
288
+ }
289
+
290
+ /**
291
+ * Build a shim script that executes a target binary
292
+ * Shows helpful error if binary not installed
293
+ */
294
+ function buildExecShim(targetPath, notInstalledMessage) {
295
+ const target = targetPath.replace(/"/g, '\\"');
296
+ const message = notInstalledMessage.replace(/"/g, '\\"');
297
+ return `#!/bin/sh
298
+ TARGET="${target}"
299
+ if [ ! -x "$TARGET" ]; then
300
+ echo "RUDI: ${message}" 1>&2
301
+ exit 127
302
+ fi
303
+ exec "$TARGET" "$@"
304
+ `;
305
+ }
306
+
307
+ /**
308
+ * Build a shim script that runs a binary with extra args prepended
309
+ * Used for things like "pip" which runs "python3 -m pip"
310
+ */
311
+ function buildArgsShim(targetPath, args, notInstalledMessage) {
312
+ const target = targetPath.replace(/"/g, '\\"');
313
+ const argsStr = args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ');
314
+ const message = notInstalledMessage.replace(/"/g, '\\"');
315
+ return `#!/bin/sh
316
+ TARGET="${target}"
317
+ if [ ! -x "$TARGET" ]; then
318
+ echo "RUDI: ${message}" 1>&2
319
+ exit 127
320
+ fi
321
+ exec "$TARGET" ${argsStr} "$@"
322
+ `;
323
+ }
324
+
325
+ /**
326
+ * Create all shims for runtimes, agents, and binaries
327
+ * Reads from packages-manifest.json (generated from registry catalog)
328
+ */
255
329
  function createShims() {
256
- // Legacy shim for direct stack access: rudi-mcp <stack>
257
- const mcpShimPath = path.join(RUDI_HOME, 'shims', 'rudi-mcp');
258
- const mcpShimContent = `#!/bin/bash
330
+ const platform = process.platform;
331
+ const isWindows = platform === 'win32';
332
+
333
+ // Skip shim creation on Windows (uses different mechanism)
334
+ if (isWindows) {
335
+ console.log(` ⚠ Shim creation skipped on Windows`);
336
+ return;
337
+ }
338
+
339
+ const manifest = loadPackagesManifest();
340
+ const shimDefs = [];
341
+
342
+ // ---------------------------------------------------------------------------
343
+ // GENERATE SHIMS FROM MANIFEST
344
+ // ---------------------------------------------------------------------------
345
+
346
+ const allPackages = [
347
+ ...manifest.packages.runtimes,
348
+ ...manifest.packages.agents,
349
+ ...manifest.packages.binaries,
350
+ ];
351
+
352
+ for (const pkg of allPackages) {
353
+ const installPath = path.join(RUDI_HOME, pkg.basePath, pkg.installDir);
354
+ const notInstalledMsg = `${pkg.name} is not installed. Run: rudi install ${pkg.kind}:${pkg.id}`;
355
+
356
+ for (const cmd of pkg.commands) {
357
+ const binPath = path.join(installPath, cmd.bin);
358
+
359
+ if (cmd.args && cmd.args.length > 0) {
360
+ // Command with prepended args (like pip -> python3 -m pip)
361
+ shimDefs.push({
362
+ name: cmd.name,
363
+ script: buildArgsShim(binPath, cmd.args, notInstalledMsg),
364
+ });
365
+ } else {
366
+ // Simple exec shim
367
+ shimDefs.push({
368
+ name: cmd.name,
369
+ script: buildExecShim(binPath, notInstalledMsg),
370
+ });
371
+ }
372
+ }
373
+ }
374
+
375
+ // ---------------------------------------------------------------------------
376
+ // RUDI SHIMS (router, mcp) - always included
377
+ // ---------------------------------------------------------------------------
378
+
379
+ // rudi-mcp shim for direct stack access
380
+ shimDefs.push({
381
+ name: 'rudi-mcp',
382
+ script: `#!/bin/bash
259
383
  # RUDI MCP Shim - Routes agent calls to rudi mcp command
260
384
  # Usage: rudi-mcp <stack-name>
261
385
  exec rudi mcp "$@"
262
- `;
263
- fs.writeFileSync(mcpShimPath, mcpShimContent);
264
- fs.chmodSync(mcpShimPath, 0o755);
265
- console.log(` ✓ Created rudi-mcp shim`);
386
+ `
387
+ });
266
388
 
267
- // New router shim: Master MCP server that aggregates all stacks
268
- const routerShimPath = path.join(RUDI_HOME, 'shims', 'rudi-router');
269
- const routerShimContent = `#!/bin/bash
389
+ // rudi-router shim for aggregated MCP server
390
+ shimDefs.push({
391
+ name: 'rudi-router',
392
+ script: `#!/bin/bash
270
393
  # RUDI Router - Master MCP server for all installed stacks
271
394
  # Reads ~/.rudi/rudi.json and proxies tool calls to correct stack
272
- # Usage: Point agent config to this shim (no args needed)
273
-
274
395
  RUDI_HOME="$HOME/.rudi"
275
-
276
- # Use bundled Node if available
277
396
  if [ -x "$RUDI_HOME/runtimes/node/bin/node" ]; then
278
397
  exec "$RUDI_HOME/runtimes/node/bin/node" "$RUDI_HOME/router/router-mcp.js" "$@"
279
398
  else
280
399
  exec node "$RUDI_HOME/router/router-mcp.js" "$@"
281
400
  fi
282
- `;
283
- fs.writeFileSync(routerShimPath, routerShimContent);
284
- fs.chmodSync(routerShimPath, 0o755);
285
- console.log(` ✓ Created rudi-router shim`);
286
-
287
- // Create router directory for the router-mcp.js file
288
- const routerDir = path.join(RUDI_HOME, 'router');
289
- if (!fs.existsSync(routerDir)) {
290
- fs.mkdirSync(routerDir, { recursive: true });
401
+ `
402
+ });
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // WRITE ALL SHIMS
406
+ // ---------------------------------------------------------------------------
407
+
408
+ let created = 0;
409
+ for (const { name, script } of shimDefs) {
410
+ const shimPath = path.join(BINS_DIR, name);
411
+ fs.writeFileSync(shimPath, script, { encoding: 'utf-8' });
412
+ fs.chmodSync(shimPath, 0o755);
413
+ created++;
291
414
  }
292
415
 
416
+ console.log(` ✓ Created ${created} shims in ~/.rudi/bins/`);
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // ROUTER SETUP
420
+ // ---------------------------------------------------------------------------
421
+
293
422
  // Create package.json for ES module support
423
+ const routerDir = path.join(RUDI_HOME, 'router');
294
424
  const routerPackageJson = path.join(routerDir, 'package.json');
295
425
  fs.writeFileSync(routerPackageJson, JSON.stringify({
296
426
  name: 'rudi-router',