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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ coverage
# Local MCP config
.mcp.json

# Claude Code worktrees
.claude/worktrees

# Proto / Moon
.moon/cache
.moon/docker
Expand Down
12 changes: 3 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"scripts": {
"cli": "bun run src/cli.ts",
"check": "bun run src/cli.ts check",
"lint": "oxlint .",
"lint": "oxlint --deny-warnings .",
"typecheck": "tsc --build",
"format": "prettier --write .",
"format:check": "prettier --check .",
Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { registerSessionContextCommand } from "./commands/session-context/index"
import { registerPluginCommand } from "./commands/plugin/index";
import { checkForUpdatesIfNeeded } from "./helpers/update-check";
import { logError } from "./helpers/log";
import { isSupportedPlatform } from "./helpers/platform";

if (typeof Bun === "undefined")
throw new Error(
Expand All @@ -24,7 +25,7 @@ if (typeof Bun === "undefined")
if (!semver.satisfies(Bun.version, ">=1.2.21"))
throw new Error("You need to update Bun to version 1.2.21 or higher");

if (!["darwin", "linux", "win32"].includes(process.platform))
if (!isSupportedPlatform())
throw new Error("Archgate only supports macOS, Linux, and Windows");

createPathIfNotExists(paths.cacheFolder);
Expand Down
102 changes: 98 additions & 4 deletions src/commands/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,105 @@
import type { Command } from "@commander-js/extra-typings";
import { semver } from "bun";
import { logError } from "../helpers/log";
import { resolveCommand } from "../helpers/platform";

const NPM_REGISTRY = "https://registry.npmjs.org/archgate/latest";

interface PackageManager {
name: string;
globalBinCmd: string[];
upgradeArgs: string[];
}

const PACKAGE_MANAGERS: PackageManager[] = [
{
name: "bun",
globalBinCmd: ["bun", "pm", "-g", "bin"],
upgradeArgs: ["add", "-g", "archgate@latest"],
},
{
name: "pnpm",
globalBinCmd: ["pnpm", "bin", "-g"],
upgradeArgs: ["add", "-g", "archgate@latest"],
},
{
name: "yarn",
globalBinCmd: ["yarn", "global", "bin"],
upgradeArgs: ["global", "add", "archgate@latest"],
},
{
name: "npm",
globalBinCmd: ["npm", "bin", "-g"],
upgradeArgs: ["install", "-g", "archgate@latest"],
},
];

/**
* Get the global bin directory for a package manager.
* Returns null if the command is not available or fails.
*/
async function getGlobalBinDir(cmd: string[]): Promise<string | null> {
try {
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
if (exitCode !== 0) return null;
return stdout.trim() || null;
} catch {
return null;
}
}

/**
* Detect which package manager installed archgate by checking whether
* the running binary lives under each manager's global bin directory.
* Resolves all candidates in parallel. Falls back to npm if none match.
*/
async function detectPackageManager(): Promise<{
cmd: string;
args: string[];
manualHint: string;
}> {
const binaryPath = process.execPath;

// Resolve all package managers in parallel
const candidates = await Promise.all(
PACKAGE_MANAGERS.map(async (pm) => {
const resolved = await resolveCommand(pm.name);
if (!resolved) return null;
const globalBinCmd = [resolved, ...pm.globalBinCmd.slice(1)];
const binDir = await getGlobalBinDir(globalBinCmd);
return { pm, resolved, binDir };
})
);

// Find which PM's global bin dir contains the running binary
const match = candidates.find(
(c) => c?.binDir && binaryPath.startsWith(c.binDir)
);

if (match) {
return {
cmd: match.resolved,
args: match.pm.upgradeArgs,
manualHint: `${match.pm.name} ${match.pm.upgradeArgs.join(" ")}`,
};
}

// Default to npm
const npmCandidate = candidates.find((c) => c?.pm.name === "npm");
const npm = PACKAGE_MANAGERS.find((pm) => pm.name === "npm")!;
return {
cmd: npmCandidate?.resolved ?? "npm",
args: npm.upgradeArgs,
manualHint: `npm ${npm.upgradeArgs.join(" ")}`,
};
}

export function registerUpgradeCommand(program: Command) {
program
.command("upgrade")
.description("Upgrade Archgate to the latest version via npm")
.description("Upgrade Archgate to the latest version")
.action(async () => {
console.log("Checking for latest Archgate release...");

Expand Down Expand Up @@ -58,16 +150,18 @@ export function registerUpgradeCommand(program: Command) {

console.log(`Upgrading ${currentVersion} -> ${latestVersion}...`);

const proc = Bun.spawn(["npm", "install", "-g", "archgate@latest"], {
const { cmd, args, manualHint } = await detectPackageManager();

const proc = Bun.spawn([cmd, ...args], {
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;

if (exitCode !== 0) {
logError(
"Failed to install the latest version via npm.",
"Try running `npm install -g archgate@latest` manually."
"Failed to install the latest version.",
`Try running \`${manualHint}\` manually.`
);
process.exit(1);
}
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function pollForAccessToken(
const deadline = Date.now() + expiresIn * 1000;
let pollInterval = interval;

// eslint-disable-next-line no-await-in-loop -- sequential polling is required by RFC 8628
/* oxlint-disable no-await-in-loop -- sequential polling is required by RFC 8628 */
while (Date.now() < deadline) {
await Bun.sleep(pollInterval * 1000);

Expand Down Expand Up @@ -148,6 +148,7 @@ export async function pollForAccessToken(
);
}
}
/* oxlint-enable no-await-in-loop */

throw new Error("Device code expired. Please try again.");
}
Expand Down
12 changes: 6 additions & 6 deletions src/helpers/git.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { logDebug } from "./log";
import { isWindows, isMacOS, resolveCommand } from "./platform";

export async function installGit() {
if (Bun.which("git")) {
if (await resolveCommand("git")) {
logDebug("Git is already installed");
return;
}
console.log("Git is not installed. Installing...");
if (process.platform === "win32") {
if (isWindows()) {
throw new Error(
"Git is not installed. Install it from https://git-scm.com/download/win and make sure it is on your PATH."
);
}
const cmd =
process.platform === "darwin"
? ["brew", "install", "git"]
: ["sudo", "apt-get", "install", "-y", "git"];
const cmd = isMacOS()
? ["brew", "install", "git"]
: ["sudo", "apt-get", "install", "-y", "git"];
const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" });
const exitCode = await proc.exited;
if (exitCode !== 0) {
Expand Down
Loading
Loading