mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
Integrate pinned Cursor ACP runtime and explicit public setup
Consolidate Cursor v10 onto current controller behavior, retain admission gates, and prepare native interaction and cleanup qualification. Add the public provisioner and ordinary provider-pack assets. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2a8a99e4a5
commit
e2b89b6302
199 files changed
+20293
-851
No files matched your search
@@ -0,0 +1,38 @@
|
||||
import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { resolveCursorProvisioner } from "../commands/runtime.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
|
||||
async function fixture() {
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "cursor-public-setup-")));
|
||||
roots.push(root);
|
||||
await mkdir(join(root, "dist/vendor/paperclip-runner/cli"), { recursive: true });
|
||||
await writeFile(join(root, "package.json"), JSON.stringify({ name: "@paperclipai/server" }));
|
||||
await writeFile(join(root, "dist/index.js"), "throw new Error('must not import server');");
|
||||
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-cursor.cjs");
|
||||
await writeFile(provisioner, "fixture");
|
||||
return { root, provisioner, url: pathToFileURL(join(root, "dist/index.js")).href };
|
||||
}
|
||||
|
||||
it("resolves the public provisioner without starting the server", async () => {
|
||||
const f = await fixture();
|
||||
expect(await resolveCursorProvisioner(f.url)).toBe(f.provisioner);
|
||||
});
|
||||
|
||||
it("rejects repository layouts and wrong package identities", async () => {
|
||||
const f = await fixture();
|
||||
await mkdir(join(f.root, "src")); await writeFile(join(f.root, "src/index.ts"), "fixture");
|
||||
await expect(resolveCursorProvisioner(pathToFileURL(join(f.root, "src/index.ts")).href)).rejects.toThrow("published server layout");
|
||||
await writeFile(join(f.root, "package.json"), JSON.stringify({ name: "untrusted" }));
|
||||
await expect(resolveCursorProvisioner(f.url)).rejects.toThrow("identity is invalid");
|
||||
});
|
||||
|
||||
it("rejects a provisioner linked outside the installed package", async () => {
|
||||
const f = await fixture(); const other = await fixture();
|
||||
await rm(f.provisioner); await symlink(other.provisioner, f.provisioner);
|
||||
await expect(resolveCursorProvisioner(f.url)).rejects.toThrow("escapes");
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { lstat, readFile, realpath } from "node:fs/promises";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Command } from "commander";
|
||||
|
||||
/** Resolve the installed public dependency without importing or starting it. */
|
||||
export async function resolveCursorProvisioner(serverUrl: string): Promise<string> {
|
||||
const url = new URL(serverUrl);
|
||||
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Cursor setup requires an installed Paperclip server");
|
||||
const entry = await realpath(fileURLToPath(url));
|
||||
if (!entry.endsWith("/dist/index.js")) throw new Error("Cursor setup requires the published server layout");
|
||||
const root = resolve(dirname(entry), "..");
|
||||
const manifest = join(root, "package.json");
|
||||
const info = await lstat(manifest);
|
||||
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536 || JSON.parse(await readFile(manifest, "utf8")).name !== "@paperclipai/server") throw new Error("Cursor setup server package identity is invalid");
|
||||
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-cursor.cjs");
|
||||
if (await realpath(provisioner) !== provisioner || !(await lstat(provisioner)).isFile()) throw new Error("Cursor setup entrypoint escapes its server package");
|
||||
return provisioner;
|
||||
}
|
||||
|
||||
export async function setupCursorRuntime(): Promise<void> {
|
||||
const provisioner = await resolveCursorProvisioner(import.meta.resolve("@paperclipai/server"));
|
||||
const child = spawn(process.execPath, [provisioner], {
|
||||
stdio: "inherit", env: { PATH: process.env.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" },
|
||||
});
|
||||
const cancel = () => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); };
|
||||
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
|
||||
try {
|
||||
await new Promise<void>((accept, reject) => {
|
||||
let spawnError: Error | undefined;
|
||||
child.on("error", error => { spawnError = error; });
|
||||
child.once("close", (code, signal) => {
|
||||
if (spawnError) reject(spawnError);
|
||||
else if (code !== 0 || signal) reject(new Error("Cursor setup did not finish. Review its error; an invalid installation is never replaced automatically."));
|
||||
else accept();
|
||||
});
|
||||
});
|
||||
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
||||
}
|
||||
|
||||
export function registerRuntimeCommands(program: Command): void {
|
||||
program.command("runtime").description("Manage explicitly installed agent runtimes")
|
||||
.command("setup <provider>")
|
||||
.description("Install and verify the pinned Cursor runtime for this host (public downloads; no model calls)")
|
||||
.action(async (provider: string) => {
|
||||
if (provider !== "cursor") throw new Error("Supported runtime setup: paperclipai runtime setup cursor");
|
||||
await setupCursorRuntime();
|
||||
});
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { applyDataDirOverride, type DataDirOptionLike } from "./config/data-dir.
|
||||
import { loadPaperclipEnvFile } from "./config/env.js";
|
||||
import { initTelemetryFromConfigFile, flushTelemetry } from "./telemetry.js";
|
||||
import { registerWorktreeCommands } from "./commands/worktree.js";
|
||||
import { registerRuntimeCommands } from "./commands/runtime.js";
|
||||
import { registerPluginCommands } from "./commands/client/plugin.js";
|
||||
import { registerClientAuthCommands } from "./commands/client/auth.js";
|
||||
import { registerConnectCommand } from "./commands/client/connect.js";
|
||||
@@ -260,6 +261,7 @@ registerSecretCommands(program);
|
||||
registerSkillsCommands(program);
|
||||
registerTeamCommands(program);
|
||||
registerWorktreeCommands(program);
|
||||
registerRuntimeCommands(program);
|
||||
registerEnvLabCommands(program);
|
||||
registerPluginCommands(program);
|
||||
|
||||
|
||||
Reference in new issue
Block a user