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
4 changes: 3 additions & 1 deletion packages/cli/src/cli/commands/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type { CLIContext } from "@/cli/types.js";
import { getDeleteCommand } from "./delete.js";
import { getDeployCommand } from "./deploy.js";
import { getListCommand } from "./list.js";
import { getPullCommand } from "./pull.js";

export function getFunctionsCommand(context: CLIContext): Command {
return new Command("functions")
.description("Manage backend functions")
.addCommand(getDeployCommand(context))
.addCommand(getDeleteCommand(context))
.addCommand(getListCommand(context));
.addCommand(getListCommand(context))
.addCommand(getPullCommand(context));
}
79 changes: 79 additions & 0 deletions packages/cli/src/cli/commands/functions/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { dirname, join } from "node:path";
import { log } from "@clack/prompts";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { readProjectConfig } from "@/core/index.js";
import { listDeployedFunctions } from "@/core/resources/function/api.js";
import { writeFunctions } from "@/core/resources/function/pull.js";

async function pullFunctionsAction(
name: string | undefined,
): Promise<RunCommandResult> {
const { project } = await readProjectConfig();

const configDir = dirname(project.configPath);
const functionsDir = join(configDir, project.functionsDir);

const remoteFunctions = await runTask(
"Fetching functions from Base44",
async () => {
const { functions } = await listDeployedFunctions();
return functions;
},
{
successMessage: "Functions fetched successfully",
errorMessage: "Failed to fetch functions",
},
);

const toPull = name
? remoteFunctions.filter((f) => f.name === name)
: remoteFunctions;

if (name && toPull.length === 0) {
return {
outroMessage: `Function "${name}" not found on remote`,
};
}

if (toPull.length === 0) {
return { outroMessage: "No functions found on remote" };
}

const { written, skipped } = await runTask(
"Writing function files",
async () => {
return await writeFunctions(functionsDir, toPull);
},
{
successMessage: "Function files written successfully",
errorMessage: "Failed to write function files",
},
);

for (const name of written) {
log.success(`${name.padEnd(25)} written`);
}
for (const name of skipped) {
log.info(`${name.padEnd(25)} unchanged`);
}

return {
outroMessage: `Pulled ${toPull.length} function${toPull.length !== 1 ? "s" : ""} to ${functionsDir}`,
};
}

export function getPullCommand(context: CLIContext): Command {
return new Command("pull")
.description("Pull deployed functions from Base44")
.argument("[name]", "Pull a single function by name")
.action(async (name: string | undefined) => {
await runCommand(
() => pullFunctionsAction(name),
{ requireAuth: true },
context,
);
});
}
1 change: 1 addition & 0 deletions packages/cli/src/core/resources/function/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./api.js";
export * from "./config.js";
export * from "./deploy.js";
export * from "./pull.js";
export * from "./resource.js";
export * from "./schema.js";
102 changes: 102 additions & 0 deletions packages/cli/src/core/resources/function/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { join } from "node:path";
import { isDeepStrictEqual } from "node:util";
import type { FunctionInfo } from "@/core/resources/function/schema.js";
import {
pathExists,
readJsonFile,
readTextFile,
writeFile,
writeJsonFile,
} from "@/core/utils/fs.js";

interface WriteFunctionsResult {
written: string[];
skipped: string[];
}

/**
* Writes remote function data to local function directories.
* Creates function.jsonc config and all source files for each function.
* Skips functions whose local files already match remote content.
*/
export async function writeFunctions(
functionsDir: string,
functions: FunctionInfo[],
): Promise<WriteFunctionsResult> {
const written: string[] = [];
const skipped: string[] = [];

for (const fn of functions) {
const functionDir = join(functionsDir, fn.name);
const configPath = join(functionDir, "function.jsonc");

// Check if all files already match remote content
if (await isFunctionUnchanged(functionDir, fn)) {
skipped.push(fn.name);
continue;
}

// Write function config
const config: Record<string, unknown> = {
name: fn.name,
entry: fn.entry,
};
if (fn.automations.length > 0) {
config.automations = fn.automations;
}
await writeJsonFile(configPath, config);

// Write all source files
for (const file of fn.files) {
await writeFile(join(functionDir, file.path), file.content);
}

written.push(fn.name);
}

return { written, skipped };
}

async function isFunctionUnchanged(
functionDir: string,
fn: FunctionInfo,
): Promise<boolean> {
if (!(await pathExists(functionDir))) {
return false;
}

// Compare function config (entry, automations)
const configPath = join(functionDir, "function.jsonc");
try {
const localConfig = (await readJsonFile(configPath)) as Record<
string,
unknown
>;
if (localConfig.entry !== fn.entry) {
return false;
}
if (!isDeepStrictEqual(localConfig.automations ?? [], fn.automations)) {
return false;
}
} catch {
return false;
}

// Compare source files
for (const file of fn.files) {
const filePath = join(functionDir, file.path);
if (!(await pathExists(filePath))) {
return false;
}
try {
const localContent = await readTextFile(filePath);
if (localContent !== file.content) {
return false;
}
} catch {
return false;
}
}

return true;
}
1 change: 1 addition & 0 deletions packages/cli/src/core/resources/function/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export type FunctionFile = z.infer<typeof FunctionFileSchema>;
export type DeployFunctionsResponse = z.infer<
typeof DeployFunctionsResponseSchema
>;
export type FunctionInfo = z.infer<typeof FunctionInfoSchema>;
export type ListFunctionsResponse = z.infer<typeof ListFunctionsResponseSchema>;

export type FunctionWithCode = Omit<BackendFunction, "filePaths"> & {
Expand Down
Loading
Loading