@elizaos/plugin-agent-skills 1.0.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/README.md +372 -0
- package/dist/index.js +3698 -0
- package/package.json +83 -0
- package/src/__tests__/clawhub.test.ts +722 -0
- package/src/__tests__/integration.test.ts +465 -0
- package/src/__tests__/parser.test.ts +304 -0
- package/src/__tests__/skill-eligibility.test.ts +575 -0
- package/src/__tests__/skill-precedence.test.ts +592 -0
- package/src/__tests__/storage.test.ts +549 -0
- package/src/actions/get-skill-details.ts +127 -0
- package/src/actions/get-skill-guidance.ts +388 -0
- package/src/actions/run-skill-script.ts +200 -0
- package/src/actions/search-skills.ts +106 -0
- package/src/actions/sync-catalog.ts +88 -0
- package/src/index.ts +124 -0
- package/src/parser.ts +478 -0
- package/src/plugin.ts +118 -0
- package/src/providers/skills.ts +443 -0
- package/src/services/install.ts +628 -0
- package/src/services/skills.ts +2363 -0
- package/src/storage.ts +544 -0
- package/src/types.ts +582 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Dependency Installation Service
|
|
3
|
+
*
|
|
4
|
+
* Handles installation of skill dependencies using various package managers:
|
|
5
|
+
* - brew (Homebrew, macOS)
|
|
6
|
+
* - apt (apt-get, Debian/Ubuntu Linux)
|
|
7
|
+
* - node (npm/pnpm/bun)
|
|
8
|
+
* - pip (Python pip/pip3)
|
|
9
|
+
* - cargo (Rust cargo)
|
|
10
|
+
*
|
|
11
|
+
* @module services/install
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IAgentRuntime } from "@elizaos/core";
|
|
15
|
+
import type {
|
|
16
|
+
OttoInstallOption,
|
|
17
|
+
InstallDependencyOptions,
|
|
18
|
+
InstallDependencyResult,
|
|
19
|
+
InstallProgressCallback,
|
|
20
|
+
SkillEligibility,
|
|
21
|
+
} from "../types";
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
/** Default timeout for installation commands (5 minutes) */
|
|
28
|
+
const DEFAULT_TIMEOUT = 300_000;
|
|
29
|
+
|
|
30
|
+
/** Node package managers in preference order */
|
|
31
|
+
const NODE_MANAGERS = ["bun", "pnpm", "npm", "yarn"] as const;
|
|
32
|
+
|
|
33
|
+
// ============================================================
|
|
34
|
+
// PLATFORM UTILITIES
|
|
35
|
+
// ============================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect the current operating system.
|
|
39
|
+
*/
|
|
40
|
+
function detectPlatform(): "darwin" | "linux" | "windows" | "unknown" {
|
|
41
|
+
if (typeof process === "undefined") return "unknown";
|
|
42
|
+
const platform = process.platform;
|
|
43
|
+
if (platform === "darwin") return "darwin";
|
|
44
|
+
if (platform === "linux") return "linux";
|
|
45
|
+
if (platform === "win32") return "windows";
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if a binary exists in PATH using which/where.
|
|
51
|
+
*/
|
|
52
|
+
async function binaryExists(name: string): Promise<boolean> {
|
|
53
|
+
try {
|
|
54
|
+
const { execSync } = await import("node:child_process");
|
|
55
|
+
const platform = detectPlatform();
|
|
56
|
+
const command = platform === "windows" ? `where ${name}` : `which ${name}`;
|
|
57
|
+
execSync(command, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// PACKAGE MANAGER DETECTION
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the preferred Node.js package manager.
|
|
70
|
+
*
|
|
71
|
+
* Order of preference:
|
|
72
|
+
* 1. OTTO_NODE_MANAGER env var if set
|
|
73
|
+
* 2. bun (fastest)
|
|
74
|
+
* 3. pnpm (efficient)
|
|
75
|
+
* 4. npm (universal fallback)
|
|
76
|
+
* 5. yarn
|
|
77
|
+
*/
|
|
78
|
+
export async function getPreferredNodeManager(): Promise<string | null> {
|
|
79
|
+
// Check for explicit preference
|
|
80
|
+
const preferred = process.env.OTTO_NODE_MANAGER;
|
|
81
|
+
if (preferred && (await binaryExists(preferred))) {
|
|
82
|
+
return preferred;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check in preference order
|
|
86
|
+
for (const manager of NODE_MANAGERS) {
|
|
87
|
+
if (await binaryExists(manager)) {
|
|
88
|
+
return manager;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if Homebrew is available (macOS).
|
|
97
|
+
*/
|
|
98
|
+
export async function isHomebrewAvailable(): Promise<boolean> {
|
|
99
|
+
return detectPlatform() === "darwin" && (await binaryExists("brew"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if apt-get is available (Debian/Ubuntu).
|
|
104
|
+
*/
|
|
105
|
+
export async function isAptAvailable(): Promise<boolean> {
|
|
106
|
+
return detectPlatform() === "linux" && (await binaryExists("apt-get"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if pip is available.
|
|
111
|
+
*/
|
|
112
|
+
export async function isPipAvailable(): Promise<boolean> {
|
|
113
|
+
return (await binaryExists("pip3")) || (await binaryExists("pip"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if cargo is available.
|
|
118
|
+
*/
|
|
119
|
+
export async function isCargoAvailable(): Promise<boolean> {
|
|
120
|
+
return binaryExists("cargo");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================
|
|
124
|
+
// COMMAND BUILDERS
|
|
125
|
+
// ============================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build the installation command for a given option.
|
|
129
|
+
*/
|
|
130
|
+
function buildInstallCommand(option: OttoInstallOption): string | null {
|
|
131
|
+
switch (option.kind) {
|
|
132
|
+
case "brew":
|
|
133
|
+
return `brew install ${option.formula || option.package}`;
|
|
134
|
+
|
|
135
|
+
case "apt":
|
|
136
|
+
return `sudo apt-get install -y ${option.package}`;
|
|
137
|
+
|
|
138
|
+
case "node": {
|
|
139
|
+
// Will be resolved at runtime to user's preferred manager
|
|
140
|
+
const pkg = option.package;
|
|
141
|
+
return `__NODE_MANAGER__ install -g ${pkg}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "pip":
|
|
145
|
+
return `pip3 install ${option.package}`;
|
|
146
|
+
|
|
147
|
+
case "cargo":
|
|
148
|
+
return `cargo install ${option.package}`;
|
|
149
|
+
|
|
150
|
+
case "manual":
|
|
151
|
+
// Manual installation - return instructions
|
|
152
|
+
return null;
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the __NODE_MANAGER__ placeholder in commands.
|
|
161
|
+
*/
|
|
162
|
+
async function resolveNodeManager(command: string): Promise<string> {
|
|
163
|
+
if (!command.includes("__NODE_MANAGER__")) {
|
|
164
|
+
return command;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const manager = await getPreferredNodeManager();
|
|
168
|
+
if (!manager) {
|
|
169
|
+
throw new Error("No Node.js package manager found (tried bun, pnpm, npm, yarn)");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return command.replace("__NODE_MANAGER__", manager);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================
|
|
176
|
+
// INSTALLATION EXECUTION
|
|
177
|
+
// ============================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Execute an installation command.
|
|
181
|
+
*/
|
|
182
|
+
async function executeInstall(
|
|
183
|
+
command: string,
|
|
184
|
+
options: {
|
|
185
|
+
timeout?: number;
|
|
186
|
+
onProgress?: InstallProgressCallback;
|
|
187
|
+
dryRun?: boolean;
|
|
188
|
+
} = {},
|
|
189
|
+
): Promise<{ success: boolean; error?: string; duration: number }> {
|
|
190
|
+
const { timeout = DEFAULT_TIMEOUT, onProgress, dryRun } = options;
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
|
|
193
|
+
if (dryRun) {
|
|
194
|
+
onProgress?.({
|
|
195
|
+
phase: "complete",
|
|
196
|
+
progress: 100,
|
|
197
|
+
message: `[DRY RUN] Would execute: ${command}`,
|
|
198
|
+
});
|
|
199
|
+
return { success: true, duration: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const { spawn } = await import("node:child_process");
|
|
204
|
+
|
|
205
|
+
onProgress?.({
|
|
206
|
+
phase: "installing",
|
|
207
|
+
progress: 10,
|
|
208
|
+
message: `Executing: ${command}`,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return await new Promise((resolve) => {
|
|
212
|
+
const child = spawn("sh", ["-c", command], {
|
|
213
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
214
|
+
timeout,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
let stdout = "";
|
|
218
|
+
let stderr = "";
|
|
219
|
+
|
|
220
|
+
child.stdout?.on("data", (data) => {
|
|
221
|
+
stdout += data.toString();
|
|
222
|
+
onProgress?.({
|
|
223
|
+
phase: "installing",
|
|
224
|
+
progress: 50,
|
|
225
|
+
message: data.toString().trim().slice(0, 200),
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
child.stderr?.on("data", (data) => {
|
|
230
|
+
stderr += data.toString();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
child.on("close", (code) => {
|
|
234
|
+
const duration = Date.now() - startTime;
|
|
235
|
+
|
|
236
|
+
if (code === 0) {
|
|
237
|
+
onProgress?.({
|
|
238
|
+
phase: "complete",
|
|
239
|
+
progress: 100,
|
|
240
|
+
message: "Installation completed successfully",
|
|
241
|
+
});
|
|
242
|
+
resolve({ success: true, duration });
|
|
243
|
+
} else {
|
|
244
|
+
const error = stderr || stdout || `Process exited with code ${code}`;
|
|
245
|
+
onProgress?.({
|
|
246
|
+
phase: "error",
|
|
247
|
+
message: "Installation failed",
|
|
248
|
+
error,
|
|
249
|
+
});
|
|
250
|
+
resolve({ success: false, error, duration });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
child.on("error", (err) => {
|
|
255
|
+
const duration = Date.now() - startTime;
|
|
256
|
+
const error = err.message;
|
|
257
|
+
onProgress?.({
|
|
258
|
+
phase: "error",
|
|
259
|
+
message: "Installation failed",
|
|
260
|
+
error,
|
|
261
|
+
});
|
|
262
|
+
resolve({ success: false, error, duration });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const duration = Date.now() - startTime;
|
|
267
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
268
|
+
onProgress?.({
|
|
269
|
+
phase: "error",
|
|
270
|
+
message: "Installation failed",
|
|
271
|
+
error: errorMessage,
|
|
272
|
+
});
|
|
273
|
+
return { success: false, error: errorMessage, duration };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================================
|
|
278
|
+
// MAIN INSTALLATION FUNCTION
|
|
279
|
+
// ============================================================
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Install a skill dependency using the specified option.
|
|
283
|
+
*
|
|
284
|
+
* @param options - Installation options including the install option to use
|
|
285
|
+
* @returns Installation result
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* const result = await installSkillDependency({
|
|
290
|
+
* option: { id: "brew", kind: "brew", formula: "jq" },
|
|
291
|
+
* onProgress: (event) => console.log(event.message),
|
|
292
|
+
* });
|
|
293
|
+
* ```
|
|
294
|
+
*/
|
|
295
|
+
export async function installSkillDependency(
|
|
296
|
+
options: InstallDependencyOptions,
|
|
297
|
+
): Promise<InstallDependencyResult> {
|
|
298
|
+
const { option, onProgress, dryRun, timeout } = options;
|
|
299
|
+
|
|
300
|
+
onProgress?.({
|
|
301
|
+
phase: "installing",
|
|
302
|
+
progress: 0,
|
|
303
|
+
message: `Preparing to install via ${option.kind}...`,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Check if the installation kind is available
|
|
307
|
+
const platform = detectPlatform();
|
|
308
|
+
|
|
309
|
+
switch (option.kind) {
|
|
310
|
+
case "brew":
|
|
311
|
+
if (!(await isHomebrewAvailable())) {
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
option,
|
|
315
|
+
error: "Homebrew is not available (macOS only)",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
|
|
320
|
+
case "apt":
|
|
321
|
+
if (!(await isAptAvailable())) {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
option,
|
|
325
|
+
error: "apt-get is not available (Debian/Ubuntu only)",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case "node":
|
|
331
|
+
if (!(await getPreferredNodeManager())) {
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
option,
|
|
335
|
+
error: "No Node.js package manager found",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case "pip":
|
|
341
|
+
if (!(await isPipAvailable())) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
option,
|
|
345
|
+
error: "pip/pip3 is not available",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
|
|
350
|
+
case "cargo":
|
|
351
|
+
if (!(await isCargoAvailable())) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
option,
|
|
355
|
+
error: "cargo is not available (Rust)",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case "manual":
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
option,
|
|
364
|
+
error: `Manual installation required: ${option.label || "See skill documentation"}`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Build and execute the command
|
|
369
|
+
let command = buildInstallCommand(option);
|
|
370
|
+
if (!command) {
|
|
371
|
+
return {
|
|
372
|
+
success: false,
|
|
373
|
+
option,
|
|
374
|
+
error: `Cannot build command for install kind: ${option.kind}`,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Resolve node manager placeholder
|
|
379
|
+
try {
|
|
380
|
+
command = await resolveNodeManager(command);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
success: false,
|
|
384
|
+
option,
|
|
385
|
+
error: error instanceof Error ? error.message : "Failed to resolve node manager",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const result = await executeInstall(command, {
|
|
390
|
+
timeout,
|
|
391
|
+
onProgress,
|
|
392
|
+
dryRun,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
...result,
|
|
397
|
+
option,
|
|
398
|
+
command,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Find the best available install option for the current platform.
|
|
404
|
+
*
|
|
405
|
+
* @param options - Available install options
|
|
406
|
+
* @returns Best option for current platform, or null if none available
|
|
407
|
+
*/
|
|
408
|
+
export async function findBestInstallOption(
|
|
409
|
+
options: OttoInstallOption[],
|
|
410
|
+
): Promise<OttoInstallOption | null> {
|
|
411
|
+
const platform = detectPlatform();
|
|
412
|
+
|
|
413
|
+
// Platform preference order
|
|
414
|
+
const preferenceOrder: Array<OttoInstallOption["kind"]> = [];
|
|
415
|
+
|
|
416
|
+
if (platform === "darwin") {
|
|
417
|
+
preferenceOrder.push("brew", "node", "pip", "cargo");
|
|
418
|
+
} else if (platform === "linux") {
|
|
419
|
+
preferenceOrder.push("apt", "node", "pip", "cargo");
|
|
420
|
+
} else {
|
|
421
|
+
preferenceOrder.push("node", "pip", "cargo");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (const kind of preferenceOrder) {
|
|
425
|
+
const option = options.find((o) => o.kind === kind);
|
|
426
|
+
if (option) {
|
|
427
|
+
// Verify the package manager is available
|
|
428
|
+
switch (kind) {
|
|
429
|
+
case "brew":
|
|
430
|
+
if (await isHomebrewAvailable()) return option;
|
|
431
|
+
break;
|
|
432
|
+
case "apt":
|
|
433
|
+
if (await isAptAvailable()) return option;
|
|
434
|
+
break;
|
|
435
|
+
case "node":
|
|
436
|
+
if (await getPreferredNodeManager()) return option;
|
|
437
|
+
break;
|
|
438
|
+
case "pip":
|
|
439
|
+
if (await isPipAvailable()) return option;
|
|
440
|
+
break;
|
|
441
|
+
case "cargo":
|
|
442
|
+
if (await isCargoAvailable()) return option;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Fall back to manual if available
|
|
449
|
+
const manual = options.find((o) => o.kind === "manual");
|
|
450
|
+
return manual || null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get installation options that are available on the current platform.
|
|
455
|
+
*
|
|
456
|
+
* @param options - All install options
|
|
457
|
+
* @returns Options available on current platform
|
|
458
|
+
*/
|
|
459
|
+
export async function getAvailableInstallOptions(
|
|
460
|
+
options: OttoInstallOption[],
|
|
461
|
+
): Promise<OttoInstallOption[]> {
|
|
462
|
+
const available: OttoInstallOption[] = [];
|
|
463
|
+
|
|
464
|
+
for (const option of options) {
|
|
465
|
+
switch (option.kind) {
|
|
466
|
+
case "brew":
|
|
467
|
+
if (await isHomebrewAvailable()) available.push(option);
|
|
468
|
+
break;
|
|
469
|
+
case "apt":
|
|
470
|
+
if (await isAptAvailable()) available.push(option);
|
|
471
|
+
break;
|
|
472
|
+
case "node":
|
|
473
|
+
if (await getPreferredNodeManager()) available.push(option);
|
|
474
|
+
break;
|
|
475
|
+
case "pip":
|
|
476
|
+
if (await isPipAvailable()) available.push(option);
|
|
477
|
+
break;
|
|
478
|
+
case "cargo":
|
|
479
|
+
if (await isCargoAvailable()) available.push(option);
|
|
480
|
+
break;
|
|
481
|
+
case "manual":
|
|
482
|
+
available.push(option);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return available;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ============================================================
|
|
491
|
+
// SKILL-LEVEL INSTALLATION
|
|
492
|
+
// ============================================================
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Install all required dependencies for a skill.
|
|
496
|
+
*
|
|
497
|
+
* @param skill - The skill with metadata containing install options
|
|
498
|
+
* @param options - Installation options
|
|
499
|
+
* @returns Array of installation results
|
|
500
|
+
*/
|
|
501
|
+
export async function installSkillDependencies(
|
|
502
|
+
skill: {
|
|
503
|
+
slug: string;
|
|
504
|
+
frontmatter: {
|
|
505
|
+
metadata?: {
|
|
506
|
+
otto?: { install?: OttoInstallOption[] };
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
options: {
|
|
511
|
+
onProgress?: InstallProgressCallback;
|
|
512
|
+
dryRun?: boolean;
|
|
513
|
+
} = {},
|
|
514
|
+
): Promise<InstallDependencyResult[]> {
|
|
515
|
+
const metadata = skill.frontmatter.metadata?.otto;
|
|
516
|
+
const installOptions = metadata?.install || [];
|
|
517
|
+
|
|
518
|
+
if (installOptions.length === 0) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const results: InstallDependencyResult[] = [];
|
|
523
|
+
const { onProgress, dryRun } = options;
|
|
524
|
+
|
|
525
|
+
// Group options by the binaries they provide
|
|
526
|
+
const binsByOption = new Map<string, OttoInstallOption[]>();
|
|
527
|
+
|
|
528
|
+
for (const option of installOptions) {
|
|
529
|
+
const bins = option.bins || [];
|
|
530
|
+
for (const bin of bins) {
|
|
531
|
+
if (!binsByOption.has(bin)) {
|
|
532
|
+
binsByOption.set(bin, []);
|
|
533
|
+
}
|
|
534
|
+
binsByOption.get(bin)!.push(option);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// For each required binary, find the best option and install
|
|
539
|
+
for (const [bin, opts] of binsByOption) {
|
|
540
|
+
// Check if already installed
|
|
541
|
+
if (await binaryExists(bin)) {
|
|
542
|
+
onProgress?.({
|
|
543
|
+
phase: "complete",
|
|
544
|
+
message: `${bin} is already installed`,
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Find best option
|
|
550
|
+
const bestOption = await findBestInstallOption(opts);
|
|
551
|
+
if (!bestOption) {
|
|
552
|
+
results.push({
|
|
553
|
+
success: false,
|
|
554
|
+
option: opts[0],
|
|
555
|
+
error: `No available installation method for ${bin}`,
|
|
556
|
+
});
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Install
|
|
561
|
+
const result = await installSkillDependency({
|
|
562
|
+
option: bestOption,
|
|
563
|
+
onProgress,
|
|
564
|
+
dryRun,
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
results.push(result);
|
|
568
|
+
|
|
569
|
+
// If failed, don't continue installing other deps
|
|
570
|
+
if (!result.success) {
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return results;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get a summary of what would be installed for a skill.
|
|
580
|
+
*/
|
|
581
|
+
export async function getInstallPlan(
|
|
582
|
+
skill: {
|
|
583
|
+
slug: string;
|
|
584
|
+
frontmatter: {
|
|
585
|
+
metadata?: {
|
|
586
|
+
otto?: { install?: OttoInstallOption[]; requires?: { bins?: string[] } };
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
): Promise<{
|
|
591
|
+
requiredBins: string[];
|
|
592
|
+
missingBins: string[];
|
|
593
|
+
availableOptions: OttoInstallOption[];
|
|
594
|
+
recommendedOptions: OttoInstallOption[];
|
|
595
|
+
}> {
|
|
596
|
+
const metadata = skill.frontmatter.metadata?.otto;
|
|
597
|
+
|
|
598
|
+
const requiredBins = metadata?.requires?.bins || [];
|
|
599
|
+
const installOptions = metadata?.install || [];
|
|
600
|
+
|
|
601
|
+
// Check which bins are missing
|
|
602
|
+
const missingBins: string[] = [];
|
|
603
|
+
for (const bin of requiredBins) {
|
|
604
|
+
if (!(await binaryExists(bin))) {
|
|
605
|
+
missingBins.push(bin);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Get available options
|
|
610
|
+
const availableOptions = await getAvailableInstallOptions(installOptions);
|
|
611
|
+
|
|
612
|
+
// Get recommended options (one per missing binary)
|
|
613
|
+
const recommendedOptions: OttoInstallOption[] = [];
|
|
614
|
+
for (const bin of missingBins) {
|
|
615
|
+
const opts = installOptions.filter((o) => o.bins?.includes(bin));
|
|
616
|
+
const best = await findBestInstallOption(opts);
|
|
617
|
+
if (best && !recommendedOptions.includes(best)) {
|
|
618
|
+
recommendedOptions.push(best);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
requiredBins,
|
|
624
|
+
missingBins,
|
|
625
|
+
availableOptions,
|
|
626
|
+
recommendedOptions,
|
|
627
|
+
};
|
|
628
|
+
}
|