Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/cli/commands/dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ async function openDashboard(): Promise<RunCommandResult> {
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);
Expand Down
16 changes: 6 additions & 10 deletions src/cli/commands/site/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,10 @@ async function deployAction(options: DeployOptions): Promise<RunCommandResult> {
}

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);
});
}
11 changes: 11 additions & 0 deletions src/cli/commands/site/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { getSiteDeployCommand } from "./deploy.js";
import { getSiteOpenCommand } from "./open.js";

export function getSiteCommand(context: CLIContext): Command {
return new Command("site")
.description("Manage app site (frontend app)")
.addCommand(getSiteDeployCommand(context))
.addCommand(getSiteOpenCommand(context));
}
24 changes: 24 additions & 0 deletions src/cli/commands/site/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Command } from "commander";
import open from "open";
import type { CLIContext } from "@/cli/types.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<RunCommandResult> {
const siteUrl = await getSiteUrl();

if (!process.env.CI) {
await open(siteUrl);
}

return { outroMessage: `Site opened at ${siteUrl}` };
}

export function getSiteOpenCommand(context: CLIContext): Command {
return new Command("open")
.description("Open the published site in your browser")
.action(async () => {
await runCommand(openAction, { requireAuth: true }, context);
});
}
6 changes: 3 additions & 3 deletions src/cli/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ 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 { 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 {
Expand Down Expand Up @@ -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;
}
24 changes: 22 additions & 2 deletions src/core/site/api.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -35,3 +36,22 @@ export async function uploadSite(archivePath: string): Promise<DeployResponse> {

return result.data;
}

export async function getSiteUrl(projectId?: string): Promise<string> {
const id = projectId ?? getAppConfig().id;

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;
}
4 changes: 4 additions & 0 deletions src/core/site/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export const DeployResponseSchema = z.object({
}));

export type DeployResponse = z.infer<typeof DeployResponseSchema>;

export const PublishedUrlResponseSchema = z.object({
url: z.string(),
});
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
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 () => {
await t.givenLoggedInWithProject(fixture("basic"));

const result = await t.run("dashboard");
const result = await t.run("dashboard", "open");

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 () => {
await t.givenLoggedIn({ email: "test@example.com", name: "Test User" });

const result = await t.run("dashboard");
const result = await t.run("dashboard", "open");

t.expectResult(result).toFail();
t.expectResult(result).toContain("No Base44 project found");
Expand Down
35 changes: 35 additions & 0 deletions tests/cli/site_open.spec.ts
Original file line number Diff line number Diff line change
@@ -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: { detail: "App not found" } });

const result = await t.run("site", "open");

t.expectResult(result).toFail();
});
});
19 changes: 19 additions & 0 deletions tests/cli/testkit/Base44APIMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export interface SiteDeployResponse {
app_url: string;
}

export interface SiteUrlResponse {
url: string;
}

export interface AgentsPushResponse {
created: string[];
updated: string[];
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down