Merge pull request #794 from roshanasingh4/fix/777-windows-openurl-quotes
Fix Antigravity OAuth login on Windows (quote URL for cmd start)
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
- TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow)
|
- TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow)
|
||||||
- Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow)
|
- Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow)
|
||||||
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
|
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
|
||||||
|
- Onboarding: quote Windows browser URLs when launching via `cmd start` to preserve OAuth query params. (#794 — thanks @roshanasingh4)
|
||||||
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
|
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
|
||||||
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
|
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
|
||||||
- Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras)
|
- Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
isCloudCodeAssistFormatError,
|
isCloudCodeAssistFormatError,
|
||||||
isCompactionFailureError,
|
isCompactionFailureError,
|
||||||
isContextOverflowError,
|
isContextOverflowError,
|
||||||
isMessagingToolDuplicate,
|
|
||||||
isFailoverErrorMessage,
|
isFailoverErrorMessage,
|
||||||
|
isMessagingToolDuplicate,
|
||||||
normalizeTextForComparison,
|
normalizeTextForComparison,
|
||||||
resolveBootstrapMaxChars,
|
resolveBootstrapMaxChars,
|
||||||
sanitizeGoogleTurnOrdering,
|
sanitizeGoogleTurnOrdering,
|
||||||
|
|||||||
47
src/commands/onboard-helpers.test.ts
Normal file
47
src/commands/onboard-helpers.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { openUrl, resolveBrowserOpenCommand } from "./onboard-helpers.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
runCommandWithTimeout: vi.fn(async () => ({
|
||||||
|
stdout: "",
|
||||||
|
stderr: "",
|
||||||
|
code: 0,
|
||||||
|
signal: null,
|
||||||
|
killed: false,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../process/exec.js", () => ({
|
||||||
|
runCommandWithTimeout: mocks.runCommandWithTimeout,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("openUrl", () => {
|
||||||
|
it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => {
|
||||||
|
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||||
|
|
||||||
|
const url =
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&redirect_uri=http%3A%2F%2Flocalhost";
|
||||||
|
|
||||||
|
const ok = await openUrl(url);
|
||||||
|
expect(ok).toBe(true);
|
||||||
|
|
||||||
|
expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1);
|
||||||
|
const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? [];
|
||||||
|
expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']);
|
||||||
|
expect(argv?.at(-1)).toBe(`"${url}"`);
|
||||||
|
expect(options).toMatchObject({
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
windowsVerbatimArguments: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveBrowserOpenCommand", () => {
|
||||||
|
it("marks win32 commands as quoteUrl=true", async () => {
|
||||||
|
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||||
|
const resolved = await resolveBrowserOpenCommand();
|
||||||
|
expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]);
|
||||||
|
expect(resolved.quoteUrl).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -134,9 +134,14 @@ type BrowserOpenCommand = {
|
|||||||
argv: string[] | null;
|
argv: string[] | null;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
command?: string;
|
command?: string;
|
||||||
|
/**
|
||||||
|
* Whether the URL must be wrapped in quotes when appended to argv.
|
||||||
|
* Needed for Windows `cmd /c start` where `&` splits commands.
|
||||||
|
*/
|
||||||
|
quoteUrl?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
|
export async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const hasDisplay = Boolean(
|
const hasDisplay = Boolean(
|
||||||
process.env.DISPLAY || process.env.WAYLAND_DISPLAY,
|
process.env.DISPLAY || process.env.WAYLAND_DISPLAY,
|
||||||
@@ -151,7 +156,11 @@ async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (platform === "win32") {
|
if (platform === "win32") {
|
||||||
return { argv: ["cmd", "/c", "start", ""], command: "cmd" };
|
return {
|
||||||
|
argv: ["cmd", "/c", "start", ""],
|
||||||
|
command: "cmd",
|
||||||
|
quoteUrl: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === "darwin") {
|
if (platform === "darwin") {
|
||||||
@@ -223,9 +232,22 @@ function resolveSshTargetHint(): string {
|
|||||||
export async function openUrl(url: string): Promise<boolean> {
|
export async function openUrl(url: string): Promise<boolean> {
|
||||||
const resolved = await resolveBrowserOpenCommand();
|
const resolved = await resolveBrowserOpenCommand();
|
||||||
if (!resolved.argv) return false;
|
if (!resolved.argv) return false;
|
||||||
const command = [...resolved.argv, url];
|
const quoteUrl = resolved.quoteUrl === true;
|
||||||
|
const command = [...resolved.argv];
|
||||||
|
if (quoteUrl) {
|
||||||
|
if (command.at(-1) === "") {
|
||||||
|
// Preserve the empty title token for `start` when using verbatim args.
|
||||||
|
command[command.length - 1] = '""';
|
||||||
|
}
|
||||||
|
command.push(`"${url}"`);
|
||||||
|
} else {
|
||||||
|
command.push(url);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await runCommandWithTimeout(command, { timeoutMs: 5_000 });
|
await runCommandWithTimeout(command, {
|
||||||
|
timeoutMs: 5_000,
|
||||||
|
windowsVerbatimArguments: quoteUrl,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
// ignore; we still print the URL for manual open
|
// ignore; we still print the URL for manual open
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export type CommandOptions = {
|
|||||||
cwd?: string;
|
cwd?: string;
|
||||||
input?: string;
|
input?: string;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
|
windowsVerbatimArguments?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runCommandWithTimeout(
|
export async function runCommandWithTimeout(
|
||||||
@@ -59,6 +60,7 @@ export async function runCommandWithTimeout(
|
|||||||
? { timeoutMs: optionsOrTimeout }
|
? { timeoutMs: optionsOrTimeout }
|
||||||
: optionsOrTimeout;
|
: optionsOrTimeout;
|
||||||
const { timeoutMs, cwd, input, env } = options;
|
const { timeoutMs, cwd, input, env } = options;
|
||||||
|
const { windowsVerbatimArguments } = options;
|
||||||
const hasInput = input !== undefined;
|
const hasInput = input !== undefined;
|
||||||
|
|
||||||
// Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed.
|
// Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed.
|
||||||
@@ -67,6 +69,7 @@ export async function runCommandWithTimeout(
|
|||||||
stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"],
|
stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"],
|
||||||
cwd,
|
cwd,
|
||||||
env: env ? { ...process.env, ...env } : process.env,
|
env: env ? { ...process.env, ...env } : process.env,
|
||||||
|
windowsVerbatimArguments,
|
||||||
});
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user