diff --git a/.archgate/adrs/ARCH-011-consistent-project-root-resolution.md b/.archgate/adrs/ARCH-011-consistent-project-root-resolution.md
new file mode 100644
index 00000000..9aa24b98
--- /dev/null
+++ b/.archgate/adrs/ARCH-011-consistent-project-root-resolution.md
@@ -0,0 +1,74 @@
+---
+id: ARCH-011
+title: Consistent Project Root Resolution
+domain: architecture
+rules: true
+files: ["src/commands/**/*.ts"]
+---
+
+# Consistent Project Root Resolution
+
+## Context
+
+Commands that operate on project-level resources (ADRs, rules, `.archgate/` config) need to locate the project root directory. Inconsistent strategies for finding the project root cause different behavior depending on the user's working directory:
+
+- Commands using `findProjectRoot()` (walks up from cwd to find `.archgate/adrs/`) work correctly from any subdirectory
+- Commands using `process.cwd()` directly fail when the user is in a subdirectory because they assume cwd IS the project root
+
+This inconsistency was discovered during a repository-wide consistency review where `archgate check` worked from subdirectories but `archgate adr list` did not.
+
+**Alternatives considered:**
+
+- **Always use `process.cwd()`** — Simplest, but breaks when the user is in a subdirectory of the project. This is a common workflow (e.g., running `archgate adr list` while editing files in `src/`).
+- **Require users to run from the project root** — Adds friction and goes against CLI conventions (git, npm, etc. all resolve upward).
+- **Walk up from cwd to find `.archgate/adrs/`** — Standard convention used by git, npm, and other project-aware CLIs. Already implemented as `findProjectRoot()` in `src/helpers/paths.ts`.
+
+## Decision
+
+All commands that operate on `.archgate/` project resources MUST use `findProjectRoot()` from `src/helpers/paths.ts` to locate the project root. Direct use of `process.cwd()` for project root resolution in command files is prohibited.
+
+**Exceptions:**
+
+- `archgate init` — Creates the `.archgate/` directory; uses `process.cwd()` because no project root exists yet
+- `archgate upgrade` — Operates on the binary, not on a project; its `findPackageRoot()` walks up from the binary path to find `package.json` for local install detection (a different concern than project root)
+- Commands that don't require a project (e.g., `clean`, `login`) are not affected
+
+## Do's and Don'ts
+
+### Do
+
+- Use `findProjectRoot()` from `src/helpers/paths.ts` in all commands that read from `.archgate/`
+- Check the return value for `null` and exit with a helpful error message
+- Pass the resolved `projectRoot` to `projectPaths()` for derived paths
+
+### Don't
+
+- Don't use `process.cwd()` to locate `.archgate/` in command files (except `init`)
+- Don't define local `findProjectRoot()` variants — use the shared implementation
+- Don't assume the user is running from the project root
+
+## Consequences
+
+### Positive
+
+- All commands work consistently regardless of the user's working directory
+- Matches user expectations from git, npm, and other project-aware CLIs
+
+### Negative
+
+- Slightly more verbose command setup (null check on `findProjectRoot()`)
+
+## Compliance and Enforcement
+
+### Automated Enforcement
+
+- **Archgate rule** `ARCH-011/no-process-cwd-for-project-root`: Scans command files for `process.cwd()` usage and flags violations. The `init` command is exempt. Severity: `error`.
+
+### Manual Enforcement
+
+Code reviewers MUST verify that new commands use `findProjectRoot()` for project-aware operations.
+
+## References
+
+- [ARCH-001 — Command Structure](./ARCH-001-command-structure.md) — Commands handle I/O only
+- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — Exit with code 1 when project not found
diff --git a/.archgate/adrs/ARCH-011-consistent-project-root-resolution.rules.ts b/.archgate/adrs/ARCH-011-consistent-project-root-resolution.rules.ts
new file mode 100644
index 00000000..af209b7f
--- /dev/null
+++ b/.archgate/adrs/ARCH-011-consistent-project-root-resolution.rules.ts
@@ -0,0 +1,38 @@
+///
+
+export default {
+ rules: {
+ "no-process-cwd-for-project-root": {
+ description:
+ "Command files must use findProjectRoot() instead of process.cwd() for project root resolution",
+ severity: "error",
+ async check(ctx) {
+ // init.ts is exempt — it creates the project, so no root exists yet
+ const files = ctx.scopedFiles.filter(
+ (f) =>
+ f.includes("commands/") &&
+ !f.endsWith("init.ts") &&
+ !f.endsWith("index.ts")
+ );
+
+ const checks = files.map(async (file) => {
+ const matches = await ctx.grep(file, /process\.cwd\(\)/);
+ for (const m of matches) {
+ // Allow process.cwd() as a fallback after findProjectRoot()
+ // e.g. findProjectRoot() ?? process.cwd()
+ if (m.content.includes("findProjectRoot")) continue;
+
+ ctx.report.violation({
+ message:
+ "Use findProjectRoot() from helpers/paths.ts instead of process.cwd() for project root resolution",
+ file: m.file,
+ line: m.line,
+ fix: "Import { findProjectRoot } from '../helpers/paths' and use findProjectRoot()",
+ });
+ }
+ });
+ await Promise.all(checks);
+ },
+ },
+ },
+} satisfies RuleSet;
diff --git a/.archgate/adrs/ARCH-012-command-error-boundaries.md b/.archgate/adrs/ARCH-012-command-error-boundaries.md
new file mode 100644
index 00000000..3406f06c
--- /dev/null
+++ b/.archgate/adrs/ARCH-012-command-error-boundaries.md
@@ -0,0 +1,92 @@
+---
+id: ARCH-012
+title: Command Error Boundaries
+domain: architecture
+rules: true
+files: ["src/commands/**/*.ts"]
+---
+
+# Command Error Boundaries
+
+## Context
+
+Async command actions that lack try-catch error boundaries produce poor user experiences when they fail. Without explicit error handling:
+
+1. Errors propagate to the top-level `main().catch()` in `cli.ts`, which exits with code 2 (internal error) and shows only the raw error message
+2. Users cannot distinguish between a command failure (code 1) and a CLI bug (code 2)
+3. Error messages lack context about what the command was trying to do
+
+This was discovered during a repository-wide review where `review-context`, `session-context claude-code`, and `session-context cursor` all lacked error boundaries.
+
+ARCH-002 defines the exit code convention and logging patterns, but does not require error boundaries in command actions. This ADR complements ARCH-002 by making error boundaries mandatory.
+
+**Why not a global Commander.js error handler?** Commander provides `.exitOverride()` and `.configureOutput()` for parsing errors (unknown options, missing arguments), but these do **not** cover errors thrown inside async `.action()` callbacks. Commander's `preAction`/`postAction` hooks could theoretically wrap actions, but they don't catch async errors from the action body. The `main().catch()` in `cli.ts` catches unhandled rejections as a safety net (exit 2), but per-command try-catch is needed to produce contextual error messages and exit with code 1 instead of 2.
+
+## Decision
+
+Every async command action MUST wrap its body in a try-catch block that:
+
+1. Catches errors from async operations
+2. Formats them with `logError()` from `src/helpers/log.ts`
+3. Exits with code 1 (expected failure) for user-facing errors
+
+The top-level `main().catch()` in `cli.ts` remains as a safety net for truly unexpected errors (code 2), but it should never be the primary error handler for commands.
+
+**Pattern:**
+
+```typescript
+.action(async (opts) => {
+ try {
+ // command logic
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
+ process.exit(1);
+ }
+});
+```
+
+**Exceptions:**
+
+- Synchronous command actions (e.g., `clean`) that cannot throw async errors
+- Command group index files that only register subcommands
+
+## Do's and Don'ts
+
+### Do
+
+- Wrap every async command action body in a try-catch
+- Use `logError()` for error messages in the catch block
+- Exit with code 1 for expected failures
+
+### Don't
+
+- Don't rely on `main().catch()` as the only error handler for commands
+- Don't catch and silently swallow errors — always log them
+- Don't exit with code 2 in command catch blocks — that code is reserved for unexpected crashes
+
+## Consequences
+
+### Positive
+
+- Users see contextual error messages instead of raw exception text
+- Exit code 1 vs 2 distinction is preserved — scripts and CI can differentiate
+- Every command handles its own failures gracefully
+
+### Negative
+
+- Minor boilerplate in every async command action
+
+## Compliance and Enforcement
+
+### Automated Enforcement
+
+- **Archgate rule** `ARCH-012/async-action-error-boundary`: Scans async command actions for try-catch blocks. Severity: `warning` (some commands may have valid reasons for alternative patterns).
+
+### Manual Enforcement
+
+Code reviewers MUST verify that new async commands include error boundaries.
+
+## References
+
+- [ARCH-002 — Error Handling](./ARCH-002-error-handling.md) — Exit code convention and logError() requirement
+- [ARCH-001 — Command Structure](./ARCH-001-command-structure.md) — Command file conventions
diff --git a/.archgate/adrs/ARCH-012-command-error-boundaries.rules.ts b/.archgate/adrs/ARCH-012-command-error-boundaries.rules.ts
new file mode 100644
index 00000000..ab45efe2
--- /dev/null
+++ b/.archgate/adrs/ARCH-012-command-error-boundaries.rules.ts
@@ -0,0 +1,41 @@
+///
+
+export default {
+ rules: {
+ "async-action-error-boundary": {
+ description:
+ "Async command actions must include try-catch error boundaries",
+ severity: "warning",
+ async check(ctx) {
+ // Only check non-index command files
+ const files = ctx.scopedFiles.filter(
+ (f) => f.includes("commands/") && !f.endsWith("index.ts")
+ );
+
+ const checks = files.map(async (file) => {
+ const content = await ctx.readFile(file);
+
+ // Find async action callbacks
+ const hasAsyncAction = /\.action\(\s*async\s/.test(content);
+ if (!hasAsyncAction) return;
+
+ // Check if the async action body contains a try block
+ // Match: .action(async (...) => { ... try { ... } ... })
+ const hasTryCatch = /\.action\(\s*async\s[\s\S]*?\btry\s*\{/.test(
+ content
+ );
+
+ if (!hasTryCatch) {
+ ctx.report.warning({
+ message:
+ "Async command action should include a try-catch error boundary",
+ file,
+ fix: "Wrap the action body in try { ... } catch (err) { logError(...); process.exit(1); }",
+ });
+ }
+ });
+ await Promise.all(checks);
+ },
+ },
+ },
+} satisfies RuleSet;
diff --git a/.archgate/adrs/ARCH-013-version-synchronization.md b/.archgate/adrs/ARCH-013-version-synchronization.md
new file mode 100644
index 00000000..833b9000
--- /dev/null
+++ b/.archgate/adrs/ARCH-013-version-synchronization.md
@@ -0,0 +1,64 @@
+---
+id: ARCH-013
+title: Version Synchronization
+domain: architecture
+rules: true
+files: ["package.json", "docs/**"]
+---
+
+# Version Synchronization
+
+## Context
+
+The CLI version appears in multiple locations that must stay in sync:
+
+1. `package.json` `version` — canonical source of truth
+2. `package.json` `optionalDependencies` — platform-specific npm packages (`archgate-darwin-arm64`, `archgate-linux-x64`, `archgate-win32-x64`) must match the CLI version
+3. `docs/astro.config.mjs` — `softwareVersion` in the JSON-LD structured data
+
+When versions diverge, npm installs pull mismatched platform binaries and search engines display outdated version info. This was discovered during a consistency review where `package.json` was at `0.16.0` but `docs/astro.config.mjs` was still at `0.11.0`.
+
+## Decision
+
+`package.json` `version` is the single source of truth. All other version references MUST match it.
+
+**Automated via release process:** The `.simple-release.js` bump hook already syncs `optionalDependencies` versions during the release workflow — it reads `package.json` after the version bump and updates all `optionalDependencies` entries to match. This is fully automated and requires no manual intervention.
+
+**Automated via release process:** The `.simple-release.js` bump hook also updates `softwareVersion` in `docs/astro.config.mjs` to match `package.json`. Both syncs are fully automated.
+
+## Do's and Don'ts
+
+### Do
+
+- Rely on `.simple-release.js` for both `optionalDependencies` and `softwareVersion` sync (do not update manually)
+- Use the companion rules to catch version drift in CI as a safety net
+
+### Don't
+
+- Don't manually edit `optionalDependencies` versions — the release hook handles this
+- Don't manually edit `softwareVersion` in `docs/astro.config.mjs` — the release hook handles this
+
+## Consequences
+
+### Positive
+
+- Consistent version information across npm packages and user-facing surfaces
+- CI catches version drift before it reaches production
+- Platform-specific npm packages always match the CLI version
+
+### Negative
+
+- None — all version sync is automated via the release hook
+
+## Compliance and Enforcement
+
+### Automated Enforcement
+
+- **Release hook** `.simple-release.js`: Syncs `optionalDependencies` versions and `docs/astro.config.mjs` `softwareVersion` during `bump()`. Fully automated.
+- **Archgate rule** `ARCH-013/docs-version-sync`: Checks that `softwareVersion` in `docs/astro.config.mjs` matches `package.json` version. Severity: `error`.
+- **Archgate rule** `ARCH-013/optional-deps-version-sync`: Checks that all `optionalDependencies` versions match `package.json` version. Severity: `error`.
+
+## References
+
+- [GEN-001 — Documentation Site](./GEN-001-documentation-site.md) — Docs site structure and configuration
+- [`.simple-release.js`](../../.simple-release.js) — Release bump hook that syncs optionalDependencies
diff --git a/.archgate/adrs/ARCH-013-version-synchronization.rules.ts b/.archgate/adrs/ARCH-013-version-synchronization.rules.ts
new file mode 100644
index 00000000..d26f4d5d
--- /dev/null
+++ b/.archgate/adrs/ARCH-013-version-synchronization.rules.ts
@@ -0,0 +1,61 @@
+///
+
+export default {
+ rules: {
+ "docs-version-sync": {
+ description:
+ "softwareVersion in docs/astro.config.mjs must match package.json version",
+ severity: "error",
+ async check(ctx) {
+ const pkgJson = (await ctx.readJSON("package.json")) as {
+ version?: string;
+ };
+ if (!pkgJson.version) return;
+
+ let astroConfig: string;
+ try {
+ astroConfig = await ctx.readFile("docs/astro.config.mjs");
+ } catch {
+ // docs/astro.config.mjs may not exist in all contexts
+ return;
+ }
+
+ const match = astroConfig.match(/softwareVersion:\s*"([^"]+)"/);
+ if (!match) return;
+
+ const docsVersion = match[1];
+ if (docsVersion !== pkgJson.version) {
+ ctx.report.violation({
+ message: `docs/astro.config.mjs softwareVersion "${docsVersion}" does not match package.json version "${pkgJson.version}"`,
+ file: "docs/astro.config.mjs",
+ fix: `Update softwareVersion to "${pkgJson.version}" in docs/astro.config.mjs`,
+ });
+ }
+ },
+ },
+ "optional-deps-version-sync": {
+ description:
+ "optionalDependencies versions must match package.json version",
+ severity: "error",
+ async check(ctx) {
+ const pkgJson = (await ctx.readJSON("package.json")) as {
+ version?: string;
+ optionalDependencies?: Record;
+ };
+ if (!pkgJson.version || !pkgJson.optionalDependencies) return;
+
+ for (const [dep, depVersion] of Object.entries(
+ pkgJson.optionalDependencies
+ )) {
+ if (depVersion !== pkgJson.version) {
+ ctx.report.violation({
+ message: `optionalDependencies "${dep}" version "${depVersion}" does not match package.json version "${pkgJson.version}"`,
+ file: "package.json",
+ fix: `Update ${dep} to "${pkgJson.version}" in optionalDependencies (normally handled by .simple-release.js during release)`,
+ });
+ }
+ }
+ },
+ },
+ },
+} satisfies RuleSet;
diff --git a/.simple-release.js b/.simple-release.js
index c1bb8884..9e6808c9 100644
--- a/.simple-release.js
+++ b/.simple-release.js
@@ -1,5 +1,5 @@
import { execSync } from "node:child_process";
-import { readFileSync, writeFileSync } from "node:fs";
+import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { NpmProject } from "@simple-release/npm";
@@ -13,6 +13,7 @@ class ArchgateProject extends NpmProject {
const version = pkg.version;
let changed = false;
+ // Sync optionalDependencies (platform-specific npm packages)
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
if (pkg.optionalDependencies[dep] !== version) {
pkg.optionalDependencies[dep] = version;
@@ -25,6 +26,20 @@ class ArchgateProject extends NpmProject {
execSync("bun install", { stdio: "inherit" });
this.changedFiles.push("bun.lock");
}
+
+ // Sync docs/astro.config.mjs softwareVersion
+ const astroConfigPath = "docs/astro.config.mjs";
+ if (existsSync(astroConfigPath)) {
+ const astroConfig = readFileSync(astroConfigPath, "utf8");
+ const updated = astroConfig.replace(
+ /softwareVersion:\s*"[^"]+"/,
+ `softwareVersion: "${version}"`
+ );
+ if (updated !== astroConfig) {
+ writeFileSync(astroConfigPath, updated);
+ this.changedFiles.push(astroConfigPath);
+ }
+ }
}
return result;
diff --git a/CLAUDE.md b/CLAUDE.md
index 4611679e..2bb261e2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -45,6 +45,10 @@ Entry point: `src/cli.ts` (shebang `#!/usr/bin/env bun`). Commands registered vi
Zod schemas are the single source of truth. Types derived via `z.infer<>` — never define separate interfaces. Use `safeParse()`. Reuse `AdrFrontmatterSchema.shape.*` to avoid duplicating enums.
+## npm Distribution
+
+The npm package is a **thin shim** — it contains only `bin/archgate.cjs` and `scripts/postinstall.cjs`. The postinstall script downloads the prebuilt platform binary. All runtime dependencies (commander, inquirer, zod) are bundled into the compiled binary via `bun build --compile`, so they belong in `devDependencies`, not `dependencies`. The `optionalDependencies` (archgate-darwin-arm64, etc.) are platform-specific binary packages synced to the CLI version by `.simple-release.js` during release.
+
## Conventions
- Commands export `register*Command(program)`, handle I/O only — no business logic
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index 9ae020b0..e5b8dbb7 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -177,7 +177,7 @@ export default defineConfig({
applicationCategory: "DeveloperApplication",
applicationSubCategory: "Code Governance",
operatingSystem: "macOS, Linux, Windows",
- softwareVersion: "0.11.0",
+ softwareVersion: "0.16.0",
license: "https://github.com/archgate/cli/blob/main/LICENSE",
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
url: "https://cli.archgate.dev",
diff --git a/src/commands/adr/create.ts b/src/commands/adr/create.ts
index 544e2b1d..7d3d7a90 100644
--- a/src/commands/adr/create.ts
+++ b/src/commands/adr/create.ts
@@ -1,5 +1,3 @@
-import { existsSync } from "node:fs";
-
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
import inquirer from "inquirer";
@@ -7,7 +5,7 @@ import inquirer from "inquirer";
import { ADR_DOMAINS, type AdrDomain } from "../../formats/adr";
import { createAdrFile } from "../../helpers/adr-writer";
import { logError } from "../../helpers/log";
-import { projectPaths } from "../../helpers/paths";
+import { findProjectRoot, projectPaths } from "../../helpers/paths";
const domainOption = new Option("--domain ", "ADR domain").choices(
ADR_DOMAINS
@@ -24,75 +22,80 @@ export function registerAdrCreateCommand(adr: Command) {
.option("--rules", "Set rules: true in frontmatter")
.option("--json", "Output as JSON")
.action(async (opts) => {
- const projectRoot = process.cwd();
- const paths = projectPaths(projectRoot);
-
- if (!existsSync(paths.root)) {
+ const projectRoot = findProjectRoot();
+ if (!projectRoot) {
logError("No .archgate/ directory found. Run `archgate init` first.");
process.exit(1);
}
- let domain: AdrDomain;
- let title: string;
- let files: string[] | undefined;
- let body: string | undefined;
+ try {
+ const paths = projectPaths(projectRoot);
- // Non-interactive mode when --title and --domain are provided
- if (opts.title && opts.domain) {
- domain = opts.domain;
- title = opts.title;
- files = opts.files
- ? opts.files
- .split(",")
- .map((f) => f.trim())
- .filter(Boolean)
- : undefined;
- body = opts.body;
- } else {
- // Interactive mode
- const answers = await inquirer.prompt([
- {
- type: "list",
- name: "domain",
- message: "Domain:",
- choices: ADR_DOMAINS.map((d) => ({ name: d, value: d })),
- },
- {
- type: "input",
- name: "title",
- message: "Title:",
- validate: (input: string) =>
- input.trim() !== "" || "Title is required",
- },
- {
- type: "input",
- name: "files",
- message: "File patterns (comma-separated, optional):",
- },
- ]);
+ let domain: AdrDomain;
+ let title: string;
+ let files: string[] | undefined;
+ let body: string | undefined;
- domain = answers.domain as AdrDomain;
- title = answers.title;
- files = answers.files
- ? answers.files
- .split(",")
- .map((f: string) => f.trim())
- .filter(Boolean)
- : undefined;
- }
+ // Non-interactive mode when --title and --domain are provided
+ if (opts.title && opts.domain) {
+ domain = opts.domain;
+ title = opts.title;
+ files = opts.files
+ ? opts.files
+ .split(",")
+ .map((f) => f.trim())
+ .filter(Boolean)
+ : undefined;
+ body = opts.body;
+ } else {
+ // Interactive mode
+ const answers = await inquirer.prompt([
+ {
+ type: "list",
+ name: "domain",
+ message: "Domain:",
+ choices: ADR_DOMAINS.map((d) => ({ name: d, value: d })),
+ },
+ {
+ type: "input",
+ name: "title",
+ message: "Title:",
+ validate: (input: string) =>
+ input.trim() !== "" || "Title is required",
+ },
+ {
+ type: "input",
+ name: "files",
+ message: "File patterns (comma-separated, optional):",
+ },
+ ]);
- const result = await createAdrFile(paths.adrsDir, {
- title,
- domain,
- files,
- body,
- rules: opts.rules,
- });
+ domain = answers.domain as AdrDomain;
+ title = answers.title;
+ files = answers.files
+ ? answers.files
+ .split(",")
+ .map((f: string) => f.trim())
+ .filter(Boolean)
+ : undefined;
+ }
- if (opts.json) {
- console.log(JSON.stringify(result, null, 2));
- } else {
- console.log(`Created ADR: ${result.filePath}`);
+ const result = await createAdrFile(paths.adrsDir, {
+ title,
+ domain,
+ files,
+ body,
+ rules: opts.rules,
+ });
+
+ if (opts.json) {
+ console.log(JSON.stringify(result, null, 2));
+ } else {
+ console.log(`Created ADR: ${result.filePath}`);
+ }
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
+ process.exit(1);
}
});
}
diff --git a/src/commands/adr/list.ts b/src/commands/adr/list.ts
index 6b04895b..e6018086 100644
--- a/src/commands/adr/list.ts
+++ b/src/commands/adr/list.ts
@@ -2,10 +2,11 @@ import { existsSync, readdirSync } from "node:fs";
import { styleText } from "node:util";
import type { Command } from "@commander-js/extra-typings";
+import { Option } from "@commander-js/extra-typings";
-import { parseAdr, type AdrDocument } from "../../formats/adr";
+import { ADR_DOMAINS, parseAdr, type AdrDocument } from "../../formats/adr";
import { logError } from "../../helpers/log";
-import { projectPaths } from "../../helpers/paths";
+import { findProjectRoot, projectPaths } from "../../helpers/paths";
async function loadAdrs(adrsDir: string): Promise {
const files = readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
@@ -27,69 +28,78 @@ export function registerAdrListCommand(adr: Command) {
.command("list")
.description("List all ADRs")
.option("--json", "Output as JSON")
- .option("--domain ", "Filter by domain")
+ .addOption(
+ new Option("--domain ", "Filter by domain").choices(ADR_DOMAINS)
+ )
.action(async (options) => {
- const projectRoot = process.cwd();
- const paths = projectPaths(projectRoot);
-
- if (!existsSync(paths.root)) {
+ const projectRoot = findProjectRoot();
+ if (!projectRoot) {
logError("No .archgate/ directory found. Run `archgate init` first.");
process.exit(1);
}
- if (!existsSync(paths.adrsDir)) {
- console.log("No ADRs found.");
- return;
- }
+ try {
+ const paths = projectPaths(projectRoot);
- const files = readdirSync(paths.adrsDir).filter((f) => f.endsWith(".md"));
+ if (!existsSync(paths.adrsDir)) {
+ console.log("No ADRs found.");
+ return;
+ }
- if (files.length === 0) {
- console.log("No ADRs found.");
- return;
- }
+ const files = readdirSync(paths.adrsDir).filter((f) =>
+ f.endsWith(".md")
+ );
- const adrs = await loadAdrs(paths.adrsDir);
+ if (files.length === 0) {
+ console.log("No ADRs found.");
+ return;
+ }
- // Filter by domain if specified
- const filtered = options.domain
- ? adrs.filter((a) => a.frontmatter.domain === options.domain)
- : adrs;
+ const adrs = await loadAdrs(paths.adrsDir);
- if (options.json) {
- console.log(
- JSON.stringify(
- filtered.map((a) => a.frontmatter),
- null,
- 2
- )
- );
- return;
- }
+ // Filter by domain if specified
+ const filtered = options.domain
+ ? adrs.filter((a) => a.frontmatter.domain === options.domain)
+ : adrs;
- // Table output
- const idWidth = 12;
- const domainWidth = 14;
- const rulesWidth = 7;
+ if (options.json) {
+ console.log(
+ JSON.stringify(
+ filtered.map((a) => a.frontmatter),
+ null,
+ 2
+ )
+ );
+ return;
+ }
- console.log(
- styleText(
- "bold",
- `${"ID".padEnd(idWidth)}${"Domain".padEnd(domainWidth)}${"Rules".padEnd(rulesWidth)}Title`
- )
- );
- console.log(
- styleText(
- "dim",
- `${"─".repeat(idWidth)}${"─".repeat(domainWidth)}${"─".repeat(rulesWidth)}${"─".repeat(30)}`
- )
- );
+ // Table output
+ const idWidth = 12;
+ const domainWidth = 14;
+ const rulesWidth = 7;
- for (const adr of filtered) {
- const fm = adr.frontmatter;
console.log(
- `${fm.id.padEnd(idWidth)}${fm.domain.padEnd(domainWidth)}${String(fm.rules).padEnd(rulesWidth)}${fm.title}`
+ styleText(
+ "bold",
+ `${"ID".padEnd(idWidth)}${"Domain".padEnd(domainWidth)}${"Rules".padEnd(rulesWidth)}Title`
+ )
);
+ console.log(
+ styleText(
+ "dim",
+ `${"─".repeat(idWidth)}${"─".repeat(domainWidth)}${"─".repeat(rulesWidth)}${"─".repeat(30)}`
+ )
+ );
+
+ for (const adr of filtered) {
+ const fm = adr.frontmatter;
+ console.log(
+ `${fm.id.padEnd(idWidth)}${fm.domain.padEnd(domainWidth)}${String(fm.rules).padEnd(rulesWidth)}${fm.title}`
+ );
+ }
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
+ process.exit(1);
}
});
}
diff --git a/src/commands/adr/show.ts b/src/commands/adr/show.ts
index b4176512..b32b8745 100644
--- a/src/commands/adr/show.ts
+++ b/src/commands/adr/show.ts
@@ -1,10 +1,8 @@
-import { existsSync } from "node:fs";
-
import type { Command } from "@commander-js/extra-typings";
import { findAdrFileById } from "../../helpers/adr-writer";
import { logError } from "../../helpers/log";
-import { projectPaths } from "../../helpers/paths";
+import { findProjectRoot, projectPaths } from "../../helpers/paths";
export function registerAdrShowCommand(adr: Command) {
adr
@@ -12,22 +10,26 @@ export function registerAdrShowCommand(adr: Command) {
.description("Show a specific ADR by ID")
.argument("", "ADR ID (e.g., GEN-001)")
.action(async (id) => {
- const projectRoot = process.cwd();
- const paths = projectPaths(projectRoot);
-
- if (!existsSync(paths.adrsDir)) {
+ const projectRoot = findProjectRoot();
+ if (!projectRoot) {
logError("No .archgate/ directory found. Run `archgate init` first.");
process.exit(1);
}
- const adr = await findAdrFileById(paths.adrsDir, id);
+ try {
+ const { adrsDir } = projectPaths(projectRoot);
+ const adr = await findAdrFileById(adrsDir, id);
- if (!adr) {
- logError(`ADR with ID '${id}' not found.`);
+ if (!adr) {
+ logError(`ADR with ID '${id}' not found.`);
+ process.exit(1);
+ }
+
+ const content = await Bun.file(adr.filePath).text();
+ console.log(content);
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
-
- const content = await Bun.file(adr.filePath).text();
- console.log(content);
});
}
diff --git a/src/commands/adr/update.ts b/src/commands/adr/update.ts
index 77519fdd..f4e2545d 100644
--- a/src/commands/adr/update.ts
+++ b/src/commands/adr/update.ts
@@ -1,12 +1,10 @@
-import { existsSync } from "node:fs";
-
import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
import { ADR_DOMAINS } from "../../formats/adr";
import { updateAdrFile } from "../../helpers/adr-writer";
import { logError } from "../../helpers/log";
-import { projectPaths } from "../../helpers/paths";
+import { findProjectRoot, projectPaths } from "../../helpers/paths";
const domainOption = new Option("--domain ", "new ADR domain").choices(
ADR_DOMAINS
@@ -27,13 +25,12 @@ export function registerAdrUpdateCommand(adr: Command) {
.option("--rules", "Set rules: true in frontmatter")
.option("--json", "Output as JSON")
.action(async (opts) => {
- const projectRoot = process.cwd();
- const paths = projectPaths(projectRoot);
-
- if (!existsSync(paths.root)) {
+ const projectRoot = findProjectRoot();
+ if (!projectRoot) {
logError("No .archgate/ directory found. Run `archgate init` first.");
process.exit(1);
}
+ const paths = projectPaths(projectRoot);
const files = opts.files
? opts.files
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 3a8df6d5..57713129 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -5,19 +5,12 @@ import { Option } from "@commander-js/extra-typings";
import inquirer from "inquirer";
import { loadCredentials } from "../helpers/auth";
-import { initProject } from "../helpers/init-project";
+import { EDITOR_LABELS, initProject } from "../helpers/init-project";
import type { EditorTarget } from "../helpers/init-project";
import { logError, logInfo, logWarn } from "../helpers/log";
import { runLoginFlow } from "../helpers/login-flow";
import { isTlsError, tlsHintMessage } from "../helpers/tls";
-const EDITOR_LABELS: Record = {
- claude: "Claude Code",
- cursor: "Cursor",
- vscode: "VS Code",
- copilot: "Copilot CLI",
-};
-
const EDITOR_DIRS: Record = {
claude: ".claude/",
cursor: ".cursor/",
diff --git a/src/commands/plugin/install.ts b/src/commands/plugin/install.ts
index eb109c61..a93b50a2 100644
--- a/src/commands/plugin/install.ts
+++ b/src/commands/plugin/install.ts
@@ -4,8 +4,9 @@ import type { Command } from "@commander-js/extra-typings";
import { Option } from "@commander-js/extra-typings";
import { loadCredentials } from "../../helpers/auth";
-import type { EditorTarget } from "../../helpers/init-project";
+import { EDITOR_LABELS } from "../../helpers/init-project";
import { logError, logInfo, logWarn } from "../../helpers/log";
+import { findProjectRoot } from "../../helpers/paths";
import {
buildMarketplaceUrl,
buildVscodeMarketplaceUrl,
@@ -17,13 +18,6 @@ import {
} from "../../helpers/plugin-install";
import { configureVscodeSettings } from "../../helpers/vscode-settings";
-const EDITOR_LABELS: Record = {
- claude: "Claude Code",
- cursor: "Cursor",
- vscode: "VS Code",
- copilot: "Copilot CLI",
-};
-
const editorOption = new Option("--editor ", "target editor")
.choices(["claude", "cursor", "vscode", "copilot"] as const)
.default("claude" as const);
@@ -83,8 +77,9 @@ export function registerPluginInstallCommand(plugin: Command) {
}
case "cursor": {
+ const projectRoot = findProjectRoot() ?? process.cwd();
const files = await installCursorPlugin(
- process.cwd(),
+ projectRoot,
credentials.token
);
logInfo(
@@ -96,7 +91,10 @@ export function registerPluginInstallCommand(plugin: Command) {
case "vscode": {
const url = buildVscodeMarketplaceUrl(credentials);
- await configureVscodeSettings(process.cwd(), url);
+ await configureVscodeSettings(
+ findProjectRoot() ?? process.cwd(),
+ url
+ );
logInfo(
`Archgate plugin configured for ${label}.`,
"Marketplace URL added to VS Code user settings."
diff --git a/src/commands/plugin/url.ts b/src/commands/plugin/url.ts
index f12f21d8..b381fc13 100644
--- a/src/commands/plugin/url.ts
+++ b/src/commands/plugin/url.ts
@@ -9,7 +9,7 @@ import {
} from "../../helpers/plugin-install";
const editorOption = new Option("--editor ", "target editor")
- .choices(["claude", "cursor", "vscode", "copilot"] as const)
+ .choices(["claude", "vscode", "copilot"] as const)
.default("claude" as const);
export function registerPluginUrlCommand(plugin: Command) {
@@ -20,20 +20,25 @@ export function registerPluginUrlCommand(plugin: Command) {
)
.addOption(editorOption)
.action(async (opts) => {
- const credentials = await loadCredentials();
- if (!credentials) {
- logError(
- "Not logged in.",
- "Run `archgate login` first to authenticate."
- );
- process.exit(1);
- }
+ 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);
+ const url =
+ opts.editor === "vscode"
+ ? buildVscodeMarketplaceUrl(credentials)
+ : buildMarketplaceUrl(credentials);
- console.log(url);
+ console.log(url);
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
+ process.exit(1);
+ }
});
}
diff --git a/src/commands/review-context.ts b/src/commands/review-context.ts
index 45d6a10d..91f62861 100644
--- a/src/commands/review-context.ts
+++ b/src/commands/review-context.ts
@@ -29,12 +29,17 @@ export function registerReviewContextCommand(program: Command) {
process.exit(1);
}
- const context = await buildReviewContext(projectRoot, {
- staged: opts.staged,
- runChecks: opts.runChecks,
- domain: opts.domain,
- });
+ try {
+ const context = await buildReviewContext(projectRoot, {
+ staged: opts.staged,
+ runChecks: opts.runChecks,
+ domain: opts.domain,
+ });
- console.log(JSON.stringify(context, null, 2));
+ console.log(JSON.stringify(context, null, 2));
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
+ process.exit(1);
+ }
});
}
diff --git a/src/commands/session-context/claude-code.ts b/src/commands/session-context/claude-code.ts
index eead8adf..b19178b2 100644
--- a/src/commands/session-context/claude-code.ts
+++ b/src/commands/session-context/claude-code.ts
@@ -16,16 +16,21 @@ export function registerClaudeCodeSessionContextCommand(parent: Command) {
.description("Read Claude Code session transcript for the project")
.addOption(maxEntriesOption)
.action(async (opts) => {
- const projectRoot = findProjectRoot();
- const result = await readClaudeCodeSession(projectRoot, {
- maxEntries: opts.maxEntries,
- });
+ try {
+ const projectRoot = findProjectRoot();
+ const result = await readClaudeCodeSession(projectRoot, {
+ maxEntries: opts.maxEntries,
+ });
- if (!result.ok) {
- logError(result.error);
+ if (!result.ok) {
+ logError(result.error);
+ process.exit(1);
+ }
+
+ console.log(JSON.stringify(result.data, null, 2));
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
-
- console.log(JSON.stringify(result.data, null, 2));
});
}
diff --git a/src/commands/session-context/cursor.ts b/src/commands/session-context/cursor.ts
index 296e6433..4c64e475 100644
--- a/src/commands/session-context/cursor.ts
+++ b/src/commands/session-context/cursor.ts
@@ -17,17 +17,22 @@ export function registerCursorSessionContextCommand(parent: Command) {
.addOption(maxEntriesOption)
.option("--session-id ", "Specific session UUID to read")
.action(async (opts) => {
- const projectRoot = findProjectRoot();
- const result = await readCursorSession(projectRoot, {
- maxEntries: opts.maxEntries,
- sessionId: opts.sessionId,
- });
+ try {
+ const projectRoot = findProjectRoot();
+ const result = await readCursorSession(projectRoot, {
+ maxEntries: opts.maxEntries,
+ sessionId: opts.sessionId,
+ });
- if (!result.ok) {
- logError(result.error);
+ if (!result.ok) {
+ logError(result.error);
+ process.exit(1);
+ }
+
+ console.log(JSON.stringify(result.data, null, 2));
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
-
- console.log(JSON.stringify(result.data, null, 2));
});
}
diff --git a/src/commands/upgrade.ts b/src/commands/upgrade.ts
index 641bfbd8..7d8f2249 100644
--- a/src/commands/upgrade.ts
+++ b/src/commands/upgrade.ts
@@ -15,10 +15,6 @@ import { logError } from "../helpers/log";
import { internalPath } from "../helpers/paths";
import { getPlatformInfo, resolveCommand } from "../helpers/platform";
-// ---------------------------------------------------------------------------
-// Install method detection
-// ---------------------------------------------------------------------------
-
type InstallMethod =
| { type: "binary"; binaryPath: string }
| { type: "proto"; protoCmd: string }
@@ -78,7 +74,8 @@ function isLocalInstall(): boolean {
return process.execPath.includes("node_modules");
}
-function findProjectRoot(): string | null {
+/** Walk up from the binary location to find the nearest package.json (for local installs). */
+function findPackageRoot(): string | null {
let dir = dirname(process.execPath);
while (true) {
if (existsSync(join(dir, "package.json"))) return dir;
@@ -101,7 +98,7 @@ async function detectLocalPm(): Promise<{
args: string[];
manualHint: string;
} | null> {
- const root = findProjectRoot();
+ const root = findPackageRoot();
if (!root) return null;
const match = LOCKFILE_TO_PM.find(([lockfile]) =>
@@ -176,10 +173,6 @@ async function detectInstallMethod(): Promise {
};
}
-// ---------------------------------------------------------------------------
-// Upgrade flows
-// ---------------------------------------------------------------------------
-
async function upgradeBinary(tag: string): Promise {
const artifact = getArtifactInfo();
if (!artifact) {
@@ -229,69 +222,66 @@ async function runExternalUpgrade(
}
}
-// ---------------------------------------------------------------------------
-// Command registration
-// ---------------------------------------------------------------------------
-
export function registerUpgradeCommand(program: Command) {
program
.command("upgrade")
.description("Upgrade Archgate to the latest version")
.action(async () => {
- console.log("Checking for latest Archgate release...");
-
- const tag = await fetchLatestGitHubVersion();
- if (!tag) {
- logError(
- "Failed to fetch release info from GitHub.",
- "Check your network connection."
- );
+ try {
+ console.log("Checking for latest Archgate release...");
+
+ const tag = await fetchLatestGitHubVersion();
+ if (!tag) {
+ logError(
+ "Failed to fetch release info from GitHub.",
+ "Check your network connection."
+ );
+ process.exit(1);
+ }
+
+ const packageJson = await import("../../package.json");
+ const currentVersion = packageJson.default.version;
+ const latestVersion = tag.replace(/^v/, "");
+ const order = semver.order(currentVersion, latestVersion);
+
+ if (order === null) {
+ logError(
+ `Could not compare versions: ${currentVersion} vs ${latestVersion}`
+ );
+ process.exit(2);
+ }
+
+ if (order >= 0) {
+ console.log(`Archgate is already up-to-date (${currentVersion}).`);
+ process.exit(0);
+ }
+
+ console.log(`Upgrading ${currentVersion} -> ${latestVersion}...`);
+
+ const method = await detectInstallMethod();
+
+ if (method.type === "binary") {
+ await upgradeBinary(tag);
+ } else if (method.type === "proto") {
+ await runExternalUpgrade(
+ [method.protoCmd, "install", "archgate", "latest", "--pin"],
+ "proto install archgate latest --pin"
+ );
+ } else {
+ await runExternalUpgrade(
+ [method.cmd, ...method.args],
+ method.manualHint
+ );
+ }
+
+ console.log(`Archgate upgraded to ${latestVersion} successfully.`);
+ } catch (err) {
+ logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
-
- const packageJson = await import("../../package.json");
- const currentVersion = packageJson.default.version;
- const latestVersion = tag.replace(/^v/, "");
- const order = semver.order(currentVersion, latestVersion);
-
- if (order === null) {
- logError(
- `Could not compare versions: ${currentVersion} vs ${latestVersion}`
- );
- process.exit(2);
- }
-
- if (order >= 0) {
- console.log(`Archgate is already up-to-date (${currentVersion}).`);
- process.exit(0);
- }
-
- console.log(`Upgrading ${currentVersion} -> ${latestVersion}...`);
-
- const method = await detectInstallMethod();
-
- if (method.type === "binary") {
- await upgradeBinary(tag);
- } else if (method.type === "proto") {
- await runExternalUpgrade(
- [method.protoCmd, "install", "archgate", "latest", "--pin"],
- "proto install archgate latest --pin"
- );
- } else {
- await runExternalUpgrade(
- [method.cmd, ...method.args],
- method.manualHint
- );
- }
-
- console.log(`Archgate upgraded to ${latestVersion} successfully.`);
});
}
-// ---------------------------------------------------------------------------
-// Exports for testing
-// ---------------------------------------------------------------------------
-
export {
isBinaryInstall as _isBinaryInstall,
isProtoInstall as _isProtoInstall,
diff --git a/src/formats/adr.ts b/src/formats/adr.ts
index 6cbc5d87..62d02777 100644
--- a/src/formats/adr.ts
+++ b/src/formats/adr.ts
@@ -53,7 +53,7 @@ function formatZodErrors(error: z.ZodError): string[] {
* Throws on invalid frontmatter.
*/
export function parseAdr(content: string, filePath: string): AdrDocument {
- const frontmatterRegex = /^---\n([\s\S]*?)\n---/;
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
const match = content.match(frontmatterRegex);
if (!match) {
diff --git a/src/helpers/binary-upgrade.ts b/src/helpers/binary-upgrade.ts
index 73504567..1f26b13d 100644
--- a/src/helpers/binary-upgrade.ts
+++ b/src/helpers/binary-upgrade.ts
@@ -68,7 +68,7 @@ const GITHUB_RELEASES_API = `https://api.github.com/repos/${GITHUB_REPO}/release
export async function fetchLatestGitHubVersion(): Promise {
const response = await fetch(GITHUB_RELEASES_API, {
headers: { "User-Agent": "archgate-cli" },
- signal: AbortSignal.timeout(15000),
+ signal: AbortSignal.timeout(15_000),
});
if (!response.ok) return null;
@@ -93,7 +93,7 @@ export async function downloadReleaseBinary(
const response = await fetch(archiveUrl, {
headers: { "User-Agent": "archgate-cli" },
- signal: AbortSignal.timeout(60000),
+ signal: AbortSignal.timeout(60_000),
});
if (!response.ok) {
diff --git a/src/helpers/git.ts b/src/helpers/git.ts
index fdc80c0e..273491ab 100644
--- a/src/helpers/git.ts
+++ b/src/helpers/git.ts
@@ -21,33 +21,3 @@ export async function installGit() {
throw new Error(`Failed to install git (exit code ${exitCode})`);
}
}
-
-/**
- * Get list of changed files (unstaged + staged) relative to project root.
- */
-export async function getChangedFiles(projectRoot: string): Promise {
- try {
- const spawnOpts = {
- cwd: projectRoot,
- stdout: "pipe" as const,
- stderr: "pipe" as const,
- };
- const unstaged = Bun.spawn(["git", "diff", "--name-only"], spawnOpts);
- const staged = Bun.spawn(
- ["git", "diff", "--cached", "--name-only"],
- spawnOpts
- );
- const [unstagedText, stagedText] = await Promise.all([
- new Response(unstaged.stdout).text(),
- new Response(staged.stdout).text(),
- ]);
- await Promise.all([unstaged.exited, staged.exited]);
- const files = [
- ...unstagedText.trim().split("\n"),
- ...stagedText.trim().split("\n"),
- ].filter(Boolean);
- return [...new Set(files)];
- } catch {
- return [];
- }
-}
diff --git a/src/helpers/init-project.ts b/src/helpers/init-project.ts
index 16bbd819..2cac1e94 100644
--- a/src/helpers/init-project.ts
+++ b/src/helpers/init-project.ts
@@ -11,6 +11,13 @@ import { configureVscodeSettings } from "./vscode-settings";
export type EditorTarget = "claude" | "cursor" | "vscode" | "copilot";
+export const EDITOR_LABELS: Record = {
+ claude: "Claude Code",
+ cursor: "Cursor",
+ vscode: "VS Code",
+ copilot: "Copilot CLI",
+};
+
export interface InitOptions {
editor?: EditorTarget;
/** When true, attempt to install the archgate plugin using stored credentials. */
diff --git a/src/helpers/paths.ts b/src/helpers/paths.ts
index 01fa8962..601614f0 100644
--- a/src/helpers/paths.ts
+++ b/src/helpers/paths.ts
@@ -1,25 +1,18 @@
import { existsSync, mkdirSync } from "node:fs";
+import { homedir } from "node:os";
import { join, dirname } from "node:path";
import { logDebug } from "./log";
export function internalPath(...path: string[]) {
- const internalFolder = join(
- process.env.HOME ?? process.env.USERPROFILE ?? "~",
- ".archgate"
- );
+ // Use process.env.HOME/USERPROFILE first (testable via env override),
+ // fall back to os.homedir() which handles platform-specific resolution.
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
+ const internalFolder = join(home, ".archgate");
return join(internalFolder, ...path);
}
-export const paths = {
- // TODO: this must follow the git tags matching the CLI version
- templatesRemoteArchive:
- "https://github.com/archgate/templates/archive/refs/heads/main.zip",
- cacheFolder: internalPath("cache"),
- templatesZipFile: internalPath("cache", "templates.zip"),
- templatesUnzippedFolder: internalPath("cache", "templates-main"),
- template: (name: string) => internalPath("cache", "templates-main", name),
-} as const;
+export const paths = { cacheFolder: internalPath("cache") } as const;
export function projectPath(projectRoot: string, ...path: string[]) {
return join(projectRoot, ".archgate", ...path);
diff --git a/src/helpers/vscode-settings.ts b/src/helpers/vscode-settings.ts
index 9635ff37..c974b347 100644
--- a/src/helpers/vscode-settings.ts
+++ b/src/helpers/vscode-settings.ts
@@ -118,15 +118,16 @@ export async function configureVscodeSettings(
): Promise {
const vscodeDir = join(projectRoot, ".vscode");
- if (!existsSync(vscodeDir)) {
- mkdirSync(vscodeDir, { recursive: true });
- }
-
// --- User-level: chat.plugins.marketplaces ---
if (marketplaceUrl) {
await addMarketplaceToUserSettings(marketplaceUrl);
}
+ // Only create .vscode/ dir when there's a reason to (marketplace URL present)
+ if (marketplaceUrl && !existsSync(vscodeDir)) {
+ mkdirSync(vscodeDir, { recursive: true });
+ }
+
return vscodeDir;
}
diff --git a/tests/commands/adr/list.test.ts b/tests/commands/adr/list.test.ts
index 4b48d0bf..07f9cc2c 100644
--- a/tests/commands/adr/list.test.ts
+++ b/tests/commands/adr/list.test.ts
@@ -195,8 +195,8 @@ describe("adr list action handler", () => {
expect(allOutput).toContain("No ADRs found.");
});
- test("prints 'No ADRs found.' when adrs directory does not exist", async () => {
- mkdirSync(join(tempDir, ".archgate"), { recursive: true });
+ test("prints 'No ADRs found.' when adrs directory is empty", async () => {
+ mkdirSync(join(tempDir, ".archgate", "adrs"), { recursive: true });
process.chdir(tempDir);
const parent = makeProgram();
diff --git a/tests/commands/plugin/url.test.ts b/tests/commands/plugin/url.test.ts
index cdd6b094..c9bc06f7 100644
--- a/tests/commands/plugin/url.test.ts
+++ b/tests/commands/plugin/url.test.ts
@@ -33,11 +33,6 @@ describe("registerPluginUrlCommand", () => {
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",
- ]);
+ expect(editorOpt.argChoices).toEqual(["claude", "vscode", "copilot"]);
});
});
diff --git a/tests/helpers/git.test.ts b/tests/helpers/git.test.ts
index ccaeb2a2..7aca5829 100644
--- a/tests/helpers/git.test.ts
+++ b/tests/helpers/git.test.ts
@@ -1,69 +1,10 @@
-import {
- describe,
- expect,
- test,
- beforeEach,
- afterEach,
- setDefaultTimeout,
-} from "bun:test";
-import { mkdtempSync, writeFileSync } from "node:fs";
-import { tmpdir } from "node:os";
-import { join } from "node:path";
+import { describe, expect, test } from "bun:test";
-import { getChangedFiles, installGit } from "../../src/helpers/git";
-import { git, safeRmSync } from "../test-utils";
-
-setDefaultTimeout(15_000);
-
-describe("getChangedFiles", () => {
- let tempDir: string;
-
- beforeEach(async () => {
- tempDir = mkdtempSync(join(tmpdir(), "archgate-git-test-"));
- await git(["init"], tempDir);
- await git(["config", "user.email", "test@test.com"], tempDir);
- await git(["config", "user.name", "Test"], tempDir);
- // Create an initial commit so HEAD exists
- writeFileSync(join(tempDir, "README.md"), "init");
- await git(["add", "."], tempDir);
- await git(["commit", "-m", "init"], tempDir);
- });
-
- afterEach(() => {
- safeRmSync(tempDir);
- });
-
- test("returns empty array when no changes", async () => {
- const files = await getChangedFiles(tempDir);
- expect(files).toEqual([]);
- });
-
- test("returns modified files", async () => {
- writeFileSync(join(tempDir, "README.md"), "changed");
- const files = await getChangedFiles(tempDir);
- expect(files).toContain("README.md");
- });
-
- test("returns staged files", async () => {
- writeFileSync(join(tempDir, "new.ts"), "export const x = 1;");
- await git(["add", "new.ts"], tempDir);
- const files = await getChangedFiles(tempDir);
- expect(files).toContain("new.ts");
- });
-
- test("deduplicates files that are both staged and modified", async () => {
- writeFileSync(join(tempDir, "file.ts"), "v1");
- await git(["add", "file.ts"], tempDir);
- writeFileSync(join(tempDir, "file.ts"), "v2");
- const files = await getChangedFiles(tempDir);
- const count = files.filter((f) => f === "file.ts").length;
- expect(count).toBe(1);
- });
-});
+import { installGit } from "../../src/helpers/git";
describe("installGit", () => {
- test("does nothing when git is already available", async () => {
- // Git is present in the test environment — installGit should return without throwing
+ test("does not throw when git is available", async () => {
+ // git is expected to be available in the test environment
await expect(installGit()).resolves.toBeUndefined();
});
diff --git a/tests/helpers/vscode-settings.test.ts b/tests/helpers/vscode-settings.test.ts
index 51a1f437..5cbf1497 100644
--- a/tests/helpers/vscode-settings.test.ts
+++ b/tests/helpers/vscode-settings.test.ts
@@ -67,16 +67,10 @@ describe("configureVscodeSettings", () => {
rmSync(tempDir, { recursive: true, force: true });
});
- test("creates .vscode/ dir when nothing exists", async () => {
+ test("does not create .vscode/ dir when no marketplace URL is provided", async () => {
await configureVscodeSettings(tempDir);
- expect(existsSync(join(tempDir, ".vscode"))).toBe(true);
- });
-
- test("does not create mcp.json", async () => {
- await configureVscodeSettings(tempDir);
-
- expect(existsSync(join(tempDir, ".vscode", "mcp.json"))).toBe(false);
+ expect(existsSync(join(tempDir, ".vscode"))).toBe(false);
});
test("returns path to .vscode/ directory", async () => {