@vellumai/cli 0.4.42 → 0.4.43

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.
@@ -1,203 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- rmSync,
7
- writeFileSync,
8
- } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
-
12
- import { skills } from "../commands/skills.js";
13
-
14
- let tempDir: string;
15
- let originalArgv: string[];
16
- let originalBaseDataDir: string | undefined;
17
- let originalExitCode: number | string | null | undefined;
18
-
19
- function getSkillsDir(): string {
20
- return join(tempDir, ".vellum", "workspace", "skills");
21
- }
22
-
23
- function getSkillsIndexPath(): string {
24
- return join(getSkillsDir(), "SKILLS.md");
25
- }
26
-
27
- function installFakeSkill(skillId: string): void {
28
- const skillDir = join(getSkillsDir(), skillId);
29
- mkdirSync(skillDir, { recursive: true });
30
- writeFileSync(join(skillDir, "SKILL.md"), `# ${skillId}\nA test skill.\n`);
31
- }
32
-
33
- function writeSkillsIndex(content: string): void {
34
- mkdirSync(getSkillsDir(), { recursive: true });
35
- writeFileSync(getSkillsIndexPath(), content);
36
- }
37
-
38
- beforeEach(() => {
39
- tempDir = join(tmpdir(), `skills-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
40
- mkdirSync(join(tempDir, ".vellum", "workspace", "skills"), {
41
- recursive: true,
42
- });
43
- originalArgv = process.argv;
44
- originalBaseDataDir = process.env.BASE_DATA_DIR;
45
- originalExitCode = process.exitCode;
46
- process.env.BASE_DATA_DIR = tempDir;
47
- });
48
-
49
- afterEach(() => {
50
- process.argv = originalArgv;
51
- process.env.BASE_DATA_DIR = originalBaseDataDir;
52
- // Bun treats `process.exitCode = undefined` as a no-op, so explicitly
53
- // reset to 0 when the original value was not set.
54
- process.exitCode = originalExitCode ?? 0;
55
- rmSync(tempDir, { recursive: true, force: true });
56
- });
57
-
58
- describe("vellum skills uninstall", () => {
59
- test("removes skill directory and SKILLS.md entry", async () => {
60
- /**
61
- * Tests the happy path for uninstalling a skill.
62
- */
63
-
64
- // GIVEN a skill is installed locally
65
- installFakeSkill("weather");
66
- writeSkillsIndex("- weather\n- google-oauth-setup\n");
67
-
68
- // WHEN we run `vellum skills uninstall weather`
69
- process.argv = ["bun", "run", "skills", "uninstall", "weather"];
70
- await skills();
71
-
72
- // THEN the skill directory should be removed
73
- expect(existsSync(join(getSkillsDir(), "weather"))).toBe(false);
74
-
75
- // AND the SKILLS.md entry should be removed
76
- const index = readFileSync(getSkillsIndexPath(), "utf-8");
77
- expect(index).not.toContain("weather");
78
-
79
- // AND other skills should remain in the index
80
- expect(index).toContain("google-oauth-setup");
81
- });
82
-
83
- test("outputs JSON on success when --json flag is passed", async () => {
84
- /**
85
- * Tests that --json flag produces machine-readable output.
86
- */
87
-
88
- // GIVEN a skill is installed locally
89
- installFakeSkill("weather");
90
- writeSkillsIndex("- weather\n");
91
-
92
- // WHEN we run `vellum skills uninstall weather --json`
93
- process.argv = ["bun", "run", "skills", "uninstall", "weather", "--json"];
94
- const logs: string[] = [];
95
- const origLog = console.log;
96
- console.log = (...args: unknown[]) => logs.push(args.join(" "));
97
- try {
98
- await skills();
99
- } finally {
100
- console.log = origLog;
101
- }
102
-
103
- // THEN JSON output should indicate success
104
- const output = JSON.parse(logs[0]);
105
- expect(output).toEqual({ ok: true, skillId: "weather" });
106
- });
107
-
108
- test("errors when skill is not installed", async () => {
109
- /**
110
- * Tests that uninstalling a non-existent skill produces an error.
111
- */
112
-
113
- // GIVEN no skills are installed
114
- // WHEN we run `vellum skills uninstall nonexistent`
115
- process.argv = ["bun", "run", "skills", "uninstall", "nonexistent"];
116
- const errors: string[] = [];
117
- const origError = console.error;
118
- console.error = (...args: unknown[]) => errors.push(args.join(" "));
119
- try {
120
- await skills();
121
- } finally {
122
- console.error = origError;
123
- }
124
-
125
- // THEN an error message should be displayed
126
- expect(errors[0]).toContain('Skill "nonexistent" is not installed.');
127
- expect(process.exitCode).toBe(1);
128
- });
129
-
130
- test("errors with JSON output when skill is not installed and --json is passed", async () => {
131
- /**
132
- * Tests that --json flag produces machine-readable error output.
133
- */
134
-
135
- // GIVEN no skills are installed
136
- // WHEN we run `vellum skills uninstall nonexistent --json`
137
- process.argv = [
138
- "bun",
139
- "run",
140
- "skills",
141
- "uninstall",
142
- "nonexistent",
143
- "--json",
144
- ];
145
- const logs: string[] = [];
146
- const origLog = console.log;
147
- console.log = (...args: unknown[]) => logs.push(args.join(" "));
148
- try {
149
- await skills();
150
- } finally {
151
- console.log = origLog;
152
- }
153
-
154
- // THEN JSON output should indicate failure
155
- const output = JSON.parse(logs[0]);
156
- expect(output.ok).toBe(false);
157
- expect(output.error).toContain('Skill "nonexistent" is not installed.');
158
- });
159
-
160
- test("works when SKILLS.md does not exist", async () => {
161
- /**
162
- * Tests that uninstall works even if the SKILLS.md index file is missing.
163
- */
164
-
165
- // GIVEN a skill directory exists but no SKILLS.md
166
- installFakeSkill("weather");
167
-
168
- // WHEN we run `vellum skills uninstall weather`
169
- process.argv = ["bun", "run", "skills", "uninstall", "weather"];
170
- await skills();
171
-
172
- // THEN the skill directory should be removed
173
- expect(existsSync(join(getSkillsDir(), "weather"))).toBe(false);
174
-
175
- // AND no SKILLS.md should have been created
176
- expect(existsSync(getSkillsIndexPath())).toBe(false);
177
- });
178
-
179
- test("removes skill with nested files", async () => {
180
- /**
181
- * Tests that uninstall recursively removes skills with nested directories.
182
- */
183
-
184
- // GIVEN a skill with nested files is installed
185
- const skillDir = join(getSkillsDir(), "weather");
186
- mkdirSync(join(skillDir, "scripts", "lib"), { recursive: true });
187
- writeFileSync(join(skillDir, "SKILL.md"), "# weather\n");
188
- writeFileSync(join(skillDir, "scripts", "fetch.sh"), "#!/bin/bash\n");
189
- writeFileSync(join(skillDir, "scripts", "lib", "utils.sh"), "# utils\n");
190
- writeSkillsIndex("- weather\n");
191
-
192
- // WHEN we run `vellum skills uninstall weather`
193
- process.argv = ["bun", "run", "skills", "uninstall", "weather"];
194
- await skills();
195
-
196
- // THEN the entire skill directory tree should be removed
197
- expect(existsSync(skillDir)).toBe(false);
198
-
199
- // AND the SKILLS.md entry should be removed
200
- const index = readFileSync(getSkillsIndexPath(), "utf-8");
201
- expect(index).not.toContain("weather");
202
- });
203
- });
@@ -1,514 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import {
4
- cpSync,
5
- existsSync,
6
- mkdirSync,
7
- readFileSync,
8
- renameSync,
9
- rmSync,
10
- writeFileSync,
11
- } from "node:fs";
12
- import { homedir } from "node:os";
13
- import { dirname, join } from "node:path";
14
- import { gunzipSync } from "node:zlib";
15
-
16
- // ---------------------------------------------------------------------------
17
- // Path helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- function getRootDir(): string {
21
- return join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
22
- }
23
-
24
- function getSkillsDir(): string {
25
- return join(getRootDir(), "workspace", "skills");
26
- }
27
-
28
- function getSkillsIndexPath(): string {
29
- return join(getSkillsDir(), "SKILLS.md");
30
- }
31
-
32
- /**
33
- * Resolve the repo-level skills/ directory when running in dev mode.
34
- * Returns the path if VELLUM_DEV is set and the directory exists, or undefined.
35
- */
36
- function getRepoSkillsDir(): string | undefined {
37
- if (!process.env.VELLUM_DEV) return undefined;
38
-
39
- // cli/src/commands/skills.ts -> ../../../skills/
40
- const candidate = join(import.meta.dir, "..", "..", "..", "skills");
41
- if (existsSync(join(candidate, "catalog.json"))) {
42
- return candidate;
43
- }
44
- return undefined;
45
- }
46
-
47
- /**
48
- * Read skills from the repo-local catalog.json.
49
- */
50
- function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
51
- try {
52
- const raw = readFileSync(
53
- join(repoSkillsDir, "catalog.json"),
54
- "utf-8",
55
- );
56
- const manifest = JSON.parse(raw) as CatalogManifest;
57
- if (!Array.isArray(manifest.skills)) return [];
58
- return manifest.skills;
59
- } catch {
60
- return [];
61
- }
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Platform API client
66
- // ---------------------------------------------------------------------------
67
-
68
- function getConfigPlatformUrl(): string | undefined {
69
- try {
70
- const configPath = join(getRootDir(), "workspace", "config.json");
71
- if (!existsSync(configPath)) return undefined;
72
- const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
73
- string,
74
- unknown
75
- >;
76
- const platform = raw.platform as Record<string, unknown> | undefined;
77
- const baseUrl = platform?.baseUrl;
78
- if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
79
- } catch {
80
- // ignore
81
- }
82
- return undefined;
83
- }
84
-
85
- function getPlatformUrl(): string {
86
- return (
87
- process.env.VELLUM_ASSISTANT_PLATFORM_URL ??
88
- getConfigPlatformUrl() ??
89
- "https://platform.vellum.ai"
90
- );
91
- }
92
-
93
- function getPlatformToken(): string | null {
94
- try {
95
- return readFileSync(join(getRootDir(), "platform-token"), "utf-8").trim();
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- function buildHeaders(): Record<string, string> {
102
- const headers: Record<string, string> = {};
103
- const token = getPlatformToken();
104
- if (token) {
105
- headers["X-Session-Token"] = token;
106
- }
107
- return headers;
108
- }
109
-
110
- // ---------------------------------------------------------------------------
111
- // Types
112
- // ---------------------------------------------------------------------------
113
-
114
- interface CatalogSkill {
115
- id: string;
116
- name: string;
117
- description: string;
118
- emoji?: string;
119
- includes?: string[];
120
- version?: string;
121
- }
122
-
123
- interface CatalogManifest {
124
- version: number;
125
- skills: CatalogSkill[];
126
- }
127
-
128
- // ---------------------------------------------------------------------------
129
- // Catalog operations
130
- // ---------------------------------------------------------------------------
131
-
132
- async function fetchCatalog(): Promise<CatalogSkill[]> {
133
- const url = `${getPlatformUrl()}/v1/skills/`;
134
- const response = await fetch(url, {
135
- headers: buildHeaders(),
136
- signal: AbortSignal.timeout(10000),
137
- });
138
-
139
- if (!response.ok) {
140
- throw new Error(
141
- `Platform API error ${response.status}: ${response.statusText}`,
142
- );
143
- }
144
-
145
- const manifest = (await response.json()) as CatalogManifest;
146
- if (!Array.isArray(manifest.skills)) {
147
- throw new Error("Platform catalog has invalid skills array");
148
- }
149
- return manifest.skills;
150
- }
151
-
152
- /**
153
- * Extract all files from a tar archive (uncompressed) into a directory.
154
- * Returns true if a SKILL.md was found in the archive.
155
- */
156
- function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
157
- let foundSkillMd = false;
158
- let offset = 0;
159
- while (offset + 512 <= tarBuffer.length) {
160
- const header = tarBuffer.subarray(offset, offset + 512);
161
-
162
- // End-of-archive (two consecutive zero blocks)
163
- if (header.every((b) => b === 0)) break;
164
-
165
- // Filename (bytes 0-99, null-terminated)
166
- const nameEnd = header.indexOf(0, 0);
167
- const name = header
168
- .subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100))
169
- .toString("utf-8");
170
-
171
- // File type (byte 156): '5' = directory, '0' or '\0' = regular file
172
- const typeFlag = header[156];
173
-
174
- // File size (bytes 124-135, octal)
175
- const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
176
- const size = parseInt(sizeStr, 8) || 0;
177
-
178
- offset += 512; // past header
179
-
180
- // Skip directories and empty names
181
- if (name && typeFlag !== 53 /* '5' */) {
182
- // Prevent path traversal
183
- const normalizedName = name.replace(/^\.\//, "");
184
- if (!normalizedName.startsWith("..") && !normalizedName.includes("/..")) {
185
- const destPath = join(destDir, normalizedName);
186
- mkdirSync(dirname(destPath), { recursive: true });
187
- writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
188
-
189
- if (
190
- normalizedName === "SKILL.md" ||
191
- normalizedName.endsWith("/SKILL.md")
192
- ) {
193
- foundSkillMd = true;
194
- }
195
- }
196
- }
197
-
198
- // Skip to next header (data padded to 512 bytes)
199
- offset += Math.ceil(size / 512) * 512;
200
- }
201
- return foundSkillMd;
202
- }
203
-
204
- async function fetchAndExtractSkill(
205
- skillId: string,
206
- destDir: string,
207
- ): Promise<void> {
208
- const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
209
- const response = await fetch(url, {
210
- headers: buildHeaders(),
211
- signal: AbortSignal.timeout(15000),
212
- });
213
-
214
- if (!response.ok) {
215
- throw new Error(
216
- `Failed to fetch skill "${skillId}": HTTP ${response.status}`,
217
- );
218
- }
219
-
220
- const gzipBuffer = Buffer.from(await response.arrayBuffer());
221
- const tarBuffer = gunzipSync(gzipBuffer);
222
- const foundSkillMd = extractTarToDir(tarBuffer, destDir);
223
-
224
- if (!foundSkillMd) {
225
- throw new Error(`SKILL.md not found in archive for "${skillId}"`);
226
- }
227
- }
228
-
229
- // ---------------------------------------------------------------------------
230
- // Managed skill installation
231
- // ---------------------------------------------------------------------------
232
-
233
- function atomicWriteFile(filePath: string, content: string): void {
234
- const dir = dirname(filePath);
235
- mkdirSync(dir, { recursive: true });
236
- const tmpPath = join(dir, `.tmp-${randomUUID()}`);
237
- writeFileSync(tmpPath, content, "utf-8");
238
- renameSync(tmpPath, filePath);
239
- }
240
-
241
- function upsertSkillsIndex(id: string): void {
242
- const indexPath = getSkillsIndexPath();
243
- let lines: string[] = [];
244
- if (existsSync(indexPath)) {
245
- lines = readFileSync(indexPath, "utf-8").split("\n");
246
- }
247
-
248
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
249
- const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
250
- if (lines.some((line) => pattern.test(line))) return;
251
-
252
- const nonEmpty = lines.filter((l) => l.trim());
253
- nonEmpty.push(`- ${id}`);
254
- const content = nonEmpty.join("\n");
255
- atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
256
- }
257
-
258
- function removeSkillsIndexEntry(id: string): void {
259
- const indexPath = getSkillsIndexPath();
260
- if (!existsSync(indexPath)) return;
261
-
262
- const lines = readFileSync(indexPath, "utf-8").split("\n");
263
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
264
- const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
265
- const filtered = lines.filter((line) => !pattern.test(line));
266
-
267
- // If nothing changed, skip the write
268
- if (filtered.length === lines.length) return;
269
-
270
- const content = filtered.join("\n");
271
- atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
272
- }
273
-
274
- function uninstallSkillLocally(skillId: string): void {
275
- const skillDir = join(getSkillsDir(), skillId);
276
-
277
- if (!existsSync(skillDir)) {
278
- throw new Error(`Skill "${skillId}" is not installed.`);
279
- }
280
-
281
- rmSync(skillDir, { recursive: true, force: true });
282
- removeSkillsIndexEntry(skillId);
283
- }
284
-
285
- async function installSkillLocally(
286
- skillId: string,
287
- catalogEntry: CatalogSkill,
288
- overwrite: boolean,
289
- ): Promise<void> {
290
- const skillDir = join(getSkillsDir(), skillId);
291
- const skillFilePath = join(skillDir, "SKILL.md");
292
-
293
- if (existsSync(skillFilePath) && !overwrite) {
294
- throw new Error(
295
- `Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
296
- );
297
- }
298
-
299
- mkdirSync(skillDir, { recursive: true });
300
-
301
- // In dev mode, install from the local repo skills directory if available
302
- const repoSkillsDir = getRepoSkillsDir();
303
- const repoSkillSource = repoSkillsDir
304
- ? join(repoSkillsDir, skillId)
305
- : undefined;
306
-
307
- if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
308
- cpSync(repoSkillSource, skillDir, { recursive: true });
309
- } else {
310
- await fetchAndExtractSkill(skillId, skillDir);
311
- }
312
-
313
- // Write version metadata
314
- if (catalogEntry.version) {
315
- const meta = {
316
- version: catalogEntry.version,
317
- installedAt: new Date().toISOString(),
318
- };
319
- atomicWriteFile(
320
- join(skillDir, "version.json"),
321
- JSON.stringify(meta, null, 2) + "\n",
322
- );
323
- }
324
-
325
- // Install npm dependencies if the skill has a package.json
326
- if (existsSync(join(skillDir, "package.json"))) {
327
- const bunPath = `${process.env.HOME || "/root"}/.bun/bin`;
328
- execSync("bun install", {
329
- cwd: skillDir,
330
- stdio: "inherit",
331
- env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
332
- });
333
- }
334
-
335
- // Register in SKILLS.md only after all steps succeed
336
- upsertSkillsIndex(skillId);
337
- }
338
-
339
- // ---------------------------------------------------------------------------
340
- // Helpers
341
- // ---------------------------------------------------------------------------
342
-
343
- function hasFlag(args: string[], flag: string): boolean {
344
- return args.includes(flag);
345
- }
346
-
347
- // ---------------------------------------------------------------------------
348
- // Usage
349
- // ---------------------------------------------------------------------------
350
-
351
- function printUsage(): void {
352
- console.log("Usage: vellum skills <subcommand> [options]");
353
- console.log("");
354
- console.log("Subcommands:");
355
- console.log(
356
- " list List available catalog skills",
357
- );
358
- console.log(
359
- " install <skill-id> [--overwrite] Install a skill from the catalog",
360
- );
361
- console.log(
362
- " uninstall <skill-id> Uninstall a previously installed skill",
363
- );
364
- console.log("");
365
- console.log("Options:");
366
- console.log(" --json Machine-readable JSON output");
367
- }
368
-
369
- // ---------------------------------------------------------------------------
370
- // Command entry point
371
- // ---------------------------------------------------------------------------
372
-
373
- export async function skills(): Promise<void> {
374
- const args = process.argv.slice(3);
375
- const subcommand = args[0];
376
- const json = hasFlag(args, "--json");
377
-
378
- if (!subcommand || subcommand === "--help" || subcommand === "-h") {
379
- printUsage();
380
- return;
381
- }
382
-
383
- switch (subcommand) {
384
- case "list": {
385
- try {
386
- const catalog = await fetchCatalog();
387
-
388
- // In dev mode, merge in skills from the repo-local skills/ directory
389
- const repoSkillsDir = getRepoSkillsDir();
390
- if (repoSkillsDir) {
391
- const localSkills = readLocalCatalog(repoSkillsDir);
392
- const remoteIds = new Set(catalog.map((s) => s.id));
393
- for (const local of localSkills) {
394
- if (!remoteIds.has(local.id)) {
395
- catalog.push(local);
396
- }
397
- }
398
- }
399
-
400
- if (json) {
401
- console.log(JSON.stringify({ ok: true, skills: catalog }));
402
- return;
403
- }
404
-
405
- if (catalog.length === 0) {
406
- console.log("No skills available in the catalog.");
407
- return;
408
- }
409
-
410
- console.log(`Available skills (${catalog.length}):\n`);
411
- for (const s of catalog) {
412
- const emoji = s.emoji ? `${s.emoji} ` : "";
413
- const deps = s.includes?.length
414
- ? ` (requires: ${s.includes.join(", ")})`
415
- : "";
416
- console.log(` ${emoji}${s.id}`);
417
- console.log(` ${s.name} — ${s.description}${deps}`);
418
- }
419
- } catch (err) {
420
- const msg = err instanceof Error ? err.message : String(err);
421
- if (json) {
422
- console.log(JSON.stringify({ ok: false, error: msg }));
423
- } else {
424
- console.error(`Error: ${msg}`);
425
- }
426
- process.exitCode = 1;
427
- }
428
- break;
429
- }
430
-
431
- case "install": {
432
- const skillId = args.find((a) => !a.startsWith("--") && a !== "install");
433
- if (!skillId) {
434
- console.error("Usage: vellum skills install <skill-id>");
435
- process.exit(1);
436
- }
437
-
438
- const overwrite = hasFlag(args, "--overwrite");
439
-
440
- try {
441
- // In dev mode, also check the repo-local skills/ directory
442
- const repoSkillsDir = getRepoSkillsDir();
443
- let localSkills: CatalogSkill[] = [];
444
- if (repoSkillsDir) {
445
- localSkills = readLocalCatalog(repoSkillsDir);
446
- }
447
-
448
- // Check local catalog first, then fall back to remote
449
- let entry = localSkills.find((s) => s.id === skillId);
450
- if (!entry) {
451
- const catalog = await fetchCatalog();
452
- entry = catalog.find((s) => s.id === skillId);
453
- }
454
-
455
- if (!entry) {
456
- throw new Error(`Skill "${skillId}" not found in the Vellum catalog`);
457
- }
458
-
459
- // Fetch, extract, and install
460
- await installSkillLocally(skillId, entry, overwrite);
461
-
462
- if (json) {
463
- console.log(JSON.stringify({ ok: true, skillId }));
464
- } else {
465
- console.log(`Installed skill "${skillId}".`);
466
- }
467
- } catch (err) {
468
- const msg = err instanceof Error ? err.message : String(err);
469
- if (json) {
470
- console.log(JSON.stringify({ ok: false, error: msg }));
471
- } else {
472
- console.error(`Error: ${msg}`);
473
- }
474
- process.exitCode = 1;
475
- }
476
- break;
477
- }
478
-
479
- case "uninstall": {
480
- const skillId = args.find(
481
- (a) => !a.startsWith("--") && a !== "uninstall",
482
- );
483
- if (!skillId) {
484
- console.error("Usage: vellum skills uninstall <skill-id>");
485
- process.exit(1);
486
- }
487
-
488
- try {
489
- uninstallSkillLocally(skillId);
490
-
491
- if (json) {
492
- console.log(JSON.stringify({ ok: true, skillId }));
493
- } else {
494
- console.log(`Uninstalled skill "${skillId}".`);
495
- }
496
- } catch (err) {
497
- const msg = err instanceof Error ? err.message : String(err);
498
- if (json) {
499
- console.log(JSON.stringify({ ok: false, error: msg }));
500
- } else {
501
- console.error(`Error: ${msg}`);
502
- }
503
- process.exitCode = 1;
504
- }
505
- break;
506
- }
507
-
508
- default: {
509
- console.error(`Unknown skills subcommand: ${subcommand}`);
510
- printUsage();
511
- process.exit(1);
512
- }
513
- }
514
- }