diff --git a/CHANGELOG.md b/CHANGELOG.md index 836bd87c9..98d78c695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Actors (realtime handlers): deploy from `base44/actors/` via `base44 actors deploy`, included in unified `base44 deploy`; `base44 types generate` emits `ActorNameRegistry`. - App visibility: `base44 visibility ` sets it on the server directly (accepts `--app-id` to target any app). Also configurable via `"visibility"` in `config.jsonc`, which `base44 deploy` applies. New projects scaffold `"visibility": "public"`. - `base44 build` runs the site's `buildCommand` with `VITE_BASE44_APP_ID` injected, so built bundles always carry the linked app's id. - `base44 deploy` (and `base44 site deploy`) can now build first: interactive runs ask, and `--build` / `--no-build` pre-answer the prompt. diff --git a/docs/resources.md b/docs/resources.md index 8fc9be4f7..201f4504c 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -1,8 +1,8 @@ # Working with Resources -**Keywords:** resource, entity, function, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData +**Keywords:** resource, entity, function, actor, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData -Resources are project-specific collections (entities, functions, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API. +Resources are project-specific collections (entities, functions, actors, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API. ## Resource Interface @@ -85,6 +85,17 @@ Deploy ships file contents verbatim — the source is never parsed or linted — Entry files may also import `secrets` and `waitUntil` from `base44:runtime`. Locally, `base44 dev` runs functions on workerd via Miniflare by default — each function is bundled with esbuild + `@deno/loader` (`src/cli/dev/dev-server/function-bundler.ts`), with `base44:runtime` served as a virtual module, secrets as real Worker env bindings and `waitUntil` riding `ctx.waitUntil`. A fallback runtime covers installations where workerd is unavailable (compiled binaries, `B44_DEV_FUNCTIONS_RUNTIME=deno`) and supplies `base44:runtime` via an import map. A project-level `deno.json` import map is not applied to functions — locally or deployed — since only files under `base44/` are uploaded. See [`packages/cli/backend-runtime/README.md`](../packages/cli/backend-runtime/README.md) for the local implementation and its intentional differences from production. +## Actors (project layout) + +Actors are stateful realtime handlers, read from the project's actors directory (`base44/actors/`, or `actorsDir` in `config.jsonc`). Discovery is zero-config only: a folder containing `entry.ts` (or `entry.js`) is an actor, and its name is the path from the actors root (e.g. `actors/ChatRoom/entry.ts` → name `ChatRoom`; nesting is allowed). All `**/*.{js,ts,json,jsonc}` files under that folder are included in the deploy payload, sent via `PUT /api/apps/{app_id}/actors/{name}`. The entry file must default-export the actor class — the deploy bundler imports the default export. + +Deliberate gaps (vs functions): no `base44/shared/` inclusion, no `--force` prune, no plugin actors, and no local `base44 dev` runtime. Authoring guidance (scaffolding, message typing, editor setup for the `base44:runtime/actors` virtual module) lives in the realtime skill, not the CLI. Type generation only emits `ActorNameRegistry` (actor names) into `types.d.ts`. + +```bash +base44 actors deploy # Deploy all actors +base44 actors deploy ChatRoom # Deploy specific actors by name +``` + ## Agent skills Agent skills are app-scoped instruction snippets shared across the app's agents. Unlike other resources they are stored as one markdown file per skill under the agent-skills directory (`base44/agent-skills/`, or `agentSkillsDir` in `config.jsonc`): the filename (without `.md`) is the skill name, the frontmatter `description` is the summary, and the body is the instruction text. Agents reference skills by name via `selected_skill_names`; `selected_workspace_skill_ids` (org-shared workspace skills) is not managed here and is passed through pull/push/deploy untouched. @@ -136,10 +147,11 @@ const { appUrl } = await deployAll(projectData); What it deploys (in order): 1. Entities (via `entityResource.push()`) 2. Functions (via `functionResource.push()`) -3. Agent skills (via `agentSkillResource.push()`) -4. Agents (via `agentResource.push()`) -5. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs -6. Site (if `site.outputDirectory` is configured) — the legacy tar.gz upload. The env-gated deployments-API lane is reachable only from `base44 site deploy`, not from here (see [deployments.md](deployments.md)). +3. Actors (via `deployActorsSequentially()`) +4. Agent skills (via `agentSkillResource.push()`) +5. Agents (via `agentResource.push()`) +6. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs +7. Site (if `site.outputDirectory` is configured) — the legacy tar.gz upload. The env-gated deployments-API lane is reachable only from `base44 site deploy`, not from here (see [deployments.md](deployments.md)). ```bash base44 deploy # With confirmation prompt diff --git a/docs/testing.md b/docs/testing.md index f34052dc6..77b4e449a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -210,6 +210,13 @@ t.api.mockFunctionsPush({ deployed: ["handler"], deleted: [], errors: null }); t.api.mockFunctionsPushError({ status: 400, body: { error: "Invalid" } }); ``` +### Actor Mocks + +```typescript +t.api.mockSingleActorDeploy({ status: "deployed" }); +t.api.mockSingleActorDeployError({ status: 400, body: { error: "Invalid" } }); +``` + ### Agent Mocks ```typescript diff --git a/packages/cli/README.md b/packages/cli/README.md index 4a48fb8c9..3f959f564 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -51,6 +51,7 @@ The CLI will guide you through project setup. For step-by-step tutorials, see th | [`login`](https://docs.base44.com/developers/references/cli/commands/login) | Authenticate with Base44 | | [`logout`](https://docs.base44.com/developers/references/cli/commands/logout) | Sign out and clear stored credentials | | [`whoami`](https://docs.base44.com/developers/references/cli/commands/whoami) | Display the current authenticated user | +| `actors deploy` | Deploy local actors to Base44 | | [`agents pull`](https://docs.base44.com/developers/references/cli/commands/agents-pull) | Pull agents from Base44 to local files | | [`agents push`](https://docs.base44.com/developers/references/cli/commands/agents-push) | Push local agents to Base44 | | [`connectors initiate`](https://docs.base44.com/developers/references/cli/commands/connectors-initiate) | Initialize a connector on an app and start its OAuth flow | diff --git a/packages/cli/src/cli/commands/actors/deploy.ts b/packages/cli/src/cli/commands/actors/deploy.ts new file mode 100644 index 000000000..6a11980af --- /dev/null +++ b/packages/cli/src/cli/commands/actors/deploy.ts @@ -0,0 +1,79 @@ +import type { Command } from "commander"; +import { CLIExitError } from "@/cli/errors.js"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { + Base44Command, + buildDeploySummary, + formatDeployResult, + parseNames, + theme, +} from "@/cli/utils/index.js"; +import { InvalidInputError } from "@/core/errors.js"; +import { readProjectConfig } from "@/core/index.js"; +import { deployActorsSequentially } from "@/core/resources/actor/deploy.js"; +import type { Actor } from "@/core/resources/actor/schema.js"; + +function resolveActorsToDeploy(names: string[], allActors: Actor[]): Actor[] { + if (names.length === 0) return allActors; + + const notFound = names.filter((n) => !allActors.some((a) => a.name === n)); + if (notFound.length > 0) { + throw new InvalidInputError( + `Actor${notFound.length > 1 ? "s" : ""} not found in project: ${notFound.join(", ")}`, + ); + } + return allActors.filter((a) => names.includes(a.name)); +} + +async function deployActorsAction( + { log }: CLIContext, + names: string[], +): Promise { + const { actors } = await readProjectConfig(); + const toDeploy = resolveActorsToDeploy(names, actors); + + if (toDeploy.length === 0) { + return { + outroMessage: "No actors found. Create actors in the 'actors' directory.", + }; + } + + log.info( + `Found ${toDeploy.length} ${toDeploy.length === 1 ? "actor" : "actors"} to deploy`, + ); + + let completed = 0; + const total = toDeploy.length; + + const results = await deployActorsSequentially(toDeploy, { + onStart: (startNames) => { + const label = + startNames.length === 1 ? startNames[0] : `${startNames.length} actors`; + log.step( + theme.styles.dim(`[${completed + 1}/${total}] Deploying ${label}...`), + ); + }, + onResult: (result) => { + completed++; + formatDeployResult(result, log); + }, + }); + + const hasFailures = results.some((r) => r.status === "error"); + if (hasFailures) { + log.message(buildDeploySummary(results, "actors")); + throw new CLIExitError(1); + } + + return { outroMessage: buildDeploySummary(results, "actors") }; +} + +export function getDeployCommand(): Command { + return new Base44Command("deploy") + .description("Deploy actors to Base44") + .argument("[names...]", "Actor names to deploy (deploys all if omitted)") + .action(async (ctx: CLIContext, rawNames: string[]) => { + const names = parseNames(rawNames); + return deployActorsAction(ctx, names); + }); +} diff --git a/packages/cli/src/cli/commands/actors/index.ts b/packages/cli/src/cli/commands/actors/index.ts new file mode 100644 index 000000000..7b256abfd --- /dev/null +++ b/packages/cli/src/cli/commands/actors/index.ts @@ -0,0 +1,8 @@ +import { Command } from "commander"; +import { getDeployCommand } from "./deploy.js"; + +export function getActorsCommand(): Command { + return new Command("actors") + .description("Manage actors") + .addCommand(getDeployCommand()); +} diff --git a/packages/cli/src/cli/commands/functions/delete.ts b/packages/cli/src/cli/commands/functions/delete.ts index 56d04789a..235f30dad 100644 --- a/packages/cli/src/cli/commands/functions/delete.ts +++ b/packages/cli/src/cli/commands/functions/delete.ts @@ -1,6 +1,6 @@ import type { Command } from "commander"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; -import { Base44Command } from "@/cli/utils/index.js"; +import { Base44Command, parseNames } from "@/cli/utils/index.js"; import { ApiError } from "@/core/errors.js"; import { deleteSingleFunction } from "@/core/resources/function/api.js"; @@ -42,14 +42,6 @@ async function deleteFunctionsAction( return { outroMessage: parts.join(", ") }; } -/** Parse names from variadic CLI args, supporting comma-separated values. */ -function parseNames(args: string[]): string[] { - return args - .flatMap((arg) => arg.split(",")) - .map((n) => n.trim()) - .filter(Boolean); -} - function validateNames(command: Command): void { const names = parseNames(command.args); if (names.length === 0) { diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index e282e6633..d2645954d 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -1,17 +1,20 @@ import type { Logger } from "@base44-cli/logger"; import type { Command } from "commander"; -import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; -import { parseNames } from "@/cli/commands/functions/parseNames.js"; import { CLIExitError } from "@/cli/errors.js"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; -import { Base44Command, theme } from "@/cli/utils/index.js"; +import { + Base44Command, + buildDeploySummary, + formatDeployResult, + parseNames, + theme, +} from "@/cli/utils/index.js"; import { InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/index.js"; import { deployFunctionsSequentially, type PruneResult, pruneRemovedFunctions, - type SingleFunctionDeployResult, } from "@/core/resources/function/deploy.js"; import type { BackendFunction } from "@/core/resources/function/schema.js"; @@ -45,18 +48,6 @@ function formatPruneSummary(pruneResults: PruneResult[], log: Logger): void { } } -function buildDeploySummary(results: SingleFunctionDeployResult[]): string { - const deployed = results.filter((r) => r.status === "deployed").length; - const unchanged = results.filter((r) => r.status === "unchanged").length; - const failed = results.filter((r) => r.status === "error").length; - - const parts: string[] = []; - if (deployed > 0) parts.push(`${deployed} deployed`); - if (unchanged > 0) parts.push(`${unchanged} unchanged`); - if (failed > 0) parts.push(`${failed} error${failed !== 1 ? "s" : ""}`); - return parts.join(", ") || "No functions deployed"; -} - async function deployFunctionsAction( { log }: CLIContext, names: string[], @@ -103,7 +94,7 @@ async function deployFunctionsAction( const hasFailures = results.some((r) => r.status === "error"); if (hasFailures) { - log.message(buildDeploySummary(results)); + log.message(buildDeploySummary(results, "functions")); throw new CLIExitError(1); } @@ -133,7 +124,7 @@ async function deployFunctionsAction( formatPruneSummary(pruneResults, log); } - return { outroMessage: buildDeploySummary(results) }; + return { outroMessage: buildDeploySummary(results, "functions") }; } export function getDeployCommand(): Command { diff --git a/packages/cli/src/cli/commands/functions/formatDeployResult.ts b/packages/cli/src/cli/commands/functions/formatDeployResult.ts deleted file mode 100644 index 39406a203..000000000 --- a/packages/cli/src/cli/commands/functions/formatDeployResult.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Logger } from "@base44-cli/logger"; -import { theme } from "@/cli/utils/theme.js"; -import type { SingleFunctionDeployResult } from "@/core/resources/function/deploy.js"; - -function formatDuration(ms: number): string { - return `${(ms / 1000).toFixed(1)}s`; -} - -export function formatDeployResult( - result: SingleFunctionDeployResult, - log: Logger, -): void { - const label = result.name.padEnd(25); - if (result.status === "deployed") { - const timing = result.durationMs - ? theme.styles.dim(` (${formatDuration(result.durationMs)})`) - : ""; - log.success(`${label} deployed${timing}`); - } else if (result.status === "unchanged") { - log.success(`${label} unchanged`); - } else { - log.error(`${label} error: ${result.error}`); - } -} diff --git a/packages/cli/src/cli/commands/project/deploy.ts b/packages/cli/src/cli/commands/project/deploy.ts index 986c01ffc..9333763d6 100644 --- a/packages/cli/src/cli/commands/project/deploy.ts +++ b/packages/cli/src/cli/commands/project/deploy.ts @@ -5,11 +5,11 @@ import { filterPendingOAuth, promptOAuthFlows, } from "@/cli/commands/connectors/oauth-prompt.js"; -import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; import { maybeBuildBeforeDeploy } from "@/cli/commands/project/site-build.js"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { Base44Command, + formatDeployResult, getConnectorsUrl, getDashboardUrl, theme, @@ -48,8 +48,15 @@ export async function deployAction( }; } - const { project, entities, functions, agents, connectors, authConfig } = - projectData; + const { + project, + entities, + functions, + actors, + agents, + connectors, + authConfig, + } = projectData; // Build summary of what will be deployed const summaryLines: string[] = []; @@ -63,6 +70,11 @@ export async function deployAction( ` - ${functions.length} ${functions.length === 1 ? "function" : "functions"}`, ); } + if (actors.length > 0) { + summaryLines.push( + ` - ${actors.length} ${actors.length === 1 ? "actor" : "actors"}`, + ); + } if (agents.length > 0) { summaryLines.push( ` - ${agents.length} ${agents.length === 1 ? "agent" : "agents"}`, @@ -102,9 +114,11 @@ export async function deployAction( await maybeBuildBeforeDeploy(ctx, project, options.build); - // Deploy resources with per-function progress + // Deploy resources with per-function and per-actor progress let functionCompleted = 0; const functionTotal = functions.length; + let actorCompleted = 0; + const actorTotal = actors.length; const result = await deployAll(projectData, { onVisibilitySet: (level) => { @@ -122,6 +136,18 @@ export async function deployAction( functionCompleted++; formatDeployResult(r, log); }, + onActorStart: (names) => { + const label = names.length === 1 ? names[0] : `${names.length} actors`; + log.step( + theme.styles.dim( + `[${actorCompleted + 1}/${actorTotal}] Deploying ${label}...`, + ), + ); + }, + onActorResult: (r) => { + actorCompleted++; + formatDeployResult(r, log); + }, }); // Handle connector-specific post-deploy flows diff --git a/packages/cli/src/cli/commands/types/generate.ts b/packages/cli/src/cli/commands/types/generate.ts index f74545af1..e06de2b45 100644 --- a/packages/cli/src/cli/commands/types/generate.ts +++ b/packages/cli/src/cli/commands/types/generate.ts @@ -9,7 +9,7 @@ const TYPES_FILE_PATH = "base44/.types/types.d.ts"; async function generateTypesAction({ runTask, }: CLIContext): Promise { - const { entities, functions, agents, connectors, project } = + const { entities, functions, agents, connectors, actors, project } = await readProjectConfig(); await runTask("Generating types", async () => { @@ -19,6 +19,7 @@ async function generateTypesAction({ functions, agents, connectors, + actors, }); }); diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index fe8bc8f6a..c1c64c022 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -1,4 +1,5 @@ import { Command, Option } from "commander"; +import { getActorsCommand } from "@/cli/commands/actors/index.js"; import { getAgentSkillsCommand } from "@/cli/commands/agent-skills/index.js"; import { getAgentsCommand } from "@/cli/commands/agents/index.js"; import { getAuthCommand } from "@/cli/commands/auth/index.js"; @@ -95,6 +96,9 @@ export function createProgram(context: CLIContext): Command { // Register functions commands program.addCommand(getFunctionsCommand()); + // Register actors commands + program.addCommand(getActorsCommand()); + // Register workflows commands program.addCommand(getWorkflowsCommand()); diff --git a/packages/cli/src/cli/utils/deploy-reporting.ts b/packages/cli/src/cli/utils/deploy-reporting.ts new file mode 100644 index 000000000..6f13518bc --- /dev/null +++ b/packages/cli/src/cli/utils/deploy-reporting.ts @@ -0,0 +1,39 @@ +import type { Logger } from "@base44-cli/logger"; +import { theme } from "@/cli/utils/theme.js"; +import type { SingleDeployResult } from "@/core/resources/types.js"; + +function formatDuration(ms: number): string { + return `${(ms / 1000).toFixed(1)}s`; +} + +export function formatDeployResult( + result: SingleDeployResult, + log: Logger, +): void { + const label = result.name.padEnd(25); + if (result.status === "deployed") { + const timing = result.durationMs + ? theme.styles.dim(` (${formatDuration(result.durationMs)})`) + : ""; + log.success(`${label} deployed${timing}`); + } else if (result.status === "unchanged") { + log.success(`${label} unchanged`); + } else { + log.error(`${label} error: ${result.error}`); + } +} + +export function buildDeploySummary( + results: SingleDeployResult[], + noun: "functions" | "actors", +): string { + const deployed = results.filter((r) => r.status === "deployed").length; + const unchanged = results.filter((r) => r.status === "unchanged").length; + const failed = results.filter((r) => r.status === "error").length; + + const parts: string[] = []; + if (deployed > 0) parts.push(`${deployed} deployed`); + if (unchanged > 0) parts.push(`${unchanged} unchanged`); + if (failed > 0) parts.push(`${failed} error${failed !== 1 ? "s" : ""}`); + return parts.join(", ") || `No ${noun} deployed`; +} diff --git a/packages/cli/src/cli/utils/index.ts b/packages/cli/src/cli/utils/index.ts index f8313a547..43253966d 100644 --- a/packages/cli/src/cli/utils/index.ts +++ b/packages/cli/src/cli/utils/index.ts @@ -3,7 +3,9 @@ export * from "./banner.js"; export * from "./command/index.js"; export * from "./confirm-push.js"; export * from "./datetime.js"; +export * from "./deploy-reporting.js"; export * from "./json.js"; +export * from "./parse-names.js"; export * from "./prompts.js"; export * from "./runTask.js"; export * from "./secret-input.js"; diff --git a/packages/cli/src/cli/commands/functions/parseNames.ts b/packages/cli/src/cli/utils/parse-names.ts similarity index 100% rename from packages/cli/src/cli/commands/functions/parseNames.ts rename to packages/cli/src/cli/utils/parse-names.ts diff --git a/packages/cli/src/core/consts.ts b/packages/cli/src/core/consts.ts index ed326e2bf..abefd699f 100644 --- a/packages/cli/src/core/consts.ts +++ b/packages/cli/src/core/consts.ts @@ -6,12 +6,12 @@ export const CONFIG_FILE_EXTENSION_GLOB = "{json,jsonc}"; /** Glob for discovering function config files at any depth under functions dir. */ export const FUNCTION_CONFIG_GLOB = `**/function.${CONFIG_FILE_EXTENSION_GLOB}`; -/** Glob for zero-config function entry files (any depth). */ +/** Glob for zero-config function and actor entry files (any depth). */ export const ENTRY_FILE_GLOB = "**/entry.{js,ts}"; /** - * Glob for source files bundled into a backend function's deploy payload — - * used for both the function directory and the shared (`base44/shared/`) dir. + * Glob for source files bundled into a function's or actor's deploy payload — + * for functions it also covers the shared (`base44/shared/`) dir. */ export const BACKEND_FILE_GLOB = "**/*.{js,ts,json,jsonc}"; diff --git a/packages/cli/src/core/project/config.ts b/packages/cli/src/core/project/config.ts index 4f32856c6..c27ec9f02 100644 --- a/packages/cli/src/core/project/config.ts +++ b/packages/cli/src/core/project/config.ts @@ -22,6 +22,7 @@ import type { ProjectRoot, ProjectWithPaths, } from "@/core/project/types.js"; +import { actorResource } from "@/core/resources/actor/index.js"; import { agentResource } from "@/core/resources/agent/index.js"; import { agentSkillResource } from "@/core/resources/agent-skill/index.js"; import { authConfigResource } from "@/core/resources/auth-config/index.js"; @@ -72,6 +73,7 @@ class ProjectConfigReader { project, entities, functions, + actors: localResources.actors, agents: localResources.agents, agentSkills: localResources.agentSkills, connectors: localResources.connectors, @@ -118,17 +120,33 @@ class ProjectConfigReader { project: ProjectConfig, ): Promise { const configDir = dirname(configPath); - const [entities, functions, agents, agentSkills, connectors, authConfig] = - await Promise.all([ - entityResource.readAll(join(configDir, project.entitiesDir)), - functionResource.readAll(join(configDir, project.functionsDir)), - agentResource.readAll(join(configDir, project.agentsDir)), - agentSkillResource.readAll(join(configDir, project.agentSkillsDir)), - connectorResource.readAll(join(configDir, project.connectorsDir)), - authConfigResource.readAll(join(configDir, project.authDir)), - ]); - - return { entities, functions, agents, agentSkills, connectors, authConfig }; + const [ + entities, + functions, + actors, + agents, + agentSkills, + connectors, + authConfig, + ] = await Promise.all([ + entityResource.readAll(join(configDir, project.entitiesDir)), + functionResource.readAll(join(configDir, project.functionsDir)), + actorResource.readAll(join(configDir, project.actorsDir)), + agentResource.readAll(join(configDir, project.agentsDir)), + agentSkillResource.readAll(join(configDir, project.agentSkillsDir)), + connectorResource.readAll(join(configDir, project.connectorsDir)), + authConfigResource.readAll(join(configDir, project.authDir)), + ]); + + return { + entities, + functions, + actors, + agents, + agentSkills, + connectors, + authConfig, + }; } private assertPluginProjectDoesNotLoadPlugins( @@ -198,6 +216,7 @@ class ProjectConfigReader { return { entities: markPluginEntities(resources.entities, namespace), functions: namespacePluginFunctions(resources.functions, namespace), + actors: [], agents: [], agentSkills: [], connectors: [], @@ -255,6 +274,7 @@ class ProjectConfigReader { return { entities, functions, + actors: [], agents: [], agentSkills: [], connectors: [], diff --git a/packages/cli/src/core/project/deploy.ts b/packages/cli/src/core/project/deploy.ts index 99ff0ef95..b474dda42 100644 --- a/packages/cli/src/core/project/deploy.ts +++ b/packages/cli/src/core/project/deploy.ts @@ -3,6 +3,10 @@ import { hasWorkspaceApiKeyAuth } from "@/core/auth/config.js"; import { setAppVisibility } from "@/core/project/api.js"; import type { Visibility } from "@/core/project/schema.js"; import type { ProjectData } from "@/core/project/types.js"; +import { + deployActorsSequentially, + type SingleActorDeployResult, +} from "@/core/resources/actor/deploy.js"; import { agentResource } from "@/core/resources/agent/index.js"; import { agentSkillResource } from "@/core/resources/agent-skill/index.js"; import { authConfigResource } from "@/core/resources/auth-config/index.js"; @@ -28,6 +32,7 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean { project, entities, functions, + actors, agents, agentSkills, connectors, @@ -36,6 +41,7 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean { const hasSite = Boolean(project.site?.outputDirectory); const hasEntities = entities.length > 0; const hasFunctions = functions.length > 0; + const hasActors = actors.length > 0; const hasAgents = agents.length > 0; const hasAgentSkills = agentSkills.length > 0; const hasConnectors = connectors.length > 0; @@ -45,6 +51,7 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean { return ( hasEntities || hasFunctions || + hasActors || hasAgents || hasAgentSkills || hasConnectors || @@ -71,11 +78,13 @@ interface DeployAllResult { interface DeployAllOptions { onFunctionStart?: (names: string[]) => void; onFunctionResult?: (result: SingleFunctionDeployResult) => void; + onActorStart?: (names: string[]) => void; + onActorResult?: (result: SingleActorDeployResult) => void; onVisibilitySet?: (visibility: Visibility) => void; } /** - * Deploys all project resources (entities, functions, agents, connectors, and site) to Base44. + * Deploys all project resources (entities, functions, actors, agents, connectors, and site) to Base44. * * @param projectData - The project configuration and resources to deploy * @param options - Optional progress callbacks for resource deployment @@ -89,6 +98,7 @@ export async function deployAll( project, entities, functions, + actors, agents, agentSkills, connectors, @@ -104,6 +114,10 @@ export async function deployAll( onStart: options?.onFunctionStart, onResult: options?.onFunctionResult, }); + await deployActorsSequentially(actors, { + onStart: options?.onActorStart, + onResult: options?.onActorResult, + }); await agentSkillResource.push(agentSkills); await agentResource.push(agents); await authConfigResource.push(authConfig); diff --git a/packages/cli/src/core/project/schema.ts b/packages/cli/src/core/project/schema.ts index 6d4412f3b..42041acfb 100644 --- a/packages/cli/src/core/project/schema.ts +++ b/packages/cli/src/core/project/schema.ts @@ -49,6 +49,7 @@ export const ProjectConfigSchema = z.object({ site: SiteConfigSchema.optional(), entitiesDir: z.string().optional().default("entities"), functionsDir: z.string().optional().default("functions"), + actorsDir: z.string().optional().default("actors"), agentsDir: z.string().optional().default("agents"), agentSkillsDir: z.string().optional().default("agent-skills"), connectorsDir: z.string().optional().default("connectors"), diff --git a/packages/cli/src/core/project/types.ts b/packages/cli/src/core/project/types.ts index b69b14682..f25f4c5d3 100644 --- a/packages/cli/src/core/project/types.ts +++ b/packages/cli/src/core/project/types.ts @@ -1,4 +1,5 @@ import type { ProjectConfig } from "@/core/project/schema.js"; +import type { Actor } from "@/core/resources/actor/index.js"; import type { AgentConfig } from "@/core/resources/agent/index.js"; import type { AgentSkill } from "@/core/resources/agent-skill/index.js"; import type { AuthConfig } from "@/core/resources/auth-config/index.js"; @@ -20,6 +21,7 @@ export interface ProjectData { project: ProjectWithPaths; entities: Entity[]; functions: BackendFunction[]; + actors: Actor[]; agents: AgentConfig[]; agentSkills: AgentSkill[]; connectors: ConnectorResource[]; diff --git a/packages/cli/src/core/resources/actor/api.ts b/packages/cli/src/core/resources/actor/api.ts new file mode 100644 index 000000000..83e3d760b --- /dev/null +++ b/packages/cli/src/core/resources/actor/api.ts @@ -0,0 +1,32 @@ +import type { KyResponse } from "ky"; +import { getAppClient } from "@/core/clients/index.js"; +import { ApiError, SchemaValidationError } from "@/core/errors.js"; +import type { DeployActorResponse } from "@/core/resources/actor/schema.js"; +import { DeployActorResponseSchema } from "@/core/resources/actor/schema.js"; +import type { FunctionFile } from "@/core/resources/function/schema.js"; + +export async function deploySingleActor( + name: string, + payload: { entry: string; files: FunctionFile[] }, +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.put(`actors/${encodeURIComponent(name)}`, { + json: payload, + timeout: false, + }); + } catch (error) { + throw await ApiError.fromHttpError(error, `deploying actor "${name}"`); + } + + const result = DeployActorResponseSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data; +} diff --git a/packages/cli/src/core/resources/actor/config.ts b/packages/cli/src/core/resources/actor/config.ts new file mode 100644 index 000000000..c35271cc9 --- /dev/null +++ b/packages/cli/src/core/resources/actor/config.ts @@ -0,0 +1,68 @@ +import { basename, dirname, relative } from "node:path"; +import { globby } from "globby"; +import { + BACKEND_FILE_GLOB, + ENTRY_FILE_GLOB, + ENTRY_IGNORE_DOT_PATHS, +} from "@/core/consts.js"; +import { ConfigInvalidError, InvalidInputError } from "@/core/errors.js"; +import type { Actor } from "@/core/resources/actor/schema.js"; +import { pathExists } from "@/core/utils/fs.js"; + +async function readActor(entryFile: string, actorsDir: string): Promise { + const actorDir = dirname(entryFile); + const filePaths = await globby(BACKEND_FILE_GLOB, { + cwd: actorDir, + absolute: true, + }); + + const name = relative(actorsDir, actorDir).split(/[/\\]/).join("/"); + if (!name) { + throw new InvalidInputError( + "entry.ts found directly in the actors directory — it must be inside a named subfolder", + { + hints: [ + { + message: `Move ${entryFile} into a subfolder (e.g. actors/MyActor/entry.ts)`, + }, + ], + }, + ); + } + + return { + name, + entry: basename(entryFile), + entryPath: entryFile, + filePaths, + source: { type: "project" }, + }; +} + +export async function readAllActors(actorsDir: string): Promise { + if (!(await pathExists(actorsDir))) { + return []; + } + + const entryFiles = await globby(ENTRY_FILE_GLOB, { + cwd: actorsDir, + absolute: true, + ignore: ENTRY_IGNORE_DOT_PATHS, + }); + + const actors = await Promise.all( + entryFiles.map((entryFile) => readActor(entryFile, actorsDir)), + ); + + const names = new Set(); + for (const actor of actors) { + if (names.has(actor.name)) { + throw new ConfigInvalidError( + `Duplicate actor name "${actor.name}" in ${actorsDir}`, + ); + } + names.add(actor.name); + } + + return actors; +} diff --git a/packages/cli/src/core/resources/actor/deploy.ts b/packages/cli/src/core/resources/actor/deploy.ts new file mode 100644 index 000000000..9ddf6ecf4 --- /dev/null +++ b/packages/cli/src/core/resources/actor/deploy.ts @@ -0,0 +1,63 @@ +import { dirname, relative } from "node:path"; +import { deploySingleActor } from "@/core/resources/actor/api.js"; +import type { Actor } from "@/core/resources/actor/schema.js"; +import type { FunctionFile } from "@/core/resources/function/schema.js"; +import type { SingleDeployResult } from "@/core/resources/types.js"; +import { readTextFile } from "@/core/utils/fs.js"; + +async function loadActorCode( + actor: Actor, +): Promise<{ name: string; entry: string; files: FunctionFile[] }> { + const actorDir = dirname(actor.entryPath); + const resolvedFiles: FunctionFile[] = await Promise.all( + actor.filePaths.map(async (filePath) => { + const content = await readTextFile(filePath); + const path = relative(actorDir, filePath).split(/[/\\]/).join("/"); + return { path, content }; + }), + ); + return { name: actor.name, entry: actor.entry, files: resolvedFiles }; +} + +export type SingleActorDeployResult = SingleDeployResult; + +async function deployOne(actor: Actor): Promise { + const start = Date.now(); + try { + const loaded = await loadActorCode(actor); + const response = await deploySingleActor(loaded.name, { + entry: loaded.entry, + files: loaded.files, + }); + return { + name: loaded.name, + status: response.status, + durationMs: Date.now() - start, + }; + } catch (error) { + return { + name: actor.name, + status: "error", + error: error instanceof Error ? error.message : String(error), + }; + } +} + +export async function deployActorsSequentially( + actors: Actor[], + options?: { + onStart?: (names: string[]) => void; + onResult?: (result: SingleActorDeployResult) => void; + }, +): Promise { + if (actors.length === 0) return []; + + const results: SingleActorDeployResult[] = []; + for (const actor of actors) { + options?.onStart?.([actor.name]); + const result = await deployOne(actor); + results.push(result); + options?.onResult?.(result); + } + return results; +} diff --git a/packages/cli/src/core/resources/actor/index.ts b/packages/cli/src/core/resources/actor/index.ts new file mode 100644 index 000000000..90b197a7b --- /dev/null +++ b/packages/cli/src/core/resources/actor/index.ts @@ -0,0 +1,5 @@ +export * from "./api.js"; +export * from "./config.js"; +export * from "./deploy.js"; +export * from "./resource.js"; +export * from "./schema.js"; diff --git a/packages/cli/src/core/resources/actor/resource.ts b/packages/cli/src/core/resources/actor/resource.ts new file mode 100644 index 000000000..60d5a88d0 --- /dev/null +++ b/packages/cli/src/core/resources/actor/resource.ts @@ -0,0 +1,9 @@ +import { readAllActors } from "@/core/resources/actor/config.js"; +import { deployActorsSequentially } from "@/core/resources/actor/deploy.js"; +import type { Actor } from "@/core/resources/actor/schema.js"; +import type { Resource } from "@/core/resources/types.js"; + +export const actorResource: Resource = { + readAll: readAllActors, + push: (actors) => deployActorsSequentially(actors), +}; diff --git a/packages/cli/src/core/resources/actor/schema.ts b/packages/cli/src/core/resources/actor/schema.ts new file mode 100644 index 000000000..3bf86decc --- /dev/null +++ b/packages/cli/src/core/resources/actor/schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; +import { ResourceSourceSchema } from "@/core/resources/types.js"; + +const ActorSchema = z.object({ + name: z.string().min(1), + entry: z.string().min(1), + entryPath: z.string().min(1), + filePaths: z.array(z.string()).min(1), + source: ResourceSourceSchema, +}); + +export const DeployActorResponseSchema = z.object({ + status: z.enum(["deployed", "unchanged"]), +}); + +export type Actor = z.infer; +export type DeployActorResponse = z.infer; diff --git a/packages/cli/src/core/resources/function/deploy.ts b/packages/cli/src/core/resources/function/deploy.ts index c0e3f66bf..fb0434f95 100644 --- a/packages/cli/src/core/resources/function/deploy.ts +++ b/packages/cli/src/core/resources/function/deploy.ts @@ -9,6 +9,7 @@ import type { FunctionFile, FunctionWithCode, } from "@/core/resources/function/schema.js"; +import type { SingleDeployResult } from "@/core/resources/types.js"; import { readTextFile } from "@/core/utils/fs.js"; async function loadFunctionCode( @@ -25,12 +26,7 @@ async function loadFunctionCode( return { ...fn, files: resolvedFiles }; } -export interface SingleFunctionDeployResult { - name: string; - status: "deployed" | "unchanged" | "error"; - error?: string | null; - durationMs?: number; -} +export type SingleFunctionDeployResult = SingleDeployResult; async function deployOne( fn: BackendFunction, diff --git a/packages/cli/src/core/resources/index.ts b/packages/cli/src/core/resources/index.ts index a8b80eaff..74bc62530 100644 --- a/packages/cli/src/core/resources/index.ts +++ b/packages/cli/src/core/resources/index.ts @@ -1,3 +1,4 @@ +export * from "./actor/index.js"; export * from "./agent/index.js"; export * from "./auth-config/index.js"; export * from "./connector/index.js"; diff --git a/packages/cli/src/core/resources/types.ts b/packages/cli/src/core/resources/types.ts index 25c4596af..a8ad75da7 100644 --- a/packages/cli/src/core/resources/types.ts +++ b/packages/cli/src/core/resources/types.ts @@ -10,6 +10,16 @@ export const ResourceSourceSchema = z.discriminatedUnion("type", [ }), ]); +/** + * Per-item outcome of a sequential deploy (functions, actors). + */ +export interface SingleDeployResult { + name: string; + status: "deployed" | "unchanged" | "error"; + error?: string | null; + durationMs?: number; +} + /** * Base interface for all project resources (entities, functions, etc.). * Resources are project-specific collections that can be loaded from the filesystem diff --git a/packages/cli/src/core/types/generator.ts b/packages/cli/src/core/types/generator.ts index 6297f9c1b..e3a5094bf 100644 --- a/packages/cli/src/core/types/generator.ts +++ b/packages/cli/src/core/types/generator.ts @@ -3,6 +3,7 @@ import type { JSONSchema4 } from "json-schema"; import { compile } from "json-schema-to-typescript"; import { getTypesOutputPath } from "@/core/config.js"; import { TypeGenerationError } from "@/core/errors.js"; +import type { Actor } from "@/core/resources/actor/index.js"; import type { AgentConfig } from "@/core/resources/agent/index.js"; import type { ConnectorResource } from "@/core/resources/connector/index.js"; import type { Entity } from "@/core/resources/entity/index.js"; @@ -15,6 +16,7 @@ interface GenerateTypesInput { functions: BackendFunction[]; agents: AgentConfig[]; connectors: ConnectorResource[]; + actors: Actor[]; } const HEADER = stripIndent` @@ -26,8 +28,8 @@ const EMPTY_TEMPLATE = stripIndent` // Auto-generated by Base44 CLI - DO NOT EDIT // Regenerate with: base44 types // - // No entities, functions, agents, or connectors found in project. - // Add resources to base44/entities/, base44/functions/, base44/agents/, or base44/connectors/ + // No entities, functions, agents, connectors, or actors found in project. + // Add resources to base44/entities/, base44/functions/, base44/agents/, base44/connectors/, or base44/actors/ // and run \`base44 types generate\` again. declare module '@base44/sdk' { @@ -46,13 +48,14 @@ export async function generateTypesFile( } async function generateContent(input: GenerateTypesInput): Promise { - const { entities, functions, agents, connectors } = input; + const { entities, functions, agents, connectors, actors } = input; if ( !entities.length && !functions.length && !agents.length && - !connectors.length + !connectors.length && + !actors.length ) { return EMPTY_TEMPLATE; } @@ -70,6 +73,7 @@ async function generateContent(input: GenerateTypesInput): Promise { ["FunctionNameRegistry", functions.map((f) => `"${f.name}": true;`)], ["AgentNameRegistry", agents.map((a) => `"${a.name}": true;`)], ["ConnectorTypeRegistry", connectors.map((c) => `"${c.type}": true;`)], + ["ActorNameRegistry", actors.map((a) => `"${a.name}": true;`)], ]; // Generate registries (only for non-empty entries) diff --git a/packages/cli/tests/cli/actors_deploy.spec.ts b/packages/cli/tests/cli/actors_deploy.spec.ts new file mode 100644 index 000000000..de2f35047 --- /dev/null +++ b/packages/cli/tests/cli/actors_deploy.spec.ts @@ -0,0 +1,90 @@ +import { describe, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("actors deploy command", () => { + const t = setupCLITests(); + + it("warns when no actors found in project", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + + const result = await t.run("actors", "deploy"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("No actors found"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("actors", "deploy"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("No Base44 app ID found"); + }); + + it("deploys actors successfully", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeploy({ status: "deployed" }); + + const result = await t.run("actors", "deploy"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Deploying ChatRoom"); + t.expectResult(result).toContain("1 deployed"); + }); + + it("reports unchanged actor", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeploy({ status: "unchanged" }); + + const result = await t.run("actors", "deploy"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("unchanged"); + t.expectResult(result).toContain("1 unchanged"); + }); + + it("deploys specific actor by name", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeploy({ status: "deployed" }); + + const result = await t.run("actors", "deploy", "ChatRoom"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Deploying ChatRoom"); + t.expectResult(result).toContain("1 deployed"); + }); + + it("accepts comma-separated actor names", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeploy({ status: "deployed" }); + + const result = await t.run("actors", "deploy", "ChatRoom,"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("1 deployed"); + }); + + it("fails when actor name not found in project", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + + const result = await t.run("actors", "deploy", "nonexistent"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("not found in project"); + }); + + it("reports error when API fails for an actor", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeployError({ + status: 400, + body: { error: "Invalid actor code" }, + }); + + const result = await t.run("actors", "deploy"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("error"); + t.expectResult(result).toContain("1 error"); + }); +}); diff --git a/packages/cli/tests/cli/deploy.spec.ts b/packages/cli/tests/cli/deploy.spec.ts index c32ed161f..1f782c451 100644 --- a/packages/cli/tests/cli/deploy.spec.ts +++ b/packages/cli/tests/cli/deploy.spec.ts @@ -96,6 +96,22 @@ describe("deploy command (unified)", () => { t.expectResult(result).toContain("App deployed successfully"); }); + it("deploys actors with unified deploy", async () => { + await t.givenLoggedInWithProject(fixture("with-actors")); + t.api.mockSingleActorDeploy({ status: "deployed" }); + t.api.mockAgentsPush({ created: [], updated: [], deleted: [] }); + t.api.mockConnectorsList({ integrations: [] }); + t.api.mockStripeStatus({ stripe_mode: null }); + + const result = await t.run("deploy", "-y"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("1 actor"); + t.expectResult(result).toContain("Deploying ChatRoom"); + t.expectResult(result).toContain("deployed"); + t.expectResult(result).toContain("App deployed successfully"); + }); + it("deploys entities successfully with --yes flag", async () => { await t.givenLoggedInWithProject(fixture("with-entities")); t.api.mockEntitiesPush({ diff --git a/packages/cli/tests/cli/testkit/TestAPIServer.ts b/packages/cli/tests/cli/testkit/TestAPIServer.ts index 17d7f6ac6..62eae15dd 100644 --- a/packages/cli/tests/cli/testkit/TestAPIServer.ts +++ b/packages/cli/tests/cli/testkit/TestAPIServer.ts @@ -115,6 +115,10 @@ interface SingleFunctionDeployResponse { status: "deployed" | "unchanged"; } +interface SingleActorDeployResponse { + status: "deployed" | "unchanged"; +} + interface AutomationBase { name: string; description?: string | null; @@ -527,6 +531,15 @@ export class TestAPIServer { ); } + /** Mock PUT /api/apps/{appId}/actors/{name} - Deploy single actor */ + mockSingleActorDeploy(response: SingleActorDeployResponse): this { + return this.addRoute( + "PUT", + `/api/apps/${this.appId}/actors/:name`, + response, + ); + } + mockSiteDeploy(response: SiteDeployResponse): this { return this.addRoute( "POST", @@ -923,6 +936,15 @@ export class TestAPIServer { ); } + /** Mock single actor deploy to return an error */ + mockSingleActorDeployError(error: ErrorResponse): this { + return this.addErrorRoute( + "PUT", + `/api/apps/${this.appId}/actors/:name`, + error, + ); + } + /** Mock single function delete to return an error */ mockSingleFunctionDeleteError(error: ErrorResponse): this { return this.addErrorRoute( diff --git a/packages/cli/tests/cli/types_generate.spec.ts b/packages/cli/tests/cli/types_generate.spec.ts index 9232d9e13..7338d42a3 100644 --- a/packages/cli/tests/cli/types_generate.spec.ts +++ b/packages/cli/tests/cli/types_generate.spec.ts @@ -45,6 +45,10 @@ describe("types generate command", () => { // Contains the ConnectorTypeRegistry with the connector type expect(typesContent).toContain("ConnectorTypeRegistry"); expect(typesContent).toContain(`"slack": true`); + + // Contains the ActorNameRegistry with the actor name + expect(typesContent).toContain("ActorNameRegistry"); + expect(typesContent).toContain(`"ChatRoom": true`); }); it("updates tsconfig.json to include types path", async () => { @@ -103,7 +107,7 @@ describe("types generate command", () => { const typesContent = await t.readProjectFile("base44/.types/types.d.ts"); expect(typesContent).not.toBeNull(); expect(typesContent).toContain( - "No entities, functions, agents, or connectors found", + "No entities, functions, agents, connectors, or actors found", ); }); diff --git a/packages/cli/tests/fixtures/with-actors/base44/.app.jsonc b/packages/cli/tests/fixtures/with-actors/base44/.app.jsonc new file mode 100644 index 000000000..e1fbc58f2 --- /dev/null +++ b/packages/cli/tests/fixtures/with-actors/base44/.app.jsonc @@ -0,0 +1,4 @@ +// Base44 App Configuration +{ + "id": "test-app-id" +} diff --git a/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/entry.ts b/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/entry.ts new file mode 100644 index 000000000..d71c001ae --- /dev/null +++ b/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/entry.ts @@ -0,0 +1,11 @@ +import { Actor, type Conn } from "@base44/sdk"; +import { formatMessage } from "./helper.js"; + +export default class ChatRoom extends Actor { + handleConnect(_conn: Conn) {} + handleMessage(conn: Conn, msg: unknown) { + conn.send(formatMessage(msg)); + } + handleTick() {} + handleClose(_conn: Conn) {} +} diff --git a/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/helper.ts b/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/helper.ts new file mode 100644 index 000000000..aa63324fe --- /dev/null +++ b/packages/cli/tests/fixtures/with-actors/base44/actors/ChatRoom/helper.ts @@ -0,0 +1,3 @@ +export function formatMessage(msg: unknown): string { + return JSON.stringify(msg); +} diff --git a/packages/cli/tests/fixtures/with-actors/base44/config.jsonc b/packages/cli/tests/fixtures/with-actors/base44/config.jsonc new file mode 100644 index 000000000..701ea3eef --- /dev/null +++ b/packages/cli/tests/fixtures/with-actors/base44/config.jsonc @@ -0,0 +1,3 @@ +{ + "name": "Actors Test Project" +} diff --git a/packages/cli/tests/fixtures/with-types-resources/base44/actors/ChatRoom/entry.ts b/packages/cli/tests/fixtures/with-types-resources/base44/actors/ChatRoom/entry.ts new file mode 100644 index 000000000..db5ad7165 --- /dev/null +++ b/packages/cli/tests/fixtures/with-types-resources/base44/actors/ChatRoom/entry.ts @@ -0,0 +1,8 @@ +import { Actor, type Conn } from "@base44/sdk"; + +export class ChatRoom extends Actor { + handleConnect(_conn: Conn) {} + handleMessage(_conn: Conn, _msg: unknown) {} + handleTick() {} + handleClose(_conn: Conn) {} +}