Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion cli/src/commands/hatch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomBytes } from "crypto";
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
import { homedir, tmpdir, userInfo } from "os";
import { join } from "path";

Expand Down Expand Up @@ -535,6 +535,43 @@ async function hatchCustom(
}
}

function installCLISymlink(): void {
const cliBinary = process.execPath;
if (!cliBinary || !existsSync(cliBinary)) return;

const symlinkPath = "/usr/local/bin/vellum";

try {
// Use lstatSync (not existsSync) to detect dangling symlinks —
// existsSync follows symlinks and returns false for broken links.
try {
const stats = lstatSync(symlinkPath);
if (!stats.isSymbolicLink()) {
// Real file — don't overwrite (developer's local install)
return;
}
// Already a symlink — skip if it already points to our binary
const dest = readlinkSync(symlinkPath);
if (dest === cliBinary) return;
// Stale or dangling symlink — remove before creating new one
unlinkSync(symlinkPath);
} catch (e: any) {
if (e?.code !== "ENOENT") throw e;
// Path doesn't exist — proceed to create symlink
}

const dir = "/usr/local/bin";
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
symlinkSync(cliBinary, symlinkPath);
console.log(` Symlinked ${symlinkPath} → ${cliBinary}`);
} catch {
// Permission denied or other error — not critical
console.log(` ⚠ Could not create symlink at ${symlinkPath} (run with sudo or create manually)`);
}
}

async function hatchLocal(species: Species, name: string | null, daemonOnly: boolean = false): Promise<void> {
const instanceName =
name ?? process.env.VELLUM_ASSISTANT_NAME ?? `${species}-${generateRandomSuffix()}`;
Expand Down Expand Up @@ -602,6 +639,10 @@ async function hatchLocal(species: Species, name: string | null, daemonOnly: boo
if (!daemonOnly) {
saveAssistantEntry(localEntry);

if (process.env.VELLUM_DESKTOP_APP) {
installCLISymlink();
}

console.log("");
console.log(`✅ Local assistant hatched!`);
console.log("");
Expand Down
7 changes: 7 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";

import cliPkg from "../package.json";
import { autonomy } from "./commands/autonomy";
import { client } from "./commands/client";
import { config } from "./commands/config";
Expand Down Expand Up @@ -71,6 +73,11 @@ async function main() {
const args = process.argv.slice(2);
const commandName = args[0];

if (commandName === "--version" || commandName === "-v") {
console.log(`@vellumai/cli v${cliPkg.version}`);
process.exit(0);
}

if (!commandName || commandName === "--help" || commandName === "-h") {
console.log("Usage: vellum <command> [options]");
console.log("");
Expand Down