feat: add CraftTable MCP OAuth CLI
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { parse } from "jsonc-parser";
|
||||
import { configureAgents, type CommandResult, type CommandRunner, launchCommand, unconfigureAgents } from "../src/agents.js";
|
||||
import { resolveClientOptions } from "../src/config.js";
|
||||
|
||||
class FakeRunner implements CommandRunner {
|
||||
readonly calls: Array<{ command: string; args: string[] }> = [];
|
||||
existing: Record<string, string | undefined> = {};
|
||||
|
||||
async run(command: string, args: string[]): Promise<CommandResult> {
|
||||
this.calls.push({ command, args });
|
||||
const agent = command === "codex" ? "codex" : "claude";
|
||||
if (args[1] === "get") {
|
||||
const value = this.existing[agent];
|
||||
return value === undefined
|
||||
? { code: 1, stdout: "", stderr: "MCP server not found" }
|
||||
: { code: 0, stdout: value, stderr: "" };
|
||||
}
|
||||
if (args[1] === "remove") {
|
||||
this.existing[agent] = undefined;
|
||||
return { code: 0, stdout: "", stderr: "" };
|
||||
}
|
||||
if (args[1] === "add") {
|
||||
const separator = args.indexOf("--");
|
||||
const commandParts = args.slice(separator + 1);
|
||||
this.existing[agent] = JSON.stringify({ transport: { command: commandParts[0], args: commandParts.slice(1) } });
|
||||
return { code: 0, stdout: "", stderr: "" };
|
||||
}
|
||||
return { code: 1, stdout: "", stderr: "unexpected command" };
|
||||
}
|
||||
}
|
||||
|
||||
const options = resolveClientOptions({ url: "https://example.test/mcp", clientId: "client", callbackPort: 48321, env: {} });
|
||||
const cliEntry = path.resolve("dist", "cli.js");
|
||||
const nodePath = path.resolve("bin", "node.exe");
|
||||
|
||||
test("Codex and Claude adapters add, detect idempotency, replace conflicts, remove, and dry-run", async () => {
|
||||
const runner = new FakeRunner();
|
||||
assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "added" }]);
|
||||
assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "unchanged" }]);
|
||||
runner.existing.codex = JSON.stringify({ transport: { command: "other", args: [] } });
|
||||
await assert.rejects(configureAgents({ target: "codex", options, cliEntry, nodePath, runner, confirm: async () => false }), /not changed/);
|
||||
assert.deepEqual(await configureAgents({ target: "codex", options, cliEntry, nodePath, runner, force: true }), [{ agent: "codex", action: "replaced" }]);
|
||||
assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "codex", action: "would-remove" }]);
|
||||
assert.deepEqual(await unconfigureAgents({ target: "codex", options, cliEntry, nodePath, runner }), [{ agent: "codex", action: "removed" }]);
|
||||
|
||||
assert.deepEqual(await configureAgents({ target: "claude", options, cliEntry, nodePath, runner, dryRun: true }), [{ agent: "claude", action: "would-add" }]);
|
||||
assert.equal(runner.existing.claude, undefined);
|
||||
});
|
||||
|
||||
test("OpenCode adapter preserves JSONC, creates one backup, is idempotent, and unconfigures", async () => {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "crafttable-mcp-agent-test-"));
|
||||
const filePath = path.join(directory, "opencode.json");
|
||||
await import("node:fs/promises").then(({ writeFile }) => writeFile(filePath, "{\n // keep this comment\n \"theme\": \"dark\"\n}\n", "utf8"));
|
||||
try {
|
||||
assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "added" }]);
|
||||
const configuredText = await readFile(filePath, "utf8");
|
||||
assert.match(configuredText, /keep this comment/);
|
||||
const configured = parse(configuredText) as { theme: string; mcp: Record<string, { type: string; command: string[] }> };
|
||||
assert.equal(configured.theme, "dark");
|
||||
assert.equal(configured.mcp.crafttable?.type, "local");
|
||||
assert.deepEqual(configured.mcp.crafttable?.command, launchCommand(options, cliEntry, nodePath));
|
||||
assert.match(await readFile(`${filePath}.crafttable-mcp.backup`, "utf8"), /keep this comment/);
|
||||
assert.deepEqual(await configureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "unchanged" }]);
|
||||
assert.deepEqual(await unconfigureAgents({ target: "opencode", options, cliEntry, nodePath, opencodePath: filePath }), [{ agent: "opencode", action: "removed" }]);
|
||||
const removed = parse(await readFile(filePath, "utf8")) as { mcp?: Record<string, unknown> };
|
||||
assert.equal(removed.mcp?.crafttable, undefined);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user