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
25 changes: 23 additions & 2 deletions packages/cli/src/core/project/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { extract } from "tar";
import { base44Client, getAppClient } from "@/core/clients/index.js";
import { ApiError, SchemaValidationError } from "@/core/errors.js";
import { getAppConfig } from "@/core/project/app-config.js";
import type { ProjectsResponse } from "@/core/project/schema.js";
import type { ProjectsResponse, Visibility } from "@/core/project/schema.js";
import {
CreateProjectResponseSchema,
ProjectsResponseSchema,
Expand All @@ -21,7 +21,7 @@ export async function createProject(projectName: string, description?: string) {
name: projectName,
user_description: description ?? `Backend for '${projectName}'`,
is_managed_source_code: false,
public_settings: "public_without_login",
public_settings: "private_with_login",
},
});
} catch (error) {
Expand All @@ -42,6 +42,27 @@ export async function createProject(projectName: string, description?: string) {
};
}

const VISIBILITY_TO_PUBLIC_SETTINGS: Record<Visibility, string> = {
public: "public_without_login",
private: "private_with_login",
workspace: "workspace_with_login",
};

export async function updateProjectVisibility(
visibility: Visibility,
): Promise<void> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we also want to support this somewhere son the create / link commands?
Do we want to support this config in our templates?

const { id } = getAppConfig();
try {
await base44Client.put(`api/apps/${id}`, {
json: {
public_settings: VISIBILITY_TO_PUBLIC_SETTINGS[visibility],
},
});
} catch (error) {
throw await ApiError.fromHttpError(error, "updating project visibility");
}
}

export async function listProjects(): Promise<ProjectsResponse> {
let response: KyResponse;
try {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/core/project/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { resolve } from "node:path";
import { updateProjectVisibility } from "@/core/project/api.js";
import type { ProjectData } from "@/core/project/types.js";
import { agentResource } from "@/core/resources/agent/index.js";
import {
Expand Down Expand Up @@ -69,6 +70,10 @@ export async function deployAll(
await agentResource.push(agents);
const { results: connectorResults } = await pushConnectors(connectors);

if (project.visibility) {
await updateProjectVisibility(project.visibility);
}

if (project.site?.outputDirectory) {
const outputDir = resolve(project.root, project.site.outputDirectory);
const { appUrl } = await deploySite(outputDir);
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/core/project/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ const SiteConfigSchema = z.object({
installCommand: z.string().optional(),
});

export const VisibilitySchema = z.enum(["public", "private", "workspace"], {
error:
'Invalid visibility value. Allowed values: "public", "private", "workspace"',
});

export type Visibility = z.infer<typeof VisibilitySchema>;

export const ProjectConfigSchema = z.object({
name: z
.string({
error: "App name cannot be empty",
})
.min(1, "App name cannot be empty"),
description: z.string().optional(),
visibility: VisibilitySchema.optional(),
site: SiteConfigSchema.optional(),
entitiesDir: z.string().optional().default("entities"),
functionsDir: z.string().optional().default("functions"),
Expand Down
61 changes: 61 additions & 0 deletions packages/cli/tests/cli/deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,64 @@ describe("deploy command (unified)", () => {
t.expectResult(result).toContain("Connectors dashboard");
});
});

describe("deploy command — visibility sync", () => {
const t = setupCLITests();

it('sends PUT to update visibility when config has visibility: "public"', async () => {
// Given
await t.givenLoggedInWithProject(fixture("with-visibility"));
t.api.mockEntitiesPush({ created: ["Item"], updated: [], deleted: [] });
t.api.mockAgentsPush({ created: [], updated: [], deleted: [] });
t.api.mockConnectorsList({ integrations: [] });
t.api.mockUpdateAppVisibility({
id: "test-app-id",
public_settings: "public_without_login",
});

// When
const result = await t.run("deploy", "-y");

// Then
t.expectResult(result).toSucceed();
t.expectResult(result).toContain("App deployed successfully");
});

it("fails deploy when visibility PUT returns an API error", async () => {
// Given
await t.givenLoggedInWithProject(fixture("with-visibility"));
t.api.mockEntitiesPush({ created: ["Item"], updated: [], deleted: [] });
t.api.mockAgentsPush({ created: [], updated: [], deleted: [] });
t.api.mockConnectorsList({ integrations: [] });
t.api.mockUpdateAppVisibilityError({
status: 403,
body: { error: "Forbidden" },
});

// When
const result = await t.run("deploy", "-y");

// Then
t.expectResult(result).toFail();
});

it("does not send PUT when visibility is omitted from config", async () => {
// Given — use fixture without visibility (with-entities has no visibility field)
await t.givenLoggedInWithProject(fixture("with-entities"));
t.api.mockEntitiesPush({
created: ["Customer", "Product"],
updated: [],
deleted: [],
});
t.api.mockAgentsPush({ created: [], updated: [], deleted: [] });
t.api.mockConnectorsList({ integrations: [] });
// No mockUpdateAppVisibility registered — PUT must NOT be called

// When
const result = await t.run("deploy", "-y");

// Then
t.expectResult(result).toSucceed();
t.expectResult(result).toContain("App deployed successfully");
});
});
18 changes: 17 additions & 1 deletion packages/cli/tests/cli/testkit/TestAPIServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,19 @@ interface ListProjectsResponse {
is_managed_source_code?: boolean;
}

interface UpdateAppVisibilityResponse {
id: string;
public_settings: string;
}

interface ErrorResponse {
status: number;
body?: unknown;
}

// ─── ROUTE HANDLER TYPES ─────────────────────────────────────

type Method = "GET" | "POST" | "PUT" | "DELETE";
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

interface RouteEntry {
method: Method;
Expand Down Expand Up @@ -289,6 +294,7 @@ export class TestAPIServer {
| "get"
| "post"
| "put"
| "patch"
| "delete";
this.app[method](entry.path, entry.handler);
}
Expand Down Expand Up @@ -517,6 +523,16 @@ export class TestAPIServer {

// ─── GENERAL ENDPOINTS ───────────────────────────────────

/** Mock PUT /api/apps/{appId} - Update app visibility */
mockUpdateAppVisibility(response: UpdateAppVisibilityResponse): this {
return this.addRoute("PUT", `/api/apps/${this.appId}`, response);
}

/** Mock PUT /api/apps/{appId} - Update app visibility error */
mockUpdateAppVisibilityError(error: ErrorResponse): this {
return this.addErrorRoute("PUT", `/api/apps/${this.appId}`, error);
}

mockCreateApp(response: CreateAppResponse): this {
return this.addRoute("POST", "/api/apps", response);
}
Expand Down
59 changes: 59 additions & 0 deletions packages/cli/tests/core/project-schema.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import { ProjectConfigSchema } from "@/core/project/schema.js";

describe("ProjectConfigSchema visibility field", () => {
it('accepts visibility: "public"', () => {
const result = ProjectConfigSchema.safeParse({
name: "My App",
visibility: "public",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.visibility).toBe("public");
}
});

it('accepts visibility: "private"', () => {
const result = ProjectConfigSchema.safeParse({
name: "My App",
visibility: "private",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.visibility).toBe("private");
}
});

it("accepts omitted visibility (no-op, field is undefined)", () => {
const result = ProjectConfigSchema.safeParse({ name: "My App" });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.visibility).toBeUndefined();
}
});

it('accepts visibility: "workspace"', () => {
const result = ProjectConfigSchema.safeParse({
name: "My App",
visibility: "workspace",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.visibility).toBe("workspace");
}
});

it("rejects invalid visibility value with a clear error", () => {
const result = ProjectConfigSchema.safeParse({
name: "My App",
visibility: "restricted",
});
expect(result.success).toBe(false);
if (!result.success) {
const message = result.error.issues[0]?.message ?? "";
expect(message).toMatch(
/Invalid visibility value|Invalid enum value|public.*private/i,
);
}
});
});
4 changes: 4 additions & 0 deletions packages/cli/tests/fixtures/with-visibility/base44/.app.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Base44 App Configuration
{
"id": "test-app-id"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "Visibility Test Project",
"visibility": "public"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "Item",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Item title"
}
},
"required": ["title"]
}
Loading