From eb761f2dc6c5a59df7f1eaf9664639efdeb70831 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 16 Mar 2026 00:43:45 +0100 Subject: [PATCH 1/5] feat: add `archgate plugin url` and `archgate plugin install` commands Adds a `plugin` command group for managing editor plugins independently of `archgate init`. `plugin url` prints the authenticated marketplace URL for manual tool configuration, and `plugin install` installs the plugin for already-initialized repos via editor CLI or manual fallback. Co-Authored-By: Claude Opus 4.6 --- src/cli.ts | 2 + src/commands/plugin/index.ts | 12 +++ src/commands/plugin/install.ts | 123 ++++++++++++++++++++++++++ src/commands/plugin/url.ts | 47 ++++++++++ tests/commands/plugin/install.test.ts | 28 ++++++ tests/commands/plugin/url.test.ts | 28 ++++++ 6 files changed, 240 insertions(+) create mode 100644 src/commands/plugin/index.ts create mode 100644 src/commands/plugin/install.ts create mode 100644 src/commands/plugin/url.ts create mode 100644 tests/commands/plugin/install.test.ts create mode 100644 tests/commands/plugin/url.test.ts diff --git a/src/cli.ts b/src/cli.ts index 44938ddf..5353bcc3 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,6 +12,7 @@ import { registerCheckCommand } from "./commands/check"; import { registerLoginCommand } from "./commands/login"; import { registerReviewContextCommand } from "./commands/review-context"; import { registerSessionContextCommand } from "./commands/session-context/index"; +import { registerPluginCommand } from "./commands/plugin/index"; import { checkForUpdatesIfNeeded } from "./helpers/update-check"; import { logError } from "./helpers/log"; @@ -42,6 +43,7 @@ async function main() { registerCheckCommand(program); registerReviewContextCommand(program); registerSessionContextCommand(program); + registerPluginCommand(program); registerUpgradeCommand(program); registerCleanCommand(program); diff --git a/src/commands/plugin/index.ts b/src/commands/plugin/index.ts new file mode 100644 index 00000000..a807289e --- /dev/null +++ b/src/commands/plugin/index.ts @@ -0,0 +1,12 @@ +import type { Command } from "@commander-js/extra-typings"; +import { registerPluginUrlCommand } from "./url"; +import { registerPluginInstallCommand } from "./install"; + +export function registerPluginCommand(program: Command) { + const plugin = program + .command("plugin") + .description("Manage archgate editor plugins"); + + registerPluginUrlCommand(plugin); + registerPluginInstallCommand(plugin); +} diff --git a/src/commands/plugin/install.ts b/src/commands/plugin/install.ts new file mode 100644 index 00000000..6234c0ff --- /dev/null +++ b/src/commands/plugin/install.ts @@ -0,0 +1,123 @@ +import type { Command } from "@commander-js/extra-typings"; +import { styleText } from "node:util"; +import { loadCredentials } from "../../helpers/auth"; +import { + buildMarketplaceUrl, + buildVscodeMarketplaceUrl, + installClaudePlugin, + installCopilotPlugin, + installCursorPlugin, + isClaudeCliAvailable, + isCopilotCliAvailable, +} from "../../helpers/plugin-install"; +import { configureVscodeSettings } from "../../helpers/vscode-settings"; +import { logError, logInfo, logWarn } from "../../helpers/log"; + +const VALID_EDITORS = ["claude", "cursor", "vscode", "copilot"] as const; +type EditorTarget = (typeof VALID_EDITORS)[number]; + +const EDITOR_LABELS: Record = { + claude: "Claude Code", + cursor: "Cursor", + vscode: "VS Code", + copilot: "Copilot CLI", +}; + +export function registerPluginInstallCommand(plugin: Command) { + plugin + .command("install") + .description("Install the archgate plugin for the specified editor") + .option( + "--editor ", + "target editor (claude, cursor, vscode, copilot)", + "claude" + ) + .action(async (opts) => { + const editor = opts.editor; + if (!VALID_EDITORS.includes(editor as EditorTarget)) { + logError( + `Unknown editor "${editor}". Supported: ${VALID_EDITORS.join(", ")}` + ); + process.exit(1); + } + + const credentials = await loadCredentials(); + if (!credentials) { + logError( + "Not logged in.", + "Run `archgate login` first to authenticate." + ); + process.exit(1); + } + + const target = editor as EditorTarget; + const label = EDITOR_LABELS[target]; + + try { + switch (target) { + case "claude": { + if (await isClaudeCliAvailable()) { + await installClaudePlugin(credentials); + logInfo(`Archgate plugin installed for ${label}.`); + } else { + const url = buildMarketplaceUrl(credentials); + logWarn( + "Claude CLI not found. To install the plugin manually, run:" + ); + console.log( + ` ${styleText("bold", "claude plugin marketplace add")} ${url}` + ); + console.log( + ` ${styleText("bold", "claude plugin install")} archgate@archgate` + ); + } + break; + } + + case "copilot": { + if (await isCopilotCliAvailable()) { + await installCopilotPlugin(credentials); + logInfo(`Archgate plugin installed for ${label}.`); + } else { + const url = buildMarketplaceUrl(credentials); + logWarn( + "Copilot CLI not found. To install the plugin manually, run:" + ); + console.log( + ` ${styleText("bold", "copilot plugin install")} ${url}` + ); + } + break; + } + + case "cursor": { + const files = await installCursorPlugin( + process.cwd(), + credentials.token + ); + logInfo( + `Archgate plugin installed for ${label}.`, + `Extracted ${files.length} files to .cursor/` + ); + break; + } + + case "vscode": { + const url = buildVscodeMarketplaceUrl(credentials); + await configureVscodeSettings(process.cwd(), url); + logInfo( + `Archgate plugin configured for ${label}.`, + "Marketplace URL added to VS Code user settings." + ); + break; + } + } + } catch (err) { + logError( + `Failed to install plugin for ${label}.`, + err instanceof Error ? err.message : String(err) + ); + process.exit(1); + } + }); +} diff --git a/src/commands/plugin/url.ts b/src/commands/plugin/url.ts new file mode 100644 index 00000000..e0f3dced --- /dev/null +++ b/src/commands/plugin/url.ts @@ -0,0 +1,47 @@ +import type { Command } from "@commander-js/extra-typings"; +import { loadCredentials } from "../../helpers/auth"; +import { + buildMarketplaceUrl, + buildVscodeMarketplaceUrl, +} from "../../helpers/plugin-install"; +import { logError } from "../../helpers/log"; + +const VALID_EDITORS = ["claude", "cursor", "vscode", "copilot"] as const; + +export function registerPluginUrlCommand(plugin: Command) { + plugin + .command("url") + .description( + "Print the authenticated plugin repository URL for manual configuration" + ) + .option( + "--editor ", + "target editor (claude, cursor, vscode, copilot)", + "claude" + ) + .action(async (opts) => { + const editor = opts.editor; + if (!VALID_EDITORS.includes(editor as (typeof VALID_EDITORS)[number])) { + logError( + `Unknown editor "${editor}". Supported: ${VALID_EDITORS.join(", ")}` + ); + process.exit(1); + } + + const credentials = await loadCredentials(); + if (!credentials) { + logError( + "Not logged in.", + "Run `archgate login` first to authenticate." + ); + process.exit(1); + } + + const url = + editor === "vscode" + ? buildVscodeMarketplaceUrl(credentials) + : buildMarketplaceUrl(credentials); + + console.log(url); + }); +} diff --git a/tests/commands/plugin/install.test.ts b/tests/commands/plugin/install.test.ts new file mode 100644 index 00000000..33176ae7 --- /dev/null +++ b/tests/commands/plugin/install.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; +import { Command } from "@commander-js/extra-typings"; +import { registerPluginInstallCommand } from "../../../src/commands/plugin/install"; + +describe("registerPluginInstallCommand", () => { + test("registers 'install' as a subcommand", () => { + const program = new Command(); + registerPluginInstallCommand(program); + const sub = program.commands.find((c) => c.name() === "install"); + expect(sub).toBeDefined(); + }); + + test("has a description", () => { + const program = new Command(); + registerPluginInstallCommand(program); + const sub = program.commands.find((c) => c.name() === "install")!; + expect(sub.description()).toBeTruthy(); + }); + + test("accepts --editor option with default 'claude'", () => { + const program = new Command(); + registerPluginInstallCommand(program); + const sub = program.commands.find((c) => c.name() === "install")!; + const editorOpt = sub.options.find((o) => o.long === "--editor"); + expect(editorOpt).toBeDefined(); + expect(editorOpt!.defaultValue).toBe("claude"); + }); +}); diff --git a/tests/commands/plugin/url.test.ts b/tests/commands/plugin/url.test.ts new file mode 100644 index 00000000..adc5be52 --- /dev/null +++ b/tests/commands/plugin/url.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; +import { Command } from "@commander-js/extra-typings"; +import { registerPluginUrlCommand } from "../../../src/commands/plugin/url"; + +describe("registerPluginUrlCommand", () => { + test("registers 'url' as a subcommand", () => { + const program = new Command(); + registerPluginUrlCommand(program); + const sub = program.commands.find((c) => c.name() === "url"); + expect(sub).toBeDefined(); + }); + + test("has a description", () => { + const program = new Command(); + registerPluginUrlCommand(program); + const sub = program.commands.find((c) => c.name() === "url")!; + expect(sub.description()).toBeTruthy(); + }); + + test("accepts --editor option with default 'claude'", () => { + const program = new Command(); + registerPluginUrlCommand(program); + const sub = program.commands.find((c) => c.name() === "url")!; + const editorOpt = sub.options.find((o) => o.long === "--editor"); + expect(editorOpt).toBeDefined(); + expect(editorOpt!.defaultValue).toBe("claude"); + }); +}); From 3a83e4d32bb6b0643aebc255cedb0ca17958ad44 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 16 Mar 2026 00:47:48 +0100 Subject: [PATCH 2/5] refactor: use addOption with choices() for typed editor options Replace .option() with new Option().choices().default() and .addOption() in plugin commands for compile-time type narrowing. Adds ARCH-008 ADR to enforce this pattern for all fixed-choice command options. Co-Authored-By: Claude Opus 4.6 --- .../adrs/ARCH-008-typed-command-options.md | 130 ++++++++++++++++++ .../ARCH-008-typed-command-options.rules.ts | 31 +++++ src/commands/plugin/install.ts | 28 ++-- src/commands/plugin/url.ts | 21 +-- tests/commands/plugin/install.test.ts | 13 ++ tests/commands/plugin/url.test.ts | 13 ++ 6 files changed, 202 insertions(+), 34 deletions(-) create mode 100644 .archgate/adrs/ARCH-008-typed-command-options.md create mode 100644 .archgate/adrs/ARCH-008-typed-command-options.rules.ts diff --git a/.archgate/adrs/ARCH-008-typed-command-options.md b/.archgate/adrs/ARCH-008-typed-command-options.md new file mode 100644 index 00000000..9838de95 --- /dev/null +++ b/.archgate/adrs/ARCH-008-typed-command-options.md @@ -0,0 +1,130 @@ +--- +id: ARCH-008 +title: Typed Command Options +domain: architecture +rules: true +files: ["src/commands/**/*.ts"] +--- + +## Context + +Commander.js `.option()` accepts arbitrary strings and produces loosely typed option values. When a command accepts a fixed set of choices (e.g., `--editor claude|cursor|vscode|copilot`), using `.option()` requires manual runtime validation and `as` casts to narrow the type — boilerplate that is easy to forget and produces unhelpful error messages when users pass invalid values. + +**Alternatives considered:** + +- **Plain `.option()` with manual validation** — The developer writes a runtime check (`if (!VALID.includes(val))`) and casts to the narrow type. This works but scatters validation logic, produces inconsistent error messages across commands, and the `opts` object remains typed as `string`, requiring `as` casts at every usage site. +- **Zod/custom parsing in `.option()` argParser** — Commander supports a custom parse function as the third argument to `.option()`. While this gives runtime validation, it does not narrow the TypeScript type in the `opts` object when using `@commander-js/extra-typings`. + +The `@commander-js/extra-typings` package provides `Option` class with a `.choices()` method that both validates at runtime (Commander rejects invalid values with a clear error) and narrows the TypeScript type in the action callback's `opts` parameter. Using `.addOption()` instead of `.option()` integrates this typed option into the command. + +## Decision + +Commands with fixed-choice options MUST use `new Option().choices().default()` with `.addOption()` instead of plain `.option()` with manual validation. + +**Key constraints:** + +1. **Use `Option` from `@commander-js/extra-typings`** — Import `Option` alongside `Command` from the extra-typings package to get full type inference. +2. **Use `.choices()` for enum-like options** — Any option accepting a fixed set of values must use `.choices()` to get both runtime validation and compile-time type narrowing. +3. **Use `.addOption()` to register** — The typed `Option` object is passed via `.addOption()`, not `.option()`. +4. **Use `as const` with `.choices()` and `.default()`** — Pass the choices array and default value with `as const` to preserve literal types. +5. **No manual validation for choice options** — Commander handles invalid value rejection automatically; do not duplicate this logic. + +## Do's and Don'ts + +### Do + +- Use `new Option().choices([...] as const).default(... as const)` for fixed-choice options +- Register typed options via `.addOption()` +- Reuse existing type definitions (e.g., `EditorTarget`) for `Record` keys and other type-level usage +- Access `opts.editor` directly in switch/case — TypeScript narrows the type + +### Don't + +- Don't use `.option()` for options with a known set of valid values +- Don't write manual `if (!VALID.includes(val))` checks for choice options — Commander does this +- Don't cast `opts.editor as SomeType` — the type should already be narrowed + +## Implementation Pattern + +### Good Example + +```typescript +import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; + +const editorOption = new Option("--editor ", "target editor") + .choices(["claude", "cursor", "vscode", "copilot"] as const) + .default("claude" as const); + +export function registerExampleCommand(program: Command) { + program + .command("example") + .addOption(editorOption) + .action(async (opts) => { + // opts.editor is typed as "claude" | "cursor" | "vscode" | "copilot" + switch (opts.editor) { + case "claude": + break; + case "cursor": + break; + // TypeScript enforces exhaustive matching + } + }); +} +``` + +### Bad Example + +```typescript +// BAD: loose typing, manual validation, casts +export function registerExampleCommand(program: Command) { + program + .command("example") + .option("--editor ", "target editor", "claude") + .action(async (opts) => { + // opts.editor is string — no narrowing + if (!["claude", "cursor"].includes(opts.editor)) { + logError(`Unknown editor "${opts.editor}"`); + process.exit(1); + } + const editor = opts.editor as EditorTarget; // unsafe cast + }); +} +``` + +## Consequences + +### Positive + +- **Compile-time safety** — Invalid option values are caught by TypeScript, not just at runtime +- **Consistent error messages** — Commander produces standard error output for invalid choices +- **No boilerplate validation** — Eliminates repeated `if (!VALID.includes(...))` patterns +- **Exhaustive switch/case** — TypeScript ensures all choices are handled when switching on the option value + +### Negative + +- **Slightly more verbose declaration** — `new Option().choices().default()` with `.addOption()` is more code than `.option()` for simple cases. This is acceptable given the type safety gained. + +### Risks + +- **Inconsistency with existing commands** — Older commands (e.g., `init --editor`) still use `.option()` with manual validation. These should be migrated incrementally. The automated rule only scans for `.choices()` usage, not for migration of existing code. + +## Compliance and Enforcement + +### Automated Enforcement + +- **Archgate rule** ARCH-008/use-add-option-for-choices: Scans command files for `.option()` calls that include a hardcoded choices-like pattern and flags them for migration to `.addOption()` with `.choices()`. Severity: warning. + +### Manual Enforcement + +Code reviewers MUST verify: + +1. New commands with fixed-choice options use `new Option().choices()` with `.addOption()` +2. The choices array and default use `as const` for literal type preservation +3. No manual validation duplicates Commander's built-in choice rejection + +## References + +- [Commander.js Option documentation](https://github.com/tj/commander.js#options) +- [@commander-js/extra-typings](https://github.com/tj/commander.js/tree/master/typings) — Typed Commander.js wrapper +- [ARCH-001 — Command Structure](./ARCH-001-command-structure.md) — Parent command registration pattern diff --git a/.archgate/adrs/ARCH-008-typed-command-options.rules.ts b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts new file mode 100644 index 00000000..39a246aa --- /dev/null +++ b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts @@ -0,0 +1,31 @@ +import { defineRules } from "../../src/formats/rules"; + +export default defineRules({ + "use-add-option-for-choices": { + description: + "Commands with fixed-choice options should use addOption with choices() instead of plain option()", + severity: "warning", + async check(ctx) { + const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); + const matches = await Promise.all( + files.map((file) => + ctx.grep( + file, + /\.option\(\s*["']--\w+\s+<\w+>["'],\s*["'][^"']*["'],\s*["'](claude|cursor|vscode|copilot)["']\)/ + ) + ) + ); + for (const fileMatches of matches) { + for (const m of fileMatches) { + ctx.report.warning({ + message: + "Use new Option().choices() with .addOption() instead of .option() for fixed-choice options", + file: m.file, + line: m.line, + fix: "Replace .option() with new Option(...).choices([...] as const).default(... as const) and register via .addOption()", + }); + } + } + }, + }, +}); diff --git a/src/commands/plugin/install.ts b/src/commands/plugin/install.ts index 6234c0ff..2cd596bd 100644 --- a/src/commands/plugin/install.ts +++ b/src/commands/plugin/install.ts @@ -1,4 +1,5 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { styleText } from "node:util"; import { loadCredentials } from "../../helpers/auth"; import { @@ -12,9 +13,7 @@ import { } from "../../helpers/plugin-install"; import { configureVscodeSettings } from "../../helpers/vscode-settings"; import { logError, logInfo, logWarn } from "../../helpers/log"; - -const VALID_EDITORS = ["claude", "cursor", "vscode", "copilot"] as const; -type EditorTarget = (typeof VALID_EDITORS)[number]; +import type { EditorTarget } from "../../helpers/init-project"; const EDITOR_LABELS: Record = { claude: "Claude Code", @@ -23,24 +22,16 @@ const EDITOR_LABELS: Record = { copilot: "Copilot CLI", }; +const editorOption = new Option("--editor ", "target editor") + .choices(["claude", "cursor", "vscode", "copilot"] as const) + .default("claude" as const); + export function registerPluginInstallCommand(plugin: Command) { plugin .command("install") .description("Install the archgate plugin for the specified editor") - .option( - "--editor ", - "target editor (claude, cursor, vscode, copilot)", - "claude" - ) + .addOption(editorOption) .action(async (opts) => { - const editor = opts.editor; - if (!VALID_EDITORS.includes(editor as EditorTarget)) { - logError( - `Unknown editor "${editor}". Supported: ${VALID_EDITORS.join(", ")}` - ); - process.exit(1); - } - const credentials = await loadCredentials(); if (!credentials) { logError( @@ -50,11 +41,10 @@ export function registerPluginInstallCommand(plugin: Command) { process.exit(1); } - const target = editor as EditorTarget; - const label = EDITOR_LABELS[target]; + const label = EDITOR_LABELS[opts.editor]; try { - switch (target) { + switch (opts.editor) { case "claude": { if (await isClaudeCliAvailable()) { await installClaudePlugin(credentials); diff --git a/src/commands/plugin/url.ts b/src/commands/plugin/url.ts index e0f3dced..7b439141 100644 --- a/src/commands/plugin/url.ts +++ b/src/commands/plugin/url.ts @@ -1,4 +1,5 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { loadCredentials } from "../../helpers/auth"; import { buildMarketplaceUrl, @@ -6,7 +7,9 @@ import { } from "../../helpers/plugin-install"; import { logError } from "../../helpers/log"; -const VALID_EDITORS = ["claude", "cursor", "vscode", "copilot"] as const; +const editorOption = new Option("--editor ", "target editor") + .choices(["claude", "cursor", "vscode", "copilot"] as const) + .default("claude" as const); export function registerPluginUrlCommand(plugin: Command) { plugin @@ -14,20 +17,8 @@ export function registerPluginUrlCommand(plugin: Command) { .description( "Print the authenticated plugin repository URL for manual configuration" ) - .option( - "--editor ", - "target editor (claude, cursor, vscode, copilot)", - "claude" - ) + .addOption(editorOption) .action(async (opts) => { - const editor = opts.editor; - if (!VALID_EDITORS.includes(editor as (typeof VALID_EDITORS)[number])) { - logError( - `Unknown editor "${editor}". Supported: ${VALID_EDITORS.join(", ")}` - ); - process.exit(1); - } - const credentials = await loadCredentials(); if (!credentials) { logError( @@ -38,7 +29,7 @@ export function registerPluginUrlCommand(plugin: Command) { } const url = - editor === "vscode" + opts.editor === "vscode" ? buildVscodeMarketplaceUrl(credentials) : buildMarketplaceUrl(credentials); diff --git a/tests/commands/plugin/install.test.ts b/tests/commands/plugin/install.test.ts index 33176ae7..635ed1bc 100644 --- a/tests/commands/plugin/install.test.ts +++ b/tests/commands/plugin/install.test.ts @@ -25,4 +25,17 @@ describe("registerPluginInstallCommand", () => { expect(editorOpt).toBeDefined(); expect(editorOpt!.defaultValue).toBe("claude"); }); + + test("--editor option restricts choices to valid editors", () => { + const program = new Command(); + registerPluginInstallCommand(program); + const sub = program.commands.find((c) => c.name() === "install")!; + const editorOpt = sub.options.find((o) => o.long === "--editor")!; + expect(editorOpt.argChoices).toEqual([ + "claude", + "cursor", + "vscode", + "copilot", + ]); + }); }); diff --git a/tests/commands/plugin/url.test.ts b/tests/commands/plugin/url.test.ts index adc5be52..527eaa03 100644 --- a/tests/commands/plugin/url.test.ts +++ b/tests/commands/plugin/url.test.ts @@ -25,4 +25,17 @@ describe("registerPluginUrlCommand", () => { expect(editorOpt).toBeDefined(); expect(editorOpt!.defaultValue).toBe("claude"); }); + + test("--editor option restricts choices to valid editors", () => { + const program = new Command(); + registerPluginUrlCommand(program); + const sub = program.commands.find((c) => c.name() === "url")!; + const editorOpt = sub.options.find((o) => o.long === "--editor")!; + expect(editorOpt.argChoices).toEqual([ + "claude", + "cursor", + "vscode", + "copilot", + ]); + }); }); From e8fc53f2fde3a584c3d977c7dee3eaa182afa141 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto Date: Mon, 16 Mar 2026 00:51:50 +0100 Subject: [PATCH 3/5] refactor: migrate all fixed-choice options to addOption with choices() Enforce ARCH-008 as error severity. Migrate --editor in init and --domain in adr create, adr update, and review-context to use new Option().choices() with .addOption(), removing manual validation and unsafe casts. Co-Authored-By: Claude Opus 4.6 --- .../adrs/ARCH-008-typed-command-options.md | 2 +- .../ARCH-008-typed-command-options.rules.ts | 13 +++++--- src/commands/adr/create.ts | 27 +++++---------- src/commands/adr/update.ts | 33 +++++-------------- src/commands/init.ts | 30 ++++++----------- src/commands/review-context.ts | 31 +++++------------ 6 files changed, 44 insertions(+), 92 deletions(-) diff --git a/.archgate/adrs/ARCH-008-typed-command-options.md b/.archgate/adrs/ARCH-008-typed-command-options.md index 9838de95..03e786dd 100644 --- a/.archgate/adrs/ARCH-008-typed-command-options.md +++ b/.archgate/adrs/ARCH-008-typed-command-options.md @@ -113,7 +113,7 @@ export function registerExampleCommand(program: Command) { ### Automated Enforcement -- **Archgate rule** ARCH-008/use-add-option-for-choices: Scans command files for `.option()` calls that include a hardcoded choices-like pattern and flags them for migration to `.addOption()` with `.choices()`. Severity: warning. +- **Archgate rule** ARCH-008/use-add-option-for-choices: Scans command files for `.option()` calls that include a hardcoded choices-like pattern and flags them for migration to `.addOption()` with `.choices()`. Severity: error. ### Manual Enforcement diff --git a/.archgate/adrs/ARCH-008-typed-command-options.rules.ts b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts index 39a246aa..1a211514 100644 --- a/.archgate/adrs/ARCH-008-typed-command-options.rules.ts +++ b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts @@ -3,26 +3,29 @@ import { defineRules } from "../../src/formats/rules"; export default defineRules({ "use-add-option-for-choices": { description: - "Commands with fixed-choice options should use addOption with choices() instead of plain option()", - severity: "warning", + "Commands with fixed-choice options must use addOption with choices() instead of plain option()", + severity: "error", async check(ctx) { const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); + // Detect .option() calls whose description enumerates known choice values + // e.g. "editor integration to configure (claude, cursor, vscode, copilot)" + // or "ADR domain: backend, frontend, data, architecture, general" const matches = await Promise.all( files.map((file) => ctx.grep( file, - /\.option\(\s*["']--\w+\s+<\w+>["'],\s*["'][^"']*["'],\s*["'](claude|cursor|vscode|copilot)["']\)/ + /\.option\(\s*["']--\w+\s+<\w+>["'],\s*["'][^"']*(?:claude.*cursor|backend.*frontend)[^"']*["']/ ) ) ); for (const fileMatches of matches) { for (const m of fileMatches) { - ctx.report.warning({ + ctx.report.violation({ message: "Use new Option().choices() with .addOption() instead of .option() for fixed-choice options", file: m.file, line: m.line, - fix: "Replace .option() with new Option(...).choices([...] as const).default(... as const) and register via .addOption()", + fix: "Replace .option() with new Option(...).choices([...] as const) and register via .addOption()", }); } } diff --git a/src/commands/adr/create.ts b/src/commands/adr/create.ts index 0aa94606..014ec596 100644 --- a/src/commands/adr/create.ts +++ b/src/commands/adr/create.ts @@ -1,24 +1,22 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { existsSync } from "node:fs"; import inquirer from "inquirer"; import { projectPaths } from "../../helpers/paths"; -import { - ADR_DOMAINS, - AdrFrontmatterSchema, - type AdrDomain, -} from "../../formats/adr"; +import { ADR_DOMAINS, type AdrDomain } from "../../formats/adr"; import { createAdrFile } from "../../helpers/adr-writer"; import { logError } from "../../helpers/log"; +const domainOption = new Option("--domain ", "ADR domain").choices( + ADR_DOMAINS +); + export function registerAdrCreateCommand(adr: Command) { adr .command("create") .description("Create a new ADR") .option("--title ", "ADR title (skip interactive prompt)") - .option( - "--domain <domain>", - "ADR domain: backend, frontend, data, architecture, general" - ) + .addOption(domainOption) .option("--files <patterns>", "File patterns, comma-separated") .option("--body <markdown>", "Full ADR body markdown (skip template)") .option("--rules", "Set rules: true in frontmatter") @@ -39,16 +37,7 @@ export function registerAdrCreateCommand(adr: Command) { // Non-interactive mode when --title and --domain are provided if (opts.title && opts.domain) { - const domainResult = AdrFrontmatterSchema.shape.domain.safeParse( - opts.domain - ); - if (!domainResult.success) { - logError( - `Invalid domain '${opts.domain}'. Must be one of: ${ADR_DOMAINS.join(", ")}` - ); - process.exit(1); - } - domain = domainResult.data; + domain = opts.domain; title = opts.title; files = opts.files ? opts.files diff --git a/src/commands/adr/update.ts b/src/commands/adr/update.ts index af4ed910..3376cb6b 100644 --- a/src/commands/adr/update.ts +++ b/src/commands/adr/update.ts @@ -1,14 +1,15 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { existsSync } from "node:fs"; import { projectPaths } from "../../helpers/paths"; -import { - ADR_DOMAINS, - AdrFrontmatterSchema, - type AdrDomain, -} from "../../formats/adr"; +import { ADR_DOMAINS } from "../../formats/adr"; import { updateAdrFile } from "../../helpers/adr-writer"; import { logError } from "../../helpers/log"; +const domainOption = new Option("--domain <domain>", "new ADR domain").choices( + ADR_DOMAINS +); + export function registerAdrUpdateCommand(adr: Command) { adr .command("update") @@ -16,10 +17,7 @@ export function registerAdrUpdateCommand(adr: Command) { .requiredOption("--id <id>", "ADR ID to update (e.g., ARCH-001)") .requiredOption("--body <markdown>", "Full replacement ADR body markdown") .option("--title <title>", "New ADR title (preserves existing if omitted)") - .option( - "--domain <domain>", - "New domain: backend, frontend, data, architecture, general" - ) + .addOption(domainOption) .option( "--files <patterns>", "New file patterns, comma-separated (preserves existing if omitted)" @@ -35,21 +33,6 @@ export function registerAdrUpdateCommand(adr: Command) { process.exit(1); } - let domain: AdrDomain | undefined; - - if (opts.domain) { - const domainResult = AdrFrontmatterSchema.shape.domain.safeParse( - opts.domain - ); - if (!domainResult.success) { - logError( - `Invalid domain '${opts.domain}'. Must be one of: ${ADR_DOMAINS.join(", ")}` - ); - process.exit(1); - } - domain = domainResult.data; - } - const files = opts.files ? opts.files .split(",") @@ -62,7 +45,7 @@ export function registerAdrUpdateCommand(adr: Command) { id: opts.id, body: opts.body, title: opts.title, - domain, + domain: opts.domain, files, rules: opts.rules, }); diff --git a/src/commands/init.ts b/src/commands/init.ts index 50bfce12..b341b02a 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,12 +1,11 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; 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", "vscode", "copilot"] as const; - const EDITOR_LABELS: Record<EditorTarget, string> = { claude: "Claude Code", cursor: "Cursor", @@ -21,41 +20,32 @@ const EDITOR_DIRS: Record<EditorTarget, string> = { copilot: ".github/copilot/", }; +const editorOption = new Option("--editor <editor>", "editor integration") + .choices(["claude", "cursor", "vscode", "copilot"] as const) + .default("claude" as const); + export function registerInitCommand(program: Command) { program .command("init") .description("Initialize Archgate governance in the current project") - .option( - "--editor <editor>", - "editor integration to configure (claude, cursor, vscode, copilot)", - "claude" - ) + .addOption(editorOption) .option( "--install-plugin", "install the archgate plugin (requires prior `archgate login`)" ) .action(async (opts) => { try { - const editor = opts.editor as string; - if (!VALID_EDITORS.includes(editor as EditorTarget)) { - logError( - `Unknown editor "${editor}". Supported: ${VALID_EDITORS.join(", ")}` - ); - 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, + editor: opts.editor, installPlugin, }); - const editorTarget = editor as EditorTarget; - const label = EDITOR_LABELS[editorTarget]; - const dir = EDITOR_DIRS[editorTarget]; + const label = EDITOR_LABELS[opts.editor]; + const dir = EDITOR_DIRS[opts.editor]; console.log(`Initialized Archgate governance in ${result.projectRoot}`); console.log(` adrs/ - architecture decision records`); @@ -72,7 +62,7 @@ export function registerInitCommand(program: Command) { } } else { // CLI not found for this editor — show manual commands - printManualInstructions(editorTarget, result.plugin.detail); + printManualInstructions(opts.editor, result.plugin.detail); } } else if (installPlugin) { // User wanted plugin but no credentials diff --git a/src/commands/review-context.ts b/src/commands/review-context.ts index 26b932b0..36c7e7cc 100644 --- a/src/commands/review-context.ts +++ b/src/commands/review-context.ts @@ -1,8 +1,14 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { logError } from "../helpers/log"; import { findProjectRoot } from "../helpers/paths"; import { buildReviewContext } from "../engine/context"; -import { AdrFrontmatterSchema } from "../formats/adr"; +import { ADR_DOMAINS } from "../formats/adr"; + +const domainOption = new Option( + "--domain <domain>", + "filter to a single domain" +).choices(ADR_DOMAINS); export function registerReviewContextCommand(program: Command) { program @@ -12,10 +18,7 @@ export function registerReviewContextCommand(program: Command) { ) .option("--staged", "Only include git-staged files") .option("--run-checks", "Include ADR compliance check results") - .option( - "--domain <domain>", - "Filter to a single domain (backend, frontend, data, architecture, general)" - ) + .addOption(domainOption) .action(async (opts) => { const projectRoot = findProjectRoot(); if (!projectRoot) { @@ -25,26 +28,10 @@ export function registerReviewContextCommand(program: Command) { process.exit(1); } - if (opts.domain) { - const result = AdrFrontmatterSchema.shape.domain.safeParse(opts.domain); - if (!result.success) { - logError( - `Invalid domain '${opts.domain}'. Use: backend, frontend, data, architecture, general` - ); - process.exit(1); - } - } - const context = await buildReviewContext(projectRoot, { staged: opts.staged, runChecks: opts.runChecks, - domain: opts.domain as - | "backend" - | "frontend" - | "data" - | "architecture" - | "general" - | undefined, + domain: opts.domain, }); console.log(JSON.stringify(context, null, 2)); From 3b887d39ab8b251e9bc51166be7eb56db2f00f54 Mon Sep 17 00:00:00 2001 From: Rhuan Barreto <rhuan@barreto.work> Date: Mon, 16 Mar 2026 00:56:41 +0100 Subject: [PATCH 4/5] refactor: migrate parseInt argParser to addOption with argParser() Replace bare parseInt passed to .option() in session-context commands with new Option().argParser() and .addOption(). Fixes a subtle bug where Commander passes the previous value as parseInt's radix argument. Extends ARCH-008 ADR with a new use-add-option-for-arg-parser rule (error severity) to enforce this pattern for all custom parsers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../adrs/ARCH-008-typed-command-options.md | 65 ++++++++++++++++--- .../ARCH-008-typed-command-options.rules.ts | 29 +++++++++ src/commands/session-context/claude-code.ts | 12 ++-- src/commands/session-context/cursor.ts | 12 ++-- 4 files changed, 98 insertions(+), 20 deletions(-) diff --git a/.archgate/adrs/ARCH-008-typed-command-options.md b/.archgate/adrs/ARCH-008-typed-command-options.md index 03e786dd..f09e3d71 100644 --- a/.archgate/adrs/ARCH-008-typed-command-options.md +++ b/.archgate/adrs/ARCH-008-typed-command-options.md @@ -8,32 +8,37 @@ files: ["src/commands/**/*.ts"] ## Context -Commander.js `.option()` accepts arbitrary strings and produces loosely typed option values. When a command accepts a fixed set of choices (e.g., `--editor claude|cursor|vscode|copilot`), using `.option()` requires manual runtime validation and `as` casts to narrow the type — boilerplate that is easy to forget and produces unhelpful error messages when users pass invalid values. +Commander.js `.option()` accepts arbitrary strings and produces loosely typed option values. This causes two problems: + +1. **Fixed choices** — When a command accepts a fixed set of values (e.g., `--editor claude|cursor|vscode|copilot`), using `.option()` requires manual runtime validation and `as` casts to narrow the type — boilerplate that is easy to forget and produces unhelpful error messages. +2. **Custom parsers** — Passing a parser function (e.g., `parseInt`) as the third argument to `.option()` loses type information. The `opts` object infers `string` instead of the parser's return type. Worse, passing `parseInt` directly is a subtle bug: Commander passes `(value, previous)` but `parseInt` interprets `previous` as `radix`. **Alternatives considered:** - **Plain `.option()` with manual validation** — The developer writes a runtime check (`if (!VALID.includes(val))`) and casts to the narrow type. This works but scatters validation logic, produces inconsistent error messages across commands, and the `opts` object remains typed as `string`, requiring `as` casts at every usage site. - **Zod/custom parsing in `.option()` argParser** — Commander supports a custom parse function as the third argument to `.option()`. While this gives runtime validation, it does not narrow the TypeScript type in the `opts` object when using `@commander-js/extra-typings`. -The `@commander-js/extra-typings` package provides `Option` class with a `.choices()` method that both validates at runtime (Commander rejects invalid values with a clear error) and narrows the TypeScript type in the action callback's `opts` parameter. Using `.addOption()` instead of `.option()` integrates this typed option into the command. +The `@commander-js/extra-typings` package provides the `Option` class with `.choices()` for enum-like options and `.argParser()` for custom parsers. Both methods correctly narrow the TypeScript type in the action callback's `opts` parameter. Using `.addOption()` instead of `.option()` integrates these typed options into the command. ## Decision -Commands with fixed-choice options MUST use `new Option().choices().default()` with `.addOption()` instead of plain `.option()` with manual validation. +Options that require type narrowing beyond plain strings MUST use `new Option()` with `.addOption()` instead of plain `.option()`. **Key constraints:** 1. **Use `Option` from `@commander-js/extra-typings`** — Import `Option` alongside `Command` from the extra-typings package to get full type inference. 2. **Use `.choices()` for enum-like options** — Any option accepting a fixed set of values must use `.choices()` to get both runtime validation and compile-time type narrowing. -3. **Use `.addOption()` to register** — The typed `Option` object is passed via `.addOption()`, not `.option()`. -4. **Use `as const` with `.choices()` and `.default()`** — Pass the choices array and default value with `as const` to preserve literal types. -5. **No manual validation for choice options** — Commander handles invalid value rejection automatically; do not duplicate this logic. +3. **Use `.argParser()` for custom parsers** — Any option requiring type conversion (e.g., string to number) must use `.argParser()` on an `Option` object, not pass a parser function as the third argument to `.option()`. +4. **Use `.addOption()` to register** — The typed `Option` object is passed via `.addOption()`, not `.option()`. +5. **Use `as const` with `.choices()` and `.default()`** — Pass the choices array and default value with `as const` to preserve literal types. +6. **No manual validation for choice options** — Commander handles invalid value rejection automatically; do not duplicate this logic. ## Do's and Don'ts ### Do - Use `new Option().choices([...] as const).default(... as const)` for fixed-choice options +- Use `new Option().argParser((val) => ...)` for options that need type conversion - Register typed options via `.addOption()` - Reuse existing type definitions (e.g., `EditorTarget`) for `Record` keys and other type-level usage - Access `opts.editor` directly in switch/case — TypeScript narrows the type @@ -41,6 +46,7 @@ Commands with fixed-choice options MUST use `new Option().choices().default()` w ### Don't - Don't use `.option()` for options with a known set of valid values +- Don't pass parser functions (e.g., `parseInt`) as the third argument to `.option()` — use `.argParser()` on an `Option` object instead - Don't write manual `if (!VALID.includes(val))` checks for choice options — Commander does this - Don't cast `opts.editor as SomeType` — the type should already be narrowed @@ -73,7 +79,29 @@ export function registerExampleCommand(program: Command) { } ``` -### Bad Example +### Good Example (argParser) + +```typescript +import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; + +const maxEntriesOption = new Option( + "--max-entries <n>", + "maximum entries to return" +).argParser((val) => parseInt(val, 10)); + +export function registerExampleCommand(program: Command) { + program + .command("example") + .addOption(maxEntriesOption) + .action(async (opts) => { + // opts.maxEntries is typed as number | undefined + const limit = opts.maxEntries ?? 200; + }); +} +``` + +### Bad Example (choices) ```typescript // BAD: loose typing, manual validation, casts @@ -92,6 +120,21 @@ export function registerExampleCommand(program: Command) { } ``` +### Bad Example (argParser) + +```typescript +// BAD: parseInt passed directly — previous value becomes radix, type is wrong +export function registerExampleCommand(program: Command) { + program + .command("example") + .option("--max-entries <n>", "maximum entries", parseInt) + .action(async (opts) => { + // opts.maxEntries type is not correctly inferred as number + // parseInt receives (value, previous) — previous becomes radix (bug) + }); +} +``` + ## Consequences ### Positive @@ -107,21 +150,23 @@ export function registerExampleCommand(program: Command) { ### Risks -- **Inconsistency with existing commands** — Older commands (e.g., `init --editor`) still use `.option()` with manual validation. These should be migrated incrementally. The automated rule only scans for `.choices()` usage, not for migration of existing code. +- **Regression** — A developer unfamiliar with this ADR may use `.option()` with manual validation or a bare parser function. The automated rules catch both patterns at check time. ## Compliance and Enforcement ### Automated Enforcement - **Archgate rule** ARCH-008/use-add-option-for-choices: Scans command files for `.option()` calls that include a hardcoded choices-like pattern and flags them for migration to `.addOption()` with `.choices()`. Severity: error. +- **Archgate rule** ARCH-008/use-add-option-for-arg-parser: Scans command files for `.option()` calls that pass a parser function (e.g., `parseInt`, `parseFloat`, or arrow functions) as the third argument, and flags them for migration to `new Option().argParser()` with `.addOption()`. Severity: error. ### Manual Enforcement Code reviewers MUST verify: 1. New commands with fixed-choice options use `new Option().choices()` with `.addOption()` -2. The choices array and default use `as const` for literal type preservation -3. No manual validation duplicates Commander's built-in choice rejection +2. New commands with custom parsers use `new Option().argParser()` with `.addOption()` +3. The choices array and default use `as const` for literal type preservation +4. No manual validation duplicates Commander's built-in choice rejection ## References diff --git a/.archgate/adrs/ARCH-008-typed-command-options.rules.ts b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts index 1a211514..478246d5 100644 --- a/.archgate/adrs/ARCH-008-typed-command-options.rules.ts +++ b/.archgate/adrs/ARCH-008-typed-command-options.rules.ts @@ -31,4 +31,33 @@ export default defineRules({ } }, }, + "use-add-option-for-arg-parser": { + description: + "Options with custom parsers must use addOption with argParser() instead of passing a parser to option()", + severity: "error", + async check(ctx) { + const files = ctx.scopedFiles.filter((f) => !f.endsWith("index.ts")); + // Detect .option() calls that pass a function as the third argument + // e.g. .option("--max-entries <n>", "...", parseInt) + const matches = await Promise.all( + files.map((file) => + ctx.grep( + file, + /\.option\(\s*["']--\w+\s+<\w+>["'],\s*["'][^"']*["'],\s*(?:parseInt|parseFloat|\()/ + ) + ) + ); + for (const fileMatches of matches) { + for (const m of fileMatches) { + ctx.report.violation({ + message: + "Use new Option().argParser() with .addOption() instead of passing a parser to .option()", + file: m.file, + line: m.line, + fix: "Replace .option() with new Option(...).argParser((val) => ...) and register via .addOption()", + }); + } + } + }, + }, }); diff --git a/src/commands/session-context/claude-code.ts b/src/commands/session-context/claude-code.ts index 433aa186..41a0d724 100644 --- a/src/commands/session-context/claude-code.ts +++ b/src/commands/session-context/claude-code.ts @@ -1,17 +1,19 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { findProjectRoot } from "../../helpers/paths"; import { readClaudeCodeSession } from "../../helpers/session-context"; import { logError } from "../../helpers/log"; +const maxEntriesOption = new Option( + "--max-entries <n>", + "maximum entries to return (default: 200)" +).argParser((val) => parseInt(val, 10)); + export function registerClaudeCodeSessionContextCommand(parent: Command) { parent .command("claude-code") .description("Read Claude Code session transcript for the project") - .option( - "--max-entries <n>", - "Maximum entries to return (default: 200)", - parseInt - ) + .addOption(maxEntriesOption) .action(async (opts) => { const projectRoot = findProjectRoot(); const result = await readClaudeCodeSession(projectRoot, { diff --git a/src/commands/session-context/cursor.ts b/src/commands/session-context/cursor.ts index 35d934ed..32e7e6dc 100644 --- a/src/commands/session-context/cursor.ts +++ b/src/commands/session-context/cursor.ts @@ -1,17 +1,19 @@ import type { Command } from "@commander-js/extra-typings"; +import { Option } from "@commander-js/extra-typings"; import { findProjectRoot } from "../../helpers/paths"; import { readCursorSession } from "../../helpers/session-context"; import { logError } from "../../helpers/log"; +const maxEntriesOption = new Option( + "--max-entries <n>", + "maximum entries to return (default: 200)" +).argParser((val) => parseInt(val, 10)); + export function registerCursorSessionContextCommand(parent: Command) { parent .command("cursor") .description("Read Cursor agent session transcript for the project") - .option( - "--max-entries <n>", - "Maximum entries to return (default: 200)", - parseInt - ) + .addOption(maxEntriesOption) .option("--session-id <id>", "Specific session UUID to read") .action(async (opts) => { const projectRoot = findProjectRoot(); From 0196d2ee8fd630f193a063872abb76e3894c11ca Mon Sep 17 00:00:00 2001 From: Rhuan Barreto <rhuan@barreto.work> Date: Mon, 16 Mar 2026 01:14:38 +0100 Subject: [PATCH 5/5] docs: add plugin command reference and update editor guides Add `archgate plugin url` and `archgate plugin install` subcommands to the CLI reference docs. Update all editor plugin guides (Claude Code, Cursor, VS Code, Copilot) with the new `archgate plugin install` command for already-initialized projects. Both English and Portuguese versions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../docs/guides/claude-code-plugin.mdx | 14 +++ .../docs/guides/copilot-cli-plugin.mdx | 6 ++ .../docs/guides/cursor-integration.mdx | 6 ++ .../src/content/docs/guides/vscode-plugin.mdx | 6 ++ .../docs/pt-br/guides/claude-code-plugin.mdx | 14 +++ .../docs/pt-br/guides/copilot-cli-plugin.mdx | 6 ++ .../docs/pt-br/guides/cursor-integration.mdx | 6 ++ .../docs/pt-br/guides/vscode-plugin.mdx | 6 ++ .../docs/pt-br/reference/cli-commands.mdx | 86 ++++++++++++++++++- .../content/docs/reference/cli-commands.mdx | 86 ++++++++++++++++++- 10 files changed, 228 insertions(+), 8 deletions(-) diff --git a/docs/src/content/docs/guides/claude-code-plugin.mdx b/docs/src/content/docs/guides/claude-code-plugin.mdx index 587515e9..ebcce6b3 100644 --- a/docs/src/content/docs/guides/claude-code-plugin.mdx +++ b/docs/src/content/docs/guides/claude-code-plugin.mdx @@ -56,6 +56,20 @@ If the `claude` CLI is on your PATH, the plugin is installed automatically via: If the `claude` CLI is not found, the command prints the manual commands for you to run. +### Installing the plugin on an existing project + +If your project is already initialized, you can install or reinstall the plugin without re-running `archgate init`: + +```bash +archgate plugin install +``` + +To get the authenticated repository URL for manual configuration: + +```bash +archgate plugin url +``` + ## Initial setup with onboard After installation, run the `archgate:onboard` skill in your project once. This skill: diff --git a/docs/src/content/docs/guides/copilot-cli-plugin.mdx b/docs/src/content/docs/guides/copilot-cli-plugin.mdx index 70599561..951483e2 100644 --- a/docs/src/content/docs/guides/copilot-cli-plugin.mdx +++ b/docs/src/content/docs/guides/copilot-cli-plugin.mdx @@ -47,6 +47,12 @@ To explicitly request plugin installation: archgate init --editor copilot --install-plugin ``` +To install or reinstall the plugin on an already-initialized project: + +```bash +archgate plugin install --editor copilot +``` + ### Generated files The command creates the following configuration: diff --git a/docs/src/content/docs/guides/cursor-integration.mdx b/docs/src/content/docs/guides/cursor-integration.mdx index 1afd62e5..8695f9ce 100644 --- a/docs/src/content/docs/guides/cursor-integration.mdx +++ b/docs/src/content/docs/guides/cursor-integration.mdx @@ -30,6 +30,12 @@ archgate init --editor cursor --install-plugin The plugin bundle is downloaded from `plugins.archgate.dev` and extracted into the `.cursor/` directory. It includes agent rules, skill definitions, and MCP configuration. +To install or reinstall the plugin on an already-initialized project: + +```bash +archgate plugin install --editor cursor +``` + ### Without plugin (free) Without the plugin, `archgate init --editor cursor` still configures the MCP server connection and a basic governance rule. The AI agent can access your ADRs and run checks, but does not get the role-based skills described below. diff --git a/docs/src/content/docs/guides/vscode-plugin.mdx b/docs/src/content/docs/guides/vscode-plugin.mdx index 197034af..77ec7186 100644 --- a/docs/src/content/docs/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/guides/vscode-plugin.mdx @@ -59,6 +59,12 @@ To explicitly request plugin installation: archgate init --editor vscode --install-plugin ``` +To install or reinstall the plugin on an already-initialized project: + +```bash +archgate plugin install --editor vscode +``` + ### Generated files The command creates or updates the following files: diff --git a/docs/src/content/docs/pt-br/guides/claude-code-plugin.mdx b/docs/src/content/docs/pt-br/guides/claude-code-plugin.mdx index 54b2fd5e..d92cd83e 100644 --- a/docs/src/content/docs/pt-br/guides/claude-code-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/claude-code-plugin.mdx @@ -56,6 +56,20 @@ Se o CLI `claude` estiver no seu PATH, o plugin é instalado automaticamente via Se o CLI `claude` não for encontrado, o comando exibe os comandos manuais para você executar. +### Instalando o plugin em um projeto existente + +Se seu projeto já foi inicializado, você pode instalar ou reinstalar o plugin sem executar `archgate init` novamente: + +```bash +archgate plugin install +``` + +Para obter a URL autenticada do repositório para configuração manual: + +```bash +archgate plugin url +``` + ## Configuração inicial com onboard Após a instalação, execute a skill `archgate:onboard` no seu projeto uma vez. Essa skill: diff --git a/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx b/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx index 4d67cf1b..a1293989 100644 --- a/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/copilot-cli-plugin.mdx @@ -47,6 +47,12 @@ Para solicitar explicitamente a instalação do plugin: archgate init --editor copilot --install-plugin ``` +Para instalar ou reinstalar o plugin em um projeto já inicializado: + +```bash +archgate plugin install --editor copilot +``` + ### Arquivos gerados O comando cria a seguinte configuração: diff --git a/docs/src/content/docs/pt-br/guides/cursor-integration.mdx b/docs/src/content/docs/pt-br/guides/cursor-integration.mdx index 1225a4a9..987d8b17 100644 --- a/docs/src/content/docs/pt-br/guides/cursor-integration.mdx +++ b/docs/src/content/docs/pt-br/guides/cursor-integration.mdx @@ -30,6 +30,12 @@ archgate init --editor cursor --install-plugin O pacote do plugin é baixado de `plugins.archgate.dev` e extraído no diretório `.cursor/`. Ele inclui regras de agente, definições de skills e configuração MCP. +Para instalar ou reinstalar o plugin em um projeto já inicializado: + +```bash +archgate plugin install --editor cursor +``` + ### Sem plugin (gratuito) Sem o plugin, `archgate init --editor cursor` ainda configura a conexão com o servidor MCP e uma regra de governança básica. O agente de IA pode acessar seus ADRs e executar verificações, mas não recebe as skills baseadas em papéis descritas abaixo. diff --git a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx index 0b0eaa92..54881134 100644 --- a/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx +++ b/docs/src/content/docs/pt-br/guides/vscode-plugin.mdx @@ -59,6 +59,12 @@ Para solicitar explicitamente a instalação do plugin: archgate init --editor vscode --install-plugin ``` +Para instalar ou reinstalar o plugin em um projeto já inicializado: + +```bash +archgate plugin install --editor vscode +``` + ### Arquivos gerados O comando cria ou atualiza os seguintes arquivos: diff --git a/docs/src/content/docs/pt-br/reference/cli-commands.mdx b/docs/src/content/docs/pt-br/reference/cli-commands.mdx index 0db6b86a..d50fd2f7 100644 --- a/docs/src/content/docs/pt-br/reference/cli-commands.mdx +++ b/docs/src/content/docs/pt-br/reference/cli-commands.mdx @@ -102,10 +102,10 @@ Cria o diretório `.archgate/` com um ADR de exemplo, arquivo de regras compleme ### Opções -| Opção | Padrão | Descrição | -| ------------------- | -------- | ------------------------------------------------------------------------ | -| `--editor <editor>` | `claude` | Integração de editor a configurar (`claude`, `cursor`) | -| `--install-plugin` | auto | Instalar o plugin de editor do Archgate (requer `archgate login` prévio) | +| Opção | Padrão | Descrição | +| ------------------- | -------- | --------------------------------------------------------------------------- | +| `--editor <editor>` | `claude` | Integração de editor a configurar (`claude`, `cursor`, `vscode`, `copilot`) | +| `--install-plugin` | auto | Instalar o plugin de editor do Archgate (requer `archgate login` prévio) | Quando `--install-plugin` é passado, a CLI instala o plugin do Archgate para o editor selecionado. Se a flag for omitida, a CLI faz detecção automática: instala o plugin quando existem credenciais válidas (de um `archgate login` anterior) e pula caso contrário. @@ -141,6 +141,84 @@ Quando `--editor cursor` é usado, a saída mostra `.cursor/` em vez de `.claude --- +## archgate plugin + +Gerencia os plugins de editor do Archgate independentemente do `archgate init`. + +```bash +archgate plugin <subcomando> [options] +``` + +Use `archgate plugin` para instalar plugins ou obter a URL autenticada do repositório em projetos que já foram inicializados. + +### Subcomandos + +#### archgate plugin url + +Exibe a URL autenticada do repositório de plugins para configuração manual de ferramentas. + +```bash +archgate plugin url [options] +``` + +| Opção | Padrão | Descrição | +| ------------------- | -------- | ----------------------------------------------------- | +| `--editor <editor>` | `claude` | Editor alvo (`claude`, `cursor`, `vscode`, `copilot`) | + +A URL inclui suas credenciais e pode ser usada para configurar manualmente as ferramentas do editor. Por exemplo, para adicionar o marketplace do Archgate no Claude Code: + +```bash +claude plugin marketplace add "$(archgate plugin url)" +claude plugin install archgate@archgate +``` + +Para o VS Code, a URL aponta para um repositório de plugin separado: + +```bash +archgate plugin url --editor vscode +``` + +#### archgate plugin install + +Instala o plugin do Archgate para o editor especificado em um projeto já inicializado. + +```bash +archgate plugin install [options] +``` + +| Opção | Padrão | Descrição | +| ------------------- | -------- | ----------------------------------------------------- | +| `--editor <editor>` | `claude` | Editor alvo (`claude`, `cursor`, `vscode`, `copilot`) | + +O comportamento de instalação varia por editor: + +- **Claude Code:** Instala automaticamente via CLI `claude` se disponível; exibe comandos manuais caso contrário. +- **Copilot CLI:** Instala automaticamente via CLI `copilot` se disponível; exibe comandos manuais caso contrário. +- **Cursor:** Baixa e extrai o pacote do plugin no diretório `.cursor/`. +- **VS Code:** Adiciona a URL do marketplace nas configurações de usuário do VS Code. + +### Exemplos + +Obter a URL do plugin para configuração manual: + +```bash +archgate plugin url +``` + +Instalar o plugin para o Claude Code: + +```bash +archgate plugin install +``` + +Instalar o plugin para o Cursor: + +```bash +archgate plugin install --editor cursor +``` + +--- + ## archgate check Executa todas as verificações automatizadas de conformidade com ADRs no codebase. diff --git a/docs/src/content/docs/reference/cli-commands.mdx b/docs/src/content/docs/reference/cli-commands.mdx index 05d94467..d363ea5f 100644 --- a/docs/src/content/docs/reference/cli-commands.mdx +++ b/docs/src/content/docs/reference/cli-commands.mdx @@ -102,10 +102,10 @@ Creates the `.archgate/` directory with an example ADR, companion rules file, an ### Options -| Option | Default | Description | -| ------------------- | -------- | -------------------------------------------------------------------- | -| `--editor <editor>` | `claude` | Editor integration to configure (`claude`, `cursor`) | -| `--install-plugin` | auto | Install the Archgate editor plugin (requires prior `archgate login`) | +| Option | Default | Description | +| ------------------- | -------- | ------------------------------------------------------------------------- | +| `--editor <editor>` | `claude` | Editor integration to configure (`claude`, `cursor`, `vscode`, `copilot`) | +| `--install-plugin` | auto | Install the Archgate editor plugin (requires prior `archgate login`) | When `--install-plugin` is passed, the CLI installs the Archgate plugin for the selected editor. If the flag is omitted, the CLI auto-detects: it installs the plugin when valid credentials exist (from a previous `archgate login`) and skips otherwise. @@ -141,6 +141,84 @@ When `--editor cursor` is used, the output shows `.cursor/` instead of `.claude/ --- +## archgate plugin + +Manage Archgate editor plugins independently of `archgate init`. + +```bash +archgate plugin <subcommand> [options] +``` + +Use `archgate plugin` to install plugins or retrieve the authenticated repository URL on projects that have already been initialized. + +### Subcommands + +#### archgate plugin url + +Print the authenticated plugin repository URL for manual tool configuration. + +```bash +archgate plugin url [options] +``` + +| Option | Default | Description | +| ------------------- | -------- | ------------------------------------------------------- | +| `--editor <editor>` | `claude` | Target editor (`claude`, `cursor`, `vscode`, `copilot`) | + +The URL includes your credentials and can be used to manually configure editor tools. For example, to add the Archgate marketplace in Claude Code: + +```bash +claude plugin marketplace add "$(archgate plugin url)" +claude plugin install archgate@archgate +``` + +For VS Code, the URL points to a separate plugin repository: + +```bash +archgate plugin url --editor vscode +``` + +#### archgate plugin install + +Install the Archgate plugin for the specified editor on an already-initialized project. + +```bash +archgate plugin install [options] +``` + +| Option | Default | Description | +| ------------------- | -------- | ------------------------------------------------------- | +| `--editor <editor>` | `claude` | Target editor (`claude`, `cursor`, `vscode`, `copilot`) | + +Installation behavior varies by editor: + +- **Claude Code:** Auto-installs via `claude` CLI if available; prints manual commands otherwise. +- **Copilot CLI:** Auto-installs via `copilot` CLI if available; prints manual commands otherwise. +- **Cursor:** Downloads and extracts the plugin bundle into `.cursor/`. +- **VS Code:** Adds the marketplace URL to VS Code user settings. + +### Examples + +Get the plugin URL for manual configuration: + +```bash +archgate plugin url +``` + +Install the plugin for Claude Code: + +```bash +archgate plugin install +``` + +Install the plugin for Cursor: + +```bash +archgate plugin install --editor cursor +``` + +--- + ## archgate check Run all automated ADR compliance checks against the codebase.