Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import { isTlsError, tlsHintMessage } from "../helpers/tls";
export function registerLoginCommand(program: Command) {
const login = program
.command("login")
.description("Authenticate with GitHub to access archgate plugins");
.description("Authenticate with GitHub and register for archgate plugins");

login.action(async () => {
try {
// Check if already logged in
// Check if already registered
const existing = await loadCredentials();
if (existing) {
logInfo(
`Already logged in as ${styleText("bold", existing.github_user)}.`,
`Already registered as ${styleText("bold", existing.github_user)}.`,
"Run `archgate login refresh` to re-authenticate."
);
return;
Expand All @@ -43,29 +43,29 @@ export function registerLoginCommand(program: Command) {

login
.command("status")
.description("Show current authentication status")
.description("Show current registration status")
.action(async () => {
const creds = await loadCredentials();
if (creds) {
console.log(
`Logged in as ${styleText("bold", creds.github_user)} (since ${creds.created_at})`
`Registered as ${styleText("bold", creds.github_user)} (since ${creds.created_at})`
);
} else {
console.log("Not logged in. Run `archgate login` to authenticate.");
console.log("Not registered. Run `archgate login` to sign up.");
}
});

login
.command("logout")
.description("Remove stored credentials")
.description("Remove stored registration info")
.action(async () => {
await clearCredentials();
console.log("Logged out successfully.");
});

login
.command("refresh")
.description("Re-authenticate and claim a new token")
.description("Re-authenticate and update registration")
.action(async () => {
try {
await clearCredentials();
Expand Down
55 changes: 38 additions & 17 deletions src/commands/plugin/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";

import { loadCredentials } from "../../helpers/auth";
import { EDITOR_LABELS } from "../../helpers/init-project";
import { logError, logInfo, logWarn } from "../../helpers/log";
import { findProjectRoot } from "../../helpers/paths";
Expand All @@ -28,25 +27,16 @@ export function registerPluginInstallCommand(plugin: Command) {
.description("Install the archgate plugin for the specified editor")
.addOption(editorOption)
.action(async (opts) => {
const credentials = await loadCredentials();
if (!credentials) {
logError(
"Not logged in.",
"Run `archgate login` first to authenticate."
);
process.exit(1);
}

const label = EDITOR_LABELS[opts.editor];

try {
switch (opts.editor) {
case "claude": {
if (await isClaudeCliAvailable()) {
await installClaudePlugin(credentials);
await installClaudePlugin();
logInfo(`Archgate plugin installed for ${label}.`);
} else {
const url = buildMarketplaceUrl(credentials);
const url = buildMarketplaceUrl();
logWarn(
"Claude CLI not found. To install the plugin manually, run:"
);
Expand All @@ -56,16 +46,22 @@ export function registerPluginInstallCommand(plugin: Command) {
console.log(
` ${styleText("bold", "claude plugin install")} archgate@archgate`
);
console.log(
`\nNote: Your git credentials must be configured for plugins.archgate.dev.`
);
console.log(
`Run ${styleText("bold", "gh auth login")} if you use GitHub CLI.`
);
}
break;
}

case "copilot": {
if (await isCopilotCliAvailable()) {
await installCopilotPlugin(credentials);
await installCopilotPlugin();
logInfo(`Archgate plugin installed for ${label}.`);
} else {
const url = buildMarketplaceUrl(credentials);
const url = buildMarketplaceUrl();
logWarn(
"Copilot CLI not found. To install the plugin manually, run:"
);
Expand All @@ -77,10 +73,34 @@ export function registerPluginInstallCommand(plugin: Command) {
}

case "cursor": {
logWarn(
"Cursor plugin installation requires GitHub credentials."
);
logInfo(
"Please provide your GitHub personal access token (PAT) for authentication."
);

const { default: inquirer } = await import("inquirer");
const { username } = await inquirer.prompt({
type: "input",
name: "username",
message: "GitHub username:",
validate: (v: string) =>
v.trim().length > 0 || "Username is required",
});
const { token } = await inquirer.prompt({
type: "password",
name: "token",
message: "GitHub token (PAT):",
validate: (v: string) =>
v.trim().length > 0 || "Token is required",
});

const projectRoot = findProjectRoot() ?? process.cwd();
const files = await installCursorPlugin(
projectRoot,
credentials.token
username.trim(),
token.trim()
);
logInfo(
`Archgate plugin installed for ${label}.`,
Expand All @@ -90,14 +110,15 @@ export function registerPluginInstallCommand(plugin: Command) {
}

case "vscode": {
const url = buildVscodeMarketplaceUrl(credentials);
const url = buildVscodeMarketplaceUrl();
await configureVscodeSettings(
findProjectRoot() ?? process.cwd(),
url
);
logInfo(
`Archgate plugin configured for ${label}.`,
"Marketplace URL added to VS Code user settings."
"Marketplace URL added to VS Code user settings.",
"Note: Your git credentials must be configured for plugins.archgate.dev."
);
break;
}
Expand Down
18 changes: 4 additions & 14 deletions src/commands/plugin/url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";

import { loadCredentials } from "../../helpers/auth";
import { logError } from "../../helpers/log";
import {
buildMarketplaceUrl,
Expand All @@ -16,24 +15,15 @@ export function registerPluginUrlCommand(plugin: Command) {
plugin
.command("url")
.description(
"Print the authenticated plugin repository URL for manual configuration"
"Print the plugin repository URL for manual configuration"
)
.addOption(editorOption)
.action(async (opts) => {
.action((opts) => {
try {
const credentials = await loadCredentials();
if (!credentials) {
logError(
"Not logged in.",
"Run `archgate login` first to authenticate."
);
process.exit(1);
}

const url =
opts.editor === "vscode"
? buildVscodeMarketplaceUrl(credentials)
: buildMarketplaceUrl(credentials);
? buildVscodeMarketplaceUrl()
: buildMarketplaceUrl();

console.log(url);
} catch (err) {
Expand Down
62 changes: 10 additions & 52 deletions src/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/**
* auth.ts — GitHub Device Flow authentication and archgate token management.
* auth.ts — GitHub Device Flow authentication and registration management.
*
* Implements RFC 8628 (OAuth 2.0 Device Authorization Grant) for CLI login,
* plus local storage of the archgate plugin token in ~/.archgate/credentials.
* Implements RFC 8628 (OAuth 2.0 Device Authorization Grant) for CLI login.
* Users authenticate via GitHub credentials (git credential helpers) — no
* archgate-specific tokens are needed. The login flow registers the user
* and stores their GitHub username in ~/.archgate/credentials.
*/

import { chmodSync, unlinkSync } from "node:fs";

import { logDebug } from "./log";
import { internalPath, createPathIfNotExists } from "./paths";
import { SignupRequiredError, isSignupRequiredError } from "./signup";

// ---------------------------------------------------------------------------
// 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";
Expand Down Expand Up @@ -60,7 +59,6 @@ type DeviceTokenResponse =
| DeviceTokenErrorResponse;

export interface StoredCredentials {
token: string;
github_user: string;
created_at: string;
}
Expand Down Expand Up @@ -182,49 +180,8 @@ export async function getGitHubUser(
return { login: data.login, email: data.email ?? null };
}

// ---------------------------------------------------------------------------
// 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<string> {
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),
redirect: "error",
});

if (!response.ok) {
const body = (await response.json().catch(() => ({}))) as {
error?: string;
};

if (isSignupRequiredError(body.error)) {
throw new SignupRequiredError();
}

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;
}

// Re-export for consumers that import from auth.ts
export { SignupRequiredError } from "./signup";
export { SignupRequiredError, isSignupRequiredError } from "./signup";

// ---------------------------------------------------------------------------
// Credential Storage
Expand All @@ -235,7 +192,8 @@ function credentialsPath(): string {
}

/**
* Persist archgate credentials to ~/.archgate/credentials (JSON).
* Persist registration info to ~/.archgate/credentials (JSON).
* Only stores GitHub username — no tokens (users authenticate via git credentials).
*/
export async function saveCredentials(
credentials: StoredCredentials
Expand All @@ -252,7 +210,7 @@ export async function saveCredentials(
}

/**
* Load stored archgate credentials, or null if none exist.
* Load stored credentials, or null if none exist.
*/
export async function loadCredentials(): Promise<StoredCredentials | null> {
const file = Bun.file(credentialsPath());
Expand All @@ -262,7 +220,7 @@ export async function loadCredentials(): Promise<StoredCredentials | null> {

try {
const data = (await file.json()) as StoredCredentials;
if (!data.token || !data.github_user) {
if (!data.github_user) {
return null;
}
return data;
Expand Down
22 changes: 9 additions & 13 deletions src/helpers/init-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async function configureEditorSettings(
const { loadCredentials } = await import("./auth");
const creds = await loadCredentials();
const marketplaceUrl = creds
? (await import("./plugin-install")).buildVscodeMarketplaceUrl(creds)
? (await import("./plugin-install")).buildVscodeMarketplaceUrl()
: undefined;
return configureVscodeSettings(projectRoot, marketplaceUrl);
}
Expand Down Expand Up @@ -227,7 +227,7 @@ async function ensureEslintrcOverride(projectRoot: string): Promise<void> {
* Returns null-safe result — never throws.
*/
async function tryInstallPlugin(
projectRoot: string,
_projectRoot: string,
editor: EditorTarget
): Promise<PluginResult> {
const { loadCredentials } = await import("./auth");
Expand All @@ -237,13 +237,9 @@ async function tryInstallPlugin(
}

if (editor === "cursor") {
const { installCursorPlugin } = await import("./plugin-install");
const files = await installCursorPlugin(projectRoot, credentials.token);
return {
installed: true,
autoInstalled: true,
detail: `Extracted ${files.length} files to .cursor/`,
};
// Cursor requires GitHub credentials for download — skip auto-install during init.
// Users can run `archgate plugin install --editor cursor` separately.
return { installed: false };
}

if (editor === "vscode") {
Expand All @@ -262,14 +258,14 @@ async function tryInstallPlugin(

if (await isCopilotCliAvailable()) {
try {
await installCopilotPlugin(credentials);
await installCopilotPlugin();
return { installed: true, autoInstalled: true };
} catch {
// Fall through to manual instructions
}
}

const url = buildMarketplaceUrl(credentials);
const url = buildMarketplaceUrl();
return { installed: true, detail: url };
}

Expand All @@ -279,13 +275,13 @@ async function tryInstallPlugin(

if (await isClaudeCliAvailable()) {
try {
await installClaudePlugin(credentials);
await installClaudePlugin();
return { installed: true, autoInstalled: true };
} catch {
// Fall through to manual instructions
}
}

const url = buildMarketplaceUrl(credentials);
const url = buildMarketplaceUrl();
return { installed: true, detail: url };
}
Loading
Loading