@base44-preview/cli 0.0.1-pr.16.c796b32 → 0.0.1-pr.18.893dad9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,45 +5,140 @@ A unified command-line interface for managing Base44 applications, entities, fun
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- # Using npm
9
- npm install
8
+ # Using npm (globally)
9
+ npm install -g base44
10
10
 
11
- # Build the project
12
- npm run build
11
+ # Or run directly with npx
12
+ npx base44 <command>
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # 1. Login to Base44
19
+ base44 login
20
+
21
+ # 2. Create a new project
22
+ base44 create
23
+
24
+ # 3. Push entities to Base44
25
+ base44 entities push
26
+ ```
27
+
28
+ ## Commands
29
+
30
+ ### Authentication
31
+
32
+ | Command | Description |
33
+ |---------|-------------|
34
+ | `base44 login` | Authenticate with Base44 using device code flow |
35
+ | `base44 whoami` | Display current authenticated user |
36
+ | `base44 logout` | Logout from current device |
37
+
38
+ ### Project Management
39
+
40
+ | Command | Description |
41
+ |---------|-------------|
42
+ | `base44 create` | Create a new Base44 project from a template |
43
+
44
+ ### Entities
45
+
46
+ | Command | Description |
47
+ |---------|-------------|
48
+ | `base44 entities push` | Push local entity schemas to Base44 |
13
49
 
14
- # Run the CLI
15
- npm start # Using node directly
16
- ./dist/cli/index.js # Run executable directly
50
+ ## Configuration
51
+
52
+ ### Project Configuration
53
+
54
+ Base44 projects are configured via a `config.jsonc` (or `config.json`) file in the `base44/` subdirectory:
55
+
56
+ ```jsonc
57
+ // base44/config.jsonc
58
+ {
59
+ "id": "your-app-id", // Set after project creation
60
+ "name": "My Project",
61
+ "entitiesDir": "./entities", // Default: ./entities
62
+ "functionsDir": "./functions" // Default: ./functions
63
+ }
64
+ ```
65
+
66
+ ### Environment Variables
67
+
68
+ | Variable | Description | Default |
69
+ |----------|-------------|---------|
70
+ | `BASE44_CLIENT_ID` | Your app ID | - |
71
+
72
+ You can set these in a `.env.local` file in your `base44/` directory:
73
+
74
+ ```bash
75
+ # base44/.env.local
76
+ BASE44_CLIENT_ID=your-app-id
77
+ ```
78
+
79
+ ## Project Structure
80
+
81
+ A typical Base44 project has this structure:
82
+
83
+ ```
84
+ my-project/
85
+ ├── base44/
86
+ │ ├── config.jsonc # Project configuration
87
+ │ ├── .env.local # Environment variables (git-ignored)
88
+ │ ├── entities/ # Entity schema files
89
+ │ │ ├── user.jsonc
90
+ │ │ └── product.jsonc
91
+ ├── src/ # Your frontend code
92
+ └── package.json
17
93
  ```
18
94
 
19
95
  ## Development
20
96
 
97
+ ### Prerequisites
98
+
99
+ - Node.js >= 20.19.0
100
+ - npm
101
+
102
+ ### Setup
103
+
21
104
  ```bash
22
- # Run in development mode
23
- npm run dev
105
+ # Clone the repository
106
+ git clone https://github.com/base44/cli.git
107
+ cd cli
24
108
 
25
- # Build the project
26
- npm run build
109
+ # Install dependencies
110
+ npm install
27
111
 
28
- # Run the built CLI
29
- npm run start
112
+ # Build
113
+ npm run build
30
114
 
31
- # Clean build artifacts
32
- npm run clean
115
+ # Run in development mode
116
+ npm run dev -- <command>
117
+ ```
33
118
 
34
- # Lint the code
35
- npm run lint
119
+ ### Available Scripts
36
120
 
121
+ ```bash
122
+ npm run build # Build with tsdown
123
+ npm run typecheck # Type check with tsc
124
+ npm run dev # Run in development mode with tsx
125
+ npm run lint # Lint with ESLint
126
+ npm test # Run tests with Vitest
37
127
  ```
38
128
 
39
- ## Commands
129
+ ### Running the Built CLI
40
130
 
41
- ### Authentication
131
+ ```bash
132
+ # After building
133
+ npm start -- <command>
134
+
135
+ # Or directly
136
+ ./dist/cli/index.js <command>
137
+ ```
138
+ ## Contributing
42
139
 
43
- - `base44 login` - Authenticate with Base44 using device code flow
44
- - `base44 whoami` - Display current authenticated user
45
- - `base44 logout` - Logout from current device
140
+ See [AGENTS.md](./AGENTS.md) for development guidelines and architecture documentation.
46
141
 
47
- ### Project
142
+ ## License
48
143
 
49
- - `base44 show-project` - Display project configuration, entities, and functions
144
+ ISC
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import { cancel, group, intro, log, select, spinner, text } from "@clack/prompts";
5
5
  import pWaitFor from "p-wait-for";
6
6
  import { z } from "zod";
7
+ import ky from "ky";
7
8
  import { dirname, join, resolve } from "node:path";
8
9
  import { homedir } from "node:os";
9
10
  import { fileURLToPath } from "node:url";
@@ -11,7 +12,6 @@ import { config } from "dotenv";
11
12
  import { globby } from "globby";
12
13
  import { access, copyFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
13
14
  import { parse, printParseErrorCode } from "jsonc-parser";
14
- import ky from "ky";
15
15
  import ejs from "ejs";
16
16
  import kebabCase from "lodash.kebabcase";
17
17
 
@@ -76,6 +76,20 @@ var AuthValidationError = class extends Error {
76
76
  }
77
77
  };
78
78
 
79
+ //#endregion
80
+ //#region src/core/consts.ts
81
+ const PROJECT_SUBDIR = "base44";
82
+ const FUNCTION_CONFIG_FILE = "function.jsonc";
83
+ function getProjectConfigPatterns() {
84
+ return [
85
+ `${PROJECT_SUBDIR}/config.jsonc`,
86
+ `${PROJECT_SUBDIR}/config.json`,
87
+ "config.jsonc",
88
+ "config.json"
89
+ ];
90
+ }
91
+ const AUTH_CLIENT_ID = "base44_cli";
92
+
79
93
  //#endregion
80
94
  //#region src/core/utils/fs.ts
81
95
  async function pathExists(path) {
@@ -124,35 +138,7 @@ async function deleteFile(filePath) {
124
138
 
125
139
  //#endregion
126
140
  //#region src/core/resources/entity/schema.ts
127
- const EntityPropertySchema = z.object({
128
- type: z.string(),
129
- description: z.string().optional(),
130
- enum: z.array(z.string()).optional(),
131
- default: z.union([
132
- z.string(),
133
- z.number(),
134
- z.boolean()
135
- ]).optional(),
136
- format: z.string().optional(),
137
- items: z.any().optional(),
138
- relation: z.object({
139
- entity: z.string(),
140
- type: z.string()
141
- }).optional()
142
- });
143
- const EntityPoliciesSchema = z.object({
144
- read: z.string().optional(),
145
- create: z.string().optional(),
146
- update: z.string().optional(),
147
- delete: z.string().optional()
148
- });
149
- const EntitySchema = z.object({
150
- name: z.string().min(1, "Entity name cannot be empty"),
151
- type: z.literal("object"),
152
- properties: z.record(z.string(), EntityPropertySchema),
153
- required: z.array(z.string()).optional(),
154
- policies: EntityPoliciesSchema.optional()
155
- });
141
+ const EntitySchema = z.object({ name: z.string().min(1, "Entity name cannot be empty") });
156
142
  const SyncEntitiesResponseSchema = z.object({
157
143
  created: z.array(z.string()),
158
144
  updated: z.array(z.string()),
@@ -176,114 +162,6 @@ async function readAllEntities(entitiesDir) {
176
162
  return await Promise.all(files.map((filePath) => readEntityFile(filePath)));
177
163
  }
178
164
 
179
- //#endregion
180
- //#region src/core/auth/config.ts
181
- const TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
182
- let refreshPromise = null;
183
- async function readAuth() {
184
- try {
185
- const parsed = await readJsonFile(getAuthFilePath());
186
- const result = AuthDataSchema.safeParse(parsed);
187
- if (!result.success) throw new Error(`Invalid authentication data: ${result.error.issues.map((e) => e.message).join(", ")}`);
188
- return result.data;
189
- } catch (error) {
190
- if (error instanceof Error && error.message.includes("Authentication")) throw error;
191
- if (error instanceof Error && error.message.includes("File not found")) throw new Error("Authentication file not found. Please login first.");
192
- throw new Error(`Failed to read authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
193
- }
194
- }
195
- async function writeAuth(authData) {
196
- const result = AuthDataSchema.safeParse(authData);
197
- if (!result.success) throw new Error(`Invalid authentication data: ${result.error.issues.map((e) => e.message).join(", ")}`);
198
- try {
199
- await writeJsonFile(getAuthFilePath(), result.data);
200
- } catch (error) {
201
- throw new Error(`Failed to write authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
202
- }
203
- }
204
- async function deleteAuth() {
205
- try {
206
- await deleteFile(getAuthFilePath());
207
- } catch (error) {
208
- throw new Error(`Failed to delete authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
209
- }
210
- }
211
- /**
212
- * Checks if the access token is expired or about to expire.
213
- */
214
- function isTokenExpired(auth) {
215
- return Date.now() >= auth.expiresAt - TOKEN_REFRESH_BUFFER_MS;
216
- }
217
- /**
218
- * Refreshes the access token and saves the new tokens.
219
- * Returns the new access token, or null if refresh failed.
220
- * Uses a lock to prevent concurrent refresh requests.
221
- */
222
- async function refreshAndSaveTokens() {
223
- if (refreshPromise) return refreshPromise;
224
- refreshPromise = (async () => {
225
- try {
226
- const auth = await readAuth();
227
- const tokenResponse = await renewAccessToken(auth.refreshToken);
228
- await writeAuth({
229
- ...auth,
230
- accessToken: tokenResponse.accessToken,
231
- refreshToken: tokenResponse.refreshToken,
232
- expiresAt: Date.now() + tokenResponse.expiresIn * 1e3
233
- });
234
- return tokenResponse.accessToken;
235
- } catch {
236
- await deleteAuth();
237
- return null;
238
- } finally {
239
- refreshPromise = null;
240
- }
241
- })();
242
- return refreshPromise;
243
- }
244
-
245
- //#endregion
246
- //#region src/core/utils/httpClient.ts
247
- const retriedRequests = /* @__PURE__ */ new WeakSet();
248
- /**
249
- * Handles 401 responses by refreshing the token and retrying the request.
250
- * Only retries once per request to prevent infinite loops.
251
- */
252
- async function handleUnauthorized(request, _options, response) {
253
- if (response.status !== 401) return;
254
- if (retriedRequests.has(request)) return;
255
- const newAccessToken = await refreshAndSaveTokens();
256
- if (!newAccessToken) return;
257
- retriedRequests.add(request);
258
- return ky(request, { headers: { Authorization: `Bearer ${newAccessToken}` } });
259
- }
260
- const base44Client = ky.create({
261
- prefixUrl: getBase44ApiUrl(),
262
- headers: { "User-Agent": "Base44 CLI" },
263
- hooks: {
264
- beforeRequest: [async (request) => {
265
- try {
266
- const auth = await readAuth();
267
- if (isTokenExpired(auth)) {
268
- const newAccessToken = await refreshAndSaveTokens();
269
- if (newAccessToken) {
270
- request.headers.set("Authorization", `Bearer ${newAccessToken}`);
271
- return;
272
- }
273
- }
274
- request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
275
- } catch {}
276
- }],
277
- afterResponse: [handleUnauthorized]
278
- }
279
- });
280
- /**
281
- * Returns an HTTP client scoped to the current app.
282
- */
283
- function getAppClient() {
284
- return base44Client.extend({ prefixUrl: new URL(`/api/apps/${getBase44ClientId()}/`, getBase44ApiUrl()).href });
285
- }
286
-
287
165
  //#endregion
288
166
  //#region src/core/resources/entity/api.ts
289
167
  async function pushEntities(entities) {
@@ -367,18 +245,50 @@ async function readAllFunctions(functionsDir) {
367
245
  const functionResource = { readAll: readAllFunctions };
368
246
 
369
247
  //#endregion
370
- //#region src/core/project/config.ts
371
- const ProjectConfigSchema = z.looseObject({
372
- name: z.string().min(1, "Project name cannot be empty"),
373
- entitiesDir: z.string().default("./entities"),
374
- functionsDir: z.string().default("./functions")
248
+ //#region src/core/project/schema.ts
249
+ const TemplateSchema = z.object({
250
+ id: z.string(),
251
+ name: z.string(),
252
+ description: z.string(),
253
+ path: z.string()
375
254
  });
255
+ const TemplatesConfigSchema = z.object({ templates: z.array(TemplateSchema) });
256
+ const SiteConfigSchema = z.object({
257
+ buildCommand: z.string().optional(),
258
+ serveCommand: z.string().optional(),
259
+ outputDirectory: z.string().optional(),
260
+ installCommand: z.string().optional()
261
+ });
262
+ const ProjectConfigSchema = z.object({
263
+ name: z.string().min(1, "App name cannot be empty"),
264
+ description: z.string().optional(),
265
+ site: SiteConfigSchema.optional(),
266
+ entitiesDir: z.string().optional().default("entities"),
267
+ functionsDir: z.string().optional().default("functions")
268
+ });
269
+ const CreateProjectResponseSchema = z.looseObject({ id: z.string() });
270
+
271
+ //#endregion
272
+ //#region src/core/project/config.ts
376
273
  async function findConfigInDir(dir) {
377
274
  return (await globby(getProjectConfigPatterns(), {
378
275
  cwd: dir,
379
276
  absolute: true
380
277
  }))[0] ?? null;
381
278
  }
279
+ /**
280
+ * Searches for a Base44 project root by looking for config files.
281
+ * Walks up the directory tree from the starting path until it finds a config file.
282
+ *
283
+ * @param startPath - Directory to start searching from. Defaults to cwd.
284
+ * @returns Project root info if found, null otherwise.
285
+ *
286
+ * @example
287
+ * const found = await findProjectRoot();
288
+ * if (found) {
289
+ * console.log(`Project found at: ${found.root}`);
290
+ * }
291
+ */
382
292
  async function findProjectRoot(startPath) {
383
293
  let current = startPath || process.cwd();
384
294
  while (current !== dirname(current)) {
@@ -391,6 +301,17 @@ async function findProjectRoot(startPath) {
391
301
  }
392
302
  return null;
393
303
  }
304
+ /**
305
+ * Reads and validates a Base44 project configuration from the filesystem.
306
+ * Also loads all entities and functions defined in the project.
307
+ *
308
+ * @param projectRoot - Optional path to start searching from. Defaults to cwd.
309
+ * @returns Project configuration including entities and functions.
310
+ * @throws {Error} If no config file is found or if the config is invalid.
311
+ *
312
+ * @example
313
+ * const { project, entities, functions } = await readProjectConfig();
314
+ */
394
315
  async function readProjectConfig(projectRoot) {
395
316
  let found;
396
317
  if (projectRoot) {
@@ -404,10 +325,7 @@ async function readProjectConfig(projectRoot) {
404
325
  const { root, configPath } = found;
405
326
  const parsed = await readJsonFile(configPath);
406
327
  const result = ProjectConfigSchema.safeParse(parsed);
407
- if (!result.success) {
408
- const errors = result.error.issues.map((e) => e.message).join(", ");
409
- throw new Error(`Invalid project configuration: ${errors}`);
410
- }
328
+ if (!result.success) throw new Error(`Invalid project configuration: ${result.error.message}`);
411
329
  const project = result.data;
412
330
  const configDir = dirname(configPath);
413
331
  const [entities, functions] = await Promise.all([entityResource.readAll(join(configDir, project.entitiesDir)), functionResource.readAll(join(configDir, project.functionsDir))]);
@@ -422,29 +340,6 @@ async function readProjectConfig(projectRoot) {
422
340
  };
423
341
  }
424
342
 
425
- //#endregion
426
- //#region src/core/project/schema.ts
427
- const TemplateSchema = z.object({
428
- id: z.string(),
429
- name: z.string(),
430
- description: z.string(),
431
- path: z.string()
432
- });
433
- const TemplatesConfigSchema = z.object({ templates: z.array(TemplateSchema) });
434
- const SiteConfigSchema = z.object({
435
- buildCommand: z.string().optional(),
436
- serveCommand: z.string().optional(),
437
- outputDirectory: z.string().optional(),
438
- installCommand: z.string().optional()
439
- });
440
- const AppConfigSchema = z.object({
441
- name: z.string().min(1, "App name cannot be empty"),
442
- description: z.string().optional(),
443
- site: SiteConfigSchema.optional(),
444
- domains: z.array(z.string()).optional()
445
- });
446
- const CreateProjectResponseSchema = z.looseObject({ id: z.string() });
447
-
448
343
  //#endregion
449
344
  //#region src/core/project/api.ts
450
345
  async function createProject(projectName, description) {
@@ -459,7 +354,7 @@ async function createProject(projectName, description) {
459
354
  //#endregion
460
355
  //#region src/core/project/template.ts
461
356
  async function listTemplates() {
462
- const parsed = await readJsonFile(join(getTemplatesDir(), "templates.json"));
357
+ const parsed = await readJsonFile(getTemplatesIndexPath());
463
358
  return TemplatesConfigSchema.parse(parsed).templates;
464
359
  }
465
360
  /**
@@ -476,8 +371,13 @@ async function renderTemplate(template, destPath, data) {
476
371
  });
477
372
  for (const file of files) {
478
373
  const srcPath = join(templateDir, file);
479
- if (file.endsWith(".ejs")) await writeFile$1(join(destPath, file.slice(0, -4)), await ejs.renderFile(srcPath, data));
480
- else await copyFile$1(srcPath, join(destPath, file));
374
+ try {
375
+ if (file.endsWith(".ejs")) await writeFile$1(join(destPath, file.replace(/\.ejs$/, "")), await ejs.renderFile(srcPath, data));
376
+ else await copyFile$1(srcPath, join(destPath, file));
377
+ } catch (error) {
378
+ const message = error instanceof Error ? error.message : String(error);
379
+ throw new Error(`Failed to process template file "${file}": ${message}`);
380
+ }
481
381
  }
482
382
  }
483
383
 
@@ -502,30 +402,21 @@ async function createProjectFiles(options) {
502
402
  //#endregion
503
403
  //#region src/core/config.ts
504
404
  const __dirname = dirname(fileURLToPath(import.meta.url));
505
- const PROJECT_SUBDIR = "base44";
506
- const FUNCTION_CONFIG_FILE = "function.jsonc";
507
- const AUTH_CLIENT_ID = "base44_cli";
508
- function getBase44Dir() {
405
+ function getBase44GlobalDir() {
509
406
  return join(homedir(), ".base44");
510
407
  }
511
408
  function getAuthFilePath() {
512
- return join(getBase44Dir(), "auth", "auth.json");
409
+ return join(getBase44GlobalDir(), "auth", "auth.json");
513
410
  }
514
411
  function getTemplatesDir() {
515
412
  return join(__dirname, "templates");
516
413
  }
517
- function getProjectConfigPatterns() {
518
- return [
519
- `${PROJECT_SUBDIR}/config.jsonc`,
520
- `${PROJECT_SUBDIR}/config.json`,
521
- "config.jsonc",
522
- "config.json"
523
- ];
414
+ function getTemplatesIndexPath() {
415
+ return join(getTemplatesDir(), "templates.json");
524
416
  }
525
417
  /**
526
418
  * Load .env.local from the project root if it exists.
527
419
  * Values won't override existing process.env variables.
528
- * Safe to call multiple times - only loads once.
529
420
  */
530
421
  async function loadProjectEnv(projectRoot) {
531
422
  const found = projectRoot ? { root: projectRoot } : await findProjectRoot();
@@ -536,38 +427,154 @@ async function loadProjectEnv(projectRoot) {
536
427
  quiet: true
537
428
  });
538
429
  }
539
- /**
540
- * Get the Base44 API URL.
541
- * Priority: process.env.BASE44_API_URL > .env.local > default
542
- */
543
430
  function getBase44ApiUrl() {
544
431
  return process.env.BASE44_API_URL || "https://app.base44.com";
545
432
  }
546
- /**
547
- * Get the Base44 Client ID (app ID).
548
- * Priority: process.env.BASE44_CLIENT_ID > .env.local
549
- * Returns undefined if not set.
550
- */
551
433
  function getBase44ClientId() {
552
434
  return process.env.BASE44_CLIENT_ID;
553
435
  }
554
436
 
555
437
  //#endregion
556
- //#region src/core/auth/authClient.ts
438
+ //#region src/core/clients/oauth-client.ts
557
439
  /**
558
- * Separate ky instance for OAuth endpoints.
559
- * These don't need Authorization headers (they use client_id + tokens in body).
440
+ * HTTP client for OAuth endpoints.
441
+ * Used only for the login flow (device code, token exchange).
442
+ * These endpoints don't need Authorization headers - they use client_id + tokens in body.
560
443
  */
561
- const authClient = ky.create({
444
+ const oauthClient = ky.create({
562
445
  prefixUrl: getBase44ApiUrl(),
563
446
  headers: { "User-Agent": "Base44 CLI" }
564
447
  });
565
- var authClient_default = authClient;
448
+
449
+ //#endregion
450
+ //#region src/core/auth/config.ts
451
+ const TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
452
+ let refreshPromise = null;
453
+ /**
454
+ * Reads and validates the stored authentication data.
455
+ *
456
+ * @returns The parsed authentication data (tokens, user info).
457
+ * @throws {Error} If not logged in or if auth data is corrupted.
458
+ *
459
+ * @example
460
+ * const auth = await readAuth();
461
+ * console.log(`Logged in as: ${auth.email}`);
462
+ */
463
+ async function readAuth() {
464
+ try {
465
+ const parsed = await readJsonFile(getAuthFilePath());
466
+ const result = AuthDataSchema.safeParse(parsed);
467
+ if (!result.success) throw new Error(`Invalid authentication data: ${result.error.issues.map((e) => e.message).join(", ")}`);
468
+ return result.data;
469
+ } catch (error) {
470
+ throw new Error(`Failed to read authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
471
+ }
472
+ }
473
+ async function writeAuth(authData) {
474
+ const result = AuthDataSchema.safeParse(authData);
475
+ if (!result.success) throw new Error(`Invalid authentication data: ${result.error.issues.map((e) => e.message).join(", ")}`);
476
+ try {
477
+ await writeJsonFile(getAuthFilePath(), result.data);
478
+ } catch (error) {
479
+ throw new Error(`Failed to write authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
480
+ }
481
+ }
482
+ async function deleteAuth() {
483
+ try {
484
+ await deleteFile(getAuthFilePath());
485
+ } catch (error) {
486
+ throw new Error(`Failed to delete authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
487
+ }
488
+ }
489
+ function isTokenExpired(auth) {
490
+ return Date.now() >= auth.expiresAt - TOKEN_REFRESH_BUFFER_MS;
491
+ }
492
+ async function refreshAndSaveTokens() {
493
+ if (refreshPromise) return refreshPromise;
494
+ refreshPromise = (async () => {
495
+ try {
496
+ const auth = await readAuth();
497
+ const tokenResponse = await renewAccessToken(auth.refreshToken);
498
+ await writeAuth({
499
+ ...auth,
500
+ accessToken: tokenResponse.accessToken,
501
+ refreshToken: tokenResponse.refreshToken,
502
+ expiresAt: Date.now() + tokenResponse.expiresIn * 1e3
503
+ });
504
+ return tokenResponse.accessToken;
505
+ } catch {
506
+ await deleteAuth();
507
+ return null;
508
+ } finally {
509
+ refreshPromise = null;
510
+ }
511
+ })();
512
+ return refreshPromise;
513
+ }
514
+
515
+ //#endregion
516
+ //#region src/core/clients/base44-client.ts
517
+ /**
518
+ * Authenticated HTTP client for Base44 API.
519
+ * Automatically handles token refresh and retry on 401 responses.
520
+ */
521
+ const retriedRequests = /* @__PURE__ */ new WeakSet();
522
+ /**
523
+ * Handles 401 responses by refreshing the token and retrying the request.
524
+ * Only retries once per request to prevent infinite loops.
525
+ */
526
+ async function handleUnauthorized(request, _options, response) {
527
+ if (response.status !== 401) return;
528
+ if (retriedRequests.has(request)) return;
529
+ const newAccessToken = await refreshAndSaveTokens();
530
+ if (!newAccessToken) return;
531
+ retriedRequests.add(request);
532
+ return ky(request, { headers: { Authorization: `Bearer ${newAccessToken}` } });
533
+ }
534
+ /**
535
+ * Base44 API client with automatic authentication.
536
+ * Use this for general API calls that require authentication.
537
+ */
538
+ const base44Client = ky.create({
539
+ prefixUrl: getBase44ApiUrl(),
540
+ headers: { "User-Agent": "Base44 CLI" },
541
+ hooks: {
542
+ beforeRequest: [async (request) => {
543
+ try {
544
+ const auth = await readAuth();
545
+ if (isTokenExpired(auth)) {
546
+ const newAccessToken = await refreshAndSaveTokens();
547
+ if (newAccessToken) {
548
+ request.headers.set("Authorization", `Bearer ${newAccessToken}`);
549
+ return;
550
+ }
551
+ }
552
+ request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
553
+ } catch {}
554
+ }],
555
+ afterResponse: [handleUnauthorized]
556
+ }
557
+ });
558
+ /**
559
+ * Returns an HTTP client scoped to the current app.
560
+ * Use this for API calls to app-specific endpoints (entities, functions, etc.).
561
+ *
562
+ * @throws {Error} If BASE44_CLIENT_ID environment variable is not set.
563
+ *
564
+ * @example
565
+ * const appClient = getAppClient();
566
+ * const response = await appClient.get("entities");
567
+ */
568
+ function getAppClient() {
569
+ const clientId = getBase44ClientId();
570
+ if (!clientId) throw new Error("BASE44_CLIENT_ID environment variable is required. Set it in your .env.local file.");
571
+ return base44Client.extend({ prefixUrl: new URL(`/api/apps/${clientId}/`, getBase44ApiUrl()).href });
572
+ }
566
573
 
567
574
  //#endregion
568
575
  //#region src/core/auth/api.ts
569
576
  async function generateDeviceCode() {
570
- const response = await authClient_default.post("oauth/device/code", {
577
+ const response = await oauthClient.post("oauth/device/code", {
571
578
  json: {
572
579
  client_id: AUTH_CLIENT_ID,
573
580
  scope: "apps:read apps:write"
@@ -584,7 +591,7 @@ async function getTokenFromDeviceCode(deviceCode) {
584
591
  searchParams.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
585
592
  searchParams.set("device_code", deviceCode);
586
593
  searchParams.set("client_id", AUTH_CLIENT_ID);
587
- const response = await authClient_default.post("oauth/token", {
594
+ const response = await oauthClient.post("oauth/token", {
588
595
  body: searchParams.toString(),
589
596
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
590
597
  throwHttpErrors: false
@@ -606,7 +613,7 @@ async function renewAccessToken(refreshToken) {
606
613
  searchParams.set("grant_type", "refresh_token");
607
614
  searchParams.set("refresh_token", refreshToken);
608
615
  searchParams.set("client_id", AUTH_CLIENT_ID);
609
- const response = await authClient_default.post("oauth/token", {
616
+ const response = await oauthClient.post("oauth/token", {
610
617
  body: searchParams.toString(),
611
618
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
612
619
  throwHttpErrors: false
@@ -623,25 +630,49 @@ async function renewAccessToken(refreshToken) {
623
630
  return result.data;
624
631
  }
625
632
  async function getUserInfo(accessToken) {
626
- const response = await authClient_default.get("oauth/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
633
+ const response = await oauthClient.get("oauth/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
627
634
  if (!response.ok) throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
628
635
  const result = UserInfoSchema.safeParse(await response.json());
629
636
  if (!result.success) throw new AuthValidationError(`Invalid UserInfo response from server: ${result.error.message}`);
630
637
  return result.data;
631
638
  }
632
639
 
640
+ //#endregion
641
+ //#region src/cli/utils/banner.ts
642
+ const orange = chalk.hex("#E86B3C");
643
+ const BANNER = `
644
+ ${orange("██████╗ █████╗ ███████╗███████╗ ██╗ ██╗██╗ ██╗")}
645
+ ${orange("██╔══██╗██╔══██╗██╔════╝██╔════╝ ██║ ██║██║ ██║")}
646
+ ${orange("██████╔╝███████║███████╗█████╗ ███████║███████║")}
647
+ ${orange("██╔══██╗██╔══██║╚════██║██╔══╝ ╚════██║╚════██║")}
648
+ ${orange("██████╔╝██║ ██║███████║███████╗ ██║ ██║")}
649
+ ${orange("╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝")}
650
+ `;
651
+ function printBanner() {
652
+ console.log(BANNER);
653
+ }
654
+
633
655
  //#endregion
634
656
  //#region src/cli/utils/runCommand.ts
635
657
  const base44Color = chalk.bgHex("#E86B3C");
636
658
  /**
637
- * Wraps a command function with the Base44 intro banner.
659
+ * Wraps a command function with the Base44 intro banner and error handling.
638
660
  * All CLI commands should use this utility to ensure consistent branding.
639
661
  * Also loads .env.local from the project root if available.
640
662
  *
641
663
  * @param commandFn - The async function to execute as the command
664
+ * @param options - Optional configuration for the command wrapper
665
+ *
666
+ * @example
667
+ * // Standard command with simple intro
668
+ * export const myCommand = new Command("my-command")
669
+ * .action(async () => {
670
+ * await runCommand(myAction);
671
+ * });
642
672
  */
643
- async function runCommand(commandFn) {
644
- intro(base44Color(" Base 44 "));
673
+ async function runCommand(commandFn, options) {
674
+ if (options?.fullBanner) printBanner();
675
+ else intro(base44Color(" Base 44 "));
645
676
  await loadProjectEnv();
646
677
  try {
647
678
  await commandFn();
@@ -660,8 +691,21 @@ async function runCommand(commandFn) {
660
691
  *
661
692
  * @param startMessage - Message to show when spinner starts
662
693
  * @param operation - The async operation to execute
663
- * @param options - Optional configuration
694
+ * @param options - Optional configuration for success/error messages
664
695
  * @returns The result of the operation
696
+ *
697
+ * @example
698
+ * const data = await runTask(
699
+ * "Fetching data...",
700
+ * async () => {
701
+ * const response = await fetch(url);
702
+ * return response.json();
703
+ * },
704
+ * {
705
+ * successMessage: "Data fetched successfully",
706
+ * errorMessage: "Failed to fetch data",
707
+ * }
708
+ * );
665
709
  */
666
710
  async function runTask(startMessage, operation, options) {
667
711
  const s = spinner();
@@ -687,21 +731,6 @@ const onPromptCancel = () => {
687
731
  process.exit(0);
688
732
  };
689
733
 
690
- //#endregion
691
- //#region src/cli/utils/banner.ts
692
- const orange = chalk.hex("#E86B3C");
693
- const BANNER = `
694
- ${orange("██████╗ █████╗ ███████╗███████╗ ██╗ ██╗██╗ ██╗")}
695
- ${orange("██╔══██╗██╔══██╗██╔════╝██╔════╝ ██║ ██║██║ ██║")}
696
- ${orange("██████╔╝███████║███████╗█████╗ ███████║███████║")}
697
- ${orange("██╔══██╗██╔══██║╚════██║██╔══╝ ╚════██║╚════██║")}
698
- ${orange("██████╔╝██║ ██║███████║███████╗ ██║ ██║")}
699
- ${orange("╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝")}
700
- `;
701
- function printBanner() {
702
- console.log(BANNER);
703
- }
704
-
705
734
  //#endregion
706
735
  //#region src/cli/commands/auth/login.ts
707
736
  async function generateAndDisplayDeviceCode() {
@@ -824,8 +853,6 @@ const entitiesPushCommand = new Command("entities").description("Manage project
824
853
  //#endregion
825
854
  //#region src/cli/commands/project/create.ts
826
855
  async function create() {
827
- printBanner();
828
- await loadProjectEnv();
829
856
  const templateOptions = (await listTemplates()).map((t) => ({
830
857
  value: t,
831
858
  label: t.name,
@@ -871,13 +898,7 @@ async function create() {
871
898
  log.success(`Project ${chalk.bold(name)} has been initialized!`);
872
899
  }
873
900
  const createCommand = new Command("create").description("Create a new Base44 project").action(async () => {
874
- try {
875
- await create();
876
- } catch (e) {
877
- if (e instanceof Error) log.error(e.stack ?? e.message);
878
- else log.error(String(e));
879
- process.exit(1);
880
- }
901
+ await runCommand(create, { fullBanner: true });
881
902
  });
882
903
 
883
904
  //#endregion
@@ -1,15 +1,16 @@
1
- import { useState, useEffect } from 'react';
2
- import { base44 } from '@/api/base44Client';
3
- import { Button } from '@/components/ui/button';
4
- import { Checkbox } from '@/components/ui/checkbox';
5
- import { Input } from '@/components/ui/input';
6
- import { Plus, Trash2, CheckCircle2 } from 'lucide-react';
1
+ import { useState, useEffect } from "react";
2
+ import { base44 } from "@/api/base44Client";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Checkbox } from "@/components/ui/checkbox";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Base44Logo } from "@/components/Base44Logo";
7
+ import { Plus, Trash2, CheckCircle2 } from "lucide-react";
7
8
 
8
9
  const Task = base44.entities.Task;
9
10
 
10
11
  export default function App() {
11
12
  const [tasks, setTasks] = useState([]);
12
- const [newTaskTitle, setNewTaskTitle] = useState('');
13
+ const [newTaskTitle, setNewTaskTitle] = useState("");
13
14
  const [isLoading, setIsLoading] = useState(true);
14
15
 
15
16
  const fetchTasks = async () => {
@@ -26,7 +27,7 @@ export default function App() {
26
27
  e.preventDefault();
27
28
  if (!newTaskTitle.trim()) return;
28
29
  await Task.create({ title: newTaskTitle.trim(), completed: false });
29
- setNewTaskTitle('');
30
+ setNewTaskTitle("");
30
31
  fetchTasks();
31
32
  };
32
33
 
@@ -41,7 +42,9 @@ export default function App() {
41
42
  };
42
43
 
43
44
  const clearCompleted = async () => {
44
- await Promise.all(tasks.filter((t) => t.completed).map((t) => Task.delete(t.id)));
45
+ await Promise.all(
46
+ tasks.filter((t) => t.completed).map((t) => Task.delete(t.id))
47
+ );
45
48
  fetchTasks();
46
49
  };
47
50
 
@@ -53,10 +56,13 @@ export default function App() {
53
56
  <div className="max-w-lg mx-auto px-6 py-16">
54
57
  {/* Header */}
55
58
  <div className="text-center mb-12">
56
- <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gradient-to-br from-orange-500 to-orange-600 shadow-lg shadow-orange-500/25 mb-6">
57
- <CheckCircle2 className="w-7 h-7 text-white" />
58
- </div>
59
- <h1 className="text-3xl font-semibold text-slate-900 tracking-tight">Tasks</h1>
59
+ <h1 className="text-3xl font-semibold text-slate-900 tracking-tight">
60
+ <span className="inline-flex items-center gap-2 align-middle">
61
+ <Base44Logo className="w-9 h-9" />
62
+ <span className="font-bold">Base44</span>
63
+ <span>Tasks</span>
64
+ </span>
65
+ </h1>
60
66
  {totalCount > 0 && (
61
67
  <p className="text-slate-500 mt-2 text-sm">
62
68
  {completedCount} of {totalCount} completed
@@ -105,7 +111,11 @@ export default function App() {
105
111
  onCheckedChange={(checked) => toggleTask(task.id, checked)}
106
112
  className="w-5 h-5 rounded-md border-slate-300 data-[state=checked]:bg-orange-500 data-[state=checked]:border-orange-500"
107
113
  />
108
- <span className={`flex-1 text-slate-700 transition-all ${task.completed ? 'line-through text-slate-400' : ''}`}>
114
+ <span
115
+ className={`flex-1 text-slate-700 transition-all ${
116
+ task.completed ? "line-through text-slate-400" : ""
117
+ }`}
118
+ >
109
119
  {task.title}
110
120
  </span>
111
121
  <Button
@@ -124,7 +134,10 @@ export default function App() {
124
134
  {/* Footer */}
125
135
  {completedCount > 0 && (
126
136
  <div className="mt-8 text-center">
127
- <button onClick={clearCompleted} className="text-sm text-slate-400 hover:text-slate-600 transition-colors">
137
+ <button
138
+ onClick={clearCompleted}
139
+ className="text-sm text-slate-400 hover:text-slate-600 transition-colors"
140
+ >
128
141
  Clear completed
129
142
  </button>
130
143
  </div>
@@ -0,0 +1,15 @@
1
+ export function Base44Logo({ className = "w-8 h-8" }) {
2
+ return (
3
+ <svg
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ fill="none"
6
+ viewBox="0 0 31 31"
7
+ className={className}
8
+ >
9
+ <path
10
+ fill="#FF631F"
11
+ d="M24.16 26.904c.04 0 .057.05.026.075a14.97 14.97 0 0 1-9.147 3.1c-3.44 0-6.612-1.156-9.146-3.1-.032-.024-.014-.075.026-.075zm3.923-4.373a15 15 0 0 1-1.842 2.544.14.14 0 0 1-.104.046H3.942a.14.14 0 0 1-.104-.046 15 15 0 0 1-1.842-2.544.056.056 0 0 1 .049-.083h25.99c.043 0 .07.046.048.083m1.698-4.5a15 15 0 0 1-.762 2.564.11.11 0 0 1-.103.07H1.163a.11.11 0 0 1-.104-.07 15 15 0 0 1-.762-2.564.056.056 0 0 1 .055-.067h29.375c.035 0 .061.032.054.067M14.938 0C23.29-.056 30.078 6.7 30.078 15.04q0 .55-.038 1.09a.056.056 0 0 1-.056.051H.094a.056.056 0 0 1-.055-.052A15 15 0 0 1 0 15.054C-.007 6.87 6.755.055 14.938 0"
12
+ ></path>
13
+ </svg>
14
+ );
15
+ }
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "id": "backend-and-client",
11
- "name": "To Do App - Backend + Client",
11
+ "name": "Backend & Client",
12
12
  "description": "Full-stack project with Base44 backend, Vite and a React client application",
13
13
  "path": "backend-and-client"
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/cli",
3
- "version": "0.0.1-pr.16.c796b32",
3
+ "version": "0.0.1-pr.18.893dad9",
4
4
  "description": "Base44 CLI - Unified interface for managing Base44 applications",
5
5
  "type": "module",
6
6
  "main": "./dist/cli/index.js",