Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { installSkill } from "../installSkill";
import { installSkills } from "../installSkills";

vi.mock("@trigger.dev/sdk/v3", () => ({
logger: { log: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));

describe("installSkill", () => {
describe("installSkills", () => {
const mockStdout = vi.fn();
const mockStderr = vi.fn();
const mockRunCommand = vi.fn();
Expand All @@ -18,48 +18,62 @@ describe("installSkill", () => {
mockStderr.mockResolvedValue("");
});

it("should not throw when skill copy fails", async () => {
// Install succeeds
it("should not throw when skills copy fails", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
// Copy fails
mockRunCommand.mockResolvedValueOnce({
exitCode: 1,
stdout: mockStdout,
stderr: vi.fn().mockResolvedValue("cp: no such file or directory"),
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should not throw when skill install fails", async () => {
// Install fails
it("should not throw when skills install fails", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 1,
stdout: mockStdout,
stderr: vi.fn().mockResolvedValue("npm ERR! 404 Not Found"),
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should succeed when both install and copy succeed", async () => {
// Install succeeds
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
// Copy succeeds
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});

await expect(installSkill(sandbox, "recoupable/setup-artist")).resolves.not.toThrow();
await expect(installSkills(sandbox, "recoupable/skills")).resolves.not.toThrow();
});

it("should copy all skills to OpenClaw workspace", async () => {
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});
mockRunCommand.mockResolvedValueOnce({
exitCode: 0,
stdout: mockStdout,
stderr: mockStderr,
});

await installSkills(sandbox, "recoupable/skills");

const copyCall = mockRunCommand.mock.calls[1];
const copyArgs = copyCall[0].args[1];
expect(copyArgs).toContain("cp -r .agents/skills/* ~/.openclaw/workspace/skills/");
});
});
16 changes: 7 additions & 9 deletions src/sandboxes/ensureSetupSandbox.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Sandbox } from "@vercel/sandbox";
import { installSkill } from "./installSkill";
import { installSkills } from "./installSkills";
import { runSetupSandboxSkill } from "./runSetupSandboxSkill";
import { runSetupArtistSkill } from "./runSetupArtistSkill";
import { runArtistWorkspaceSkill } from "./runArtistWorkspaceSkill";
import { logStep } from "./logStep";
/**
* Ensures the sandbox has the org/artist folder structure set up.
* Installs skills, runs setup-sandbox, then setup-artist for each artist.
* Installs skills from recoupable/skills, runs setup-sandbox, then artist-workspace.
* Idempotent — skips if `orgs/` directory already exists.
*
* @param sandbox - The Vercel Sandbox instance
Expand All @@ -17,9 +17,7 @@ export async function ensureSetupSandbox(
): Promise<void> {
logStep("Installing skills");

await installSkill(sandbox, "recoupable/setup-sandbox");
await installSkill(sandbox, "recoupable/setup-artist");
await installSkill(sandbox, "recoupable/release-management");
await installSkills(sandbox, "recoupable/skills");

if (!process.env.RECOUP_API_KEY) {
throw new Error("Missing RECOUP_API_KEY environment variable");
Expand All @@ -34,7 +32,7 @@ export async function ensureSetupSandbox(
await runSetupSandboxSkill(sandbox, env);
logStep("Setup-sandbox complete", false);

logStep("Running setup-artist skill");
await runSetupArtistSkill(sandbox, env);
logStep("Setup-artist complete", false);
logStep("Running artist-workspace skill");
await runArtistWorkspaceSkill(sandbox, env);
logStep("Artist-workspace complete", false);
}
63 changes: 0 additions & 63 deletions src/sandboxes/installSkill.ts

This file was deleted.

61 changes: 61 additions & 0 deletions src/sandboxes/installSkills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Sandbox } from "@vercel/sandbox";
import { logger } from "@trigger.dev/sdk/v3";

/**
* Installs all skills from a skills.sh repo into the OpenClaw workspace.
*
* Runs `npx skills add <source> -y` which installs to .agents/skills/,
* then copies everything to ~/.openclaw/workspace/skills/ so OpenClaw
* discovers them natively.
*
* @param sandbox - The Vercel Sandbox instance
* @param source - The skills.sh source (e.g. "recoupable/skills")
*/
export async function installSkills(
sandbox: Sandbox,
source: string
): Promise<void> {
logger.log("Installing skills via skills.sh", { source });

const install = await sandbox.runCommand({
cmd: "npx",
args: ["skills", "add", source, "-y"],
});

const installStdout = (await install.stdout()) || "";
const installStderr = (await install.stderr()) || "";

logger.log("skills.sh install result", {
exitCode: install.exitCode,
stdout: installStdout,
stderr: installStderr,
});

if (install.exitCode !== 0) {
logger.warn("Skills install failed, continuing without them", {
source,
stderr: installStderr,
});
return;
}

const copy = await sandbox.runCommand({
cmd: "sh",
args: [
"-c",
"mkdir -p ~/.openclaw/workspace/skills && cp -r .agents/skills/* ~/.openclaw/workspace/skills/",
],
});

const copyStderr = (await copy.stderr()) || "";

if (copy.exitCode !== 0) {
logger.warn("Failed to copy skills to OpenClaw workspace, continuing without them", {
source,
stderr: copyStderr,
});
return;
}

logger.log("Skills installed to OpenClaw workspace", { source });
}
25 changes: 25 additions & 0 deletions src/sandboxes/runArtistWorkspaceSkill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Sandbox } from "@vercel/sandbox";
import { runOpenClawAgent } from "./runOpenClawAgent";

/**
* Runs the /artist-workspace skill via OpenClaw to set up context
* files for artists under orgs/.
*
* @param sandbox - The Vercel Sandbox instance
* @param env - Environment variables for the agent
*/
export async function runArtistWorkspaceSkill(
sandbox: Sandbox,
env: Record<string, string>
): Promise<void> {
const result = await runOpenClawAgent(sandbox, {
label: "Running artist-workspace skill",
message:
"Read the /artist-workspace skill and follow it when working across your roster's artist workspaces.\n\nRECOUP_API_KEY and RECOUP_ACCOUNT_ID are available as environment variables.",
env,
});

if (result.exitCode !== 0) {
throw new Error("Failed to run artist-workspace skill via OpenClaw");
}
}
25 changes: 0 additions & 25 deletions src/sandboxes/runSetupArtistSkill.ts

This file was deleted.

Loading