diff --git a/src/sandboxes/__tests__/installSkill.test.ts b/src/sandboxes/__tests__/installSkills.test.ts similarity index 54% rename from src/sandboxes/__tests__/installSkill.test.ts rename to src/sandboxes/__tests__/installSkills.test.ts index a44f76a..c8aaa4b 100644 --- a/src/sandboxes/__tests__/installSkill.test.ts +++ b/src/sandboxes/__tests__/installSkills.test.ts @@ -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(); @@ -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/"); }); }); diff --git a/src/sandboxes/ensureSetupSandbox.ts b/src/sandboxes/ensureSetupSandbox.ts index f0ad567..94cf90b 100644 --- a/src/sandboxes/ensureSetupSandbox.ts +++ b/src/sandboxes/ensureSetupSandbox.ts @@ -1,12 +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 { 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. - * Idempotent — skips if `orgs/` directory already exists. + * Installs skills from recoupable/skills, then runs setup-sandbox. * * @param sandbox - The Vercel Sandbox instance * @param accountId - The account ID for the sandbox owner @@ -16,10 +15,7 @@ export async function ensureSetupSandbox( accountId: string ): Promise { 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"); @@ -33,8 +29,4 @@ export async function ensureSetupSandbox( logStep("Running setup-sandbox skill"); await runSetupSandboxSkill(sandbox, env); logStep("Setup-sandbox complete", false); - - logStep("Running setup-artist skill"); - await runSetupArtistSkill(sandbox, env); - logStep("Setup-artist complete", false); } diff --git a/src/sandboxes/installSkill.ts b/src/sandboxes/installSkill.ts deleted file mode 100644 index 4b974f1..0000000 --- a/src/sandboxes/installSkill.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Sandbox } from "@vercel/sandbox"; -import { logger } from "@trigger.dev/sdk/v3"; - -/** - * Installs a skills.sh skill into the OpenClaw workspace skills directory. - * - * skills.sh installs to .agents/skills/ — this function copies the result - * to ~/.openclaw/workspace/skills/ so OpenClaw discovers it natively. - * - * @param sandbox - The Vercel Sandbox instance - * @param skill - The skills.sh skill identifier (e.g. "recoupable/setup-sandbox") - */ -export async function installSkill( - sandbox: Sandbox, - skill: string -): Promise { - const skillName = skill.split("/").pop()!; - - logger.log("Installing skill via skills.sh", { skill }); - - const install = await sandbox.runCommand({ - cmd: "npx", - args: ["skills", "add", skill, "-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("Skill install failed, continuing without it", { - skill, - stderr: installStderr, - }); - return; - } - - // Copy from skills.sh location to OpenClaw workspace skills directory - const copy = await sandbox.runCommand({ - cmd: "sh", - args: [ - "-c", - `mkdir -p ~/.openclaw/workspace/skills && rm -rf ~/.openclaw/workspace/skills/${skillName} && cp -r .agents/skills/${skillName} ~/.openclaw/workspace/skills/${skillName}`, - ], - }); - - const copyStderr = (await copy.stderr()) || ""; - - if (copy.exitCode !== 0) { - logger.warn("Failed to copy skill to OpenClaw workspace, continuing without it", { - skill, - stderr: copyStderr, - }); - return; - } - - logger.log("Skill installed to OpenClaw workspace", { skillName }); -} diff --git a/src/sandboxes/installSkills.ts b/src/sandboxes/installSkills.ts new file mode 100644 index 0000000..15fc88a --- /dev/null +++ b/src/sandboxes/installSkills.ts @@ -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 -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 { + 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 }); +} diff --git a/src/sandboxes/runSetupArtistSkill.ts b/src/sandboxes/runSetupArtistSkill.ts deleted file mode 100644 index b5956f9..0000000 --- a/src/sandboxes/runSetupArtistSkill.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Sandbox } from "@vercel/sandbox"; -import { runOpenClawAgent } from "./runOpenClawAgent"; - -/** - * Runs the /setup-artist skill via OpenClaw for each artist - * folder that exists under orgs/. - * - * @param sandbox - The Vercel Sandbox instance - * @param env - Environment variables for the agent - */ -export async function runSetupArtistSkill( - sandbox: Sandbox, - env: Record -): Promise { - const result = await runOpenClawAgent(sandbox, { - label: "Running setup-artist skill", - message: - "Run the /setup-artist skill for EACH artist folder that exists under orgs/.\n\nRECOUP_API_KEY and RECOUP_ACCOUNT_ID are available as environment variables.", - env, - }); - - if (result.exitCode !== 0) { - throw new Error("Failed to set up artists via OpenClaw"); - } -}