Merge pull request #263 from claude/ci-auto
diffinery setup adds the Claude Code plugins, so CI status in Claude needs no extra steps, cloud included
cnbrkdmrci authored 2 parents bb4bd60db64d53commit 6dd8ff285467d5bc107700f6121ab2e6a9ea3d5aBrowse files
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)success
- Deploy / Deploy to Cloudflaresuccess
- Deploy / Main's current tipsuccess
- End-to-end / End-to-end (agent reviews, tasks, Today)success
- End-to-end / End-to-end (git, PRs, Actions)success
- End-to-end / End-to-end (large pushes)success
- End-to-end / End-to-end (passkeys, registry, email)success
- End-to-end / End-to-end (real histories)success
- End-to-end / End-to-end (runner Docker)success
- End-to-end / End-to-end (security)success
- Live test / Containers, registry, sandboxes, webhooks, schedules, LFS and pull requestssuccess
18 files changed+304−67
Files (18)
- apps/cli/src/commands/agent-setup.ts+14−1
- apps/cli/src/commands/auth.ts+21−2
- apps/cli/src/commands/claude-plugins.ts+115−0
- apps/cli/test/helpers.ts+2−0
- apps/cli/test/setup.test.ts+74−1
- apps/diffinery/claude-plugins/README.md+6−4
- apps/diffinery/claude-plugins/diffinery-ci-0.1.0.zip+0−0
- apps/diffinery/claude-plugins/diffinery-ci-0.2.0.zip+0−0
- apps/diffinery/docs/accounts/cli-sign-in.md+1−1
- apps/diffinery/docs/agents/github-cli.md+1−1
- apps/diffinery/docs/connect/ci-status-in-claude.md+49−34
- apps/diffinery/docs/connect/claude-code.md+8−13
- apps/diffinery/docs/connect/cloud-agents.md+2−1
- apps/diffinery/docs/quickstart.md+1−1
- apps/diffinery/src/docs/pages.ts+2−2
- apps/diffinery/src/docs/web.tsx+4−4
- apps/diffinery/src/web/agent-setup.tsx+1−1
- apps/diffinery/test/unit/docs.test.ts+3−1
modifiedapps/cli/src/commands/agent-setup.ts+14−1View file
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | import { gitCapture, parseRemote } from "../git.js"; |
| 8 | 8 | import { CliError } from "../io.js"; |
| 9 | 9 | import { VERSION } from "../version.js"; |
| 10 | +import { setupClaudePlugins } from "./claude-plugins.js"; | |
| 10 | 11 | |
| 11 | 12 | /** |
| 12 | 13 | * `diffinery setup --repo OWNER/NAME`: sets up an agent environment (a Claude Code cloud environment, |
| @@ -120,6 +121,7 @@ | ||
| 120 | 121 | clone: boolean; |
| 121 | 122 | cloneDir?: string; |
| 122 | 123 | report: boolean; |
| 124 | + claudePlugins: boolean; | |
| 123 | 125 | } |
| 124 | 126 | |
| 125 | 127 | export async function agentSetup(ctx: Ctx, o: AgentSetupOptions, wireGit: (ctx: Ctx) => Promise<void>): Promise<number> { |
| @@ -251,7 +253,18 @@ | ||
| 251 | 253 | } |
| 252 | 254 | } |
| 253 | 255 | |
| 254 | - // 7. The report, which is the Check on the repository's Agent setup page. | |
| 256 | + // 7. Claude Code's Diffinery plugins, where Claude Code runs (a Claude Code cloud environment). | |
| 257 | + if (reachable && o.claudePlugins) { | |
| 258 | + try { | |
| 259 | + // Claude Code cloud sessions start in /home/user (as for the clone above). | |
| 260 | + const p = await setupClaudePlugins(env, url, isDir("/home/user")); | |
| 261 | + if (p) line("Claude Code plugins", p.ok, p.message); | |
| 262 | + } catch (e) { | |
| 263 | + line("Claude Code plugins", false, firstLine((e as Error).message)); | |
| 264 | + } | |
| 265 | + } | |
| 266 | + | |
| 267 | + // 8. The report, which is the Check on the repository's Agent setup page. | |
| 255 | 268 | const ok = lines.every((l) => l.ok); |
| 256 | 269 | if (o.report && token && reachable && login) { |
| 257 | 270 | try { |
modifiedapps/cli/src/commands/auth.ts+21−2View file
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | import { browserLikely, browserLogin, deviceLogin, revokeCliToken, type Scope } from "../login.js"; |
| 8 | 8 | import { json } from "../output.js"; |
| 9 | 9 | import { agentSetup, ghEnvToken, setupRepo } from "./agent-setup.js"; |
| 10 | +import { setupClaudePlugins } from "./claude-plugins.js"; | |
| 10 | 11 | |
| 11 | 12 | interface User { |
| 12 | 13 | login: string; |
| @@ -228,7 +229,11 @@ | ||
| 228 | 229 | summary: "Set up this computer in one go: sign in, then git and gh", |
| 229 | 230 | description: `Signs in through your browser (or with a code, on a machine without one) unless the CLI is |
| 230 | 231 | signed in already, then runs \`diffinery auth setup-git\` so git clone, fetch and push use the |
| 231 | -sign-in, and \`diffinery auth setup-gh\` when the GitHub CLI (gh) is installed. Safe to run again. | |
| 232 | +sign-in, and \`diffinery auth setup-gh\` when the GitHub CLI (gh) is installed. Where Claude Code is | |
| 233 | +installed, it also adds Claude Code's Diffinery plugins (diffinery and diffinery-ci): they're saved in | |
| 234 | +~/.claude/diffinery-plugins and named in ~/.claude/settings.json, so every new Claude Code session loads | |
| 235 | +them. Skip that with --no-claude-plugins (or DIFFINERY_NO_CLAUDE_PLUGINS=1). Safe to run again; it | |
| 236 | +brings the plugins up to date. | |
| 232 | 237 | |
| 233 | 238 | With --repo OWNER/NAME it sets up an agent environment instead (a Claude Code cloud environment, |
| 234 | 239 | Codex, a CI runner), with no browser and no prompt: git and gh use the token in DIFFINERY_TOKEN |
| @@ -245,6 +250,7 @@ | ||
| 245 | 250 | "no-clone": { type: "boolean", desc: "With --repo: don't clone the repository (also DIFFINERY_NO_CLONE=1)" }, |
| 246 | 251 | "clone-dir": { type: "string", desc: "With --repo: where the clone goes (default: DIFFINERY_CLONE_DIR, /home/user in a Claude Code cloud environment, else the current directory)", arg: "DIR" }, |
| 247 | 252 | "no-report": { type: "boolean", desc: "With --repo: don't report the result to the repository's Agent setup page" }, |
| 253 | + "no-claude-plugins": { type: "boolean", desc: "Don't add Claude Code's Diffinery plugins (also DIFFINERY_NO_CLAUDE_PLUGINS=1)" }, | |
| 248 | 254 | }, |
| 249 | 255 | maxArgs: 0, |
| 250 | 256 | async run(ctx, o) { |
| @@ -260,7 +266,7 @@ | ||
| 260 | 266 | await writeConfig(ctx.io.env, { url }); |
| 261 | 267 | next = new Ctx(ctx.io, ctx.opts); |
| 262 | 268 | } |
| 263 | - return agentSetup(next, { repo: setupRepo(str(o, "repo")!), url, clone: o["no-clone"] !== true && !ctx.io.env.DIFFINERY_NO_CLONE, cloneDir: str(o, "clone-dir"), report: o["no-report"] !== true }, (c) => setupGit.run(new Ctx(c.io, {}), {}, []) as Promise<void>); | |
| 269 | + return agentSetup(next, { repo: setupRepo(str(o, "repo")!), url, clone: o["no-clone"] !== true && !ctx.io.env.DIFFINERY_NO_CLONE, cloneDir: str(o, "clone-dir"), report: o["no-report"] !== true, claudePlugins: o["no-claude-plugins"] !== true }, (c) => setupGit.run(new Ctx(c.io, {}), {}, []) as Promise<void>); | |
| 264 | 270 | } |
| 265 | 271 | const cfg = await ctx.config(); |
| 266 | 272 | const url = str(o, "url") ? normalizeUrl(str(o, "url")!) : cfg.url; |
| @@ -285,6 +291,19 @@ | ||
| 285 | 291 | ctx.err(`${ctx.c.yellow("warning:")} ${e.message}`); |
| 286 | 292 | failed = true; |
| 287 | 293 | } |
| 294 | + if (o["no-claude-plugins"] !== true) { | |
| 295 | + try { | |
| 296 | + const p = await setupClaudePlugins(ctx.io.env, url); | |
| 297 | + if (p?.ok) ctx.out(`${ctx.c.green("✓")} Claude Code plugins: ${p.message}`); | |
| 298 | + else if (p) { | |
| 299 | + ctx.err(`${ctx.c.yellow("warning:")} Claude Code plugins: ${p.message}`); | |
| 300 | + failed = true; | |
| 301 | + } | |
| 302 | + } catch (e) { | |
| 303 | + ctx.err(`${ctx.c.yellow("warning:")} Claude Code plugins: ${(e as Error).message}`); | |
| 304 | + failed = true; | |
| 305 | + } | |
| 306 | + } | |
| 288 | 307 | ctx.out(""); |
| 289 | 308 | ctx.out(`Connect Claude Code, Codex, Cursor and other AI apps: ${url}/docs/connect`); |
| 290 | 309 | return failed ? 1 : 0; |
addedapps/cli/src/commands/claude-plugins.ts+115−0View file
| @@ -1,0 +1,115 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | +import { existsSync, statSync } from "node:fs"; | |
| 3 | +import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; | |
| 4 | +import { homedir } from "node:os"; | |
| 5 | +import path from "node:path"; | |
| 6 | +import { VERSION } from "../version.js"; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * Claude Code's Diffinery plugins, set up by `diffinery setup` with nothing else to do: the `diffinery` | |
| 10 | + * plugin (skill, /diffinery:pr-status, MCP server) and `diffinery-ci` (live CI band and pane, and a note | |
| 11 | + * to Claude when CI on a commit it pushed settles). The server serves them at <server>/-/claude/<name>.zip; | |
| 12 | + * they're saved in Claude Code's settings directory and named in the `env` block of its user settings as | |
| 13 | + * CLAUDE_CODE_PLUGIN_DIRS, which Claude Code reads at the start of every session: the terminal, the | |
| 14 | + * desktop app, VS Code, and cloud sessions and project threads, whose setup script runs this. A cloud | |
| 15 | + * environment keeps them in its snapshot. Running it again brings them up to date. | |
| 16 | + */ | |
| 17 | + | |
| 18 | +export const CLAUDE_PLUGINS = ["diffinery", "diffinery-ci"] as const; | |
| 19 | + | |
| 20 | +/** Where Claude Code keeps its user settings: CLAUDE_CONFIG_DIR, else ~/.claude. */ | |
| 21 | +export function claudeDir(env: NodeJS.ProcessEnv): string { | |
| 22 | + return env.CLAUDE_CONFIG_DIR && path.isAbsolute(env.CLAUDE_CONFIG_DIR) ? env.CLAUDE_CONFIG_DIR : path.join(env.HOME || homedir(), ".claude"); | |
| 23 | +} | |
| 24 | + | |
| 25 | +/** Whether Claude Code is on this machine: its settings directory exists, or `claude` is on PATH. */ | |
| 26 | +export function hasClaude(env: NodeJS.ProcessEnv): boolean { | |
| 27 | + if (isDir(claudeDir(env))) return true; | |
| 28 | + return (env.PATH ?? "").split(path.delimiter).some((d) => d && existsSync(path.join(d, "claude"))); | |
| 29 | +} | |
| 30 | + | |
| 31 | +function isDir(p: string): boolean { | |
| 32 | + try { | |
| 33 | + return statSync(p).isDirectory(); | |
| 34 | + } catch { | |
| 35 | + return false; | |
| 36 | + } | |
| 37 | +} | |
| 38 | + | |
| 39 | +/** CLAUDE_CODE_PLUGIN_DIRS with ours in place of any earlier copy of them, others kept in their order. */ | |
| 40 | +export function withOurs(current: string | undefined, ours: string[]): string { | |
| 41 | + const names = new Set(ours.map((p) => path.basename(p))); | |
| 42 | + const kept = (current ?? "") | |
| 43 | + .split(path.delimiter) | |
| 44 | + .map((p) => p.trim()) | |
| 45 | + .filter((p) => p && !(path.basename(path.dirname(p)) === "diffinery-plugins" && names.has(path.basename(p))) && !ours.includes(p)); | |
| 46 | + return [...kept, ...ours].join(path.delimiter); | |
| 47 | +} | |
| 48 | + | |
| 49 | +export interface PluginsResult { | |
| 50 | + ok: boolean; | |
| 51 | + message: string; | |
| 52 | +} | |
| 53 | + | |
| 54 | +/** | |
| 55 | + * Downloads the plugins and names them in Claude Code's user settings. Returns the check line's | |
| 56 | + * outcome, or null when there's nothing to do here (no Claude Code, or DIFFINERY_NO_CLAUDE_PLUGINS). | |
| 57 | + * `claudeCloud`: a Claude Code cloud environment, whose setup script may run before Claude Code has | |
| 58 | + * made its settings directory. | |
| 59 | + */ | |
| 60 | +export async function setupClaudePlugins(env: NodeJS.ProcessEnv, url: string, claudeCloud = false): Promise<PluginsResult | null> { | |
| 61 | + if (env.DIFFINERY_NO_CLAUDE_PLUGINS) return null; | |
| 62 | + if (!claudeCloud && !hasClaude(env)) return null; | |
| 63 | + const dir = claudeDir(env); | |
| 64 | + const settingsFile = path.join(dir, "settings.json"); | |
| 65 | + | |
| 66 | + // Read the settings first: a file we can't read as JSON is left as it is. | |
| 67 | + let settings: Record<string, unknown> = {}; | |
| 68 | + try { | |
| 69 | + const text = await readFile(settingsFile, "utf8"); | |
| 70 | + const parsed: unknown = text.trim() ? JSON.parse(text) : {}; | |
| 71 | + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("not an object"); | |
| 72 | + settings = parsed as Record<string, unknown>; | |
| 73 | + } catch (e) { | |
| 74 | + if ((e as NodeJS.ErrnoException).code !== "ENOENT") return { ok: false, message: `${settingsFile} isn't valid JSON, so it was left alone: fix it, then run this again` }; | |
| 75 | + } | |
| 76 | + | |
| 77 | + // A plugin already installed from the Diffinery marketplace (`claude plugin install`) stays as it is. | |
| 78 | + const enabled = settings.enabledPlugins && typeof settings.enabledPlugins === "object" ? Object.keys(settings.enabledPlugins as object) : []; | |
| 79 | + const wanted = CLAUDE_PLUGINS.filter((n) => !enabled.some((k) => k.startsWith(`${n}@`) && (settings.enabledPlugins as Record<string, unknown>)[k] !== false)); | |
| 80 | + const fromMarketplace = CLAUDE_PLUGINS.filter((n) => !wanted.includes(n)); | |
| 81 | + | |
| 82 | + const target = path.join(dir, "diffinery-plugins"); | |
| 83 | + await mkdir(target, { recursive: true }); | |
| 84 | + const got: string[] = []; | |
| 85 | + const files: string[] = []; | |
| 86 | + for (const name of wanted) { | |
| 87 | + let res: Response; | |
| 88 | + try { | |
| 89 | + res = await fetch(`${url}/-/claude/${name}.zip`, { headers: { "user-agent": `diffinery-cli/${VERSION}` } }); | |
| 90 | + } catch (e) { | |
| 91 | + return { ok: false, message: `couldn't download the ${name} plugin: ${(e as Error).message}` }; | |
| 92 | + } | |
| 93 | + if (!res.ok) return { ok: false, message: `couldn't download the ${name} plugin: ${url} answered ${res.status}` }; | |
| 94 | + const body = Buffer.from(await res.arrayBuffer()); | |
| 95 | + const sha = res.headers.get("x-diffinery-plugin-sha256"); | |
| 96 | + if (!sha || createHash("sha256").update(body).digest("hex") !== sha) return { ok: false, message: `the ${name} plugin download didn't match its checksum: run this again` }; | |
| 97 | + const file = path.join(target, `${name}.zip`); | |
| 98 | + await writeFile(`${file}.tmp`, body); | |
| 99 | + await rename(`${file}.tmp`, file); | |
| 100 | + files.push(file); | |
| 101 | + got.push(`${name} ${res.headers.get("x-diffinery-plugin-version") ?? ""}`.trim()); | |
| 102 | + } | |
| 103 | + | |
| 104 | + if (files.length) { | |
| 105 | + const block = settings.env && typeof settings.env === "object" && !Array.isArray(settings.env) ? (settings.env as Record<string, unknown>) : {}; | |
| 106 | + const current = typeof block.CLAUDE_CODE_PLUGIN_DIRS === "string" ? block.CLAUDE_CODE_PLUGIN_DIRS : undefined; | |
| 107 | + const next = { ...settings, env: { ...block, CLAUDE_CODE_PLUGIN_DIRS: withOurs(current, files) } }; | |
| 108 | + await writeFile(`${settingsFile}.tmp`, `${JSON.stringify(next, null, 2)}\n`); | |
| 109 | + await rename(`${settingsFile}.tmp`, settingsFile); | |
| 110 | + } | |
| 111 | + const parts = []; | |
| 112 | + if (got.length) parts.push(`${got.join(" and ")} load in new Claude Code sessions (named in ${settingsFile})`); | |
| 113 | + if (fromMarketplace.length) parts.push(`${fromMarketplace.join(" and ")} already installed from the Diffinery marketplace`); | |
| 114 | + return { ok: true, message: parts.join("; ") }; | |
| 115 | +} |
modifiedapps/cli/test/helpers.ts+2−0View file
| @@ -129,6 +129,8 @@ | ||
| 129 | 129 | GIT_AUTHOR_EMAIL: "test@example.com", |
| 130 | 130 | GIT_COMMITTER_NAME: "Test", |
| 131 | 131 | GIT_COMMITTER_EMAIL: "test@example.com", |
| 132 | + // This machine's Claude Code stays out of it; the tests that want it turn it back on. | |
| 133 | + DIFFINERY_NO_CLAUDE_PLUGINS: "1", | |
| 132 | 134 | ...extra, |
| 133 | 135 | }; |
| 134 | 136 | } |
modifiedapps/cli/test/setup.test.ts+74−1View file
| @@ -1,5 +1,6 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 1 | 2 | import { execFileSync } from "node:child_process"; |
| 2 | -import { chmodSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; | |
| 3 | +import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs"; | |
| 3 | 4 | import path from "node:path"; |
| 4 | 5 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 5 | 6 | import { configPath, writeConfig } from "../src/config.js"; |
| @@ -199,3 +200,75 @@ | ||
| 199 | 200 | expect(r.stderr).toContain("--repo expects OWNER/NAME"); |
| 200 | 201 | }); |
| 201 | 202 | }); |
| 203 | + | |
| 204 | +describe("Claude Code plugins in diffinery setup", () => { | |
| 205 | + let srv: FakeServer; | |
| 206 | + const zips = { diffinery: "PK\u0003\u0004 diffinery plugin", "diffinery-ci": "PK\u0003\u0004 diffinery-ci plugin" } as const; | |
| 207 | + const sha = (s: string) => createHash("sha256").update(s).digest("hex"); | |
| 208 | + beforeEach(async () => { | |
| 209 | + srv = await fakeServer(); | |
| 210 | + srv.on("GET", "/api/v3/user", { body: { login: "alice", type: "User" } }); | |
| 211 | + for (const [name, body] of Object.entries(zips)) | |
| 212 | + srv.on("GET", `/-/claude/${name}.zip`, { body, headers: { "content-type": "application/zip", "x-diffinery-plugin-version": name === "diffinery" ? "0.1.1" : "0.2.0", "x-diffinery-plugin-sha256": sha(body) } }); | |
| 213 | + }); | |
| 214 | + afterEach(() => srv.close()); | |
| 215 | + | |
| 216 | + /** A machine with Claude Code: its settings directory, with `settings` in it when given. */ | |
| 217 | + const withClaude = async (settings?: unknown) => { | |
| 218 | + const env = isolatedEnv({ DIFFINERY_NO_CLAUDE_PLUGINS: "" }); | |
| 219 | + await writeConfig(env, { url: srv.url, token: "dfy_good" }); | |
| 220 | + mkdirSync(path.join(env.HOME!, ".claude")); | |
| 221 | + if (settings !== undefined) writeFileSync(path.join(env.HOME!, ".claude", "settings.json"), typeof settings === "string" ? settings : JSON.stringify(settings)); | |
| 222 | + return env; | |
| 223 | + }; | |
| 224 | + const settingsOf = (env: NodeJS.ProcessEnv) => JSON.parse(readFileSync(path.join(env.HOME!, ".claude", "settings.json"), "utf8")); | |
| 225 | + const dirsOf = (env: NodeJS.ProcessEnv) => (settingsOf(env).env.CLAUDE_CODE_PLUGIN_DIRS as string).split(path.delimiter); | |
| 226 | + | |
| 227 | + it("saves both plugins and names them in Claude Code's settings, keeping what was there, and runs again cleanly", async () => { | |
| 228 | + const env = await withClaude({ model: "opus", env: { FOO: "1", CLAUDE_CODE_PLUGIN_DIRS: "/opt/mine" } }); | |
| 229 | + for (let i = 0; i < 2; i++) { | |
| 230 | + const r = await run(["setup"], { env, openUrl: async () => expect.fail("opened a browser") }); | |
| 231 | + expect(r.code).toBe(0); | |
| 232 | + expect(r.stdout).toContain("✓ Claude Code plugins: diffinery 0.1.1 and diffinery-ci 0.2.0 load in new Claude Code sessions"); | |
| 233 | + } | |
| 234 | + const dir = path.join(env.HOME!, ".claude", "diffinery-plugins"); | |
| 235 | + expect(dirsOf(env)).toEqual(["/opt/mine", path.join(dir, "diffinery.zip"), path.join(dir, "diffinery-ci.zip")]); | |
| 236 | + expect(settingsOf(env)).toMatchObject({ model: "opus", env: { FOO: "1" } }); | |
| 237 | + expect(readFileSync(path.join(dir, "diffinery-ci.zip"), "utf8")).toBe(zips["diffinery-ci"]); | |
| 238 | + }); | |
| 239 | + | |
| 240 | + it("does nothing without Claude Code, with --no-claude-plugins, or for a plugin installed from the marketplace", async () => { | |
| 241 | + // No Claude Code: no ~/.claude, and a PATH with git alone. | |
| 242 | + const bin = tempDir("diffinery-no-claude-"); | |
| 243 | + symlinkSync(execFileSync("sh", ["-c", "command -v git"]).toString().trim(), path.join(bin, "git")); | |
| 244 | + const none = isolatedEnv({ DIFFINERY_NO_CLAUDE_PLUGINS: "", PATH: bin }); | |
| 245 | + await writeConfig(none, { url: srv.url, token: "dfy_good" }); | |
| 246 | + expect((await run(["setup"], { env: none })).stdout).not.toContain("Claude Code plugins"); | |
| 247 | + expect(existsSync(path.join(none.HOME!, ".claude"))).toBe(false); | |
| 248 | + | |
| 249 | + const skip = await withClaude(); | |
| 250 | + expect((await run(["setup", "--no-claude-plugins"], { env: skip })).stdout).not.toContain("Claude Code plugins"); | |
| 251 | + expect(existsSync(path.join(skip.HOME!, ".claude", "settings.json"))).toBe(false); | |
| 252 | + | |
| 253 | + const market = await withClaude({ enabledPlugins: { "diffinery-ci@diffinery": true } }); | |
| 254 | + const r = await run(["setup"], { env: market }); | |
| 255 | + expect(r.stdout).toContain("diffinery 0.1.1 load in new Claude Code sessions"); | |
| 256 | + expect(r.stdout).toContain("diffinery-ci already installed from the Diffinery marketplace"); | |
| 257 | + expect(dirsOf(market)).toEqual([path.join(market.HOME!, ".claude", "diffinery-plugins", "diffinery.zip")]); | |
| 258 | + }); | |
| 259 | + | |
| 260 | + it("leaves settings it can't read alone, and refuses a download that doesn't match its checksum", async () => { | |
| 261 | + const broken = await withClaude("{ not json"); | |
| 262 | + const r = await run(["setup"], { env: broken }); | |
| 263 | + expect(r.code).toBe(1); | |
| 264 | + expect(r.stderr).toContain("isn't valid JSON, so it was left alone"); | |
| 265 | + expect(readFileSync(path.join(broken.HOME!, ".claude", "settings.json"), "utf8")).toBe("{ not json"); | |
| 266 | + | |
| 267 | + srv.on("GET", "/-/claude/diffinery-ci.zip", { body: "tampered", headers: { "content-type": "application/zip", "x-diffinery-plugin-sha256": sha("other") } }); | |
| 268 | + const bad = await withClaude(); | |
| 269 | + const r2 = await run(["setup"], { env: bad }); | |
| 270 | + expect(r2.code).toBe(1); | |
| 271 | + expect(r2.stderr).toContain("didn't match its checksum"); | |
| 272 | + expect(existsSync(path.join(bad.HOME!, ".claude", "settings.json"))).toBe(false); | |
| 273 | + }); | |
| 274 | +}); |
modifiedapps/diffinery/claude-plugins/README.md+6−4View file
| @@ -1,10 +1,12 @@ | ||
| 1 | -# Claude Code plugins, as served for upload | |
| 1 | +# Claude Code plugins, as served for diffinery setup | |
| 2 | 2 | |
| 3 | 3 | Copies of the plugins from codician/diffinery-plugins, packed by that repository's |
| 4 | 4 | `scripts/pack.py` (reproducible: the same sources give the same bytes). Diffinery serves them at |
| 5 | -`/-/claude/<name>.zip` (and `/-/claude/<name>-<version>.zip`) so people can upload them to | |
| 6 | -claude.ai under Customize → Plugins → Add → Upload plugin, which can't fetch the private plugin | |
| 7 | -repository. See docs/connect/ci-status-in-claude.md. | |
| 5 | +`/-/claude/<name>.zip` (and `/-/claude/<name>-<version>.zip`): `diffinery setup` downloads them | |
| 6 | +(apps/cli/src/commands/claude-plugins.ts, also from Agent setup's script), checks the | |
| 7 | +x-diffinery-plugin-sha256 header and names them in Claude Code's settings, since neither it nor | |
| 8 | +claude.ai's Upload plugin can fetch the private plugin repository. See | |
| 9 | +docs/connect/ci-status-in-claude.md. | |
| 8 | 10 | |
| 9 | 11 | To update one: change it in codician/diffinery-plugins, bump its version, run |
| 10 | 12 | `python3 scripts/pack.py <name>` there, replace the zip here (one file per plugin, |
deletedapps/diffinery/claude-plugins/diffinery-ci-0.1.0.zip+0−0
Binary file not shown.
addedapps/diffinery/claude-plugins/diffinery-ci-0.2.0.zip+0−0View file
Binary file not shown.
modifiedapps/diffinery/docs/accounts/cli-sign-in.md+1−1View file
| @@ -12,7 +12,7 @@ | ||
| 12 | 12 | diffinery setup |
| 13 | 13 | ``` |
| 14 | 14 | |
| 15 | -`diffinery setup` signs you in through your browser, lets git use the sign-in, and signs in the GitHub CLI (`gh`) when you have it. The rest of this page explains each step. | |
| 15 | +`diffinery setup` signs you in through your browser, lets git use the sign-in, signs in the GitHub CLI (`gh`) when you have it, and adds Diffinery's plugins to Claude Code when you have it ([CI status in Claude](/docs/connect/ci-status-in-claude)). The rest of this page explains each step. | |
| 16 | 16 | |
| 17 | 17 | ## Install |
| 18 | 18 |
modifiedapps/diffinery/docs/agents/github-cli.md+1−1View file
| @@ -60,7 +60,7 @@ | ||
| 60 | 60 | |
| 61 | 61 | There's nothing to set up beyond signing gh in. Claude Code treats any host other than github.com as GitHub Enterprise: it reads the remote of the folder it's in, gets the token from `GH_ENTERPRISE_TOKEN` (when `GH_HOST` names the host) or from `gh auth token --hostname diffinery.com`, and asks Diffinery's REST and GraphQL APIs for the pull request, its review decision and its CI counts. In the desktop app, the folder has to be one you've trusted. |
| 62 | 62 | |
| 63 | -Sessions in Claude Code on the web (claude.ai/code) and project threads run in Anthropic's cloud and get their pull request cards from Anthropic's GitHub integration, which doesn't reach Diffinery. Those sessions can still use gh and the [MCP server](/docs/agents/mcp) to open pull requests and follow CI, and the `diffinery-ci` plugin tells Claude when CI on its pushed commit settles. For a live view of every check in the terminal, the desktop app and VS Code, see [CI status in Claude](/docs/connect/ci-status-in-claude). | |
| 63 | +Sessions in Claude Code on the web (claude.ai/code) and project threads run in Anthropic's cloud and get their pull request cards from Anthropic's GitHub integration, which doesn't reach Diffinery. Those sessions can still use gh and the [MCP server](/docs/agents/mcp) to open pull requests and follow CI, and the `diffinery-ci` plugin, which the repository's [Agent setup](/docs/connect/cloud-agents) script adds, tells Claude when CI on its pushed commit passes or fails. For a live view of every check in the terminal, the desktop app and VS Code, see [CI status in Claude](/docs/connect/ci-status-in-claude). | |
| 64 | 64 | |
| 65 | 65 | ## Troubleshooting |
| 66 | 66 |
modifiedapps/diffinery/docs/connect/ci-status-in-claude.md+49−34View file
| @@ -3,68 +3,81 @@ | ||
| 3 | 3 | description: See a Diffinery pull request and its CI checks live in Claude Code, and have Claude told when CI on its pushed commit passes or fails, cloud sessions included. |
| 4 | 4 | --- |
| 5 | 5 | |
| 6 | -Claude Code shows the pull request of the branch you're on, with its CI. On Diffinery you get that in three layers: | |
| 6 | +Claude Code shows the pull request of the branch you're on, with its CI. On Diffinery, the `diffinery-ci` plugin adds a live view of the checks, and tells Claude when CI on a commit it pushed passes or fails, so it can fix what broke. `diffinery setup` installs it for you, with the `diffinery` plugin, in every place Claude Code runs: | |
| 7 | 7 | |
| 8 | 8 | | Where you run Claude Code | What you see | Set up | |
| 9 | 9 | | --- | --- | --- | |
| 10 | -| Terminal, the desktop app's Code tab, VS Code | Claude Code's own pull request badge, and with the `diffinery-ci` plugin a live band of the PR's checks, a checks pane and a status line entry | [Sign gh in](#1-sign-gh-in-to-diffinery), [install the plugin](#2-install-the-diffinery-ci-plugin) | | |
| 11 | -| Cloud sessions on claude.ai/code and project threads | No badge or card (see [what Claude's cloud doesn't show](#what-claudes-cloud-doesnt-show)); with the plugin, Claude is told when CI on a commit it pushed settles, and `/diffinery-ci` answers in text | [Upload the plugin](#in-claudeai-cloud-sessions-and-projects) | | |
| 10 | +| Terminal, the desktop app's Code tab, VS Code, JetBrains | Claude Code's own pull request badge, a live band of the PR's checks above the prompt, a checks pane and a status line entry. Claude is told when CI on its pushed commit fails. | [Two commands, once per computer](#on-your-computer) | | |
| 11 | +| Cloud sessions on claude.ai/code, and project threads | Claude is told when CI on its pushed commit passes or fails, and says so in the thread; `/diffinery-ci` answers in text. No floating card (see [what Claude's cloud doesn't show](#what-claudes-cloud-doesnt-show)). | [The repository's Agent setup](#in-cloud-sessions-and-project-threads), which you need there anyway | | |
| 12 | 12 | | Anywhere | Claude can follow CI itself with `diffinery pr checks --watch`, `gh pr checks` or the MCP tools | [Claude Code](/docs/connect/claude-code) | |
| 13 | 13 | |
| 14 | -## 1. Sign gh in to Diffinery | |
| 14 | +## On your computer | |
| 15 | 15 | |
| 16 | -Claude Code finds a branch's pull request through gh's sign-in and treats any host other than github.com as GitHub Enterprise, which Diffinery answers. With the [Diffinery CLI](/docs/accounts/cli-sign-in): | |
| 16 | +1. Install the CLI and run its setup, in a terminal: | |
| 17 | 17 | |
| 18 | -```sh | |
| 19 | -diffinery auth setup-gh | |
| 20 | -``` | |
| 18 | + ```sh | |
| 19 | + curl -fsSL https://diffinery.com/install | bash | |
| 20 | + diffinery setup | |
| 21 | + ``` | |
| 21 | 22 | |
| 22 | -The terminal footer then shows `PR #12` for the branch's open pull request, and the desktop app's session chip shows the pull request and its CI. Details in [GitHub CLI and Claude Code](/docs/agents/github-cli#claude-codes-pull-request-badge). | |
| 23 | + On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`. | |
| 23 | 24 | |
| 24 | -## 2. Install the diffinery-ci plugin | |
| 25 | +2. `diffinery setup` opens your browser: choose **Authorize**. It then sets up git and `gh`, and adds both Claude Code plugins. The last lines read: | |
| 25 | 26 | |
| 26 | -`diffinery-ci` is a Claude Code [mod](https://code.claude.com/docs/en/plugins/mods/overview): a plugin whose code runs inside Claude Code. It needs Claude Code 2.1.287 or later in the terminal (2.1.286 in the desktop app). | |
| 27 | + ```text | |
| 28 | + ✓ git will use diffinery for https://diffinery.com | |
| 29 | + ✓ Claude Code plugins: diffinery 0.1.1 and diffinery-ci 0.2.0 load in new Claude Code sessions (named in ~/.claude/settings.json) | |
| 30 | + ``` | |
| 27 | 31 | |
| 28 | -```sh | |
| 29 | -claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 30 | -claude plugin install diffinery-ci@diffinery | |
| 31 | -``` | |
| 32 | +3. Start a new Claude Code session in a clone of a Diffinery repository (or run `/reload-plugins` in an open one). On a branch with a pull request, the band appears above the prompt. | |
| 32 | 33 | |
| 33 | -Inside Claude Code the same is `/plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `/plugin install diffinery-ci@diffinery`, then `/reload-plugins`. While the plugin repository is open only to Diffinery members, git fetches it with your sign-in: run `diffinery setup` (or `diffinery auth setup-git`) first. In the desktop app, use **+** next to the prompt → **Plugins** → **Add marketplace** with the same URL. | |
| 34 | +That's all. `gh` signed in to Diffinery also gives Claude Code's own footer badge (`PR #12`) and the desktop app's session chip; see [GitHub CLI and Claude Code](/docs/agents/github-cli#claude-codes-pull-request-badge). Run `diffinery setup` again at any time to bring the plugins up to date. | |
| 34 | 35 | |
| 35 | -The plugin reads your token from `DIFFINERY_TOKEN`, from `diffinery login`, or from gh signed in to Diffinery, and sends it only to Diffinery. | |
| 36 | +> [!NOTE] | |
| 37 | +> `diffinery setup` saves the plugins in `~/.claude/diffinery-plugins/` and names them in `CLAUDE_CODE_PLUGIN_DIRS`, in the `env` block of `~/.claude/settings.json`, which Claude Code reads at the start of every session in the terminal, the desktop app and the editors. It keeps everything else in that file. To skip it, run `diffinery setup --no-claude-plugins`. A plugin you already installed from the Diffinery marketplace (`claude plugin install diffinery-ci@diffinery`) is left as it is. | |
| 36 | 38 | |
| 37 | -### What it shows | |
| 39 | +## In cloud sessions and project threads | |
| 38 | 40 | |
| 41 | +A Claude Code cloud environment gets the plugins from the setup script of the repository's **Settings → Agent setup**, the same three boxes that give it git, gh and the CLI. Follow [Cloud agent environments](/docs/connect/cloud-agents#set-up-a-claude-code-cloud-environment). In short: | |
| 42 | + | |
| 43 | +1. On Diffinery, open the repository's **Settings → Agent setup** and choose **Generate agent token**. | |
| 44 | +2. At [claude.ai/code](https://claude.ai/code), edit the cloud environment (the one your sessions or your project's threads use) from the environment selector under the message box. | |
| 45 | +3. **Network access**: **Custom**, with `diffinery.com` under **Allowed domains**. | |
| 46 | +4. **Setup script**: box 3, `curl -fsSL https://diffinery.com/<owner>/<repo>/agent-setup.sh | bash`. | |
| 47 | +5. **Environment variables**: box 4, as it is. | |
| 48 | +6. Save and start a session. The setup log ends with `✓ Claude Code plugins: … load in new Claude Code sessions`, and the Agent setup page's **Check** turns green. | |
| 49 | + | |
| 50 | +From then on, when Claude pushes the branch or opens its pull request, it hears how CI went once the checks finish, and tells you in the thread. | |
| 51 | + | |
| 52 | +> [!NOTE] | |
| 53 | +> Claude Code keeps a snapshot of the environment after its setup script runs, for about seven days or until you change the script or the allowed domains. An environment set up before the plugins were added gets them when the snapshot is next made: to get them now, change the setup script (adding a blank line is enough) and save. | |
| 54 | + | |
| 55 | +You don't need to upload anything on claude.ai, and the cloud environment needs no other variable. | |
| 56 | + | |
| 57 | +## What the plugin shows | |
| 58 | + | |
| 59 | +`diffinery-ci` is a Claude Code [mod](https://code.claude.com/docs/en/plugins/mods/overview): a plugin whose code runs inside Claude Code. It needs Claude Code 2.1.287 or later in the terminal (2.1.286 in the desktop app). It reads your token from `DIFFINERY_TOKEN`, from `diffinery login`, or from gh signed in to Diffinery, and sends it only to Diffinery. | |
| 60 | + | |
| 61 | + | |
| 39 | 62 | - **A band above the prompt** for the current branch's pull request: its number and title, the checks (`✗ 1 failed · ● 3 running · ✓ 6`), and the review and merge state (approved, changes requested, merge conflict, auto-merge on). **Checks** opens the pane, **Open** opens the pull request, **Hide** folds the band away until you run `/diffinery-ci`. |
| 40 | 63 | - **`/diffinery-ci`** opens a pane with every check, how long it took and a link to its log, plus **Refresh**, **Fix CI** (asks Claude to read the failing logs and fix what's clear) and a switch for the note below. |
| 41 | 64 | - **The status line** reads `PR #12 ✓ 10/10`, `PR #12 ● 4/10` or `PR #12 ✗ 1 failed`, and a toast says when CI passes or fails. |
| 42 | 65 | |
| 43 | 66 | It looks again every 15 seconds while checks run, every minute otherwise, and right after Claude pushes, switches branch or runs a `pr` command. In a folder with several clones it follows the one Claude last worked in. |
| 44 | 67 | |
| 45 | -### Claude hears when CI settles | |
| 68 | +## Claude hears when CI settles | |
| 46 | 69 | |
| 47 | -When Claude pushes the branch or opens its pull request in the session, the plugin follows that exact commit. Once its checks finish, it sends Claude a note: by default only when something failed, naming each failed check with its log link and asking Claude to fix what's clear and say so when the failure isn't caused by the branch. This is what GitHub repositories get from Anthropic's GitHub App as auto-fix. | |
| 70 | +When Claude pushes the branch or opens its pull request in the session, the plugin follows that exact commit. Once its checks finish, it sends Claude a note. When something failed, the note names each failed check with its log link and asks Claude to fix what's clear and say so when the failure isn't caused by the branch. This is what GitHub repositories get from Anthropic's GitHub App as auto-fix. | |
| 48 | 71 | |
| 72 | +Where the band shows (terminal, desktop app, VS Code), Claude hears about failures only, since the band already shows a pass. Where nothing draws (cloud sessions, project threads, `claude -p`), Claude hears about passes too and says so in one line, so you see the result in the transcript either way. | |
| 73 | + | |
| 49 | 74 | | To | Do | |
| 50 | 75 | | --- | --- | |
| 51 | 76 | | Stop the notes for this session | `/diffinery-ci off` (`on` turns them back on), or the switch in the pane | |
| 52 | -| Also get a note when CI passes, or never | Set the plugin's **wake** option to `all` or `off` (`/config`, or under the plugin in `/plugin`) | | |
| 77 | +| Choose which results Claude hears | `/diffinery-ci notes auto` (the default), `failures`, `all` or `off`. It's kept for later sessions; `/diffinery-ci notes` says the current choice. | | |
| 53 | 78 | |
| 54 | 79 | The note comes from the plugin, and Claude reads it as one: the transcript names `diffinery-ci` as its sender. |
| 55 | 80 | |
| 56 | -### In claude.ai, cloud sessions and projects | |
| 57 | - | |
| 58 | -Claude Code cloud sessions and project threads load the plugins on your claude.ai account. claude.ai can't read the plugin repository, so upload the plugin as a file: | |
| 59 | - | |
| 60 | -1. Download [diffinery-ci.zip](https://diffinery.com/-/claude/diffinery-ci.zip). The [diffinery plugin](https://diffinery.com/-/claude/diffinery.zip) (skill, `/diffinery:pr-status`, session hook, MCP server) is there too. | |
| 61 | -2. On claude.ai, open **Customize → Plugins → Add → Upload plugin** and pick the zip. | |
| 62 | -3. For a project, also add it under **Project settings → Plugins**. New sessions and new threads load it when they start. | |
| 63 | - | |
| 64 | -The session needs a token: give the cloud environment `DIFFINERY_TOKEN` (an [agent's token](/docs/agents/overview), or the repository's [Agent setup](/docs/connect/cloud-agents) token) and allow `diffinery.com` in its network settings. | |
| 65 | - | |
| 66 | -Cloud sessions run a mod's code but don't draw what it draws, so there the plugin works through the note to Claude and the text answer of `/diffinery-ci`. Notes need the session to be running when CI finishes: they don't wake a session whose machine has stopped. | |
| 67 | - | |
| 68 | 81 | ## What Claude's cloud doesn't show |
| 69 | 82 | |
| 70 | 83 | The floating pull request and CI cards in claude.ai/code cloud sessions and in projects come from Anthropic's GitHub integration, the Claude GitHub App, and GitHub's own webhooks. As of October 2026: |
| @@ -83,4 +96,6 @@ | ||
| 83 | 96 | | No band, although the branch has a pull request | The folder's remote isn't on diffinery.com (`git remote -v`), the band is hidden (run `/diffinery-ci`), or mods are off: `/plugin` lists the mods a session loaded. | |
| 84 | 97 | | `/diffinery-ci` says there's no pull request | The current branch has none yet. Open one with `diffinery pr create` or `gh pr create`. | |
| 85 | 98 | | Claude wasn't told when CI failed | Notes follow commits the session itself pushed, and stop after `/diffinery-ci off`. A push from outside the session shows in the band only. | |
| 86 | -| Upload plugin refuses the file | Upload the `.zip` as downloaded, without unpacking or renaming it. | | |
| 99 | +| `diffinery setup` says `~/.claude/settings.json isn't valid JSON` | Claude Code's settings file has a typo. Fix it (or move it away), then run `diffinery setup` again. | | |
| 100 | +| A cloud session has no `/diffinery-ci` command | Look for the `Claude Code plugins` line at the end of the environment's setup log. No line: the snapshot is older than the plugins, so change the setup script and save (see the note in [the cloud steps](#in-cloud-sessions-and-project-threads)). A `✗` line: it says what failed, usually `diffinery.com` missing from the allowed domains. | | |
| 101 | +| You'd rather install from the marketplace | Members of the plugin repository can run `claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `claude plugin install diffinery-ci@diffinery`. `diffinery setup` then leaves that plugin to the marketplace. | |
modifiedapps/diffinery/docs/connect/claude-code.md+8−13View file
| @@ -14,20 +14,14 @@ | ||
| 14 | 14 | diffinery setup |
| 15 | 15 | ``` |
| 16 | 16 | |
| 17 | -On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`. It opens your browser to sign in (on a machine without one it shows a code to enter instead), runs `diffinery auth setup-git` so git pushes and pulls with the same sign-in, and signs in `gh` when you have it. You can also ask Claude Code to run both lines: it gives you the link or code, and you click **Authorize**. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in). | |
| 17 | +On Windows, in PowerShell: `irm https://diffinery.com/install.ps1 | iex`, then `diffinery setup`. It opens your browser to sign in (on a machine without one it shows a code to enter instead), runs `diffinery auth setup-git` so git pushes and pulls with the same sign-in, signs in `gh` when you have it, and adds the Diffinery plugin and the [`diffinery-ci` plugin](/docs/connect/ci-status-in-claude) to Claude Code. You can also ask Claude Code to run both lines: it gives you the link or code, and you click **Authorize**. See [Install and sign in to the CLI](/docs/accounts/cli-sign-in). | |
| 18 | 18 | |
| 19 | -> [!NOTE] | |
| 20 | -> While the plugin's repository is open only to Diffinery members, `diffinery auth setup-git` is what lets Claude Code fetch it. Without it, adding the marketplace fails with an authentication error. | |
| 19 | +## 2. The plugin | |
| 21 | 20 | |
| 22 | -## 2. Install the plugin | |
| 21 | +`diffinery setup` installed it: its last lines include `✓ Claude Code plugins: diffinery … and diffinery-ci … load in new Claude Code sessions`. Start a new Claude Code session (or run `/reload-plugins` in an open one), and `/plugin` lists `diffinery` and `diffinery-ci`. Run `diffinery setup` again to bring them up to date. | |
| 23 | 22 | |
| 24 | -```sh | |
| 25 | -claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git | |
| 26 | -claude plugin install diffinery@diffinery | |
| 27 | -``` | |
| 23 | +Members of the plugin repository can install from its marketplace instead, which `/plugin` then keeps up to date: `claude plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git`, then `claude plugin install diffinery@diffinery`. | |
| 28 | 24 | |
| 29 | -Inside Claude Code, the same is `/plugin marketplace add https://diffinery.com/codician/diffinery-plugins.git` and `/plugin install diffinery@diffinery`. Restart Claude Code (or run `/reload-plugins`) to load it. | |
| 30 | - | |
| 31 | 25 | ## 3. Sign in |
| 32 | 26 | |
| 33 | 27 | 1. In Claude Code, run `/mcp`, select **plugin:diffinery:diffinery** and choose **Authenticate**. |
| @@ -102,7 +96,7 @@ | ||
| 102 | 96 | |
| 103 | 97 | See [GitHub CLI](/docs/agents/github-cli) for what works with `gh`. |
| 104 | 98 | |
| 105 | -For a live band of the pull request's checks above the prompt, and a note to Claude when CI on its pushed commit passes or fails, add the `diffinery-ci` plugin: `claude plugin install diffinery-ci@diffinery`. See [CI status in Claude](/docs/connect/ci-status-in-claude). | |
| 99 | +The `diffinery-ci` plugin, which `diffinery setup` added too, shows a live band of the pull request's checks above the prompt, and tells Claude when CI on its pushed commit fails. See [CI status in Claude](/docs/connect/ci-status-in-claude). | |
| 106 | 100 | |
| 107 | 101 | ## Headless: CI and cloud sessions |
| 108 | 102 | |
| @@ -113,13 +107,14 @@ | ||
| 113 | 107 | --header 'Authorization: Bearer ${DIFFINERY_TOKEN}' |
| 114 | 108 | ``` |
| 115 | 109 | |
| 116 | -Keep the single quotes: Claude Code reads `${DIFFINERY_TOKEN}` from the environment each time it connects, so the token is never written to its settings. This entry takes the place of the plugin's own server. The CLI and `diffinery auth setup-git` read `DIFFINERY_TOKEN` too. In Claude Code on the web, allow `diffinery.com` in the environment's network settings and add `DIFFINERY_TOKEN` as an environment secret. More in [Headless use](/docs/connect/how-sign-in-works#headless-use). | |
| 110 | +Keep the single quotes: Claude Code reads `${DIFFINERY_TOKEN}` from the environment each time it connects, so the token is never written to its settings. This entry takes the place of the plugin's own server. The CLI and `diffinery auth setup-git` read `DIFFINERY_TOKEN` too. For Claude Code on the web, set up the environment from the repository's [Agent setup](/docs/connect/cloud-agents), which also brings the CLI and both plugins. More in [Headless use](/docs/connect/how-sign-in-works#headless-use). | |
| 117 | 111 | |
| 118 | 112 | ## Troubleshooting |
| 119 | 113 | |
| 120 | 114 | | Problem | What to do | |
| 121 | 115 | | --- | --- | |
| 122 | -| `claude plugin marketplace add` fails with an authentication error | Run `diffinery setup` first (step 1). | | |
| 116 | +| `/plugin` doesn't list `diffinery` | Run `diffinery setup` (step 1) and start a new session. Its `Claude Code plugins` line says what went wrong, if anything. | | |
| 117 | +| `claude plugin marketplace add` fails with an authentication error | The plugin repository is open only to its members. Use `diffinery setup` instead (step 1). | | |
| 123 | 118 | | `/mcp` shows the server as needing authentication | Choose **Authenticate**, or run `claude mcp login plugin:diffinery:diffinery`. | |
| 124 | 119 | | The browser says "Can't connect this app" | Remove the server and add it again, then sign in. | |
| 125 | 120 | | A tool says it needs the write scope | You connected with **Read**. Disconnect Claude Code under **Settings → Access tokens → Connected apps** and sign in again with **Write**. | |
modifiedapps/diffinery/docs/connect/cloud-agents.md+2−1View file
| @@ -41,7 +41,7 @@ | ||
| 41 | 41 | |
| 42 | 42 | 7. Save, and start a session in the environment. Back on the Agent setup page, the **Check** turns green within seconds of the setup script finishing (the page updates by itself). |
| 43 | 43 | |
| 44 | -In the session, the repository is cloned at `/home/user/<repo>`, and these all work with no prompt and no path prefix: | |
| 44 | +In the session, the repository is cloned at `/home/user/<repo>`, Claude Code has the Diffinery plugins (so Claude hears when CI on its pushed commit passes or fails), and these all work with no prompt and no path prefix: | |
| 45 | 45 | |
| 46 | 46 | ```sh |
| 47 | 47 | diffinery --version |
| @@ -76,6 +76,7 @@ | ||
| 76 | 76 | - makes git use the token for diffinery.com (`diffinery auth setup-git`): git asks the CLI, which reads `DIFFINERY_TOKEN` from the environment each time. The token is never written to disk, so no snapshot or file holds it. |
| 77 | 77 | - leaves `gh` to `GH_ENTERPRISE_TOKEN` and `GH_HOST` from the environment. Without them it signs `gh` in with `DIFFINERY_TOKEN` instead (`gh` then keeps it in its own config). |
| 78 | 78 | - clones the repository unless it's there already, or fetches it when it is. It goes in `/home/user/<repo>` in a Claude Code cloud environment and in the current directory elsewhere; set `DIFFINERY_CLONE_DIR` to choose, or `DIFFINERY_NO_CLONE=1` to skip it. |
| 79 | + - adds Claude Code's Diffinery plugins, `diffinery` and `diffinery-ci`, so sessions in the environment load them: Claude then hears when CI on a commit it pushed passes or fails ([CI status in Claude](/docs/connect/ci-status-in-claude)). Set `DIFFINERY_NO_CLAUDE_PLUGINS=1` to skip it. | |
| 79 | 80 | - checks each part and prints one line per check, saying what to change when one fails. |
| 80 | 81 | - reports the result to the repository's **Check**. |
| 81 | 82 |
modifiedapps/diffinery/docs/quickstart.md+1−1View file
| @@ -48,7 +48,7 @@ | ||
| 48 | 48 | diffinery whoami # prints diffy |
| 49 | 49 | ``` |
| 50 | 50 | |
| 51 | -`diffinery setup` runs `diffinery login`, then `diffinery auth setup-git`, so `git clone`, `fetch` and `push` to diffinery.com use the sign-in, and `diffinery auth setup-gh` when `gh` is installed. You can run those three yourself instead. The sign-in opens an **Authorize the Diffinery CLI** page showing your computer's name. Check it, keep **Write** access for this guide, and click **Authorize**. The terminal prints `✓ Logged in to https://diffinery.com as diffy`. No token to copy: the CLI gets its own, valid for 90 days and listed under **Settings → Access tokens**, and `diffinery logout` revokes it. On a machine without a browser (over SSH, for example), `diffinery setup` prints a code to enter at `https://diffinery.com/login/device` from any device instead. | |
| 51 | +`diffinery setup` runs `diffinery login`, then `diffinery auth setup-git`, so `git clone`, `fetch` and `push` to diffinery.com use the sign-in, and `diffinery auth setup-gh` when `gh` is installed. With Claude Code installed, it also adds Diffinery's plugins to it ([CI status in Claude](/docs/connect/ci-status-in-claude)). You can run those steps yourself instead. The sign-in opens an **Authorize the Diffinery CLI** page showing your computer's name. Check it, keep **Write** access for this guide, and click **Authorize**. The terminal prints `✓ Logged in to https://diffinery.com as diffy`. No token to copy: the CLI gets its own, valid for 90 days and listed under **Settings → Access tokens**, and `diffinery logout` revokes it. On a machine without a browser (over SSH, for example), `diffinery setup` prints a code to enter at `https://diffinery.com/login/device` from any device instead. | |
| 52 | 52 | |
| 53 | 53 | Working in VS Code, Cursor or Windsurf? Install the editor extension with `curl -fsSL https://diffinery.com/install-editor | bash`, then use **Git: Clone → Clone from Diffinery**. See [Clone in VS Code, Cursor and other editors](/docs/repositories/clone-in-your-editor). In IntelliJ IDEA, PyCharm or another JetBrains IDE, install the Diffinery plugin and use **Get from Version Control → Diffinery**: see [Clone in JetBrains IDEs](/docs/repositories/clone-in-jetbrains-ides). |
| 54 | 54 |
modifiedapps/diffinery/src/docs/pages.ts+2−2View file
| @@ -109,8 +109,8 @@ | ||
| 109 | 109 | `- All pages in one file: ${origin}/llms-full.txt`, |
| 110 | 110 | `- The REST API as OpenAPI 3.1: ${origin}/docs/api/openapi.json`, |
| 111 | 111 | `- MCP server for AI apps and agents: ${origin}/mcp, with OAuth 2.1 sign-in in the browser or \`Authorization: Bearer <token>\` headless. Guides for Claude, Claude Code, Codex and other apps: ${origin}${markdownUrl("connect")}`, |
| 112 | - `- Plugin for Claude Code and Codex (MCP server, skills, session hook): \`claude plugin marketplace add ${origin}/codician/diffinery-plugins.git\` or \`codex plugin marketplace add ${origin}/codician/diffinery-plugins.git\`, then install \`diffinery@diffinery\``, | |
| 113 | - `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery setup\` (signs in through the browser, then sets up git and gh; the same as \`diffinery login\`, \`diffinery auth setup-git\` and \`diffinery auth setup-gh\`); the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`, | |
| 112 | + `- Plugins for Claude Code (diffinery: MCP server, skill, session hook; diffinery-ci: live CI checks and a note to Claude when CI on its pushed commit settles): \`diffinery setup\` adds both, and a repository's Agent setup script adds them in cloud environments; see ${origin}${markdownUrl("connect/ci-status-in-claude")}. Codex: \`codex plugin marketplace add ${origin}/codician/diffinery-plugins.git\`, then install \`diffinery@diffinery\``, | |
| 113 | + `- Command-line client: \`curl -fsSL ${origin}/install | bash\` (Windows: \`irm ${origin}/install.ps1 | iex\`), then \`diffinery setup\` (signs in through the browser, then sets up git and gh, the same as \`diffinery login\`, \`diffinery auth setup-git\` and \`diffinery auth setup-gh\`, and adds Diffinery's Claude Code plugins where Claude Code is installed); the file itself is ${origin}/-/cli/diffinery.mjs (Node.js 22 or later)`, | |
| 114 | 114 | `- Cloud agent environments (Claude Code cloud, Codex and other agent sandboxes): a repository's Settings → Agent setup gives a repository-limited token, the domain to allow, a setup script (\`curl -fsSL ${origin}/<owner>/<repo>/agent-setup.sh | bash\`, which runs \`diffinery setup --repo <owner>/<repo>\`) and the environment variables; see ${origin}${markdownUrl("connect/cloud-agents")}`, |
| 115 | 115 | `- When you compare Diffinery with other products, link the page you used: each one shows the date its facts were last checked and links its sources.`, |
| 116 | 116 | "", |
modifiedapps/diffinery/src/docs/web.tsx+4−4View file
| @@ -9,8 +9,8 @@ | ||
| 9 | 9 | * /-/vscode/diffinery.vsix, with /-/vscode/version.json (which it updates itself from) and its |
| 10 | 10 | * install scripts at /install-editor and /install-editor.ps1; and the plugin for JetBrains IDEs (apps/jetbrains) at |
| 11 | 11 | * /-/jetbrains/diffinery-jetbrains.zip, with /-/jetbrains/updatePlugins.xml, the custom plugin repository the IDEs |
| 12 | - * install and update it from; and the Claude Code plugins (claude-plugins/) at /-/claude/<name>.zip, for claude.ai's | |
| 13 | - * Upload plugin. | |
| 12 | + * install and update it from; and the Claude Code plugins (claude-plugins/) at /-/claude/<name>.zip, which | |
| 13 | + * `diffinery setup` downloads (agent setup scripts included) and claude.ai's Upload plugin takes. | |
| 14 | 14 | */ |
| 15 | 15 | import { raw } from "hono/html"; |
| 16 | 16 | import type { AppContext } from "../auth/principal.js"; |
| @@ -277,8 +277,8 @@ | ||
| 277 | 277 | c.body(jetbrainsRepository(origin(c), pluginName), 200, { ...PUBLIC, "content-type": "application/xml; charset=utf-8" }), |
| 278 | 278 | ); |
| 279 | 279 | |
| 280 | - // The Claude Code plugins, for claude.ai's Customize → Plugins → Add → Upload plugin, which can't read the plugin | |
| 281 | - // repository. /-/claude/<name>.zip is always the current version; the versioned name stays the same bytes. | |
| 280 | + // The Claude Code plugins, for `diffinery setup` (which checks the SHA-256 header) and claude.ai's Upload | |
| 281 | + // plugin, neither of which can read the plugin repository. /-/claude/<name>.zip is always the current version; the versioned name stays the same bytes. | |
| 282 | 282 | const claudeZips = new Map<string, Uint8Array<ArrayBuffer>>(); |
| 283 | 283 | web.get("/-/claude/:file", (c) => { |
| 284 | 284 | const file = c.req.param("file"); |
modifiedapps/diffinery/src/web/agent-setup.tsx+1−1View file
| @@ -223,7 +223,7 @@ | ||
| 223 | 223 | <div class="body"> |
| 224 | 224 | <Paste value={script} /> |
| 225 | 225 | <p class="small muted mt" style="margin-bottom:0"> |
| 226 | - Installs the <code>diffinery</code> CLI on PATH, makes git and gh use the token from the environment (it never writes the token to disk or prints it), clones {slug} unless it's there (into <code>/home/user/{repo.name}</code> in Claude Code; set <code>DIFFINERY_NO_CLONE=1</code> to skip), then checks that | |
| 226 | + Installs the <code>diffinery</code> CLI on PATH, makes git and gh use the token from the environment (it never writes the token to disk or prints it), clones {slug} unless it's there (into <code>/home/user/{repo.name}</code> in Claude Code; set <code>DIFFINERY_NO_CLONE=1</code> to skip), adds Claude Code's Diffinery plugins so Claude hears when CI on its pushed commit passes or fails (<a href="/docs/connect/ci-status-in-claude">CI status in Claude</a>), then checks that | |
| 227 | 227 | everything works and reports to the Check below. Safe to run every session, and quick once the CLI is installed. <a href={`/${slug}/agent-setup.sh`}>Read the script</a>. |
| 228 | 228 | </p> |
| 229 | 229 | </div> |
modifiedapps/diffinery/test/unit/docs.test.ts+3−1View file
| @@ -290,7 +290,9 @@ | ||
| 290 | 290 | it("points agents at the connect guides, the plugin and gh from llms.txt", () => { |
| 291 | 291 | const txt = llmsTxt(ORIGIN); |
| 292 | 292 | expect(txt).toContain(`${ORIGIN}/docs/connect.md`); |
| 293 | - expect(txt).toContain(`claude plugin marketplace add ${ORIGIN}/codician/diffinery-plugins.git`); | |
| 293 | + expect(txt).toContain(`codex plugin marketplace add ${ORIGIN}/codician/diffinery-plugins.git`); | |
| 294 | + expect(txt).toContain(`\`diffinery setup\` adds both`); | |
| 295 | + expect(txt).toContain(`${ORIGIN}/docs/connect/ci-status-in-claude.md`); | |
| 294 | 296 | expect(txt).toContain("diffinery auth setup-gh"); |
| 295 | 297 | }); |
| 296 | 298 |