From b3f2fe89c249aa149794476cfccf5e0cdeb9628a Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Sun, 1 Feb 2026 21:26:32 +0200 Subject: [PATCH 1/9] browser commands --- src/cli/commands/site/browse.ts | 23 +++++++++++++++++++++++ src/cli/commands/site/deploy.ts | 16 ++++++---------- src/cli/commands/site/index.ts | 11 +++++++++++ src/cli/program.ts | 4 ++-- src/cli/utils/urls.ts | 20 ++++++++++++++++++++ src/core/resources/index.ts | 2 +- tests/cli/dashboard.spec.ts | 4 ++-- 7 files changed, 65 insertions(+), 15 deletions(-) create mode 100644 src/cli/commands/site/browse.ts create mode 100644 src/cli/commands/site/index.ts diff --git a/src/cli/commands/site/browse.ts b/src/cli/commands/site/browse.ts new file mode 100644 index 00000000..077646ec --- /dev/null +++ b/src/cli/commands/site/browse.ts @@ -0,0 +1,23 @@ +import { Command } from "commander"; +import open from "open"; +import type { CLIContext } from "@/cli/types.js"; +import { runCommand, getSiteUrl } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; + +async function browseAction(): Promise { + const siteUrl = await getSiteUrl(); + + if (!process.env.CI) { + await open(siteUrl); + } + + return { outroMessage: `Site opened at ${siteUrl}` }; +} + +export function getSiteBrowseCommand(context: CLIContext): Command { + return new Command("browse") + .description("Open the published site in your browser") + .action(async () => { + await runCommand(browseAction, { requireAuth: true }, context); + }); +} diff --git a/src/cli/commands/site/deploy.ts b/src/cli/commands/site/deploy.ts index b33efa6b..5c64337b 100644 --- a/src/cli/commands/site/deploy.ts +++ b/src/cli/commands/site/deploy.ts @@ -53,14 +53,10 @@ async function deployAction(options: DeployOptions): Promise { } export function getSiteDeployCommand(context: CLIContext): Command { - return new Command("site") - .description("Manage site deployments") - .addCommand( - new Command("deploy") - .description("Deploy built site files to Base44 hosting") - .option("-y, --yes", "Skip confirmation prompt") - .action(async (options: DeployOptions) => { - await runCommand(() => deployAction(options), { requireAuth: true }, context); - }) - ); + return new Command("deploy") + .description("Deploy built site files to Base44 hosting") + .option("-y, --yes", "Skip confirmation prompt") + .action(async (options: DeployOptions) => { + await runCommand(() => deployAction(options), { requireAuth: true }, context); + }); } diff --git a/src/cli/commands/site/index.ts b/src/cli/commands/site/index.ts new file mode 100644 index 00000000..909fdf04 --- /dev/null +++ b/src/cli/commands/site/index.ts @@ -0,0 +1,11 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getSiteDeployCommand } from "./deploy.js"; +import { getSiteBrowseCommand } from "./browse.js"; + +export function getSiteCommand(context: CLIContext): Command { + return new Command("site") + .description("Manage site") + .addCommand(getSiteDeployCommand(context)) + .addCommand(getSiteBrowseCommand(context)); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 77474222..6477c10b 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -10,7 +10,7 @@ import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDashboardCommand } from "@/cli/commands/project/dashboard.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; -import { getSiteDeployCommand } from "@/cli/commands/site/deploy.js"; +import { getSiteCommand } from "@/cli/commands/site/index.js"; import packageJson from "../../package.json"; export function createProgram(context: CLIContext): Command { @@ -48,7 +48,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getFunctionsDeployCommand(context)); // Register site commands - program.addCommand(getSiteDeployCommand(context)); + program.addCommand(getSiteCommand(context)); return program; } diff --git a/src/cli/utils/urls.ts b/src/cli/utils/urls.ts index c86fd43a..c7113677 100644 --- a/src/cli/utils/urls.ts +++ b/src/cli/utils/urls.ts @@ -1,5 +1,11 @@ +import { z } from "zod"; import { getBase44ApiUrl } from "@/core/config.js"; import { getAppConfig } from "@/core/project/index.js"; +import { base44Client } from "@/core/clients/index.js"; + +const PublishedUrlResponseSchema = z.object({ + url: z.string(), +}); /** * Gets the dashboard URL for a project. @@ -12,3 +18,17 @@ export function getDashboardUrl(projectId?: string): string { const id = projectId ?? getAppConfig().id; return `${getBase44ApiUrl()}/apps/${id}/editor/workspace/overview`; } + +/** + * Gets the published site URL for a project by calling the API. + * + * @param projectId - Optional project ID. If not provided, uses cached appId from getAppConfig(). + * @returns The published site URL (e.g., https://myapp.base44.app) + * @throws Error if no projectId provided and app config is not initialized, or if app has no slug + */ +export async function getSiteUrl(projectId?: string): Promise { + const id = projectId ?? getAppConfig().id; + const response = await base44Client.get(`api/apps/platform/${id}/published-url`); + const data = PublishedUrlResponseSchema.parse(await response.json()); + return data.url; +} diff --git a/src/core/resources/index.ts b/src/core/resources/index.ts index 4f4aef06..a4da42c7 100644 --- a/src/core/resources/index.ts +++ b/src/core/resources/index.ts @@ -1,4 +1,4 @@ export type { Resource } from "./types.js"; export * from "./entity/index.js"; export * from "./function/index.js"; -export * from "./agent/index.js"; +export * from "./agent/index.js"; \ No newline at end of file diff --git a/tests/cli/dashboard.spec.ts b/tests/cli/dashboard.spec.ts index 13f83c8a..aaf88e8c 100644 --- a/tests/cli/dashboard.spec.ts +++ b/tests/cli/dashboard.spec.ts @@ -7,7 +7,7 @@ describe("dashboard command", () => { it("opens dashboard URL when in a project", async () => { await t.givenLoggedInWithProject(fixture("basic")); - const result = await t.run("dashboard"); + const result = await t.run("dashboard", "browse"); t.expectResult(result).toSucceed(); t.expectResult(result).toContain("Dashboard opened"); @@ -17,7 +17,7 @@ describe("dashboard command", () => { it("fails when not in a project directory", async () => { await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); - const result = await t.run("dashboard"); + const result = await t.run("dashboard", "browse"); t.expectResult(result).toFail(); t.expectResult(result).toContain("No Base44 project found"); From d2b524fdb9b245820e822ce46d9c1e5dc5c00252 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 13:41:50 +0200 Subject: [PATCH 2/9] fixes --- src/cli/commands/dashboard/index.ts | 9 +++++++++ .../commands/{project/dashboard.ts => dashboard/open.ts} | 4 ++-- src/cli/commands/site/index.ts | 4 ++-- src/cli/commands/site/{browse.ts => open.ts} | 8 ++++---- src/cli/program.ts | 2 +- tests/cli/dashboard.spec.ts | 4 ++-- 6 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 src/cli/commands/dashboard/index.ts rename src/cli/commands/{project/dashboard.ts => dashboard/open.ts} (86%) rename src/cli/commands/site/{browse.ts => open.ts} (68%) diff --git a/src/cli/commands/dashboard/index.ts b/src/cli/commands/dashboard/index.ts new file mode 100644 index 00000000..7d63e9b0 --- /dev/null +++ b/src/cli/commands/dashboard/index.ts @@ -0,0 +1,9 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getDashboardOpenCommand } from "./open.js"; + +export function getDashboardCommand(context: CLIContext): Command { + return new Command("dashboard") + .description("Manage app dashboard") + .addCommand(getDashboardOpenCommand(context)); +} diff --git a/src/cli/commands/project/dashboard.ts b/src/cli/commands/dashboard/open.ts similarity index 86% rename from src/cli/commands/project/dashboard.ts rename to src/cli/commands/dashboard/open.ts index 7174ac9e..bcf02565 100644 --- a/src/cli/commands/project/dashboard.ts +++ b/src/cli/commands/dashboard/open.ts @@ -14,8 +14,8 @@ async function openDashboard(): Promise { return { outroMessage: `Dashboard opened at ${dashboardUrl}` }; } -export function getDashboardCommand(context: CLIContext): Command { - return new Command("dashboard") +export function getDashboardOpenCommand(context: CLIContext): Command { + return new Command("open") .description("Open the app dashboard in your browser") .action(async () => { await runCommand(openDashboard, { requireAuth: true }, context); diff --git a/src/cli/commands/site/index.ts b/src/cli/commands/site/index.ts index 909fdf04..ef5c0d58 100644 --- a/src/cli/commands/site/index.ts +++ b/src/cli/commands/site/index.ts @@ -1,11 +1,11 @@ import { Command } from "commander"; import type { CLIContext } from "@/cli/types.js"; import { getSiteDeployCommand } from "./deploy.js"; -import { getSiteBrowseCommand } from "./browse.js"; +import { getSiteOpenCommand } from "./open.js"; export function getSiteCommand(context: CLIContext): Command { return new Command("site") .description("Manage site") .addCommand(getSiteDeployCommand(context)) - .addCommand(getSiteBrowseCommand(context)); + .addCommand(getSiteOpenCommand(context)); } diff --git a/src/cli/commands/site/browse.ts b/src/cli/commands/site/open.ts similarity index 68% rename from src/cli/commands/site/browse.ts rename to src/cli/commands/site/open.ts index 077646ec..a79d05e4 100644 --- a/src/cli/commands/site/browse.ts +++ b/src/cli/commands/site/open.ts @@ -4,7 +4,7 @@ import type { CLIContext } from "@/cli/types.js"; import { runCommand, getSiteUrl } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; -async function browseAction(): Promise { +async function openAction(): Promise { const siteUrl = await getSiteUrl(); if (!process.env.CI) { @@ -14,10 +14,10 @@ async function browseAction(): Promise { return { outroMessage: `Site opened at ${siteUrl}` }; } -export function getSiteBrowseCommand(context: CLIContext): Command { - return new Command("browse") +export function getSiteOpenCommand(context: CLIContext): Command { + return new Command("open") .description("Open the published site in your browser") .action(async () => { - await runCommand(browseAction, { requireAuth: true }, context); + await runCommand(openAction, { requireAuth: true }, context); }); } diff --git a/src/cli/program.ts b/src/cli/program.ts index 6477c10b..bc98e4e8 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -7,7 +7,7 @@ import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js"; import { getAgentsCommand } from "@/cli/commands/agents/index.js"; import { getFunctionsDeployCommand } from "@/cli/commands/functions/deploy.js"; import { getCreateCommand } from "@/cli/commands/project/create.js"; -import { getDashboardCommand } from "@/cli/commands/project/dashboard.js"; +import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; import { getSiteCommand } from "@/cli/commands/site/index.js"; diff --git a/tests/cli/dashboard.spec.ts b/tests/cli/dashboard.spec.ts index aaf88e8c..360d771b 100644 --- a/tests/cli/dashboard.spec.ts +++ b/tests/cli/dashboard.spec.ts @@ -7,7 +7,7 @@ describe("dashboard command", () => { it("opens dashboard URL when in a project", async () => { await t.givenLoggedInWithProject(fixture("basic")); - const result = await t.run("dashboard", "browse"); + const result = await t.run("dashboard", "open"); t.expectResult(result).toSucceed(); t.expectResult(result).toContain("Dashboard opened"); @@ -17,7 +17,7 @@ describe("dashboard command", () => { it("fails when not in a project directory", async () => { await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); - const result = await t.run("dashboard", "browse"); + const result = await t.run("dashboard", "open"); t.expectResult(result).toFail(); t.expectResult(result).toContain("No Base44 project found"); From a11f3014cbb9ffae8e8a263e26145d6022892224 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 13:47:47 +0200 Subject: [PATCH 3/9] minor comments --- src/cli/commands/site/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/site/index.ts b/src/cli/commands/site/index.ts index ef5c0d58..4e6bc080 100644 --- a/src/cli/commands/site/index.ts +++ b/src/cli/commands/site/index.ts @@ -5,7 +5,7 @@ import { getSiteOpenCommand } from "./open.js"; export function getSiteCommand(context: CLIContext): Command { return new Command("site") - .description("Manage site") + .description("Manage site (frontend app)") .addCommand(getSiteDeployCommand(context)) .addCommand(getSiteOpenCommand(context)); } From 252135113d89fc975d9f22af0bac9b1a654bb671 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 13:52:12 +0200 Subject: [PATCH 4/9] minor comments --- src/cli/commands/site/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/site/index.ts b/src/cli/commands/site/index.ts index 4e6bc080..775e816c 100644 --- a/src/cli/commands/site/index.ts +++ b/src/cli/commands/site/index.ts @@ -5,7 +5,7 @@ import { getSiteOpenCommand } from "./open.js"; export function getSiteCommand(context: CLIContext): Command { return new Command("site") - .description("Manage site (frontend app)") + .description("Manage app site (frontend app)") .addCommand(getSiteDeployCommand(context)) .addCommand(getSiteOpenCommand(context)); } From 422d03f923ce1908cd376df2746f021ce55d4e55 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 13:54:38 +0200 Subject: [PATCH 5/9] deslop --- src/core/resources/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/resources/index.ts b/src/core/resources/index.ts index a4da42c7..4f4aef06 100644 --- a/src/core/resources/index.ts +++ b/src/core/resources/index.ts @@ -1,4 +1,4 @@ export type { Resource } from "./types.js"; export * from "./entity/index.js"; export * from "./function/index.js"; -export * from "./agent/index.js"; \ No newline at end of file +export * from "./agent/index.js"; From 1d85859ee57274e12b57a84d51fc222e46eaf510 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 14:03:18 +0200 Subject: [PATCH 6/9] better structure --- src/cli/commands/site/open.ts | 3 ++- src/cli/utils/urls.ts | 20 -------------------- src/core/site/api.ts | 12 ++++++++++-- src/core/site/schema.ts | 4 ++++ 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/cli/commands/site/open.ts b/src/cli/commands/site/open.ts index a79d05e4..94fbcd71 100644 --- a/src/cli/commands/site/open.ts +++ b/src/cli/commands/site/open.ts @@ -1,7 +1,8 @@ import { Command } from "commander"; import open from "open"; import type { CLIContext } from "@/cli/types.js"; -import { runCommand, getSiteUrl } from "@/cli/utils/index.js"; +import { runCommand } from "@/cli/utils/index.js"; +import { getSiteUrl } from "@/core/site/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; async function openAction(): Promise { diff --git a/src/cli/utils/urls.ts b/src/cli/utils/urls.ts index c7113677..c86fd43a 100644 --- a/src/cli/utils/urls.ts +++ b/src/cli/utils/urls.ts @@ -1,11 +1,5 @@ -import { z } from "zod"; import { getBase44ApiUrl } from "@/core/config.js"; import { getAppConfig } from "@/core/project/index.js"; -import { base44Client } from "@/core/clients/index.js"; - -const PublishedUrlResponseSchema = z.object({ - url: z.string(), -}); /** * Gets the dashboard URL for a project. @@ -18,17 +12,3 @@ export function getDashboardUrl(projectId?: string): string { const id = projectId ?? getAppConfig().id; return `${getBase44ApiUrl()}/apps/${id}/editor/workspace/overview`; } - -/** - * Gets the published site URL for a project by calling the API. - * - * @param projectId - Optional project ID. If not provided, uses cached appId from getAppConfig(). - * @returns The published site URL (e.g., https://myapp.base44.app) - * @throws Error if no projectId provided and app config is not initialized, or if app has no slug - */ -export async function getSiteUrl(projectId?: string): Promise { - const id = projectId ?? getAppConfig().id; - const response = await base44Client.get(`api/apps/platform/${id}/published-url`); - const data = PublishedUrlResponseSchema.parse(await response.json()); - return data.url; -} diff --git a/src/core/site/api.ts b/src/core/site/api.ts index 983c74a4..d7895d1e 100644 --- a/src/core/site/api.ts +++ b/src/core/site/api.ts @@ -1,6 +1,7 @@ -import { getAppClient } from "@/core/clients/index.js"; +import { getAppClient, base44Client } from "@/core/clients/index.js"; +import { getAppConfig } from "@/core/project/index.js"; import { readFile } from "@/core/utils/fs.js"; -import { DeployResponseSchema } from "@/core/site/schema.js"; +import { DeployResponseSchema, PublishedUrlResponseSchema } from "@/core/site/schema.js"; import type { DeployResponse } from "@/core/site/schema.js"; import { ApiError, SchemaValidationError } from "@/core/errors.js"; @@ -35,3 +36,10 @@ export async function uploadSite(archivePath: string): Promise { return result.data; } + +export async function getSiteUrl(projectId?: string): Promise { + const id = projectId ?? getAppConfig().id; + const response = await base44Client.get(`api/apps/platform/${id}/published-url`); + const data = PublishedUrlResponseSchema.parse(await response.json()); + return data.url; +} diff --git a/src/core/site/schema.ts b/src/core/site/schema.ts index dcffc39e..633d6e05 100644 --- a/src/core/site/schema.ts +++ b/src/core/site/schema.ts @@ -10,3 +10,7 @@ export const DeployResponseSchema = z.object({ })); export type DeployResponse = z.infer; + +export const PublishedUrlResponseSchema = z.object({ + url: z.string(), +}); From 468196f9df2af50d5e11f651d7a9ea0543a68efa Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 14:31:53 +0200 Subject: [PATCH 7/9] tests --- ...shboard.spec.ts => dashboard_open.spec.ts} | 4 +-- tests/cli/site_open.spec.ts | 35 +++++++++++++++++++ tests/cli/testkit/Base44APIMock.ts | 19 ++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) rename tests/cli/{dashboard.spec.ts => dashboard_open.spec.ts} (86%) create mode 100644 tests/cli/site_open.spec.ts diff --git a/tests/cli/dashboard.spec.ts b/tests/cli/dashboard_open.spec.ts similarity index 86% rename from tests/cli/dashboard.spec.ts rename to tests/cli/dashboard_open.spec.ts index 360d771b..61d4d4b6 100644 --- a/tests/cli/dashboard.spec.ts +++ b/tests/cli/dashboard_open.spec.ts @@ -1,7 +1,7 @@ import { describe, it } from "vitest"; import { setupCLITests, fixture } from "./testkit/index.js"; -describe("dashboard command", () => { +describe("dashboard open command", () => { const t = setupCLITests(); it("opens dashboard URL when in a project", async () => { @@ -11,7 +11,7 @@ describe("dashboard command", () => { t.expectResult(result).toSucceed(); t.expectResult(result).toContain("Dashboard opened"); - t.expectResult(result).toContain("test-app-id"); // App ID from fixture + t.expectResult(result).toContain("test-app-id"); }); it("fails when not in a project directory", async () => { diff --git a/tests/cli/site_open.spec.ts b/tests/cli/site_open.spec.ts new file mode 100644 index 00000000..d0144616 --- /dev/null +++ b/tests/cli/site_open.spec.ts @@ -0,0 +1,35 @@ +import { describe, it } from "vitest"; +import { setupCLITests, fixture } from "./testkit/index.js"; + +describe("site open command", () => { + const t = setupCLITests(); + + it("opens site URL when in a project", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockSiteUrl({ url: "https://my-app.base44.app" }); + + const result = await t.run("site", "open"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Site opened"); + t.expectResult(result).toContain("https://my-app.base44.app"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("site", "open"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("No Base44 project found"); + }); + + it("fails when API returns error", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockSiteUrlError({ status: 404, body: { error: "Site not published" } }); + + const result = await t.run("site", "open"); + + t.expectResult(result).toFail(); + }); +}); diff --git a/tests/cli/testkit/Base44APIMock.ts b/tests/cli/testkit/Base44APIMock.ts index f2ac3ed6..f4a8cb54 100644 --- a/tests/cli/testkit/Base44APIMock.ts +++ b/tests/cli/testkit/Base44APIMock.ts @@ -42,6 +42,10 @@ export interface SiteDeployResponse { app_url: string; } +export interface SiteUrlResponse { + url: string; +} + export interface AgentsPushResponse { created: string[]; updated: string[]; @@ -145,6 +149,16 @@ export class Base44APIMock { return this; } + /** Mock GET /api/apps/platform/{appId}/published-url - Get site URL */ + mockSiteUrl(response: SiteUrlResponse): this { + this.handlers.push( + http.get(`${BASE_URL}/api/apps/platform/${this.appId}/published-url`, () => + HttpResponse.json(response) + ) + ); + return this; + } + /** Mock PUT /api/apps/{appId}/agent-configs - Push agents */ mockAgentsPush(response: AgentsPushResponse): this { this.handlers.push( @@ -201,6 +215,11 @@ export class Base44APIMock { return this.mockError("post", `/api/apps/${this.appId}/deploy-dist`, error); } + /** Mock site URL to return an error */ + mockSiteUrlError(error: ErrorResponse): this { + return this.mockError("get", `/api/apps/platform/${this.appId}/published-url`, error); + } + /** Mock agents push to return an error */ mockAgentsPushError(error: ErrorResponse): this { return this.mockError("put", `/api/apps/${this.appId}/agent-configs`, error); From a43cb0ad0e5a67cfb836257d5d9bc6960d32c055 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 14:42:30 +0200 Subject: [PATCH 8/9] better errors --- tests/cli/site_open.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli/site_open.spec.ts b/tests/cli/site_open.spec.ts index d0144616..ad44e568 100644 --- a/tests/cli/site_open.spec.ts +++ b/tests/cli/site_open.spec.ts @@ -26,7 +26,7 @@ describe("site open command", () => { it("fails when API returns error", async () => { await t.givenLoggedInWithProject(fixture("basic")); - t.api.mockSiteUrlError({ status: 404, body: { error: "Site not published" } }); + t.api.mockSiteUrlError({ status: 404, body: { detail: "App not found" } }); const result = await t.run("site", "open"); From ca36c6e62227f2d8b01ebeee6617a8d2dc853f73 Mon Sep 17 00:00:00 2001 From: Pavel Tarnopolsky Date: Mon, 2 Feb 2026 15:06:47 +0200 Subject: [PATCH 9/9] fix: use safeParse pattern in getSiteUrl for consistent error handling Co-Authored-By: Claude Opus 4.5 --- src/core/site/api.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/core/site/api.ts b/src/core/site/api.ts index d7895d1e..4eda7cb6 100644 --- a/src/core/site/api.ts +++ b/src/core/site/api.ts @@ -39,7 +39,19 @@ export async function uploadSite(archivePath: string): Promise { export async function getSiteUrl(projectId?: string): Promise { const id = projectId ?? getAppConfig().id; - const response = await base44Client.get(`api/apps/platform/${id}/published-url`); - const data = PublishedUrlResponseSchema.parse(await response.json()); - return data.url; + + let response; + try { + response = await base44Client.get(`api/apps/platform/${id}/published-url`); + } catch (error) { + throw await ApiError.fromHttpError(error, "fetching site URL"); + } + + const result = PublishedUrlResponseSchema.safeParse(await response.json()); + + if (!result.success) { + throw new SchemaValidationError("Invalid response from server", result.error); + } + + return result.data.url; }