@base44-preview/cli 0.0.1-pr.15.8b10ac1 → 0.0.1-pr.16.514b62d

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.
Files changed (23) hide show
  1. package/dist/cli/index.js +548 -363
  2. package/dist/cli/templates/backend-and-client/README.md +41 -0
  3. package/dist/cli/templates/backend-and-client/base44/config.jsonc.ejs +17 -0
  4. package/dist/cli/templates/backend-and-client/base44/entities/task.jsonc +16 -0
  5. package/dist/cli/templates/backend-and-client/components.json +16 -0
  6. package/dist/cli/templates/backend-and-client/index.html +13 -0
  7. package/dist/cli/templates/backend-and-client/jsconfig.json +13 -0
  8. package/dist/cli/templates/backend-and-client/package.json +24 -0
  9. package/dist/cli/templates/backend-and-client/postcss.config.js +6 -0
  10. package/dist/cli/templates/backend-and-client/src/App.jsx +148 -0
  11. package/dist/cli/templates/backend-and-client/src/api/base44Client.js.ejs +5 -0
  12. package/dist/cli/templates/backend-and-client/src/components/Base44Logo.jsx +15 -0
  13. package/dist/cli/templates/backend-and-client/src/components/ui/button.jsx +23 -0
  14. package/dist/cli/templates/backend-and-client/src/components/ui/checkbox.jsx +20 -0
  15. package/dist/cli/templates/backend-and-client/src/components/ui/input.jsx +13 -0
  16. package/dist/cli/templates/backend-and-client/src/index.css +37 -0
  17. package/dist/cli/templates/backend-and-client/src/main.jsx +6 -0
  18. package/dist/cli/templates/backend-and-client/tailwind.config.js +41 -0
  19. package/dist/cli/templates/backend-and-client/vite.config.js +12 -0
  20. package/dist/cli/templates/backend-only/base44/.env.local.ejs +6 -0
  21. package/dist/cli/templates/backend-only/base44/config.jsonc.ejs +17 -0
  22. package/dist/cli/templates/templates.json +16 -0
  23. package/package.json +6 -1
package/dist/cli/index.js CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
- import { intro, log, spinner } from "@clack/prompts";
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 { dirname, join } from "node:path";
7
+ import { dirname, join, resolve } from "node:path";
8
8
  import { homedir } from "node:os";
9
- import ky from "ky";
10
- import { access, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
11
- import { parse, printParseErrorCode } from "jsonc-parser";
9
+ import { fileURLToPath } from "node:url";
10
+ import { config } from "dotenv";
12
11
  import { globby } from "globby";
12
+ import { access, copyFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
13
+ import { parse, printParseErrorCode } from "jsonc-parser";
14
+ import ky from "ky";
15
+ import ejs from "ejs";
16
+ import kebabCase from "lodash.kebabcase";
13
17
 
14
18
  //#region src/core/auth/schema.ts
15
19
  const AuthDataSchema = z.object({
@@ -73,116 +77,24 @@ var AuthValidationError = class extends Error {
73
77
  };
74
78
 
75
79
  //#endregion
76
- //#region src/core/consts.ts
77
- const PROJECT_SUBDIR = "base44";
78
- const FUNCTION_CONFIG_FILE = "function.jsonc";
79
- function getBase44Dir() {
80
- return join(homedir(), ".base44");
81
- }
82
- function getAuthFilePath() {
83
- return join(getBase44Dir(), "auth", "auth.json");
84
- }
85
- function getProjectConfigPatterns() {
86
- return [
87
- `${PROJECT_SUBDIR}/config.jsonc`,
88
- `${PROJECT_SUBDIR}/config.json`,
89
- "config.jsonc",
90
- "config.json"
91
- ];
92
- }
93
- const AUTH_CLIENT_ID = "base44_cli";
94
- const DEFAULT_API_URL = "https://app.base44.com";
95
- function getBase44ApiUrl() {
96
- return process.env.BASE44_API_URL || DEFAULT_API_URL;
97
- }
98
- function getAppId() {
99
- const appId = process.env.BASE44_CLIENT_ID;
100
- if (!appId) throw new Error("BASE44_CLIENT_ID environment variable is not set");
101
- return appId;
102
- }
103
-
104
- //#endregion
105
- //#region src/core/auth/authClient.ts
106
- /**
107
- * Separate ky instance for OAuth endpoints.
108
- * These don't need Authorization headers (they use client_id + tokens in body).
109
- */
110
- const authClient = ky.create({
111
- prefixUrl: getBase44ApiUrl(),
112
- headers: { "User-Agent": "Base44 CLI" }
113
- });
114
- var authClient_default = authClient;
115
-
116
- //#endregion
117
- //#region src/core/auth/api.ts
118
- async function generateDeviceCode() {
119
- const response = await authClient_default.post("oauth/device/code", {
120
- json: {
121
- client_id: AUTH_CLIENT_ID,
122
- scope: "apps:read apps:write"
123
- },
124
- throwHttpErrors: false
125
- });
126
- if (!response.ok) throw new AuthApiError(`Failed to generate device code: ${response.status} ${response.statusText}`);
127
- const result = DeviceCodeResponseSchema.safeParse(await response.json());
128
- if (!result.success) throw new AuthValidationError(`Invalid device code response from server: ${result.error.message}`);
129
- return result.data;
130
- }
131
- async function getTokenFromDeviceCode(deviceCode) {
132
- const searchParams = new URLSearchParams();
133
- searchParams.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
134
- searchParams.set("device_code", deviceCode);
135
- searchParams.set("client_id", AUTH_CLIENT_ID);
136
- const response = await authClient_default.post("oauth/token", {
137
- body: searchParams.toString(),
138
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
139
- throwHttpErrors: false
140
- });
141
- const json = await response.json();
142
- if (!response.ok) {
143
- const errorResult = OAuthErrorSchema.safeParse(json);
144
- if (!errorResult.success) throw new AuthValidationError(`Token request failed: ${errorResult.error.message}`);
145
- const { error, error_description } = errorResult.data;
146
- if (error === "authorization_pending" || error === "slow_down") return null;
147
- throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
148
- }
149
- const result = TokenResponseSchema.safeParse(json);
150
- if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
151
- return result.data;
152
- }
153
- async function renewAccessToken(refreshToken) {
154
- const searchParams = new URLSearchParams();
155
- searchParams.set("grant_type", "refresh_token");
156
- searchParams.set("refresh_token", refreshToken);
157
- searchParams.set("client_id", AUTH_CLIENT_ID);
158
- const response = await authClient_default.post("oauth/token", {
159
- body: searchParams.toString(),
160
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
161
- throwHttpErrors: false
162
- });
163
- const json = await response.json();
164
- if (!response.ok) {
165
- const errorResult = OAuthErrorSchema.safeParse(json);
166
- if (!errorResult.success) throw new AuthApiError(`Token refresh failed: ${response.statusText}`);
167
- const { error, error_description } = errorResult.data;
168
- throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
80
+ //#region src/core/utils/fs.ts
81
+ async function pathExists(path) {
82
+ try {
83
+ await access(path);
84
+ return true;
85
+ } catch {
86
+ return false;
169
87
  }
170
- const result = TokenResponseSchema.safeParse(json);
171
- if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
172
- return result.data;
173
88
  }
174
- async function getUserInfo(accessToken) {
175
- const response = await authClient_default.get("oauth/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
176
- if (!response.ok) throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
177
- const result = UserInfoSchema.safeParse(await response.json());
178
- if (!result.success) throw new AuthValidationError(`Invalid UserInfo response from server: ${result.error.message}`);
179
- return result.data;
89
+ async function writeFile$1(filePath, content) {
90
+ const dir = dirname(filePath);
91
+ if (!await pathExists(dir)) await mkdir(dir, { recursive: true });
92
+ await writeFile(filePath, content, "utf-8");
180
93
  }
181
-
182
- //#endregion
183
- //#region src/core/utils/fs.ts
184
- function pathExists(path) {
185
- return access(path).then(() => true).catch(() => false);
94
+ async function copyFile$1(src, dest) {
95
+ const dir = dirname(dest);
96
+ if (!await pathExists(dir)) await mkdir(dir, { recursive: true });
97
+ await copyFile(src, dest);
186
98
  }
187
99
  async function readJsonFile(filePath) {
188
100
  if (!await pathExists(filePath)) throw new Error(`File not found: ${filePath}`);
@@ -201,21 +113,67 @@ async function readJsonFile(filePath) {
201
113
  }
202
114
  }
203
115
  async function writeJsonFile(filePath, data) {
204
- try {
205
- const dir = dirname(filePath);
206
- if (!await pathExists(dir)) await mkdir(dir, { recursive: true });
207
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
208
- } catch (error) {
209
- throw new Error(`Failed to write file ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
210
- }
116
+ const dir = dirname(filePath);
117
+ if (!await pathExists(dir)) await mkdir(dir, { recursive: true });
118
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
211
119
  }
212
120
  async function deleteFile(filePath) {
213
121
  if (!await pathExists(filePath)) return;
214
- try {
215
- await unlink(filePath);
216
- } catch (error) {
217
- throw new Error(`Failed to delete file ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
218
- }
122
+ await unlink(filePath);
123
+ }
124
+
125
+ //#endregion
126
+ //#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
+ });
156
+ const SyncEntitiesResponseSchema = z.object({
157
+ created: z.array(z.string()),
158
+ updated: z.array(z.string()),
159
+ deleted: z.array(z.string())
160
+ });
161
+
162
+ //#endregion
163
+ //#region src/core/resources/entity/config.ts
164
+ async function readEntityFile(entityPath) {
165
+ const parsed = await readJsonFile(entityPath);
166
+ const result = EntitySchema.safeParse(parsed);
167
+ if (!result.success) throw new Error(`Invalid entity configuration in ${entityPath}: ${result.error.issues.map((e) => e.message).join(", ")}`);
168
+ return result.data;
169
+ }
170
+ async function readAllEntities(entitiesDir) {
171
+ if (!await pathExists(entitiesDir)) return [];
172
+ const files = await globby("*.{json,jsonc}", {
173
+ cwd: entitiesDir,
174
+ absolute: true
175
+ });
176
+ return await Promise.all(files.map((filePath) => readEntityFile(filePath)));
219
177
  }
220
178
 
221
179
  //#endregion
@@ -285,239 +243,62 @@ async function refreshAndSaveTokens() {
285
243
  }
286
244
 
287
245
  //#endregion
288
- //#region src/cli/utils/runCommand.ts
289
- const base44Color = chalk.bgHex("#E86B3C");
246
+ //#region src/core/utils/httpClient.ts
247
+ const retriedRequests = /* @__PURE__ */ new WeakSet();
290
248
  /**
291
- * Wraps a command function with the Base44 intro banner.
292
- * All CLI commands should use this utility to ensure consistent branding.
293
- *
294
- * @param commandFn - The async function to execute as the command
249
+ * Handles 401 responses by refreshing the token and retrying the request.
250
+ * Only retries once per request to prevent infinite loops.
295
251
  */
296
- async function runCommand(commandFn) {
297
- intro(base44Color(" Base 44 "));
298
- try {
299
- await commandFn();
300
- } catch (e) {
301
- if (e instanceof Error) log.error(e.stack ?? e.message);
302
- else log.error(String(e));
303
- process.exit(1);
304
- }
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}` } });
305
259
  }
306
-
307
- //#endregion
308
- //#region src/cli/utils/runTask.ts
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
+ });
309
280
  /**
310
- * Wraps an async operation with automatic spinner management.
311
- * The spinner is automatically started, and stopped on both success and error.
312
- *
313
- * @param startMessage - Message to show when spinner starts
314
- * @param operation - The async operation to execute
315
- * @param options - Optional configuration
316
- * @returns The result of the operation
281
+ * Returns an HTTP client scoped to the current app.
317
282
  */
318
- async function runTask(startMessage, operation, options) {
319
- const s = spinner();
320
- s.start(startMessage);
321
- try {
322
- const result = await operation();
323
- s.stop(options?.successMessage || startMessage);
324
- return result;
325
- } catch (error) {
326
- s.stop(options?.errorMessage || "Failed");
327
- throw error;
328
- }
283
+ function getAppClient() {
284
+ return base44Client.extend({ prefixUrl: new URL(`/api/apps/${getBase44ClientId()}/`, getBase44ApiUrl()).href });
329
285
  }
330
286
 
331
287
  //#endregion
332
- //#region src/cli/commands/auth/login.ts
333
- async function generateAndDisplayDeviceCode() {
334
- const deviceCodeResponse = await runTask("Generating device code...", async () => {
335
- return await generateDeviceCode();
336
- }, {
337
- successMessage: "Device code generated",
338
- errorMessage: "Failed to generate device code"
288
+ //#region src/core/resources/entity/api.ts
289
+ async function pushEntities(entities) {
290
+ const appClient = getAppClient();
291
+ const schemaSyncPayload = Object.fromEntries(entities.map((entity) => [entity.name, entity]));
292
+ const response = await appClient.put("entities-schemas/sync-all", {
293
+ json: { entityNameToSchema: schemaSyncPayload },
294
+ throwHttpErrors: false
339
295
  });
340
- log.info(`Your code is: ${chalk.bold(deviceCodeResponse.userCode)}\nPlease visit: ${deviceCodeResponse.verificationUriComplete}`);
341
- return deviceCodeResponse;
342
- }
343
- async function waitForAuthentication(deviceCode, expiresIn, interval) {
344
- let tokenResponse;
345
- try {
346
- await runTask("Waiting for you to complete authentication...", async () => {
347
- await pWaitFor(async () => {
348
- const result = await getTokenFromDeviceCode(deviceCode);
349
- if (result !== null) {
350
- tokenResponse = result;
351
- return true;
352
- }
353
- return false;
354
- }, {
355
- interval: interval * 1e3,
356
- timeout: expiresIn * 1e3
357
- });
358
- }, {
359
- successMessage: "Authentication completed!",
360
- errorMessage: "Authentication failed"
361
- });
362
- } catch (error) {
363
- if (error instanceof Error && error.message.includes("timed out")) throw new Error("Authentication timed out. Please try again.");
364
- throw error;
365
- }
366
- if (tokenResponse === void 0) throw new Error("Failed to retrieve authentication token.");
367
- return tokenResponse;
368
- }
369
- async function saveAuthData(response, userInfo) {
370
- const expiresAt = Date.now() + response.expiresIn * 1e3;
371
- await writeAuth({
372
- accessToken: response.accessToken,
373
- refreshToken: response.refreshToken,
374
- expiresAt,
375
- email: userInfo.email,
376
- name: userInfo.name
377
- });
378
- }
379
- async function login() {
380
- const deviceCodeResponse = await generateAndDisplayDeviceCode();
381
- const token = await waitForAuthentication(deviceCodeResponse.deviceCode, deviceCodeResponse.expiresIn, deviceCodeResponse.interval);
382
- const userInfo = await getUserInfo(token.accessToken);
383
- await saveAuthData(token, userInfo);
384
- log.success(`Successfully logged in as ${chalk.bold(userInfo.email)}`);
385
- }
386
- const loginCommand = new Command("login").description("Authenticate with Base44").action(async () => {
387
- await runCommand(login);
388
- });
389
-
390
- //#endregion
391
- //#region src/cli/commands/auth/whoami.ts
392
- async function whoami() {
393
- const auth = await readAuth();
394
- log.info(`Logged in as: ${auth.name} (${auth.email})`);
395
- }
396
- const whoamiCommand = new Command("whoami").description("Display current authenticated user").action(async () => {
397
- await runCommand(whoami);
398
- });
399
-
400
- //#endregion
401
- //#region src/cli/commands/auth/logout.ts
402
- async function logout() {
403
- await deleteAuth();
404
- log.info("Logged out successfully");
405
- }
406
- const logoutCommand = new Command("logout").description("Logout from current device").action(async () => {
407
- await runCommand(logout);
408
- });
409
-
410
- //#endregion
411
- //#region src/core/resources/entity/schema.ts
412
- const EntityPropertySchema = z.object({
413
- type: z.string(),
414
- description: z.string().optional(),
415
- enum: z.array(z.string()).optional(),
416
- default: z.union([
417
- z.string(),
418
- z.number(),
419
- z.boolean()
420
- ]).optional(),
421
- format: z.string().optional(),
422
- items: z.any().optional(),
423
- relation: z.object({
424
- entity: z.string(),
425
- type: z.string()
426
- }).optional()
427
- });
428
- const EntityPoliciesSchema = z.object({
429
- read: z.string().optional(),
430
- create: z.string().optional(),
431
- update: z.string().optional(),
432
- delete: z.string().optional()
433
- });
434
- const EntitySchema = z.object({
435
- name: z.string().min(1, "Entity name cannot be empty"),
436
- type: z.literal("object"),
437
- properties: z.record(z.string(), EntityPropertySchema),
438
- required: z.array(z.string()).optional(),
439
- policies: EntityPoliciesSchema.optional()
440
- });
441
- const SyncEntitiesResponseSchema = z.object({
442
- created: z.array(z.string()),
443
- updated: z.array(z.string()),
444
- deleted: z.array(z.string())
445
- });
446
-
447
- //#endregion
448
- //#region src/core/resources/entity/config.ts
449
- async function readEntityFile(entityPath) {
450
- const parsed = await readJsonFile(entityPath);
451
- const result = EntitySchema.safeParse(parsed);
452
- if (!result.success) throw new Error(`Invalid entity configuration in ${entityPath}: ${result.error.issues.map((e) => e.message).join(", ")}`);
453
- return result.data;
454
- }
455
- async function readAllEntities(entitiesDir) {
456
- if (!await pathExists(entitiesDir)) return [];
457
- const files = await globby("*.{json,jsonc}", {
458
- cwd: entitiesDir,
459
- absolute: true
460
- });
461
- return await Promise.all(files.map((filePath) => readEntityFile(filePath)));
462
- }
463
-
464
- //#endregion
465
- //#region src/core/utils/httpClient.ts
466
- const retriedRequests = /* @__PURE__ */ new WeakSet();
467
- /**
468
- * Handles 401 responses by refreshing the token and retrying the request.
469
- * Only retries once per request to prevent infinite loops.
470
- */
471
- async function handleUnauthorized(request, _options, response) {
472
- if (response.status !== 401) return;
473
- if (retriedRequests.has(request)) return;
474
- const newAccessToken = await refreshAndSaveTokens();
475
- if (!newAccessToken) return;
476
- retriedRequests.add(request);
477
- return ky(request, { headers: { Authorization: `Bearer ${newAccessToken}` } });
478
- }
479
- const base44Client = ky.create({
480
- prefixUrl: getBase44ApiUrl(),
481
- headers: { "User-Agent": "Base44 CLI" },
482
- hooks: {
483
- beforeRequest: [async (request) => {
484
- try {
485
- const auth = await readAuth();
486
- if (isTokenExpired(auth)) {
487
- const newAccessToken = await refreshAndSaveTokens();
488
- if (newAccessToken) {
489
- request.headers.set("Authorization", `Bearer ${newAccessToken}`);
490
- return;
491
- }
492
- }
493
- request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
494
- } catch {}
495
- }],
496
- afterResponse: [handleUnauthorized]
497
- }
498
- });
499
- /**
500
- * Returns an HTTP client scoped to the current app.
501
- */
502
- function getAppClient() {
503
- return base44Client.extend({ prefixUrl: new URL(`/api/apps/${getAppId()}/`, getBase44ApiUrl()).href });
504
- }
505
-
506
- //#endregion
507
- //#region src/core/resources/entity/api.ts
508
- async function pushEntities(entities) {
509
- const appClient = getAppClient();
510
- const schemaSyncPayload = Object.fromEntries(entities.map((entity) => [entity.name, entity]));
511
- const response = await appClient.put("entities-schemas/sync-all", {
512
- json: { entityNameToSchema: schemaSyncPayload },
513
- throwHttpErrors: false
514
- });
515
- if (!response.ok) {
516
- const errorJson = await response.json();
517
- if (response.status === 428) throw new Error(`Failed to delete entity: ${errorJson.message}`);
518
- throw new Error(`Error occurred while syncing entities ${errorJson.message}`);
519
- }
520
- return SyncEntitiesResponseSchema.parse(await response.json());
296
+ if (!response.ok) {
297
+ const errorJson = await response.json();
298
+ if (response.status === 428) throw new Error(`Failed to delete entity: ${errorJson.message}`);
299
+ throw new Error(`Error occurred while syncing entities ${errorJson.message}`);
300
+ }
301
+ return SyncEntitiesResponseSchema.parse(await response.json());
521
302
  }
522
303
 
523
304
  //#endregion
@@ -641,6 +422,365 @@ async function readProjectConfig(projectRoot) {
641
422
  };
642
423
  }
643
424
 
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
+ //#endregion
449
+ //#region src/core/project/api.ts
450
+ async function createProject(projectName, description) {
451
+ const response = await base44Client.post("api/apps", { json: {
452
+ name: projectName,
453
+ user_description: description ?? `Backend for '${projectName}'`,
454
+ app_type: "baas"
455
+ } });
456
+ return { projectId: CreateProjectResponseSchema.parse(await response.json()).id };
457
+ }
458
+
459
+ //#endregion
460
+ //#region src/core/project/template.ts
461
+ async function listTemplates() {
462
+ const parsed = await readJsonFile(join(getTemplatesDir(), "templates.json"));
463
+ return TemplatesConfigSchema.parse(parsed).templates;
464
+ }
465
+ /**
466
+ * Render a template directory to a destination path.
467
+ * - Files ending in .ejs are rendered with EJS and written without the .ejs extension
468
+ * - All other files are copied directly
469
+ */
470
+ async function renderTemplate(template, destPath, data) {
471
+ const templateDir = join(getTemplatesDir(), template.path);
472
+ const files = await globby("**/*", {
473
+ cwd: templateDir,
474
+ dot: true,
475
+ onlyFiles: true
476
+ });
477
+ for (const file of files) {
478
+ const srcPath = join(templateDir, file);
479
+ if (file.endsWith(".ejs")) await writeFile$1(join(destPath, file.replace(".ejs", "")), await ejs.renderFile(srcPath, data));
480
+ else await copyFile$1(srcPath, join(destPath, file));
481
+ }
482
+ }
483
+
484
+ //#endregion
485
+ //#region src/core/project/create.ts
486
+ async function createProjectFiles(options) {
487
+ const { name, description, path: basePath, template } = options;
488
+ const existingConfigs = await globby(getProjectConfigPatterns(), {
489
+ cwd: basePath,
490
+ absolute: true
491
+ });
492
+ if (existingConfigs.length > 0) throw new Error(`A Base44 project already exists at ${existingConfigs[0]}. Please choose a different location.`);
493
+ const { projectId } = await createProject(name, description);
494
+ await renderTemplate(template, basePath, {
495
+ name,
496
+ description,
497
+ projectId
498
+ });
499
+ return { projectDir: basePath };
500
+ }
501
+
502
+ //#endregion
503
+ //#region src/core/config.ts
504
+ 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() {
509
+ return join(homedir(), ".base44");
510
+ }
511
+ function getAuthFilePath() {
512
+ return join(getBase44Dir(), "auth", "auth.json");
513
+ }
514
+ function getTemplatesDir() {
515
+ return join(__dirname, "templates");
516
+ }
517
+ function getProjectConfigPatterns() {
518
+ return [
519
+ `${PROJECT_SUBDIR}/config.jsonc`,
520
+ `${PROJECT_SUBDIR}/config.json`,
521
+ "config.jsonc",
522
+ "config.json"
523
+ ];
524
+ }
525
+ /**
526
+ * Load .env.local from the project root if it exists.
527
+ * Values won't override existing process.env variables.
528
+ * Safe to call multiple times - only loads once.
529
+ */
530
+ async function loadProjectEnv(projectRoot) {
531
+ const found = projectRoot ? { root: projectRoot } : await findProjectRoot();
532
+ if (!found) return;
533
+ config({
534
+ path: join(found.root, PROJECT_SUBDIR, ".env.local"),
535
+ override: false,
536
+ quiet: true
537
+ });
538
+ }
539
+ /**
540
+ * Get the Base44 API URL.
541
+ * Priority: process.env.BASE44_API_URL > .env.local > default
542
+ */
543
+ function getBase44ApiUrl() {
544
+ return process.env.BASE44_API_URL || "https://app.base44.com";
545
+ }
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
+ function getBase44ClientId() {
552
+ return process.env.BASE44_CLIENT_ID;
553
+ }
554
+
555
+ //#endregion
556
+ //#region src/core/auth/authClient.ts
557
+ /**
558
+ * Separate ky instance for OAuth endpoints.
559
+ * These don't need Authorization headers (they use client_id + tokens in body).
560
+ */
561
+ const authClient = ky.create({
562
+ prefixUrl: getBase44ApiUrl(),
563
+ headers: { "User-Agent": "Base44 CLI" }
564
+ });
565
+ var authClient_default = authClient;
566
+
567
+ //#endregion
568
+ //#region src/core/auth/api.ts
569
+ async function generateDeviceCode() {
570
+ const response = await authClient_default.post("oauth/device/code", {
571
+ json: {
572
+ client_id: AUTH_CLIENT_ID,
573
+ scope: "apps:read apps:write"
574
+ },
575
+ throwHttpErrors: false
576
+ });
577
+ if (!response.ok) throw new AuthApiError(`Failed to generate device code: ${response.status} ${response.statusText}`);
578
+ const result = DeviceCodeResponseSchema.safeParse(await response.json());
579
+ if (!result.success) throw new AuthValidationError(`Invalid device code response from server: ${result.error.message}`);
580
+ return result.data;
581
+ }
582
+ async function getTokenFromDeviceCode(deviceCode) {
583
+ const searchParams = new URLSearchParams();
584
+ searchParams.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
585
+ searchParams.set("device_code", deviceCode);
586
+ searchParams.set("client_id", AUTH_CLIENT_ID);
587
+ const response = await authClient_default.post("oauth/token", {
588
+ body: searchParams.toString(),
589
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
590
+ throwHttpErrors: false
591
+ });
592
+ const json = await response.json();
593
+ if (!response.ok) {
594
+ const errorResult = OAuthErrorSchema.safeParse(json);
595
+ if (!errorResult.success) throw new AuthValidationError(`Token request failed: ${errorResult.error.message}`);
596
+ const { error, error_description } = errorResult.data;
597
+ if (error === "authorization_pending" || error === "slow_down") return null;
598
+ throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
599
+ }
600
+ const result = TokenResponseSchema.safeParse(json);
601
+ if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
602
+ return result.data;
603
+ }
604
+ async function renewAccessToken(refreshToken) {
605
+ const searchParams = new URLSearchParams();
606
+ searchParams.set("grant_type", "refresh_token");
607
+ searchParams.set("refresh_token", refreshToken);
608
+ searchParams.set("client_id", AUTH_CLIENT_ID);
609
+ const response = await authClient_default.post("oauth/token", {
610
+ body: searchParams.toString(),
611
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
612
+ throwHttpErrors: false
613
+ });
614
+ const json = await response.json();
615
+ if (!response.ok) {
616
+ const errorResult = OAuthErrorSchema.safeParse(json);
617
+ if (!errorResult.success) throw new AuthApiError(`Token refresh failed: ${response.statusText}`);
618
+ const { error, error_description } = errorResult.data;
619
+ throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
620
+ }
621
+ const result = TokenResponseSchema.safeParse(json);
622
+ if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
623
+ return result.data;
624
+ }
625
+ async function getUserInfo(accessToken) {
626
+ const response = await authClient_default.get("oauth/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
627
+ if (!response.ok) throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
628
+ const result = UserInfoSchema.safeParse(await response.json());
629
+ if (!result.success) throw new AuthValidationError(`Invalid UserInfo response from server: ${result.error.message}`);
630
+ return result.data;
631
+ }
632
+
633
+ //#endregion
634
+ //#region src/cli/utils/runCommand.ts
635
+ const base44Color = chalk.bgHex("#E86B3C");
636
+ /**
637
+ * Wraps a command function with the Base44 intro banner.
638
+ * All CLI commands should use this utility to ensure consistent branding.
639
+ * Also loads .env.local from the project root if available.
640
+ *
641
+ * @param commandFn - The async function to execute as the command
642
+ */
643
+ async function runCommand(commandFn) {
644
+ intro(base44Color(" Base 44 "));
645
+ await loadProjectEnv();
646
+ try {
647
+ await commandFn();
648
+ } catch (e) {
649
+ if (e instanceof Error) log.error(e.stack ?? e.message);
650
+ else log.error(String(e));
651
+ process.exit(1);
652
+ }
653
+ }
654
+
655
+ //#endregion
656
+ //#region src/cli/utils/runTask.ts
657
+ /**
658
+ * Wraps an async operation with automatic spinner management.
659
+ * The spinner is automatically started, and stopped on both success and error.
660
+ *
661
+ * @param startMessage - Message to show when spinner starts
662
+ * @param operation - The async operation to execute
663
+ * @param options - Optional configuration
664
+ * @returns The result of the operation
665
+ */
666
+ async function runTask(startMessage, operation, options) {
667
+ const s = spinner();
668
+ s.start(startMessage);
669
+ try {
670
+ const result = await operation();
671
+ s.stop(options?.successMessage || startMessage);
672
+ return result;
673
+ } catch (error) {
674
+ s.stop(options?.errorMessage || "Failed");
675
+ throw error;
676
+ }
677
+ }
678
+
679
+ //#endregion
680
+ //#region src/cli/utils/prompts.ts
681
+ /**
682
+ * Standard onCancel handler for prompt groups.
683
+ * Exits the process gracefully when the user cancels.
684
+ */
685
+ const onPromptCancel = () => {
686
+ cancel("Operation cancelled.");
687
+ process.exit(0);
688
+ };
689
+
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
+ //#endregion
706
+ //#region src/cli/commands/auth/login.ts
707
+ async function generateAndDisplayDeviceCode() {
708
+ const deviceCodeResponse = await runTask("Generating device code...", async () => {
709
+ return await generateDeviceCode();
710
+ }, {
711
+ successMessage: "Device code generated",
712
+ errorMessage: "Failed to generate device code"
713
+ });
714
+ log.info(`Your code is: ${chalk.bold(deviceCodeResponse.userCode)}\nPlease visit: ${deviceCodeResponse.verificationUriComplete}`);
715
+ return deviceCodeResponse;
716
+ }
717
+ async function waitForAuthentication(deviceCode, expiresIn, interval) {
718
+ let tokenResponse;
719
+ try {
720
+ await runTask("Waiting for you to complete authentication...", async () => {
721
+ await pWaitFor(async () => {
722
+ const result = await getTokenFromDeviceCode(deviceCode);
723
+ if (result !== null) {
724
+ tokenResponse = result;
725
+ return true;
726
+ }
727
+ return false;
728
+ }, {
729
+ interval: interval * 1e3,
730
+ timeout: expiresIn * 1e3
731
+ });
732
+ }, {
733
+ successMessage: "Authentication completed!",
734
+ errorMessage: "Authentication failed"
735
+ });
736
+ } catch (error) {
737
+ if (error instanceof Error && error.message.includes("timed out")) throw new Error("Authentication timed out. Please try again.");
738
+ throw error;
739
+ }
740
+ if (tokenResponse === void 0) throw new Error("Failed to retrieve authentication token.");
741
+ return tokenResponse;
742
+ }
743
+ async function saveAuthData(response, userInfo) {
744
+ const expiresAt = Date.now() + response.expiresIn * 1e3;
745
+ await writeAuth({
746
+ accessToken: response.accessToken,
747
+ refreshToken: response.refreshToken,
748
+ expiresAt,
749
+ email: userInfo.email,
750
+ name: userInfo.name
751
+ });
752
+ }
753
+ async function login() {
754
+ const deviceCodeResponse = await generateAndDisplayDeviceCode();
755
+ const token = await waitForAuthentication(deviceCodeResponse.deviceCode, deviceCodeResponse.expiresIn, deviceCodeResponse.interval);
756
+ const userInfo = await getUserInfo(token.accessToken);
757
+ await saveAuthData(token, userInfo);
758
+ log.success(`Successfully logged in as ${chalk.bold(userInfo.email)}`);
759
+ }
760
+ const loginCommand = new Command("login").description("Authenticate with Base44").action(async () => {
761
+ await runCommand(login);
762
+ });
763
+
764
+ //#endregion
765
+ //#region src/cli/commands/auth/whoami.ts
766
+ async function whoami() {
767
+ const auth = await readAuth();
768
+ log.info(`Logged in as: ${auth.name} (${auth.email})`);
769
+ }
770
+ const whoamiCommand = new Command("whoami").description("Display current authenticated user").action(async () => {
771
+ await runCommand(whoami);
772
+ });
773
+
774
+ //#endregion
775
+ //#region src/cli/commands/auth/logout.ts
776
+ async function logout() {
777
+ await deleteAuth();
778
+ log.info("Logged out successfully");
779
+ }
780
+ const logoutCommand = new Command("logout").description("Logout from current device").action(async () => {
781
+ await runCommand(logout);
782
+ });
783
+
644
784
  //#endregion
645
785
  //#region src/cli/commands/project/show-project.ts
646
786
  async function showProject() {
@@ -657,21 +797,6 @@ const showProjectCommand = new Command("show-project").description("Display proj
657
797
  await runCommand(showProject);
658
798
  });
659
799
 
660
- //#endregion
661
- //#region src/core/project/schema.ts
662
- const SiteConfigSchema = z.object({
663
- buildCommand: z.string().optional(),
664
- serveCommand: z.string().optional(),
665
- outputDirectory: z.string().optional(),
666
- installCommand: z.string().optional()
667
- });
668
- const AppConfigSchema = z.object({
669
- name: z.string().min(1, "App name cannot be empty"),
670
- description: z.string().optional(),
671
- site: SiteConfigSchema.optional(),
672
- domains: z.array(z.string()).optional()
673
- });
674
-
675
800
  //#endregion
676
801
  //#region src/cli/commands/entities/push.ts
677
802
  async function pushEntitiesAction() {
@@ -696,6 +821,65 @@ const entitiesPushCommand = new Command("entities").description("Manage project
696
821
  await runCommand(pushEntitiesAction);
697
822
  }));
698
823
 
824
+ //#endregion
825
+ //#region src/cli/commands/project/create.ts
826
+ async function create() {
827
+ printBanner();
828
+ await loadProjectEnv();
829
+ const templateOptions = (await listTemplates()).map((t) => ({
830
+ value: t,
831
+ label: t.name,
832
+ hint: t.description
833
+ }));
834
+ const { template, name, description, projectPath } = await group({
835
+ template: () => select({
836
+ message: "Select a project template",
837
+ options: templateOptions
838
+ }),
839
+ name: () => text({
840
+ message: "What is the name of your project?",
841
+ placeholder: "my-app-backend",
842
+ validate: (value) => {
843
+ if (!value || value.trim().length === 0) return "Project name is required";
844
+ }
845
+ }),
846
+ description: () => text({
847
+ message: "Project description (optional)",
848
+ placeholder: "A brief description of your project"
849
+ }),
850
+ projectPath: async ({ results }) => {
851
+ const suggestedPath = `./${kebabCase(results.name)}`;
852
+ return text({
853
+ message: "Where should we create the base44 folder?",
854
+ placeholder: suggestedPath,
855
+ initialValue: suggestedPath
856
+ });
857
+ }
858
+ }, { onCancel: onPromptCancel });
859
+ const resolvedPath = resolve(projectPath);
860
+ await runTask("Creating project...", async () => {
861
+ return await createProjectFiles({
862
+ name: name.trim(),
863
+ description: description ? description.trim() : void 0,
864
+ path: resolvedPath,
865
+ template
866
+ });
867
+ }, {
868
+ successMessage: "Project created successfully",
869
+ errorMessage: "Failed to create project"
870
+ });
871
+ log.success(`Project ${chalk.bold(name)} has been initialized!`);
872
+ }
873
+ 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
+ }
881
+ });
882
+
699
883
  //#endregion
700
884
  //#region package.json
701
885
  var version = "0.0.1";
@@ -707,6 +891,7 @@ program.name("base44").description("Base44 CLI - Unified interface for managing
707
891
  program.addCommand(loginCommand);
708
892
  program.addCommand(whoamiCommand);
709
893
  program.addCommand(logoutCommand);
894
+ program.addCommand(createCommand);
710
895
  program.addCommand(showProjectCommand);
711
896
  program.addCommand(entitiesPushCommand);
712
897
  program.parse();