Skip to content

Commit ad63659

Browse files
rhuanbarretoclaude
andauthored
fix: handle Windows backslash paths in encodeProjectPath (#69)
* fix: handle Windows backslash paths in encodeProjectPath encodeProjectPath only replaced forward slashes with dashes, but on Windows process.cwd() returns backslash paths (e.g. C:\Users\...). This caused session-context lookups to fail on Windows PowerShell. * feat: add WSL/cross-environment support and unify platform checks - Add src/helpers/platform.ts with WSL detection, path conversion, cross-environment command resolution, and platform helpers - Unify all process.platform checks to use platform helpers - Make encodeProjectPath async with WSL Windows path conversion - Make getVscodeUserSettingsPath async with WSL AppData resolution - Use resolveCommand for claude/copilot/git/npm CLI detection - Detect install package manager (bun/pnpm/yarn/npm) in upgrade command * fix: update bun.lock to match package.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reject lint warnings with --deny-warnings flag Adds --deny-warnings to the oxlint command so warnings are treated as errors. Suppresses the legitimate no-await-in-loop in the OAuth polling loop. Also adds .claude/worktrees to .gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3945291 commit ad63659

14 files changed

Lines changed: 618 additions & 64 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ coverage
2323
# Local MCP config
2424
.mcp.json
2525

26+
# Claude Code worktrees
27+
.claude/worktrees
28+
2629
# Proto / Moon
2730
.moon/cache
2831
.moon/docker

bun.lock

Lines changed: 3 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"scripts": {
4343
"cli": "bun run src/cli.ts",
4444
"check": "bun run src/cli.ts check",
45-
"lint": "oxlint .",
45+
"lint": "oxlint --deny-warnings .",
4646
"typecheck": "tsc --build",
4747
"format": "prettier --write .",
4848
"format:check": "prettier --check .",

src/cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { registerSessionContextCommand } from "./commands/session-context/index"
1515
import { registerPluginCommand } from "./commands/plugin/index";
1616
import { checkForUpdatesIfNeeded } from "./helpers/update-check";
1717
import { logError } from "./helpers/log";
18+
import { isSupportedPlatform } from "./helpers/platform";
1819

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

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

3031
createPathIfNotExists(paths.cacheFolder);

src/commands/upgrade.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,105 @@
11
import type { Command } from "@commander-js/extra-typings";
22
import { semver } from "bun";
33
import { logError } from "../helpers/log";
4+
import { resolveCommand } from "../helpers/platform";
45

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

8+
interface PackageManager {
9+
name: string;
10+
globalBinCmd: string[];
11+
upgradeArgs: string[];
12+
}
13+
14+
const PACKAGE_MANAGERS: PackageManager[] = [
15+
{
16+
name: "bun",
17+
globalBinCmd: ["bun", "pm", "-g", "bin"],
18+
upgradeArgs: ["add", "-g", "archgate@latest"],
19+
},
20+
{
21+
name: "pnpm",
22+
globalBinCmd: ["pnpm", "bin", "-g"],
23+
upgradeArgs: ["add", "-g", "archgate@latest"],
24+
},
25+
{
26+
name: "yarn",
27+
globalBinCmd: ["yarn", "global", "bin"],
28+
upgradeArgs: ["global", "add", "archgate@latest"],
29+
},
30+
{
31+
name: "npm",
32+
globalBinCmd: ["npm", "bin", "-g"],
33+
upgradeArgs: ["install", "-g", "archgate@latest"],
34+
},
35+
];
36+
37+
/**
38+
* Get the global bin directory for a package manager.
39+
* Returns null if the command is not available or fails.
40+
*/
41+
async function getGlobalBinDir(cmd: string[]): Promise<string | null> {
42+
try {
43+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
44+
const stdout = await new Response(proc.stdout).text();
45+
const exitCode = await proc.exited;
46+
if (exitCode !== 0) return null;
47+
return stdout.trim() || null;
48+
} catch {
49+
return null;
50+
}
51+
}
52+
53+
/**
54+
* Detect which package manager installed archgate by checking whether
55+
* the running binary lives under each manager's global bin directory.
56+
* Resolves all candidates in parallel. Falls back to npm if none match.
57+
*/
58+
async function detectPackageManager(): Promise<{
59+
cmd: string;
60+
args: string[];
61+
manualHint: string;
62+
}> {
63+
const binaryPath = process.execPath;
64+
65+
// Resolve all package managers in parallel
66+
const candidates = await Promise.all(
67+
PACKAGE_MANAGERS.map(async (pm) => {
68+
const resolved = await resolveCommand(pm.name);
69+
if (!resolved) return null;
70+
const globalBinCmd = [resolved, ...pm.globalBinCmd.slice(1)];
71+
const binDir = await getGlobalBinDir(globalBinCmd);
72+
return { pm, resolved, binDir };
73+
})
74+
);
75+
76+
// Find which PM's global bin dir contains the running binary
77+
const match = candidates.find(
78+
(c) => c?.binDir && binaryPath.startsWith(c.binDir)
79+
);
80+
81+
if (match) {
82+
return {
83+
cmd: match.resolved,
84+
args: match.pm.upgradeArgs,
85+
manualHint: `${match.pm.name} ${match.pm.upgradeArgs.join(" ")}`,
86+
};
87+
}
88+
89+
// Default to npm
90+
const npmCandidate = candidates.find((c) => c?.pm.name === "npm");
91+
const npm = PACKAGE_MANAGERS.find((pm) => pm.name === "npm")!;
92+
return {
93+
cmd: npmCandidate?.resolved ?? "npm",
94+
args: npm.upgradeArgs,
95+
manualHint: `npm ${npm.upgradeArgs.join(" ")}`,
96+
};
97+
}
98+
799
export function registerUpgradeCommand(program: Command) {
8100
program
9101
.command("upgrade")
10-
.description("Upgrade Archgate to the latest version via npm")
102+
.description("Upgrade Archgate to the latest version")
11103
.action(async () => {
12104
console.log("Checking for latest Archgate release...");
13105

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

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

61-
const proc = Bun.spawn(["npm", "install", "-g", "archgate@latest"], {
153+
const { cmd, args, manualHint } = await detectPackageManager();
154+
155+
const proc = Bun.spawn([cmd, ...args], {
62156
stdout: "inherit",
63157
stderr: "inherit",
64158
});
65159
const exitCode = await proc.exited;
66160

67161
if (exitCode !== 0) {
68162
logError(
69-
"Failed to install the latest version via npm.",
70-
"Try running `npm install -g archgate@latest` manually."
163+
"Failed to install the latest version.",
164+
`Try running \`${manualHint}\` manually.`
71165
);
72166
process.exit(1);
73167
}

src/helpers/auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export async function pollForAccessToken(
106106
const deadline = Date.now() + expiresIn * 1000;
107107
let pollInterval = interval;
108108

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

@@ -148,6 +148,7 @@ export async function pollForAccessToken(
148148
);
149149
}
150150
}
151+
/* oxlint-enable no-await-in-loop */
151152

152153
throw new Error("Device code expired. Please try again.");
153154
}

src/helpers/git.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { logDebug } from "./log";
2+
import { isWindows, isMacOS, resolveCommand } from "./platform";
23

34
export async function installGit() {
4-
if (Bun.which("git")) {
5+
if (await resolveCommand("git")) {
56
logDebug("Git is already installed");
67
return;
78
}
89
console.log("Git is not installed. Installing...");
9-
if (process.platform === "win32") {
10+
if (isWindows()) {
1011
throw new Error(
1112
"Git is not installed. Install it from https://git-scm.com/download/win and make sure it is on your PATH."
1213
);
1314
}
14-
const cmd =
15-
process.platform === "darwin"
16-
? ["brew", "install", "git"]
17-
: ["sudo", "apt-get", "install", "-y", "git"];
15+
const cmd = isMacOS()
16+
? ["brew", "install", "git"]
17+
: ["sudo", "apt-get", "install", "-y", "git"];
1818
const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" });
1919
const exitCode = await proc.exited;
2020
if (exitCode !== 0) {

0 commit comments

Comments
 (0)