From 899d5c8a6987cc46ba51f2675b590adca6039f68 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:16:17 +0000 Subject: [PATCH 1/4] feat: add visibility config field (public/private) to CLI - Add VisibilitySchema and Visibility type to packages/cli/src/core/project/schema.ts - Add updateProjectVisibility() API function in packages/cli/src/core/project/api.ts - Change default public_settings for new projects from public_without_login to private - Integrate visibility sync into deployAll() in packages/cli/src/core/project/deploy.ts - Add PATCH support and mockUpdateAppVisibility helpers to TestAPIServer - Add project-schema unit tests and with-visibility fixture - Add integration tests for visibility sync in deploy.spec.ts Rebased onto monorepo structure (packages/cli/). Co-authored-by: Netanel Gilad --- packages/cli/src/core/project/api.ts | 26 +++++++- packages/cli/src/core/project/deploy.ts | 5 ++ packages/cli/src/core/project/schema.ts | 7 +++ packages/cli/tests/cli/deploy.spec.ts | 61 +++++++++++++++++++ .../cli/tests/cli/testkit/TestAPIServer.ts | 18 +++++- .../cli/tests/core/project-schema.spec.ts | 48 +++++++++++++++ .../with-visibility/base44/.app.jsonc | 4 ++ .../with-visibility/base44/config.jsonc | 4 ++ .../with-visibility/base44/entities/Item.json | 11 ++++ 9 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 packages/cli/tests/core/project-schema.spec.ts create mode 100644 packages/cli/tests/fixtures/with-visibility/base44/.app.jsonc create mode 100644 packages/cli/tests/fixtures/with-visibility/base44/config.jsonc create mode 100644 packages/cli/tests/fixtures/with-visibility/base44/entities/Item.json diff --git a/packages/cli/src/core/project/api.ts b/packages/cli/src/core/project/api.ts index ed6157f6..f1362a06 100644 --- a/packages/cli/src/core/project/api.ts +++ b/packages/cli/src/core/project/api.ts @@ -2,9 +2,9 @@ import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { KyResponse } from "ky"; import { extract } from "tar"; -import { base44Client } from "@/core/clients/index.js"; +import { base44Client, getAppClient } from "@/core/clients/index.js"; import { ApiError, SchemaValidationError } from "@/core/errors.js"; -import type { ProjectsResponse } from "@/core/project/schema.js"; +import type { ProjectsResponse, Visibility } from "@/core/project/schema.js"; import { CreateProjectResponseSchema, ProjectsResponseSchema, @@ -19,7 +19,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", }, }); } catch (error) { @@ -40,6 +40,26 @@ export async function createProject(projectName: string, description?: string) { }; } +const VISIBILITY_TO_PUBLIC_SETTINGS: Record = { + public: "public_without_login", + private: "private", +}; + +export async function updateProjectVisibility( + visibility: Visibility, +): Promise { + const appClient = getAppClient(); + try { + await appClient.patch("", { + json: { + public_settings: VISIBILITY_TO_PUBLIC_SETTINGS[visibility], + }, + }); + } catch (error) { + throw await ApiError.fromHttpError(error, "updating project visibility"); + } +} + export async function listProjects(): Promise { let response: KyResponse; try { diff --git a/packages/cli/src/core/project/deploy.ts b/packages/cli/src/core/project/deploy.ts index adcb6f5e..51963136 100644 --- a/packages/cli/src/core/project/deploy.ts +++ b/packages/cli/src/core/project/deploy.ts @@ -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 { @@ -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); diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 60bf4a01..cdd3e212 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -19,6 +19,12 @@ const SiteConfigSchema = z.object({ installCommand: z.string().optional(), }); +export const VisibilitySchema = z.enum(["public", "private"], { + error: 'Invalid visibility value. Allowed values: "public", "private"', +}); + +export type Visibility = z.infer; + export const ProjectConfigSchema = z.object({ name: z .string({ @@ -26,6 +32,7 @@ export const ProjectConfigSchema = z.object({ }) .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"), diff --git a/packages/cli/tests/cli/deploy.spec.ts b/packages/cli/tests/cli/deploy.spec.ts index c2f5cb92..fe204001 100644 --- a/packages/cli/tests/cli/deploy.spec.ts +++ b/packages/cli/tests/cli/deploy.spec.ts @@ -197,3 +197,64 @@ describe("deploy command (unified)", () => { t.expectResult(result).toContain("Connectors dashboard"); }); }); + +describe("deploy command — visibility sync", () => { + const t = setupCLITests(); + + it('sends PATCH 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 PATCH 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 PATCH 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 — PATCH must NOT be called + + // When + const result = await t.run("deploy", "-y"); + + // Then + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("App deployed successfully"); + }); +}); diff --git a/packages/cli/tests/cli/testkit/TestAPIServer.ts b/packages/cli/tests/cli/testkit/TestAPIServer.ts index 44185bd0..222c0a52 100644 --- a/packages/cli/tests/cli/testkit/TestAPIServer.ts +++ b/packages/cli/tests/cli/testkit/TestAPIServer.ts @@ -204,6 +204,11 @@ interface ListProjectsResponse { is_managed_source_code?: boolean; } +interface UpdateAppVisibilityResponse { + id: string; + public_settings: string; +} + interface ErrorResponse { status: number; body?: unknown; @@ -211,7 +216,7 @@ interface ErrorResponse { // ─── ROUTE HANDLER TYPES ───────────────────────────────────── -type Method = "GET" | "POST" | "PUT" | "DELETE"; +type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; interface RouteEntry { method: Method; @@ -289,6 +294,7 @@ export class TestAPIServer { | "get" | "post" | "put" + | "patch" | "delete"; this.app[method](entry.path, entry.handler); } @@ -510,6 +516,16 @@ export class TestAPIServer { // ─── GENERAL ENDPOINTS ─────────────────────────────────── + /** Mock PATCH /api/apps/{appId}/ - Update app visibility */ + mockUpdateAppVisibility(response: UpdateAppVisibilityResponse): this { + return this.addRoute("PATCH", `/api/apps/${this.appId}/`, response); + } + + /** Mock PATCH /api/apps/{appId}/ - Update app visibility error */ + mockUpdateAppVisibilityError(error: ErrorResponse): this { + return this.addErrorRoute("PATCH", `/api/apps/${this.appId}/`, error); + } + mockCreateApp(response: CreateAppResponse): this { return this.addRoute("POST", "/api/apps", response); } diff --git a/packages/cli/tests/core/project-schema.spec.ts b/packages/cli/tests/core/project-schema.spec.ts new file mode 100644 index 00000000..4a590cdf --- /dev/null +++ b/packages/cli/tests/core/project-schema.spec.ts @@ -0,0 +1,48 @@ +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("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, + ); + } + }); +}); diff --git a/packages/cli/tests/fixtures/with-visibility/base44/.app.jsonc b/packages/cli/tests/fixtures/with-visibility/base44/.app.jsonc new file mode 100644 index 00000000..d7852426 --- /dev/null +++ b/packages/cli/tests/fixtures/with-visibility/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/packages/cli/tests/fixtures/with-visibility/base44/config.jsonc b/packages/cli/tests/fixtures/with-visibility/base44/config.jsonc new file mode 100644 index 00000000..07a3d81e --- /dev/null +++ b/packages/cli/tests/fixtures/with-visibility/base44/config.jsonc @@ -0,0 +1,4 @@ +{ + "name": "Visibility Test Project", + "visibility": "public" +} diff --git a/packages/cli/tests/fixtures/with-visibility/base44/entities/Item.json b/packages/cli/tests/fixtures/with-visibility/base44/entities/Item.json new file mode 100644 index 00000000..f9abb57e --- /dev/null +++ b/packages/cli/tests/fixtures/with-visibility/base44/entities/Item.json @@ -0,0 +1,11 @@ +{ + "name": "Item", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Item title" + } + }, + "required": ["title"] +} From 71ea0f5fccf332de8f80041af2719b54c1dee353 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Sun, 22 Mar 2026 09:44:59 +0200 Subject: [PATCH 2/4] fix: use PUT /api/apps/{id} for visibility update and correct private mapping The platform exposes PUT (not PATCH) for app updates, and expects "private_with_login" rather than "private" for the public_settings field. Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/core/project/api.ts | 11 ++++++----- packages/cli/tests/cli/deploy.spec.ts | 8 ++++---- packages/cli/tests/cli/testkit/TestAPIServer.ts | 8 ++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/core/project/api.ts b/packages/cli/src/core/project/api.ts index f1362a06..85d9363f 100644 --- a/packages/cli/src/core/project/api.ts +++ b/packages/cli/src/core/project/api.ts @@ -2,8 +2,9 @@ import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; import type { KyResponse } from "ky"; import { extract } from "tar"; -import { base44Client, getAppClient } from "@/core/clients/index.js"; +import { base44Client } from "@/core/clients/index.js"; import { ApiError, SchemaValidationError } from "@/core/errors.js"; +import { getAppConfig } from "@/core/project/app-config.js"; import type { ProjectsResponse, Visibility } from "@/core/project/schema.js"; import { CreateProjectResponseSchema, @@ -19,7 +20,7 @@ export async function createProject(projectName: string, description?: string) { name: projectName, user_description: description ?? `Backend for '${projectName}'`, is_managed_source_code: false, - public_settings: "private", + public_settings: "private_with_login", }, }); } catch (error) { @@ -42,15 +43,15 @@ export async function createProject(projectName: string, description?: string) { const VISIBILITY_TO_PUBLIC_SETTINGS: Record = { public: "public_without_login", - private: "private", + private: "private_with_login", }; export async function updateProjectVisibility( visibility: Visibility, ): Promise { - const appClient = getAppClient(); + const { id } = getAppConfig(); try { - await appClient.patch("", { + await base44Client.put(`api/apps/${id}`, { json: { public_settings: VISIBILITY_TO_PUBLIC_SETTINGS[visibility], }, diff --git a/packages/cli/tests/cli/deploy.spec.ts b/packages/cli/tests/cli/deploy.spec.ts index fe204001..c80d90f5 100644 --- a/packages/cli/tests/cli/deploy.spec.ts +++ b/packages/cli/tests/cli/deploy.spec.ts @@ -201,7 +201,7 @@ describe("deploy command (unified)", () => { describe("deploy command — visibility sync", () => { const t = setupCLITests(); - it('sends PATCH to update visibility when config has visibility: "public"', async () => { + 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: [] }); @@ -220,7 +220,7 @@ describe("deploy command — visibility sync", () => { t.expectResult(result).toContain("App deployed successfully"); }); - it("fails deploy when visibility PATCH returns an API error", async () => { + 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: [] }); @@ -238,7 +238,7 @@ describe("deploy command — visibility sync", () => { t.expectResult(result).toFail(); }); - it("does not send PATCH when visibility is omitted from config", async () => { + 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({ @@ -248,7 +248,7 @@ describe("deploy command — visibility sync", () => { }); t.api.mockAgentsPush({ created: [], updated: [], deleted: [] }); t.api.mockConnectorsList({ integrations: [] }); - // No mockUpdateAppVisibility registered — PATCH must NOT be called + // No mockUpdateAppVisibility registered — PUT must NOT be called // When const result = await t.run("deploy", "-y"); diff --git a/packages/cli/tests/cli/testkit/TestAPIServer.ts b/packages/cli/tests/cli/testkit/TestAPIServer.ts index 222c0a52..9e134335 100644 --- a/packages/cli/tests/cli/testkit/TestAPIServer.ts +++ b/packages/cli/tests/cli/testkit/TestAPIServer.ts @@ -516,14 +516,14 @@ export class TestAPIServer { // ─── GENERAL ENDPOINTS ─────────────────────────────────── - /** Mock PATCH /api/apps/{appId}/ - Update app visibility */ + /** Mock PUT /api/apps/{appId} - Update app visibility */ mockUpdateAppVisibility(response: UpdateAppVisibilityResponse): this { - return this.addRoute("PATCH", `/api/apps/${this.appId}/`, response); + return this.addRoute("PUT", `/api/apps/${this.appId}`, response); } - /** Mock PATCH /api/apps/{appId}/ - Update app visibility error */ + /** Mock PUT /api/apps/{appId} - Update app visibility error */ mockUpdateAppVisibilityError(error: ErrorResponse): this { - return this.addErrorRoute("PATCH", `/api/apps/${this.appId}/`, error); + return this.addErrorRoute("PUT", `/api/apps/${this.appId}`, error); } mockCreateApp(response: CreateAppResponse): this { From 6df88cfa67a4aeeca0ae551716da1da5dc225e20 Mon Sep 17 00:00:00 2001 From: Goal Champion 797 Date: Sun, 22 Mar 2026 10:07:00 +0000 Subject: [PATCH 3/4] feat: add workspace visibility level Adds "workspace" as a valid visibility value mapping to `workspace_with_login` (apps visible only to workspace members). This completes coverage of the platform's 4 visibility levels: public, private, and workspace. Adds a unit test for the new enum value. --- packages/cli/src/core/project/api.ts | 1 + packages/cli/src/core/project/schema.ts | 4 ++-- packages/cli/tests/core/project-schema.spec.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/core/project/api.ts b/packages/cli/src/core/project/api.ts index ed92649b..133d1d95 100644 --- a/packages/cli/src/core/project/api.ts +++ b/packages/cli/src/core/project/api.ts @@ -45,6 +45,7 @@ export async function createProject(projectName: string, description?: string) { const VISIBILITY_TO_PUBLIC_SETTINGS: Record = { public: "public_without_login", private: "private_with_login", + workspace: "workspace_with_login", }; export async function updateProjectVisibility( diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index cdd3e212..0b4a86e9 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -19,8 +19,8 @@ const SiteConfigSchema = z.object({ installCommand: z.string().optional(), }); -export const VisibilitySchema = z.enum(["public", "private"], { - error: 'Invalid visibility value. Allowed values: "public", "private"', +export const VisibilitySchema = z.enum(["public", "private", "workspace"], { + error: 'Invalid visibility value. Allowed values: "public", "private", "workspace"', }); export type Visibility = z.infer; diff --git a/packages/cli/tests/core/project-schema.spec.ts b/packages/cli/tests/core/project-schema.spec.ts index 4a590cdf..4ebfe012 100644 --- a/packages/cli/tests/core/project-schema.spec.ts +++ b/packages/cli/tests/core/project-schema.spec.ts @@ -32,6 +32,17 @@ describe("ProjectConfigSchema visibility field", () => { } }); + 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", From 101b4fdc6895adb8aa2dbcf56217888019f6ae63 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Sun, 22 Mar 2026 18:32:53 +0200 Subject: [PATCH 4/4] fix: format long line in VisibilitySchema to pass Biome lint Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/core/project/schema.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 0b4a86e9..280ab7ec 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -20,7 +20,8 @@ const SiteConfigSchema = z.object({ }); export const VisibilitySchema = z.enum(["public", "private", "workspace"], { - error: 'Invalid visibility value. Allowed values: "public", "private", "workspace"', + error: + 'Invalid visibility value. Allowed values: "public", "private", "workspace"', }); export type Visibility = z.infer;