From 1a86fab702b1ee84d3a30ab0083b9a45ebc146e9 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 02:18:18 +0100 Subject: [PATCH 1/8] feat(login): add GitHub device flow auth and plugin install for init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `archgate login` command that authenticates users via GitHub's OAuth device flow (RFC 8628) and exchanges the GitHub token for an archgate plugin token via the plugins service. Tokens are stored locally in ~/.archgate/credentials. The `init` command now auto-detects stored credentials and: - For Claude Code: prints the marketplace URL with embedded token - For Cursor: downloads and extracts the cursor plugin bundle New files: - src/helpers/auth.ts — device flow, token claim, credential storage - src/helpers/plugin-install.ts — marketplace URL builder, cursor download - src/commands/login.ts — login/logout/status/refresh subcommands - tests/ — 19 new tests covering auth, login, and plugin-install Co-Authored-By: Claude Opus 4.6 --- src/cli.ts | 2 + src/commands/init.ts | 37 +++- src/commands/login.ts | 116 ++++++++++++ src/helpers/auth.ts | 265 +++++++++++++++++++++++++++ src/helpers/init-project.ts | 45 +++++ src/helpers/plugin-install.ts | 109 +++++++++++ tests/commands/login.test.ts | 43 +++++ tests/helpers/auth.test.ts | 215 ++++++++++++++++++++++ tests/helpers/plugin-install.test.ts | 27 +++ 9 files changed, 858 insertions(+), 1 deletion(-) create mode 100644 src/commands/login.ts create mode 100644 src/helpers/auth.ts create mode 100644 src/helpers/plugin-install.ts create mode 100644 tests/commands/login.test.ts create mode 100644 tests/helpers/auth.test.ts create mode 100644 tests/helpers/plugin-install.test.ts diff --git a/src/cli.ts b/src/cli.ts index 01cd1844..b9df07b1 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,6 +9,7 @@ import { registerAdrCommand } from "./commands/adr/index"; import { registerUpgradeCommand } from "./commands/upgrade"; import { registerCleanCommand } from "./commands/clean"; import { registerCheckCommand } from "./commands/check"; +import { registerLoginCommand } from "./commands/login"; import { registerMcpCommand } from "./commands/mcp"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; import { logError } from "./helpers/log"; @@ -35,6 +36,7 @@ async function main() { .description("AI governance for software development"); registerInitCommand(program); + registerLoginCommand(program); registerAdrCommand(program); registerCheckCommand(program); registerMcpCommand(program); diff --git a/src/commands/init.ts b/src/commands/init.ts index 2fb6b664..8ef00101 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,7 +1,9 @@ import type { Command } from "@commander-js/extra-typings"; -import { logError } from "../helpers/log"; +import { styleText } from "node:util"; +import { logError, logInfo, logWarn } from "../helpers/log"; import { initProject } from "../helpers/init-project"; import type { EditorTarget } from "../helpers/init-project"; +import { loadCredentials } from "../helpers/auth"; const VALID_EDITORS = ["claude", "cursor"] as const; @@ -14,6 +16,10 @@ export function registerInitCommand(program: Command) { "editor integration to configure (claude, cursor)", "claude" ) + .option( + "--install-plugin", + "install the archgate plugin (requires prior `archgate login`)" + ) .action(async (opts) => { try { const editor = opts.editor as string; @@ -24,9 +30,15 @@ export function registerInitCommand(program: Command) { process.exit(1); } + // Auto-detect: install plugin if credentials exist (unless explicitly off) + const installPlugin = + opts.installPlugin ?? (await loadCredentials()) !== null; + const result = await initProject(process.cwd(), { editor: editor as EditorTarget, + installPlugin, }); + console.log(`Initialized Archgate governance in ${result.projectRoot}`); console.log(` adrs/ - architecture decision records`); console.log(` lint/ - linter-specific rules`); @@ -35,6 +47,29 @@ export function registerInitCommand(program: Command) { } else { console.log(` .claude/ - Claude Code settings configured`); } + + // Plugin install output + if (result.plugin?.installed) { + console.log(""); + if (editor === "cursor") { + logInfo("Archgate plugin installed for Cursor."); + console.log(` ${result.plugin.detail}`); + } else { + logInfo("To install the archgate plugin in Claude Code, run:"); + console.log( + ` ${styleText("bold", "/plugin marketplace add")} ${result.plugin.detail}` + ); + console.log( + ` ${styleText("bold", "/plugin install")} archgate-governance@archgate` + ); + } + } else if (installPlugin) { + // User wanted plugin but no credentials + logWarn( + "Plugin not installed — not logged in.", + "Run `archgate login` first, then re-run `archgate init --install-plugin`." + ); + } } catch (err) { logError(err instanceof Error ? err.message : String(err)); process.exit(1); diff --git a/src/commands/login.ts b/src/commands/login.ts new file mode 100644 index 00000000..5e6df87b --- /dev/null +++ b/src/commands/login.ts @@ -0,0 +1,116 @@ +import type { Command } from "@commander-js/extra-typings"; +import { styleText } from "node:util"; +import { logError, logInfo } from "../helpers/log"; +import { + requestDeviceCode, + pollForAccessToken, + getGitHubUser, + claimArchgateToken, + saveCredentials, + loadCredentials, + clearCredentials, +} from "../helpers/auth"; + +export function registerLoginCommand(program: Command) { + const login = program + .command("login") + .description("Authenticate with GitHub to access archgate plugins"); + + login.action(async () => { + try { + // Check if already logged in + const existing = await loadCredentials(); + if (existing) { + logInfo( + `Already logged in as ${styleText("bold", existing.github_user)}.`, + "Run `archgate login --refresh` to re-authenticate." + ); + return; + } + + await runDeviceFlow(); + } catch (err) { + logError(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); + + login + .command("status") + .description("Show current authentication status") + .action(async () => { + const creds = await loadCredentials(); + if (creds) { + console.log( + `Logged in as ${styleText("bold", creds.github_user)} (since ${creds.created_at})` + ); + } else { + console.log("Not logged in. Run `archgate login` to authenticate."); + } + }); + + login + .command("logout") + .description("Remove stored credentials") + .action(async () => { + await clearCredentials(); + console.log("Logged out successfully."); + }); + + login + .command("refresh") + .description("Re-authenticate and claim a new token") + .action(async () => { + try { + await clearCredentials(); + await runDeviceFlow(); + } catch (err) { + logError(err instanceof Error ? err.message : String(err)); + process.exit(1); + } + }); +} + +async function runDeviceFlow(): Promise { + console.log("Authenticating with GitHub...\n"); + + // Step 1: Request device code + const deviceCode = await requestDeviceCode(); + + console.log( + `Open ${styleText("bold", deviceCode.verification_uri)} in your browser` + ); + console.log( + `and enter the code: ${styleText(["bold", "green"], deviceCode.user_code)}\n` + ); + console.log("Waiting for authorization..."); + + // Step 2: Poll for access token + const githubToken = await pollForAccessToken( + deviceCode.device_code, + deviceCode.interval, + deviceCode.expires_in + ); + + // Step 3: Get GitHub username + const githubUser = await getGitHubUser(githubToken); + logInfo(`GitHub user: ${styleText("bold", githubUser)}`); + + // Step 4: Exchange GitHub token for archgate plugin token + console.log("Claiming archgate plugin token..."); + const archgateToken = await claimArchgateToken(githubToken); + + // Step 5: Store credentials + await saveCredentials({ + token: archgateToken, + github_user: githubUser, + created_at: new Date().toISOString().split("T")[0], + }); + + console.log( + `\nAuthenticated as ${styleText("bold", githubUser)}. Plugin access is now available.` + ); + console.log( + "Run `archgate init` to set up a project with the archgate plugin." + ); +} diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts new file mode 100644 index 00000000..4f74ee15 --- /dev/null +++ b/src/helpers/auth.ts @@ -0,0 +1,265 @@ +/** + * auth.ts — GitHub Device Flow authentication and archgate token management. + * + * Implements RFC 8628 (OAuth 2.0 Device Authorization Grant) for CLI login, + * plus local storage of the archgate plugin token in ~/.archgate/credentials. + */ + +import { internalPath, createPathIfNotExists } from "./paths"; +import { logDebug } from "./log"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const PLUGINS_API = "https://plugins.archgate.dev"; +const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"; +const GITHUB_DEVICE_TOKEN_URL = "https://github.com/login/oauth/access_token"; +const CREDENTIALS_FILE = "credentials"; + +/** + * GitHub OAuth App client ID for the archgate CLI (public client — no secret). + * Device flow apps are public clients; the client_id is not confidential. + */ +const GITHUB_CLIENT_ID = "Ov23liZUI9Aiv2ZrSAgn"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface DeviceCodeResponse { + device_code: string; + user_code: string; + verification_uri: string; + expires_in: number; + interval: number; +} + +interface DeviceTokenSuccessResponse { + access_token: string; + token_type: string; + scope: string; +} + +interface DeviceTokenPendingResponse { + error: "authorization_pending" | "slow_down"; + error_description?: string; +} + +interface DeviceTokenErrorResponse { + error: "expired_token" | "access_denied" | string; + error_description?: string; +} + +type DeviceTokenResponse = + | DeviceTokenSuccessResponse + | DeviceTokenPendingResponse + | DeviceTokenErrorResponse; + +export interface StoredCredentials { + token: string; + github_user: string; + created_at: string; +} + +// --------------------------------------------------------------------------- +// GitHub Device Flow +// --------------------------------------------------------------------------- + +/** + * Step 1: Request a device code from GitHub. + * The user will be shown the `user_code` and asked to visit `verification_uri`. + */ +export async function requestDeviceCode(): Promise { + const response = await fetch(GITHUB_DEVICE_CODE_URL, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + scope: "read:user", + }), + signal: AbortSignal.timeout(15_000), + }); + + if (!response.ok) { + throw new Error( + `GitHub device code request failed (HTTP ${response.status})` + ); + } + + return (await response.json()) as DeviceCodeResponse; +} + +/** + * Step 2: Poll GitHub until the user authorizes (or the code expires). + * Returns the GitHub access token on success. + */ +export async function pollForAccessToken( + deviceCode: string, + interval: number, + expiresIn: number +): Promise { + const deadline = Date.now() + expiresIn * 1000; + let pollInterval = interval; + + // eslint-disable-next-line no-await-in-loop -- sequential polling is required by RFC 8628 + while (Date.now() < deadline) { + await Bun.sleep(pollInterval * 1000); + + const response = await fetch(GITHUB_DEVICE_TOKEN_URL, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + signal: AbortSignal.timeout(15_000), + }); + + if (!response.ok) { + throw new Error(`GitHub token poll failed (HTTP ${response.status})`); + } + + const data = (await response.json()) as DeviceTokenResponse; + + if ("access_token" in data) { + return data.access_token; + } + + if ("error" in data) { + if (data.error === "authorization_pending") { + continue; + } + if (data.error === "slow_down") { + pollInterval += 5; + continue; + } + // expired_token, access_denied, or other terminal error + throw new Error( + data.error_description ?? `GitHub authorization failed: ${data.error}` + ); + } + } + + throw new Error("Device code expired. Please try again."); +} + +/** + * Step 3: Get the authenticated GitHub username. + */ +export async function getGitHubUser(accessToken: string): Promise { + const response = await fetch("https://api.github.com/user", { + headers: { + Authorization: `Bearer ${accessToken}`, + "User-Agent": "archgate-cli", + }, + signal: AbortSignal.timeout(15_000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch GitHub user (HTTP ${response.status})`); + } + + const data = (await response.json()) as { login?: string }; + if (!data.login) { + throw new Error("GitHub API did not return a username"); + } + return data.login; +} + +// --------------------------------------------------------------------------- +// Token Claim +// --------------------------------------------------------------------------- + +/** + * Exchange a GitHub access token for an archgate plugin token + * via POST /api/token/claim on the plugins service. + */ +export async function claimArchgateToken(githubToken: string): Promise { + const response = await fetch(`${PLUGINS_API}/api/token/claim`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": "archgate-cli", + }, + body: JSON.stringify({ github_token: githubToken }), + signal: AbortSignal.timeout(15_000), + }); + + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { + error?: string; + }; + const message = + body.error ?? `Token claim failed (HTTP ${response.status})`; + throw new Error(message); + } + + const data = (await response.json()) as { token?: string }; + if (!data.token) { + throw new Error("Plugins service did not return a token"); + } + return data.token; +} + +// --------------------------------------------------------------------------- +// Credential Storage +// --------------------------------------------------------------------------- + +function credentialsPath(): string { + return internalPath(CREDENTIALS_FILE); +} + +/** + * Persist archgate credentials to ~/.archgate/credentials (JSON). + */ +export async function saveCredentials( + credentials: StoredCredentials +): Promise { + createPathIfNotExists(internalPath()); + await Bun.write( + credentialsPath(), + JSON.stringify(credentials, null, 2) + "\n" + ); + logDebug("Credentials saved to", credentialsPath()); +} + +/** + * Load stored archgate credentials, or null if none exist. + */ +export async function loadCredentials(): Promise { + const file = Bun.file(credentialsPath()); + if (!(await file.exists())) { + return null; + } + + try { + const data = (await file.json()) as StoredCredentials; + if (!data.token || !data.github_user) { + return null; + } + return data; + } catch { + logDebug("Failed to parse credentials file"); + return null; + } +} + +/** + * Remove stored credentials (logout). + */ +export async function clearCredentials(): Promise { + const file = Bun.file(credentialsPath()); + if (await file.exists()) { + const { unlinkSync } = await import("node:fs"); + unlinkSync(credentialsPath()); + logDebug("Credentials removed"); + } +} diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 6e616163..60eacd52 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -9,6 +9,14 @@ export type EditorTarget = "claude" | "cursor"; export interface InitOptions { editor?: EditorTarget; + /** When true, attempt to install the archgate plugin using stored credentials. */ + installPlugin?: boolean; +} + +export interface PluginResult { + installed: boolean; + /** For claude: marketplace URL; for cursor: list of extracted files */ + detail?: string; } export interface InitResult { @@ -16,6 +24,7 @@ export interface InitResult { adrsDir: string; lintDir: string; editorSettingsPath: string; + plugin?: PluginResult; } /** @@ -80,10 +89,46 @@ Archgate standardizes \`.archgate/lint/\` as the location for linter rules that ? await configureCursorSettings(projectRoot) : await configureClaudeSettings(projectRoot); + // Plugin installation (optional — requires stored credentials) + let plugin: PluginResult | undefined; + if (options?.installPlugin) { + plugin = await tryInstallPlugin(projectRoot, editor); + } + return { projectRoot, adrsDir: paths.adrsDir, lintDir: paths.lintDir, editorSettingsPath, + plugin, }; } + +/** + * Attempt to install the archgate plugin using stored credentials. + * Returns null-safe result — never throws. + */ +async function tryInstallPlugin( + projectRoot: string, + editor: EditorTarget +): Promise { + const { loadCredentials } = await import("./auth"); + const credentials = await loadCredentials(); + if (!credentials) { + return { installed: false }; + } + + if (editor === "cursor") { + const { installCursorPlugin } = await import("./plugin-install"); + const files = await installCursorPlugin(projectRoot, credentials.token); + return { + installed: true, + detail: `Extracted ${files.length} files to .cursor/`, + }; + } + + // Claude Code — generate marketplace URL for the user to paste + const { buildMarketplaceUrl } = await import("./plugin-install"); + const url = buildMarketplaceUrl(credentials); + return { installed: true, detail: url }; +} diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts new file mode 100644 index 00000000..675d6630 --- /dev/null +++ b/src/helpers/plugin-install.ts @@ -0,0 +1,109 @@ +/** + * plugin-install.ts — Download and install the archgate plugin for supported editors. + * + * - Claude Code: generates the marketplace URL (plugin installed via Claude Code slash commands) + * - Cursor: downloads cursor.tar.gz from the plugins service and extracts it + */ + +import { join } from "node:path"; +import { $ } from "bun"; +import { logDebug } from "./log"; +import type { StoredCredentials } from "./auth"; + +const PLUGINS_API = "https://plugins.archgate.dev"; + +// --------------------------------------------------------------------------- +// Claude Code — marketplace URL generation +// --------------------------------------------------------------------------- + +/** + * Build the authenticated git marketplace URL for Claude Code plugin installation. + */ +export function buildMarketplaceUrl(credentials: StoredCredentials): string { + return `https://${credentials.github_user}:${credentials.token}@plugins.archgate.dev/archgate.git`; +} + +// --------------------------------------------------------------------------- +// Cursor — download and extract plugin bundle +// --------------------------------------------------------------------------- + +/** + * Download the cursor.tar.gz from the plugins service and extract it to the project root. + * Creates/overwrites .cursor/ folder contents with the pre-built agent and skills. + */ +export async function installCursorPlugin( + projectRoot: string, + token: string +): Promise { + const response = await fetch(`${PLUGINS_API}/api/cursor`, { + headers: { + Authorization: `Bearer ${token}`, + "User-Agent": "archgate-cli", + }, + signal: AbortSignal.timeout(30_000), + }); + + if (response.status === 401) { + throw new Error( + "Plugin download unauthorized. Your token may have expired — run `archgate login refresh`." + ); + } + + if (!response.ok) { + throw new Error( + `Plugin download failed (HTTP ${response.status}). Try again later.` + ); + } + + const tarGzBuffer = await response.arrayBuffer(); + + logDebug( + `Downloaded cursor plugin archive (${Math.round(tarGzBuffer.byteLength / 1024)} KB)` + ); + + const extractedFiles = await extractTarGz( + new Uint8Array(tarGzBuffer), + projectRoot + ); + + return extractedFiles; +} + +/** + * Extract a .tar.gz buffer to a destination directory. + * Uses Bun's shell to run tar (available on macOS and Linux). + */ +async function extractTarGz( + data: Uint8Array, + destDir: string +): Promise { + // Write the archive to a temporary file + const tmpArchive = join(destDir, ".archgate-cursor-plugin.tar.gz"); + await Bun.write(tmpArchive, data); + + try { + // Ensure destination exists + await $`mkdir -p ${destDir}`.quiet(); + + // Extract using tar + const result = await $`tar -xzf ${tmpArchive} -C ${destDir}`.nothrow(); + + if (result.exitCode !== 0) { + throw new Error( + `Failed to extract plugin archive (tar exit code ${result.exitCode})` + ); + } + + // List extracted files for reporting + const listResult = await $`tar -tzf ${tmpArchive}`.nothrow().text(); + const files = listResult + .split("\n") + .map((f) => f.trim()) + .filter(Boolean); + + return files; + } finally { + // Clean up temp archive + await $`rm -f ${tmpArchive}`.nothrow().quiet(); + } +} diff --git a/tests/commands/login.test.ts b/tests/commands/login.test.ts new file mode 100644 index 00000000..67a7714c --- /dev/null +++ b/tests/commands/login.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test"; +import { Command } from "@commander-js/extra-typings"; +import { registerLoginCommand } from "../../src/commands/login"; + +describe("registerLoginCommand", () => { + test("registers 'login' as a subcommand", () => { + const program = new Command(); + registerLoginCommand(program); + const sub = program.commands.find((c) => c.name() === "login"); + expect(sub).toBeDefined(); + }); + + test("has a description", () => { + const program = new Command(); + registerLoginCommand(program); + const sub = program.commands.find((c) => c.name() === "login")!; + expect(sub.description()).toBeTruthy(); + }); + + test("registers status subcommand", () => { + const program = new Command(); + registerLoginCommand(program); + const login = program.commands.find((c) => c.name() === "login")!; + const status = login.commands.find((c) => c.name() === "status"); + expect(status).toBeDefined(); + }); + + test("registers logout subcommand", () => { + const program = new Command(); + registerLoginCommand(program); + const login = program.commands.find((c) => c.name() === "login")!; + const logout = login.commands.find((c) => c.name() === "logout"); + expect(logout).toBeDefined(); + }); + + test("registers refresh subcommand", () => { + const program = new Command(); + registerLoginCommand(program); + const login = program.commands.find((c) => c.name() === "login")!; + const refresh = login.commands.find((c) => c.name() === "refresh"); + expect(refresh).toBeDefined(); + }); +}); diff --git a/tests/helpers/auth.test.ts b/tests/helpers/auth.test.ts new file mode 100644 index 00000000..9cda859d --- /dev/null +++ b/tests/helpers/auth.test.ts @@ -0,0 +1,215 @@ +import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +/** Type-safe fetch mock — Bun's fetch type includes `preconnect` which mock() doesn't provide. */ +function mockFetch(handler: () => Promise) { + globalThis.fetch = mock(handler) as unknown as typeof fetch; +} + +describe("auth", () => { + let tempDir: string; + let originalHome: string | undefined; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), "archgate-auth-test-")); + originalHome = process.env.HOME; + process.env.HOME = tempDir; + }); + + afterEach(() => { + process.env.HOME = originalHome; + rmSync(tempDir, { recursive: true, force: true }); + }); + + describe("saveCredentials / loadCredentials", () => { + test("round-trips credentials to ~/.archgate/credentials", async () => { + const { saveCredentials, loadCredentials } = await import( + "../../src/helpers/auth" + ); + + await saveCredentials({ + token: "ag_beta_abc123", + github_user: "testuser", + created_at: "2026-01-15", + }); + + const loaded = await loadCredentials(); + expect(loaded).not.toBeNull(); + expect(loaded!.token).toBe("ag_beta_abc123"); + expect(loaded!.github_user).toBe("testuser"); + expect(loaded!.created_at).toBe("2026-01-15"); + }); + + test("returns null when no credentials file exists", async () => { + const { loadCredentials } = await import("../../src/helpers/auth"); + + const result = await loadCredentials(); + expect(result).toBeNull(); + }); + + test("returns null when credentials file is invalid JSON", async () => { + const { loadCredentials } = await import("../../src/helpers/auth"); + + const credPath = join(tempDir, ".archgate", "credentials"); + const { mkdirSync } = await import("node:fs"); + mkdirSync(join(tempDir, ".archgate"), { recursive: true }); + await Bun.write(credPath, "not-json"); + + const result = await loadCredentials(); + expect(result).toBeNull(); + }); + + test("returns null when credentials file is missing required fields", async () => { + const { loadCredentials } = await import("../../src/helpers/auth"); + + const credPath = join(tempDir, ".archgate", "credentials"); + const { mkdirSync } = await import("node:fs"); + mkdirSync(join(tempDir, ".archgate"), { recursive: true }); + await Bun.write(credPath, JSON.stringify({ token: "abc" })); + + const result = await loadCredentials(); + expect(result).toBeNull(); + }); + }); + + describe("clearCredentials", () => { + test("removes credentials file", async () => { + const { saveCredentials, clearCredentials, loadCredentials } = + await import("../../src/helpers/auth"); + + await saveCredentials({ + token: "ag_beta_abc123", + github_user: "testuser", + created_at: "2026-01-15", + }); + + await clearCredentials(); + const loaded = await loadCredentials(); + expect(loaded).toBeNull(); + }); + + test("does not throw when no credentials file exists", async () => { + const { clearCredentials } = await import("../../src/helpers/auth"); + + // Should not throw + await clearCredentials(); + }); + }); + + describe("requestDeviceCode", () => { + test("sends POST to GitHub device code endpoint", async () => { + const { requestDeviceCode } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => + Promise.resolve( + Response.json({ + device_code: "dc_123", + user_code: "ABCD-1234", + verification_uri: "https://github.com/login/device", + expires_in: 900, + interval: 5, + }) + ) + ); + + try { + const result = await requestDeviceCode(); + expect(result.device_code).toBe("dc_123"); + expect(result.user_code).toBe("ABCD-1234"); + expect(result.verification_uri).toBe("https://github.com/login/device"); + expect(result.interval).toBe(5); + } finally { + globalThis.fetch = originalFetch; + } + }); + + test("throws on non-200 response", async () => { + const { requestDeviceCode } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => + Promise.resolve(new Response("Bad Request", { status: 400 })) + ); + + try { + await expect(requestDeviceCode()).rejects.toThrow("HTTP 400"); + } finally { + globalThis.fetch = originalFetch; + } + }); + }); + + describe("getGitHubUser", () => { + test("returns login from GitHub API", async () => { + const { getGitHubUser } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => Promise.resolve(Response.json({ login: "octocat" }))); + + try { + const user = await getGitHubUser("gho_test_token"); + expect(user).toBe("octocat"); + } finally { + globalThis.fetch = originalFetch; + } + }); + + test("throws when GitHub API returns non-200", async () => { + const { getGitHubUser } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => + Promise.resolve(new Response("Unauthorized", { status: 401 })) + ); + + try { + await expect(getGitHubUser("bad_token")).rejects.toThrow("HTTP 401"); + } finally { + globalThis.fetch = originalFetch; + } + }); + }); + + describe("claimArchgateToken", () => { + test("returns token from plugins service", async () => { + const { claimArchgateToken } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => + Promise.resolve(Response.json({ token: "ag_beta_claimed_token" })) + ); + + try { + const token = await claimArchgateToken("gho_github_token"); + expect(token).toBe("ag_beta_claimed_token"); + } finally { + globalThis.fetch = originalFetch; + } + }); + + test("throws with error message from service on 403", async () => { + const { claimArchgateToken } = await import("../../src/helpers/auth"); + + const originalFetch = globalThis.fetch; + mockFetch(() => + Promise.resolve( + Response.json( + { error: "No approved signup found for this GitHub account" }, + { status: 403 } + ) + ) + ); + + try { + await expect(claimArchgateToken("gho_token")).rejects.toThrow( + "No approved signup" + ); + } finally { + globalThis.fetch = originalFetch; + } + }); + }); +}); diff --git a/tests/helpers/plugin-install.test.ts b/tests/helpers/plugin-install.test.ts new file mode 100644 index 00000000..c3154ca6 --- /dev/null +++ b/tests/helpers/plugin-install.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; +import { buildMarketplaceUrl } from "../../src/helpers/plugin-install"; + +describe("plugin-install", () => { + describe("buildMarketplaceUrl", () => { + test("builds URL with credentials embedded", () => { + const url = buildMarketplaceUrl({ + token: "ag_beta_abc123def456", + github_user: "octocat", + created_at: "2026-01-15", + }); + expect(url).toBe( + "https://octocat:ag_beta_abc123def456@plugins.archgate.dev/archgate.git" + ); + }); + + test("uses github_user as the URL username", () => { + const url = buildMarketplaceUrl({ + token: "ag_beta_token", + github_user: "my-handle", + created_at: "2026-02-01", + }); + expect(url).toContain("my-handle:"); + expect(url).toContain(":ag_beta_token@"); + }); + }); +}); From 4bb527fdf4a1bbac8852653b04e2f018e1c8122a Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 02:49:42 +0100 Subject: [PATCH 2/8] fix: use cross-platform node:fs APIs for Windows compatibility Replace shell commands (mkdir -p, rm -f) with mkdirSync/unlinkSync from node:fs in plugin-install.ts. Use static import for unlinkSync in auth.ts instead of dynamic import. tar is available on macOS, Linux, and Windows 10+. Co-Authored-By: Claude Opus 4.6 --- src/helpers/auth.ts | 5 ++--- src/helpers/plugin-install.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts index 4f74ee15..041a3849 100644 --- a/src/helpers/auth.ts +++ b/src/helpers/auth.ts @@ -5,6 +5,7 @@ * plus local storage of the archgate plugin token in ~/.archgate/credentials. */ +import { unlinkSync } from "node:fs"; import { internalPath, createPathIfNotExists } from "./paths"; import { logDebug } from "./log"; @@ -256,9 +257,7 @@ export async function loadCredentials(): Promise { * Remove stored credentials (logout). */ export async function clearCredentials(): Promise { - const file = Bun.file(credentialsPath()); - if (await file.exists()) { - const { unlinkSync } = await import("node:fs"); + if (await Bun.file(credentialsPath()).exists()) { unlinkSync(credentialsPath()); logDebug("Credentials removed"); } diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index 675d6630..1c1c9059 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -6,6 +6,7 @@ */ import { join } from "node:path"; +import { mkdirSync, unlinkSync } from "node:fs"; import { $ } from "bun"; import { logDebug } from "./log"; import type { StoredCredentials } from "./auth"; @@ -71,7 +72,7 @@ export async function installCursorPlugin( /** * Extract a .tar.gz buffer to a destination directory. - * Uses Bun's shell to run tar (available on macOS and Linux). + * Uses system tar (available on macOS, Linux, and Windows 10+). */ async function extractTarGz( data: Uint8Array, @@ -82,10 +83,9 @@ async function extractTarGz( await Bun.write(tmpArchive, data); try { - // Ensure destination exists - await $`mkdir -p ${destDir}`.quiet(); + mkdirSync(destDir, { recursive: true }); - // Extract using tar + // Extract using tar (available on macOS, Linux, and Windows 10+) const result = await $`tar -xzf ${tmpArchive} -C ${destDir}`.nothrow(); if (result.exitCode !== 0) { @@ -103,7 +103,10 @@ async function extractTarGz( return files; } finally { - // Clean up temp archive - await $`rm -f ${tmpArchive}`.nothrow().quiet(); + try { + unlinkSync(tmpArchive); + } catch { + // Ignore cleanup errors + } } } From 2c2cbdcb96e88cc9b0c8c02714ce5d8c87548fd3 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:21:57 +0100 Subject: [PATCH 3/8] feat(login): auto-install claude plugin via CLI when available Use `claude plugin marketplace add` and `claude plugin install` to automatically install the archgate plugin when the claude CLI is on PATH. Falls back to printing manual commands when the CLI is not found. Co-Authored-By: Claude Opus 4.6 --- src/commands/init.ts | 21 +++++++++---- src/helpers/init-project.ts | 20 ++++++++++-- src/helpers/plugin-install.ts | 47 ++++++++++++++++++++++++++-- tests/helpers/plugin-install.test.ts | 12 ++++++- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/commands/init.ts b/src/commands/init.ts index 8ef00101..0c0c2692 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -51,16 +51,25 @@ export function registerInitCommand(program: Command) { // Plugin install output if (result.plugin?.installed) { console.log(""); - if (editor === "cursor") { - logInfo("Archgate plugin installed for Cursor."); - console.log(` ${result.plugin.detail}`); + if (result.plugin.autoInstalled) { + logInfo( + editor === "cursor" + ? "Archgate plugin installed for Cursor." + : "Archgate plugin installed for Claude Code." + ); + if (result.plugin.detail) { + console.log(` ${result.plugin.detail}`); + } } else { - logInfo("To install the archgate plugin in Claude Code, run:"); + // Claude Code — claude CLI not found, show manual commands + logWarn( + "Claude CLI not found. To install the plugin manually, run:" + ); console.log( - ` ${styleText("bold", "/plugin marketplace add")} ${result.plugin.detail}` + ` ${styleText("bold", "claude plugin marketplace add")} ${result.plugin.detail}` ); console.log( - ` ${styleText("bold", "/plugin install")} archgate-governance@archgate` + ` ${styleText("bold", "claude plugin install")} archgate@archgate` ); } } else if (installPlugin) { diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts index 60eacd52..4187c77e 100644 --- a/src/helpers/init-project.ts +++ b/src/helpers/init-project.ts @@ -15,8 +15,10 @@ export interface InitOptions { export interface PluginResult { installed: boolean; - /** For claude: marketplace URL; for cursor: list of extracted files */ + /** For claude manual: marketplace URL; for cursor: file count summary */ detail?: string; + /** When true, plugin was auto-installed via editor CLI (no manual steps needed). */ + autoInstalled?: boolean; } export interface InitResult { @@ -123,12 +125,24 @@ async function tryInstallPlugin( const files = await installCursorPlugin(projectRoot, credentials.token); return { installed: true, + autoInstalled: true, detail: `Extracted ${files.length} files to .cursor/`, }; } - // Claude Code — generate marketplace URL for the user to paste - const { buildMarketplaceUrl } = await import("./plugin-install"); + // Claude Code — try auto-install via `claude` CLI, fall back to manual URL + const { isClaudeCliAvailable, installClaudePlugin, buildMarketplaceUrl } = + await import("./plugin-install"); + + if (await isClaudeCliAvailable()) { + try { + await installClaudePlugin(credentials); + return { installed: true, autoInstalled: true }; + } catch { + // Fall through to manual instructions + } + } + const url = buildMarketplaceUrl(credentials); return { installed: true, detail: url }; } diff --git a/src/helpers/plugin-install.ts b/src/helpers/plugin-install.ts index 1c1c9059..8058d6fc 100644 --- a/src/helpers/plugin-install.ts +++ b/src/helpers/plugin-install.ts @@ -1,7 +1,7 @@ /** * plugin-install.ts — Download and install the archgate plugin for supported editors. * - * - Claude Code: generates the marketplace URL (plugin installed via Claude Code slash commands) + * - Claude Code: auto-installs via `claude` CLI, or prints manual commands as fallback * - Cursor: downloads cursor.tar.gz from the plugins service and extracts it */ @@ -14,7 +14,7 @@ import type { StoredCredentials } from "./auth"; const PLUGINS_API = "https://plugins.archgate.dev"; // --------------------------------------------------------------------------- -// Claude Code — marketplace URL generation +// Claude Code — CLI auto-install + manual fallback // --------------------------------------------------------------------------- /** @@ -24,6 +24,49 @@ export function buildMarketplaceUrl(credentials: StoredCredentials): string { return `https://${credentials.github_user}:${credentials.token}@plugins.archgate.dev/archgate.git`; } +/** + * Check whether the `claude` CLI is available on the system PATH. + */ +export async function isClaudeCliAvailable(): Promise { + const result = await $`claude --version`.nothrow().quiet(); + return result.exitCode === 0; +} + +/** + * Install the archgate plugin via the `claude` CLI. + * + * Runs: + * claude plugin marketplace add + * claude plugin install archgate@archgate + * + * Throws on failure so the caller can fall back to manual instructions. + */ +export async function installClaudePlugin( + credentials: StoredCredentials +): Promise { + const url = buildMarketplaceUrl(credentials); + + logDebug("Adding archgate marketplace to claude CLI"); + const addResult = await $`claude plugin marketplace add ${url}` + .nothrow() + .quiet(); + if (addResult.exitCode !== 0) { + throw new Error( + `claude plugin marketplace add failed (exit ${addResult.exitCode})` + ); + } + + logDebug("Installing archgate plugin via claude CLI"); + const installResult = await $`claude plugin install archgate@archgate` + .nothrow() + .quiet(); + if (installResult.exitCode !== 0) { + throw new Error( + `claude plugin install failed (exit ${installResult.exitCode})` + ); + } +} + // --------------------------------------------------------------------------- // Cursor — download and extract plugin bundle // --------------------------------------------------------------------------- diff --git a/tests/helpers/plugin-install.test.ts b/tests/helpers/plugin-install.test.ts index c3154ca6..9a4fc696 100644 --- a/tests/helpers/plugin-install.test.ts +++ b/tests/helpers/plugin-install.test.ts @@ -1,5 +1,8 @@ import { describe, expect, test } from "bun:test"; -import { buildMarketplaceUrl } from "../../src/helpers/plugin-install"; +import { + buildMarketplaceUrl, + isClaudeCliAvailable, +} from "../../src/helpers/plugin-install"; describe("plugin-install", () => { describe("buildMarketplaceUrl", () => { @@ -24,4 +27,11 @@ describe("plugin-install", () => { expect(url).toContain(":ag_beta_token@"); }); }); + + describe("isClaudeCliAvailable", () => { + test("returns a boolean", async () => { + const result = await isClaudeCliAvailable(); + expect(typeof result).toBe("boolean"); + }); + }); }); From fd4da469cfc20c9d532568cb1d5613848d341f76 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:29:26 +0100 Subject: [PATCH 4/8] docs: improve init flow and plugin install documentation Rewrite quick start to include login step and editor flag. Add archgate login command docs. Replace old claude-code-plugin GitHub link with plugins.archgate.dev. Add dedicated Claude Code and Cursor plugin sections with install instructions. Co-Authored-By: Claude Opus 4.6 --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0704aec2..32d83c36 100644 --- a/README.md +++ b/README.md @@ -62,21 +62,25 @@ npm install -g archgate # 1. Install npm install -g archgate -# 2. Initialize governance in your project +# 2. Log in to enable plugin access (one-time) +archgate login + +# 3. Initialize governance in your project cd my-project -archgate init +archgate init # Claude Code (default) +archgate init --editor cursor # or Cursor -# 3. Edit the generated ADR to document a real decision +# 4. Edit the generated ADR to document a real decision # .archgate/adrs/ARCH-001-*.md -# 4. Add a companion .rules.ts to enforce it automatically +# 5. Add a companion .rules.ts to enforce it automatically # .archgate/adrs/ARCH-001-*.rules.ts -# 5. Run checks +# 6. Run checks archgate check ``` -`archgate init` creates the `.archgate/adrs/` directory, an example ADR with a companion rules file to show the pattern, and configures the Claude Code plugin if you use it. +`archgate init` creates the `.archgate/adrs/` directory with an example ADR and rules file, configures editor settings (`.claude/` or `.cursor/`), and installs the archgate plugin if you are logged in. If the `claude` CLI is on your PATH, the plugin is installed automatically; otherwise the command prints the manual install steps. ## Writing rules @@ -117,15 +121,37 @@ Rules receive the list of files to check (filtered by the ADR's `files` glob if ## Commands +### `archgate login` + +Authenticate with GitHub to access archgate plugins. + +```bash +archgate login # authenticate via GitHub Device Flow +archgate login status # show current auth status +archgate login logout # remove stored credentials +archgate login refresh # re-authenticate and claim a new token +``` + +Opens a browser-based GitHub Device Flow. Once authorized, an archgate plugin token is stored in `~/.archgate/credentials`. This token is used by `archgate init` to install the editor plugin. + ### `archgate init` Initialize governance in the current project. ```bash -archgate init +archgate init # Claude Code (default) +archgate init --editor cursor # Cursor +archgate init --install-plugin # force plugin install attempt ``` -Creates `.archgate/adrs/` with an example ADR and rules file, and optionally wires up the Claude Code plugin. +Creates `.archgate/adrs/` with an example ADR and rules file, configures editor settings, and installs the archgate plugin when credentials are available. + +**Plugin install behavior:** + +- If you are logged in (`archgate login`), init auto-detects your credentials and installs the plugin. +- For **Claude Code**: if the `claude` CLI is on PATH, the plugin is installed automatically via `claude plugin marketplace add` and `claude plugin install`. If not, the manual commands are printed. +- For **Cursor**: the plugin bundle is downloaded from [plugins.archgate.dev](https://plugins.archgate.dev) and extracted into `.cursor/`. +- Use `--install-plugin` to explicitly request plugin installation (useful if auto-detection is skipped). ### `archgate check` @@ -230,15 +256,36 @@ pre-commit: run: archgate check --staged ``` -## Claude Code plugin +## Editor plugins + +Archgate ships editor plugins that give AI agents a full governance workflow — reading applicable ADRs before writing code, validating changes after implementation, and capturing new patterns back into ADRs. + +Plugins are distributed from [plugins.archgate.dev](https://plugins.archgate.dev). Run `archgate login` once to authenticate, then `archgate init` handles installation. -The Claude Code plugin (`archgate:developer`) gives AI agents a full governance workflow: +### Claude Code -- Reads applicable ADRs before writing code -- Validates changes after implementation -- Captures new patterns back into ADRs +```bash +archgate login +archgate init +``` + +If the `claude` CLI is on your PATH, the plugin is installed automatically. Otherwise, run the printed commands manually: + +```bash +claude plugin marketplace add https://:@plugins.archgate.dev/archgate.git +claude plugin install archgate@archgate +``` + +Once installed, run `archgate:onboard` in Claude Code to initialize governance for your project. + +### Cursor + +```bash +archgate login +archgate init --editor cursor +``` -Install the plugin from [archgate/claude-code-plugin](https://github.com/archgate/claude-code-plugin), then run `archgate:onboard` once in your project to initialize governance. +The Cursor plugin bundle is downloaded and extracted into `.cursor/` automatically. ## Contributing From b5078e790825c62a950c24f5255d593bd39a3aa4 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:39:07 +0100 Subject: [PATCH 5/8] add initialized configs for claude and cursor --- .archgate/lint/README.md | 26 ++++++++++++++++++++++++ .claude/settings.local.json | 15 ++++++++++++++ .cursor/mcp.json | 8 ++++++++ .cursor/rules/archgate-governance.mdc | 29 +++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 .archgate/lint/README.md create mode 100644 .claude/settings.local.json create mode 100644 .cursor/mcp.json create mode 100644 .cursor/rules/archgate-governance.mdc diff --git a/.archgate/lint/README.md b/.archgate/lint/README.md new file mode 100644 index 00000000..9c7e1d65 --- /dev/null +++ b/.archgate/lint/README.md @@ -0,0 +1,26 @@ +# Linter Rules + +This directory hosts linter-specific rules that enforce your ADRs at the linter level. + +## Convention + +Place linter plugin files here, named by tool: + +- `oxlint.js` — Custom oxlint rules (JavaScript plugin) +- `eslint.js` — Custom ESLint rules +- `biome.js` — Custom Biome rules + +## Usage with oxlint + +1. Create `.archgate/lint/oxlint.js` exporting your plugin rules. +2. Reference it in your oxlint config: + +```json +{ + "plugins": [".archgate/lint/oxlint.js"] +} +``` + +## Why here? + +Archgate standardizes `.archgate/lint/` as the location for linter rules that complement ADR checks. This keeps governance artifacts together — ADRs in `adrs/`, linter rules in `lint/`. diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..5d307421 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "agent": "archgate:developer", + "enableAllProjectMcpServers": true, + "enabledMcpjsonServers": [ + "archgate" + ], + "permissions": { + "allow": [ + "mcp__plugin_archgate_archgate__*", + "Skill(archgate:architect)", + "Skill(archgate:quality-manager)", + "Skill(archgate:adr-author)" + ] + } +} diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..0206fe30 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "archgate": { + "command": "archgate", + "args": ["mcp"] + } + } +} diff --git a/.cursor/rules/archgate-governance.mdc b/.cursor/rules/archgate-governance.mdc new file mode 100644 index 00000000..853a1dd6 --- /dev/null +++ b/.cursor/rules/archgate-governance.mdc @@ -0,0 +1,29 @@ +--- +description: Archgate ADR governance — enforces architecture decision records +globs: +alwaysApply: true +--- + +# Archgate Governance + +This project uses Archgate to enforce Architecture Decision Records (ADRs). + +## Before writing code + +- Use the `review_context` MCP tool to get applicable ADR briefings for changed files +- Review the Decision and Do's/Don'ts sections of each applicable ADR + +## After writing code + +- Run the `check` MCP tool to validate compliance with all ADR rules +- Fix any violations before considering work complete + +## ADR commands + +- `list_adrs` — List all active ADRs with metadata +- `check` — Run automated compliance checks (use `staged: true` for pre-commit) +- `review_context` — Get changed files grouped by domain with ADR briefings + +## Key principle + +Architectural decisions are enforced, not suggested. If `check` reports violations, they must be fixed. From 624fb3037935aca9c69d648f6f6a1417d59338d4 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:42:53 +0100 Subject: [PATCH 6/8] docs: clarify free CLI vs optional paid plugins Emphasize that the CLI is free and fully functional without plugins or an account. Move login out of quick start since it is only needed for the optional paid editor plugins. Add callouts throughout the README. Co-Authored-By: Claude Opus 4.6 --- README.md | 55 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 32d83c36..6c1c2a0c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ Archgate has two layers: When a rule is violated, `archgate check` reports the file, line, and which ADR was broken. Exit code 1 means violations — wire it into CI and it blocks merges automatically. -**The AI integration layer** is a Claude Code plugin that gives AI agents live access to your ADRs through the MCP server (`archgate mcp`). Agents read the decisions before writing code, validate changes after, and capture new patterns into the governance base. Archgate ships as its own governance subject — its own development is governed by the ADRs in `.archgate/adrs/`. +**The CLI is free and open source.** Writing ADRs, enforcing rules, running checks in CI, and wiring up pre-commit hooks all work without an account or subscription. + +**Editor plugins are an optional paid add-on** for teams that want AI agents (Claude Code, Cursor) to read ADRs, validate changes, and capture new patterns automatically. Plugins are distributed from [plugins.archgate.dev](https://plugins.archgate.dev). See [Editor plugins](#editor-plugins) for details. ## Installation @@ -62,25 +64,23 @@ npm install -g archgate # 1. Install npm install -g archgate -# 2. Log in to enable plugin access (one-time) -archgate login - -# 3. Initialize governance in your project +# 2. Initialize governance in your project cd my-project -archgate init # Claude Code (default) -archgate init --editor cursor # or Cursor +archgate init -# 4. Edit the generated ADR to document a real decision +# 3. Edit the generated ADR to document a real decision # .archgate/adrs/ARCH-001-*.md -# 5. Add a companion .rules.ts to enforce it automatically +# 4. Add a companion .rules.ts to enforce it automatically # .archgate/adrs/ARCH-001-*.rules.ts -# 6. Run checks +# 5. Run checks archgate check ``` -`archgate init` creates the `.archgate/adrs/` directory with an example ADR and rules file, configures editor settings (`.claude/` or `.cursor/`), and installs the archgate plugin if you are logged in. If the `claude` CLI is on your PATH, the plugin is installed automatically; otherwise the command prints the manual install steps. +`archgate init` creates the `.archgate/adrs/` directory with an example ADR and rules file, and configures editor settings. No account or login is needed — the CLI is fully functional without plugins. + +**Want AI agent integration?** See [Editor plugins](#editor-plugins) to add the optional paid plugin for Claude Code or Cursor. ## Writing rules @@ -123,7 +123,7 @@ Rules receive the list of files to check (filtered by the ADR's `files` glob if ### `archgate login` -Authenticate with GitHub to access archgate plugins. +Authenticate with GitHub to access the optional paid editor plugins. ```bash archgate login # authenticate via GitHub Device Flow @@ -132,7 +132,7 @@ archgate login logout # remove stored credentials archgate login refresh # re-authenticate and claim a new token ``` -Opens a browser-based GitHub Device Flow. Once authorized, an archgate plugin token is stored in `~/.archgate/credentials`. This token is used by `archgate init` to install the editor plugin. +Opens a browser-based GitHub Device Flow. Once authorized, an archgate plugin token is stored in `~/.archgate/credentials`. This token is used by `archgate init` to install the editor plugin. Not required for CLI-only usage. ### `archgate init` @@ -144,11 +144,11 @@ archgate init --editor cursor # Cursor archgate init --install-plugin # force plugin install attempt ``` -Creates `.archgate/adrs/` with an example ADR and rules file, configures editor settings, and installs the archgate plugin when credentials are available. +Creates `.archgate/adrs/` with an example ADR and rules file and configures editor settings. Works without an account — plugin installation only happens when you are logged in. -**Plugin install behavior:** +**Plugin install behavior** (optional — requires `archgate login`): -- If you are logged in (`archgate login`), init auto-detects your credentials and installs the plugin. +- If you are logged in, init auto-detects your credentials and installs the plugin. - For **Claude Code**: if the `claude` CLI is on PATH, the plugin is installed automatically via `claude plugin marketplace add` and `claude plugin install`. If not, the manual commands are printed. - For **Cursor**: the plugin bundle is downloaded from [plugins.archgate.dev](https://plugins.archgate.dev) and extracted into `.cursor/`. - Use `--install-plugin` to explicitly request plugin installation (useful if auto-detection is skipped). @@ -258,17 +258,25 @@ pre-commit: ## Editor plugins -Archgate ships editor plugins that give AI agents a full governance workflow — reading applicable ADRs before writing code, validating changes after implementation, and capturing new patterns back into ADRs. +> **Plugins are an optional paid add-on.** The CLI works fully without them. Plugins add AI agent integration — your AI coding agent reads ADRs before writing code, validates changes after implementation, and captures new patterns back into ADRs. -Plugins are distributed from [plugins.archgate.dev](https://plugins.archgate.dev). Run `archgate login` once to authenticate, then `archgate init` handles installation. +Plugins are distributed from [plugins.archgate.dev](https://plugins.archgate.dev). -### Claude Code +### Setup ```bash +# 1. Log in (one-time) — links your GitHub account and issues a plugin token archgate login -archgate init + +# 2. Initialize a project with the plugin +archgate init # Claude Code (default) +archgate init --editor cursor # or Cursor ``` +If you are logged in, `archgate init` auto-detects your credentials and installs the plugin. You can also pass `--install-plugin` explicitly. + +### Claude Code + If the `claude` CLI is on your PATH, the plugin is installed automatically. Otherwise, run the printed commands manually: ```bash @@ -280,12 +288,7 @@ Once installed, run `archgate:onboard` in Claude Code to initialize governance f ### Cursor -```bash -archgate login -archgate init --editor cursor -``` - -The Cursor plugin bundle is downloaded and extracted into `.cursor/` automatically. +The Cursor plugin bundle is downloaded from [plugins.archgate.dev](https://plugins.archgate.dev) and extracted into `.cursor/` automatically. ## Contributing From f7ba1bdab2e54beaf2fb619aa5c6b9bff5ae6759 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:44:14 +0100 Subject: [PATCH 7/8] docs: add cursor onboarding step for ag-onboard skill Co-Authored-By: Claude Opus 4.6 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6c1c2a0c..30639b90 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,8 @@ Once installed, run `archgate:onboard` in Claude Code to initialize governance f The Cursor plugin bundle is downloaded from [plugins.archgate.dev](https://plugins.archgate.dev) and extracted into `.cursor/` automatically. +Once installed, run the `ag-onboard` skill in Cursor to initialize governance for your project. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and workflow. From 8e7e76687cfc7f916b7420b0d3fe6927d7d783b8 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Sat, 28 Feb 2026 03:48:25 +0100 Subject: [PATCH 8/8] style: fix prettier formatting in settings.local.json Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5d307421..86310cc7 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,9 +1,7 @@ { "agent": "archgate:developer", "enableAllProjectMcpServers": true, - "enabledMcpjsonServers": [ - "archgate" - ], + "enabledMcpjsonServers": ["archgate"], "permissions": { "allow": [ "mcp__plugin_archgate_archgate__*",