Skip to content
Merged
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
26 changes: 21 additions & 5 deletions packages/cli/src/cli/commands/agents/push.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { Base44Command, confirmPush } from "@/cli/utils/index.js";
import { readProjectConfig } from "@/core/index.js";
import { pushAgents } from "@/core/resources/agent/index.js";

async function pushAgentsAction({
log,
runTask,
}: CLIContext): Promise<RunCommandResult> {
interface PushOptions {
yes?: boolean;
}

async function pushAgentsAction(
{ isNonInteractive, log, runTask }: CLIContext,
options: PushOptions,
): Promise<RunCommandResult> {
const { agents } = await readProjectConfig();

log.info(
Expand All @@ -16,6 +20,17 @@ async function pushAgentsAction({
: `Found ${agents.length} agents to push`,
);

const proceed = await confirmPush({
isNonInteractive,
yes: options.yes,
Comment thread
davidsu marked this conversation as resolved.
log,
warning:
"This will replace all remote agent configs with your local agents and delete any not present locally.",
});
if (!proceed) {
return { outroMessage: "Push cancelled" };
}

const result = await runTask(
"Pushing agents to Base44",
async () => {
Expand Down Expand Up @@ -45,5 +60,6 @@ export function getAgentsPushCommand(): Command {
.description(
"Push local agents to Base44 (replaces all remote agent configs)",
)
.option("-y, --yes", "Skip confirmation prompt")
.action(pushAgentsAction);
}
15 changes: 14 additions & 1 deletion packages/cli/src/cli/commands/connectors/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { resolve } from "node:path";
import type { Logger } from "@base44-cli/logger";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, theme } from "@/cli/utils/index.js";
import { Base44Command, confirmPush, theme } from "@/cli/utils/index.js";
import { getConnectorsUrl } from "@/cli/utils/urls.js";
import { readProjectConfig } from "@/core/index.js";
import { getAppContext } from "@/core/project/index.js";
Expand All @@ -22,6 +22,7 @@ import {

interface PushOptions {
dir?: string;
yes?: boolean;
}

/**
Expand Down Expand Up @@ -133,6 +134,17 @@ async function pushConnectorsAction(
}
}

const proceed = await confirmPush({
isNonInteractive,
yes: options.yes,
log,
warning:
"This will overwrite your app's connectors with your local copy and remove any not present locally.",
});
if (!proceed) {
return { outroMessage: "Push cancelled" };
}

const { results } = await runTask(
"Pushing connectors to Base44",
async () => {
Expand Down Expand Up @@ -180,5 +192,6 @@ export function getConnectorsPushCommand(): Command {
"--dir <path>",
"Directory to read connector files from (default: ./connectors when using --app-id)",
)
.option("-y, --yes", "Skip confirmation prompt")
.action(pushConnectorsAction);
}
26 changes: 21 additions & 5 deletions packages/cli/src/cli/commands/entities/push.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { Base44Command, confirmPush } from "@/cli/utils/index.js";
import { readProjectConfig } from "@/core/index.js";
import { pushEntities } from "@/core/resources/entity/index.js";

async function pushEntitiesAction({
log,
runTask,
}: CLIContext): Promise<RunCommandResult> {
interface PushOptions {
yes?: boolean;
}

async function pushEntitiesAction(
{ isNonInteractive, log, runTask }: CLIContext,
options: PushOptions,
): Promise<RunCommandResult> {
const { entities } = await readProjectConfig();

if (entities.length === 0) {
Expand All @@ -17,6 +21,17 @@ async function pushEntitiesAction({
const entityNames = entities.map((e) => e.name).join(", ");
log.info(`Found ${entities.length} entities to push: ${entityNames}`);

const proceed = await confirmPush({
isNonInteractive,
yes: options.yes,
log,
warning:
"This will overwrite your app's entities with your local copy and delete any not present locally.",
});
if (!proceed) {
return { outroMessage: "Push cancelled" };
}

const result = await runTask(
"Pushing entities to Base44",
async () => {
Expand Down Expand Up @@ -48,6 +63,7 @@ export function getEntitiesPushCommand(): Command {
.addCommand(
new Base44Command("push")
.description("Push local entities to Base44")
.option("-y, --yes", "Skip confirmation prompt")
.action(pushEntitiesAction),
);
}
34 changes: 34 additions & 0 deletions packages/cli/src/cli/utils/confirm-push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Logger } from "@base44-cli/logger";
import { confirm, isCancel } from "@clack/prompts";
import { InvalidInputError } from "@/core/errors.js";

interface ConfirmPushOptions {
isNonInteractive: boolean;
yes: boolean | undefined;
log: Logger;
warning: string;
}

/**
* Guard a destructive push: in non-interactive mode require --yes (throws
* otherwise, so --json/CI never hang on a prompt); interactively, warn and
* ask for confirmation. Returns false when the user declines.
*/
export async function confirmPush({
isNonInteractive,
yes,
log,
warning,
}: ConfirmPushOptions): Promise<boolean> {
if (isNonInteractive && !yes) {
throw new InvalidInputError("--yes is required in non-interactive mode");
}
if (yes) {
return true;
}
log.warn(warning);
const proceed = await confirm({
message: "Are you sure you want to continue?",
});
return !isCancel(proceed) && proceed;
}
1 change: 1 addition & 0 deletions packages/cli/src/cli/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "@base44-cli/logger";
export * from "./banner.js";
export * from "./command/index.js";
export * from "./confirm-push.js";
export * from "./json.js";
export * from "./prompts.js";
export * from "./runTask.js";
Expand Down
21 changes: 16 additions & 5 deletions packages/cli/tests/cli/agents_push.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("agents push command", () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockAgentsPush({ created: [], updated: [], deleted: [] });

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("No local agents found");
Expand All @@ -17,7 +17,7 @@ describe("agents push 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("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toFail();
t.expectResult(result).toContain("No Base44 app ID found");
Expand All @@ -31,7 +31,7 @@ describe("agents push command", () => {
deleted: [],
});

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Found 3 agents to push");
Expand All @@ -45,7 +45,7 @@ describe("agents push command", () => {
deleted: ["old_agent"],
});

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Agents pushed successfully");
Expand All @@ -57,7 +57,7 @@ describe("agents push command", () => {
it("fails with helpful error when agent has empty name", async () => {
await t.givenLoggedInWithProject(fixture("invalid-agent"));

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toFail();
});
Expand All @@ -66,8 +66,19 @@ describe("agents push command", () => {
await t.givenLoggedInWithProject(fixture("with-agents"));
t.api.mockAgentsPushError({ status: 401, body: { error: "Unauthorized" } });

const result = await t.run("agents", "push", "--yes");

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

it("fails when --yes is not provided in non-interactive mode", async () => {
await t.givenLoggedInWithProject(fixture("with-agents"));

const result = await t.run("agents", "push");

t.expectResult(result).toFail();
t.expectResult(result).toContain(
"--yes is required in non-interactive mode",
);
});
});
9 changes: 6 additions & 3 deletions packages/cli/tests/cli/authorization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("token refresh on 401", () => {
deleted: [],
});

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Agents pushed successfully");
Expand All @@ -59,7 +59,7 @@ describe("token refresh on 401", () => {
deleted: [],
});

const result = await t.run("entities", "push");
const result = await t.run("entities", "push", "--yes");

t.expectResult(result).toSucceed();
t.expectResult(result).toContain("Entities pushed");
Expand All @@ -78,8 +78,11 @@ describe("token refresh on 401", () => {
res.status(401).json({ error: "Unauthorized" });
});

const result = await t.run("agents", "push");
const result = await t.run("agents", "push", "--yes");

t.expectResult(result).toFail();
// Assert the real auth-failure reason, not just a non-zero exit — otherwise
// an unrelated early failure (e.g. a missing --yes guard) would pass this.
t.expectResult(result).toContain("Unauthorized");
});
});
Loading
Loading