mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 06:37:28 +00:00
Add OpenAI OAuth TLS preflight and doctor prerequisite check
This commit is contained in:
committed by
George Pickett
parent
0f1388fa15
commit
f181b7dbe6
41
PR_DRAFT_OAUTH_TLS_PREFLIGHT.md
Normal file
41
PR_DRAFT_OAUTH_TLS_PREFLIGHT.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Add an OpenAI OAuth TLS preflight to detect local certificate-chain problems early and provide actionable remediation, instead of surfacing only `TypeError: fetch failed`.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Add `runOpenAIOAuthTlsPreflight()` and remediation formatter in `src/commands/oauth-tls-preflight.ts`.
|
||||||
|
- Run TLS preflight before `loginOpenAICodex()` in `src/commands/openai-codex-oauth.ts`.
|
||||||
|
- Add doctor check via `noteOpenAIOAuthTlsPrerequisites()` in `src/commands/doctor.ts`.
|
||||||
|
- Keep doctor fast-path tests deterministic by mocking preflight in `src/commands/doctor.fast-path-mocks.ts`.
|
||||||
|
|
||||||
|
### User-visible behavior
|
||||||
|
|
||||||
|
- During OpenAI Codex OAuth, TLS trust failures now produce actionable guidance, including:
|
||||||
|
- `brew postinstall ca-certificates`
|
||||||
|
- `brew postinstall openssl@3`
|
||||||
|
- expected cert bundle location when Homebrew prefix is detectable.
|
||||||
|
- `openclaw doctor` now reports an `OAuth TLS prerequisites` warning when TLS trust is broken for OpenAI auth calls.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
On some Homebrew Node/OpenSSL setups, missing or broken cert bundle links cause OAuth failures like:
|
||||||
|
|
||||||
|
- `OpenAI OAuth failed`
|
||||||
|
- `TypeError: fetch failed`
|
||||||
|
- `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`
|
||||||
|
|
||||||
|
This change turns that failure mode into an explicit prerequisite check with concrete fixes.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Ran:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm vitest run \
|
||||||
|
src/commands/openai-codex-oauth.test.ts \
|
||||||
|
src/commands/oauth-tls-preflight.test.ts \
|
||||||
|
src/commands/oauth-tls-preflight.doctor.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
All passed.
|
||||||
@@ -49,3 +49,7 @@ vi.mock("./doctor-ui.js", () => ({
|
|||||||
vi.mock("./doctor-workspace-status.js", () => ({
|
vi.mock("./doctor-workspace-status.js", () => ({
|
||||||
noteWorkspaceStatus: vi.fn(),
|
noteWorkspaceStatus: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("./oauth-tls-preflight.js", () => ({
|
||||||
|
noteOpenAIOAuthTlsPrerequisites: vi.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import { maybeRepairUiProtocolFreshness } from "./doctor-ui.js";
|
|||||||
import { maybeOfferUpdateBeforeDoctor } from "./doctor-update.js";
|
import { maybeOfferUpdateBeforeDoctor } from "./doctor-update.js";
|
||||||
import { noteWorkspaceStatus } from "./doctor-workspace-status.js";
|
import { noteWorkspaceStatus } from "./doctor-workspace-status.js";
|
||||||
import { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } from "./doctor-workspace.js";
|
import { MEMORY_SYSTEM_PROMPT, shouldSuggestMemorySystem } from "./doctor-workspace.js";
|
||||||
|
import { noteOpenAIOAuthTlsPrerequisites } from "./oauth-tls-preflight.js";
|
||||||
import { applyWizardMetadata, printWizardHeader, randomToken } from "./onboard-helpers.js";
|
import { applyWizardMetadata, printWizardHeader, randomToken } from "./onboard-helpers.js";
|
||||||
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
|
||||||
|
|
||||||
@@ -200,6 +201,7 @@ export async function doctorCommand(
|
|||||||
await noteMacLaunchctlGatewayEnvOverrides(cfg);
|
await noteMacLaunchctlGatewayEnvOverrides(cfg);
|
||||||
|
|
||||||
await noteSecurityWarnings(cfg);
|
await noteSecurityWarnings(cfg);
|
||||||
|
await noteOpenAIOAuthTlsPrerequisites();
|
||||||
|
|
||||||
if (cfg.hooks?.gmail?.model?.trim()) {
|
if (cfg.hooks?.gmail?.model?.trim()) {
|
||||||
const hooksModelRef = resolveHooksGmailModel({
|
const hooksModelRef = resolveHooksGmailModel({
|
||||||
|
|||||||
50
src/commands/oauth-tls-preflight.doctor.test.ts
Normal file
50
src/commands/oauth-tls-preflight.doctor.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const note = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock("../terminal/note.js", () => ({
|
||||||
|
note,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { noteOpenAIOAuthTlsPrerequisites } from "./oauth-tls-preflight.js";
|
||||||
|
|
||||||
|
describe("noteOpenAIOAuthTlsPrerequisites", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
note.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits OAuth TLS prerequisite guidance when cert chain validation fails", async () => {
|
||||||
|
const cause = new Error("unable to get local issuer certificate") as Error & { code?: string };
|
||||||
|
cause.code = "UNABLE_TO_GET_ISSUER_CERT_LOCALLY";
|
||||||
|
const fetchMock = vi.fn(async () => {
|
||||||
|
throw new TypeError("fetch failed", { cause });
|
||||||
|
});
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
vi.stubGlobal("fetch", fetchMock);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await noteOpenAIOAuthTlsPrerequisites();
|
||||||
|
} finally {
|
||||||
|
vi.stubGlobal("fetch", originalFetch);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(note).toHaveBeenCalledTimes(1);
|
||||||
|
const [message, title] = note.mock.calls[0] as [string, string];
|
||||||
|
expect(title).toBe("OAuth TLS prerequisites");
|
||||||
|
expect(message).toContain("brew postinstall ca-certificates");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stays quiet when preflight succeeds", async () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => new Response("", { status: 400 })),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await noteOpenAIOAuthTlsPrerequisites();
|
||||||
|
} finally {
|
||||||
|
vi.stubGlobal("fetch", originalFetch);
|
||||||
|
}
|
||||||
|
expect(note).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
46
src/commands/oauth-tls-preflight.test.ts
Normal file
46
src/commands/oauth-tls-preflight.test.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
formatOpenAIOAuthTlsPreflightFix,
|
||||||
|
runOpenAIOAuthTlsPreflight,
|
||||||
|
} from "./oauth-tls-preflight.js";
|
||||||
|
|
||||||
|
describe("runOpenAIOAuthTlsPreflight", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns ok when OpenAI auth endpoint is reachable", async () => {
|
||||||
|
const fetchImpl = vi.fn(async () => new Response("", { status: 400 }));
|
||||||
|
const result = await runOpenAIOAuthTlsPreflight({ fetchImpl, timeoutMs: 20 });
|
||||||
|
expect(result).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("classifies TLS trust failures from fetch cause code", async () => {
|
||||||
|
const tlsFetchImpl = vi.fn(async () => {
|
||||||
|
const cause = new Error("unable to get local issuer certificate") as Error & {
|
||||||
|
code?: string;
|
||||||
|
};
|
||||||
|
cause.code = "UNABLE_TO_GET_ISSUER_CERT_LOCALLY";
|
||||||
|
throw new TypeError("fetch failed", { cause });
|
||||||
|
});
|
||||||
|
const result = await runOpenAIOAuthTlsPreflight({ fetchImpl: tlsFetchImpl, timeoutMs: 20 });
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
kind: "tls-cert",
|
||||||
|
code: "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatOpenAIOAuthTlsPreflightFix", () => {
|
||||||
|
it("includes remediation commands for TLS failures", () => {
|
||||||
|
const text = formatOpenAIOAuthTlsPreflightFix({
|
||||||
|
ok: false,
|
||||||
|
kind: "tls-cert",
|
||||||
|
code: "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||||
|
message: "unable to get local issuer certificate",
|
||||||
|
});
|
||||||
|
expect(text).toContain("brew postinstall ca-certificates");
|
||||||
|
expect(text).toContain("brew postinstall openssl@3");
|
||||||
|
});
|
||||||
|
});
|
||||||
139
src/commands/oauth-tls-preflight.ts
Normal file
139
src/commands/oauth-tls-preflight.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
|
import { note } from "../terminal/note.js";
|
||||||
|
|
||||||
|
const TLS_CERT_ERROR_CODES = new Set([
|
||||||
|
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||||
|
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
||||||
|
"CERT_HAS_EXPIRED",
|
||||||
|
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
||||||
|
"SELF_SIGNED_CERT_IN_CHAIN",
|
||||||
|
"ERR_TLS_CERT_ALTNAME_INVALID",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const TLS_CERT_ERROR_PATTERNS = [
|
||||||
|
/unable to get local issuer certificate/i,
|
||||||
|
/unable to verify the first certificate/i,
|
||||||
|
/self[- ]signed certificate/i,
|
||||||
|
/certificate has expired/i,
|
||||||
|
/tls/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const OPENAI_AUTH_PROBE_URL =
|
||||||
|
"https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
||||||
|
|
||||||
|
type PreflightFailureKind = "tls-cert" | "network";
|
||||||
|
|
||||||
|
export type OpenAIOAuthTlsPreflightResult =
|
||||||
|
| { ok: true }
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
kind: PreflightFailureKind;
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFailure(error: unknown): {
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
kind: PreflightFailureKind;
|
||||||
|
} {
|
||||||
|
const root = asRecord(error);
|
||||||
|
const rootCause = asRecord(root?.cause);
|
||||||
|
const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
|
||||||
|
const message =
|
||||||
|
typeof rootCause?.message === "string"
|
||||||
|
? rootCause.message
|
||||||
|
: typeof root?.message === "string"
|
||||||
|
? root.message
|
||||||
|
: String(error);
|
||||||
|
const isTlsCertError =
|
||||||
|
(code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
|
||||||
|
TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
kind: isTlsCertError ? "tls-cert" : "network",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHomebrewPrefixFromExecPath(execPath: string): string | null {
|
||||||
|
const marker = `${path.sep}Cellar${path.sep}`;
|
||||||
|
const idx = execPath.indexOf(marker);
|
||||||
|
if (idx > 0) {
|
||||||
|
return execPath.slice(0, idx);
|
||||||
|
}
|
||||||
|
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
|
||||||
|
return envPrefix ? envPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCertBundlePath(): string | null {
|
||||||
|
const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
|
||||||
|
if (!prefix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path.join(prefix, "etc", "openssl@3", "cert.pem");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runOpenAIOAuthTlsPreflight(options?: {
|
||||||
|
timeoutMs?: number;
|
||||||
|
fetchImpl?: typeof fetch;
|
||||||
|
}): Promise<OpenAIOAuthTlsPreflightResult> {
|
||||||
|
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||||
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
||||||
|
try {
|
||||||
|
await fetchImpl(OPENAI_AUTH_PROBE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
redirect: "manual",
|
||||||
|
signal: AbortSignal.timeout(timeoutMs),
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
const failure = extractFailure(error);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
kind: failure.kind,
|
||||||
|
code: failure.code,
|
||||||
|
message: failure.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOpenAIOAuthTlsPreflightFix(
|
||||||
|
result: Exclude<OpenAIOAuthTlsPreflightResult, { ok: true }>,
|
||||||
|
): string {
|
||||||
|
if (result.kind !== "tls-cert") {
|
||||||
|
return [
|
||||||
|
"OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
|
||||||
|
`Cause: ${result.message}`,
|
||||||
|
"Verify DNS/firewall/proxy access to auth.openai.com and retry.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
const certBundlePath = resolveCertBundlePath();
|
||||||
|
const lines = [
|
||||||
|
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
||||||
|
`Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
|
||||||
|
"",
|
||||||
|
"Fix (Homebrew Node/OpenSSL):",
|
||||||
|
`- ${formatCliCommand("brew postinstall ca-certificates")}`,
|
||||||
|
`- ${formatCliCommand("brew postinstall openssl@3")}`,
|
||||||
|
];
|
||||||
|
if (certBundlePath) {
|
||||||
|
lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
|
||||||
|
}
|
||||||
|
lines.push("- Retry the OAuth login flow.");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function noteOpenAIOAuthTlsPrerequisites(): Promise<void> {
|
||||||
|
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
|
||||||
|
if (result.ok || result.kind !== "tls-cert") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import type { WizardPrompter } from "../wizard/prompts.js";
|
|||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
loginOpenAICodex: vi.fn(),
|
loginOpenAICodex: vi.fn(),
|
||||||
createVpsAwareOAuthHandlers: vi.fn(),
|
createVpsAwareOAuthHandlers: vi.fn(),
|
||||||
|
runOpenAIOAuthTlsPreflight: vi.fn(),
|
||||||
|
formatOpenAIOAuthTlsPreflightFix: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@mariozechner/pi-ai", () => ({
|
vi.mock("@mariozechner/pi-ai", () => ({
|
||||||
@@ -15,6 +17,11 @@ vi.mock("./oauth-flow.js", () => ({
|
|||||||
createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers,
|
createVpsAwareOAuthHandlers: mocks.createVpsAwareOAuthHandlers,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("./oauth-tls-preflight.js", () => ({
|
||||||
|
runOpenAIOAuthTlsPreflight: mocks.runOpenAIOAuthTlsPreflight,
|
||||||
|
formatOpenAIOAuthTlsPreflightFix: mocks.formatOpenAIOAuthTlsPreflightFix,
|
||||||
|
}));
|
||||||
|
|
||||||
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
|
import { loginOpenAICodexOAuth } from "./openai-codex-oauth.js";
|
||||||
|
|
||||||
function createPrompter() {
|
function createPrompter() {
|
||||||
@@ -39,6 +46,8 @@ function createRuntime(): RuntimeEnv {
|
|||||||
describe("loginOpenAICodexOAuth", () => {
|
describe("loginOpenAICodexOAuth", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
mocks.runOpenAIOAuthTlsPreflight.mockResolvedValue({ ok: true });
|
||||||
|
mocks.formatOpenAIOAuthTlsPreflightFix.mockReturnValue("tls fix");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns credentials on successful oauth login", async () => {
|
it("returns credentials on successful oauth login", async () => {
|
||||||
@@ -95,4 +104,33 @@ describe("loginOpenAICodexOAuth", () => {
|
|||||||
"OAuth help",
|
"OAuth help",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails early with actionable message when TLS preflight fails", async () => {
|
||||||
|
mocks.runOpenAIOAuthTlsPreflight.mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
kind: "tls-cert",
|
||||||
|
code: "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||||
|
message: "unable to get local issuer certificate",
|
||||||
|
});
|
||||||
|
mocks.formatOpenAIOAuthTlsPreflightFix.mockReturnValue("Run brew postinstall openssl@3");
|
||||||
|
|
||||||
|
const { prompter } = createPrompter();
|
||||||
|
const runtime = createRuntime();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loginOpenAICodexOAuth({
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
isRemote: false,
|
||||||
|
openUrl: async () => {},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("unable to get local issuer certificate");
|
||||||
|
|
||||||
|
expect(mocks.loginOpenAICodex).not.toHaveBeenCalled();
|
||||||
|
expect(runtime.error).toHaveBeenCalledWith("Run brew postinstall openssl@3");
|
||||||
|
expect(prompter.note).toHaveBeenCalledWith(
|
||||||
|
"Run brew postinstall openssl@3",
|
||||||
|
"OAuth prerequisites",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { loginOpenAICodex } from "@mariozechner/pi-ai";
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||||
|
import {
|
||||||
|
formatOpenAIOAuthTlsPreflightFix,
|
||||||
|
runOpenAIOAuthTlsPreflight,
|
||||||
|
} from "./oauth-tls-preflight.js";
|
||||||
|
|
||||||
export async function loginOpenAICodexOAuth(params: {
|
export async function loginOpenAICodexOAuth(params: {
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
@@ -12,6 +16,13 @@ export async function loginOpenAICodexOAuth(params: {
|
|||||||
localBrowserMessage?: string;
|
localBrowserMessage?: string;
|
||||||
}): Promise<OAuthCredentials | null> {
|
}): Promise<OAuthCredentials | null> {
|
||||||
const { prompter, runtime, isRemote, openUrl, localBrowserMessage } = params;
|
const { prompter, runtime, isRemote, openUrl, localBrowserMessage } = params;
|
||||||
|
const preflight = await runOpenAIOAuthTlsPreflight();
|
||||||
|
if (!preflight.ok && preflight.kind === "tls-cert") {
|
||||||
|
const hint = formatOpenAIOAuthTlsPreflightFix(preflight);
|
||||||
|
runtime.error(hint);
|
||||||
|
await prompter.note(hint, "OAuth prerequisites");
|
||||||
|
throw new Error(preflight.message);
|
||||||
|
}
|
||||||
|
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
isRemote
|
isRemote
|
||||||
|
|||||||
Reference in New Issue
Block a user