Skip to content
Merged
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
10 changes: 7 additions & 3 deletions .archgate/adrs/ARCH-004-no-barrel-files.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
id: ARCH-004
title: No Barrel Files
title: No Barrel Files or Re-Exports
domain: architecture
rules: true
files: ["src/**/*.ts"]
---

# No Barrel Files
# No Barrel Files or Re-Exports

## Context

Expand All @@ -30,7 +30,7 @@ This decision aligns with [ARCH-001 — Command Structure](./ARCH-001-command-st

## Decision

**Barrel files are forbidden.** All imports MUST point directly to the module that defines the symbol.
**Barrel files and re-exports are forbidden.** All imports MUST point directly to the module that defines the symbol.

This ADR covers all TypeScript source files under `src/`. It does not cover test files or configuration files.

Expand All @@ -39,6 +39,8 @@ A barrel file is defined as an `index.ts` file that:
- Contains **only** `export`, `export type`, or `import type` statements (re-exports)
- Has **no** function definitions, class definitions, variable declarations, or executable logic

A **re-export** is any `export { X } from "./other-module"` or `export type { X } from "./other-module"` statement in any file (not just `index.ts`). Re-exports create the same indirection problems as barrel files: hidden coupling, grep-unfriendly navigation, and obscured dependency graphs.

Files named `index.ts` that contain actual logic are **not** barrel files and are permitted. Examples of permitted `index.ts` files:

- `src/commands/adr/index.ts` — defines `registerAdrCommand()` with command group composition logic
Expand All @@ -57,10 +59,12 @@ Files named `index.ts` that contain actual logic are **not** barrel files and ar
### Don't

- **DON'T** create `index.ts` files that only re-export symbols from sibling modules
- **DON'T** re-export symbols from other modules via `export { X } from "./other"` in any file — consumers must import directly from the defining module
- **DON'T** import from a directory path (e.g., `from "../formats"`) expecting implicit `index.ts` resolution
- **DON'T** use barrel files as a "public API" facade — this project has no external module consumers
- **DON'T** add re-export-only statements to an otherwise legitimate `index.ts` — keep composition logic and re-exports separate
- **DON'T** create `index.ts` files to "simplify" imports — the verbosity of direct imports is the feature, not a problem
- **DON'T** use a module as a "facade" that re-exports from multiple sources — each import should point to exactly one defining module

## Implementation Pattern

Expand Down
11 changes: 6 additions & 5 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"agent": "archgate:developer",
"permissions": {
"defaultMode": "bypassPermissions",
"allow": [
"Skill(archgate:architect)",
"Skill(archgate:quality-manager)",
"Skill(archgate:adr-author)"
]
}
"Skill(archgate:adr-author)",
"WebSearch"
],
"defaultMode": "bypassPermissions"
},
"agent": "archgate:developer"
}
2 changes: 1 addition & 1 deletion src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
import inquirer from "inquirer";

import { loadCredentials } from "../helpers/auth";
import { loadCredentials } from "../helpers/credential-store";
import { EDITOR_LABELS, initProject } from "../helpers/init-project";
import type { EditorTarget } from "../helpers/init-project";
import { logError, logInfo, logWarn } from "../helpers/log";
Expand Down
2 changes: 1 addition & 1 deletion src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { styleText } from "node:util";

import type { Command } from "@commander-js/extra-typings";

import { loadCredentials, clearCredentials } from "../helpers/auth";
import { loadCredentials, clearCredentials } from "../helpers/credential-store";
import { logError, logInfo } from "../helpers/log";
import { runLoginFlow } from "../helpers/login-flow";
import { findProjectRoot } from "../helpers/paths";
Expand Down
12 changes: 6 additions & 6 deletions src/commands/plugin/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { loadCredentials } from "../../helpers/credential-store";
import { EDITOR_LABELS } from "../../helpers/init-project";
import { logError, logInfo, logWarn } from "../../helpers/log";
import { findProjectRoot } from "../../helpers/paths";
Expand Down Expand Up @@ -43,10 +43,10 @@ export function registerPluginInstallCommand(plugin: Command) {
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 @@ -62,10 +62,10 @@ export function registerPluginInstallCommand(plugin: Command) {

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 @@ -90,7 +90,7 @@ export function registerPluginInstallCommand(plugin: Command) {
}

case "vscode": {
const url = buildVscodeMarketplaceUrl(credentials);
const url = buildVscodeMarketplaceUrl();
await configureVscodeSettings(
findProjectRoot() ?? process.cwd(),
url
Expand Down
20 changes: 4 additions & 16 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 @@ -15,25 +14,14 @@ const editorOption = new Option("--editor <editor>", "target editor")
export function registerPluginUrlCommand(plugin: Command) {
plugin
.command("url")
.description(
"Print the authenticated plugin repository URL for manual configuration"
)
.description("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
75 changes: 3 additions & 72 deletions src/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
/**
* 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.
* Implements RFC 8628 (OAuth 2.0 Device Authorization Grant) for CLI login.
* Token storage is delegated to credential-store.ts which uses git's native
* credential helpers (macOS Keychain, Windows Credential Manager, libsecret).
*/

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

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

// ---------------------------------------------------------------------------
Expand All @@ -18,7 +15,6 @@ import { SignupRequiredError, isSignupRequiredError } from "./signup";
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).
Expand Down Expand Up @@ -59,12 +55,6 @@ type DeviceTokenResponse =
| DeviceTokenPendingResponse
| DeviceTokenErrorResponse;

export interface StoredCredentials {
token: string;
github_user: string;
created_at: string;
}

// ---------------------------------------------------------------------------
// GitHub Device Flow
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -222,62 +212,3 @@ export async function claimArchgateToken(githubToken: string): Promise<string> {
}
return data.token;
}

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

// ---------------------------------------------------------------------------
// Credential Storage
// ---------------------------------------------------------------------------

function credentialsPath(): string {
return internalPath(CREDENTIALS_FILE);
}

/**
* Persist archgate credentials to ~/.archgate/credentials (JSON).
*/
export async function saveCredentials(
credentials: StoredCredentials
): Promise<void> {
createPathIfNotExists(internalPath());
const filePath = credentialsPath();
await Bun.write(filePath, JSON.stringify(credentials, null, 2) + "\n");
try {
chmodSync(filePath, 0o600);
} catch {
// chmod may fail on Windows — NTFS uses ACLs instead
}
logDebug("Credentials saved to", filePath);
}

/**
* Load stored archgate credentials, or null if none exist.
*/
export async function loadCredentials(): Promise<StoredCredentials | null> {
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<void> {
if (await Bun.file(credentialsPath()).exists()) {
unlinkSync(credentialsPath());
logDebug("Credentials removed");
}
}
Loading
Loading