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
111 changes: 111 additions & 0 deletions .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---

Default to using Bun instead of Node.js.

- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.

## APIs

- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.

## Testing

Use `bun test` to run tests.

```ts#index.test.ts
import { test, expect } from "bun:test";

test("hello world", () => {
expect(1).toBe(1);
});
```

## Frontend

Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.

Server:

```ts#index.ts
import index from "./index.html"

Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```

HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.

```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```

With the following `frontend.tsx`:

```tsx#frontend.tsx
import React from "react";

// import .css files directly and it works
import './index.css';

import { createRoot } from "react-dom/client";

const root = createRoot(document.body);

export default function Frontend() {
return <h1>Hello, world!</h1>;
}

root.render(<Frontend />);
```

Then, run index.ts

```sh
bun --hot ./index.ts
```

For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ coverage/
*.seed
*.pid.lock

# Worktrees
.worktrees/

13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# AI Agent Guidelines for Base44 CLI Development

@.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc

This document provides essential context and guidelines for AI agents working on the Base44 CLI project.

**Important**: Keep this file updated when making significant architectural changes.
Expand Down Expand Up @@ -78,6 +80,14 @@ cli/
│ │ │ │ ├── resource.ts
│ │ │ │ ├── api.ts
│ │ │ │ └── index.ts
│ │ │ ├── connector/
│ │ │ │ ├── schema.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── resource.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── push.ts
│ │ │ │ ├── oauth.ts
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── site/ # Site deployment (NOT a Resource)
│ │ │ ├── schema.ts # DeployResponse Zod schema
Expand Down Expand Up @@ -118,6 +128,9 @@ cli/
│ │ │ ├── index.ts # getAgentsCommand(context) - parent command
│ │ │ ├── pull.ts
│ │ │ └── push.ts
│ │ ├── connectors/
│ │ │ ├── index.ts # getConnectorsCommand(context) - parent command
│ │ │ └── push.ts
│ │ ├── functions/
│ │ │ └── deploy.ts
│ │ ├── site/
Expand Down
21 changes: 9 additions & 12 deletions src/cli/commands/connectors/push.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { confirm, isCancel, log } from "@clack/prompts";
import chalk from "chalk";
import { Command } from "commander";
import type { CLIContext } from "@/cli/types.js";
import { runCommand, runTask } from "@/cli/utils/index.js";
import { runCommand, runTask, theme } from "@/cli/utils/index.js";
import type { RunCommandResult } from "@/cli/utils/runCommand.js";
import { readProjectConfig } from "@/core/index.js";
import {
Expand Down Expand Up @@ -54,19 +53,19 @@ function printSummary(
}

log.info("");
log.info(chalk.bold("Summary:"));
log.info(theme.styles.bold("Summary:"));

if (synced.length > 0) {
log.info(chalk.green(` Synced: ${synced.join(", ")}`));
log.success(`Synced: ${synced.join(", ")}`);
}
if (added.length > 0) {
log.info(chalk.green(` Added: ${added.join(", ")}`));
log.success(`Added: ${added.join(", ")}`);
}
if (removed.length > 0) {
log.info(chalk.dim(` Removed: ${removed.join(", ")}`));
log.info(theme.styles.dim(`Removed: ${removed.join(", ")}`));
}
for (const r of failed) {
log.info(chalk.red(` Failed: ${r.type}${r.error ? ` - ${r.error}` : ""}`));
log.error(`Failed: ${r.type}${r.error ? ` - ${r.error}` : ""}`);
}
}

Expand Down Expand Up @@ -101,13 +100,11 @@ async function pushConnectorsAction(): Promise<RunCommandResult> {

if (needsOAuth.length > 0) {
log.info("");
log.info(
chalk.yellow(
`${needsOAuth.length} connector(s) require authorization in your browser:`
)
log.warn(
`${needsOAuth.length} connector(s) require authorization in your browser:`
);
for (const connector of needsOAuth) {
log.info(` ${connector.type}: ${chalk.dim(connector.redirectUrl)}`);
log.info(` ${connector.type}: ${theme.styles.dim(connector.redirectUrl)}`);
}

const pending = needsOAuth.map((c) => c.type).join(", ");
Expand Down
13 changes: 11 additions & 2 deletions src/core/resources/connector/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { globby } from "globby";
import { SchemaValidationError } from "@/core/errors.js";
import { InvalidInputError, SchemaValidationError } from "@/core/errors.js";
import { CONFIG_FILE_EXTENSION_GLOB } from "../../consts.js";
import { pathExists, readJsonFile } from "../../utils/fs.js";
import type { ConnectorResource } from "./schema.js";
Expand Down Expand Up @@ -45,7 +45,16 @@ export async function readAllConnectors(
const types = new Set<string>();
for (const connector of connectors) {
if (types.has(connector.type)) {
throw new Error(`Duplicate connector type "${connector.type}"`);
throw new InvalidInputError(
`Duplicate connector type "${connector.type}"`,
{
hints: [
{
message: `Remove duplicate connectors with type "${connector.type}" - only one connector per type is allowed`,
},
],
}
);
}
types.add(connector.type);
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/resources/connector/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function setResponseToResult(
action: "error",
error:
response.error_message ||
`Already connected by ${response.other_user_email}`,
`Already connected by ${response.other_user_email ?? "another user"}`,
};
}

Expand Down
25 changes: 23 additions & 2 deletions src/core/resources/connector/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,17 @@ export const TikTokConnectorSchema = z.object({
scopes: z.array(z.string()).default([]),
});

export const ConnectorResourceSchema = z.discriminatedUnion("type", [
/** Generic connector schema for arbitrary providers */
const GenericConnectorSchema = z.object({
type: z.string().min(1).regex(/^[a-z0-9_-]+$/i),
scopes: z.array(z.string()).default([]),
});

/**
* Connector resource schema that accepts both known providers (with specific schemas)
* and arbitrary provider strings (with generic schema).
*/
export const ConnectorResourceSchema = z.union([
GoogleCalendarConnectorSchema,
GoogleDriveConnectorSchema,
GmailConnectorSchema,
Expand All @@ -85,11 +95,13 @@ export const ConnectorResourceSchema = z.discriminatedUnion("type", [
HubspotConnectorSchema,
LinkedInConnectorSchema,
TikTokConnectorSchema,
GenericConnectorSchema,
]);

export type ConnectorResource = z.infer<typeof ConnectorResourceSchema>;

export const IntegrationTypeSchema = z.enum([
/** Known integration types with first-class support */
export const KnownIntegrationTypes = [
"googlecalendar",
"googledrive",
"gmail",
Expand All @@ -102,6 +114,15 @@ export const IntegrationTypeSchema = z.enum([
"hubspot",
"linkedin",
"tiktok",
] as const;

/**
* Integration type schema that accepts both known providers and arbitrary strings.
* This allows users to use custom OAuth providers not yet supported by Base44.
*/
export const IntegrationTypeSchema = z.union([
z.enum(KnownIntegrationTypes),
z.string().min(1).regex(/^[a-z0-9_-]+$/i),
]);

export type IntegrationType = z.infer<typeof IntegrationTypeSchema>;
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/connectors_push.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("connectors push command", () => {
await t.givenLoggedInWithProject(fixture("basic"));
t.api.mockConnectorsList({
integrations: [
{ integration_type: "slack", status: "ACTIVE", scopes: ["chat:write"] },
{ integration_type: "slack", status: "active", scopes: ["chat:write"] },
],
});
t.api.mockConnectorRemove({ status: "removed", integration_type: "slack" });
Expand Down
Loading
Loading