@base44-preview/cli 0.0.1-pr.14.bd57bd7 → 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 (24) hide show
  1. package/dist/cli/index.js +492 -492
  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/templates.json +16 -0
  21. package/package.json +3 -1
  22. package/dist/cli/templates/index.ts +0 -30
  23. /package/dist/cli/templates/{env.local.ejs → backend-only/base44/.env.local.ejs} +0 -0
  24. /package/dist/cli/templates/{config.jsonc.ejs → backend-only/base44/config.jsonc.ejs} +0 -0
package/dist/cli/index.js CHANGED
@@ -1,36 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
- import { cancel, intro, isCancel, log, spinner, text } 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
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { homedir } from "node:os";
9
+ import { fileURLToPath } from "node:url";
9
10
  import { config } from "dotenv";
10
- import ky from "ky";
11
- import { access, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
12
- import { parse, printParseErrorCode } from "jsonc-parser";
13
11
  import { globby } from "globby";
14
- import { fileURLToPath } from "node:url";
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
15
  import ejs from "ejs";
16
+ import kebabCase from "lodash.kebabcase";
16
17
 
17
- //#region rolldown:runtime
18
- var __defProp = Object.defineProperty;
19
- var __exportAll = (all, symbols) => {
20
- let target = {};
21
- for (var name in all) {
22
- __defProp(target, name, {
23
- get: all[name],
24
- enumerable: true
25
- });
26
- }
27
- if (symbols) {
28
- __defProp(target, Symbol.toStringTag, { value: "Module" });
29
- }
30
- return target;
31
- };
32
-
33
- //#endregion
34
18
  //#region src/core/auth/schema.ts
35
19
  const AuthDataSchema = z.object({
36
20
  accessToken: z.string().min(1, "Token cannot be empty"),
@@ -92,134 +76,6 @@ var AuthValidationError = class extends Error {
92
76
  }
93
77
  };
94
78
 
95
- //#endregion
96
- //#region src/core/config.ts
97
- const PROJECT_SUBDIR = "base44";
98
- const FUNCTION_CONFIG_FILE = "function.jsonc";
99
- const AUTH_CLIENT_ID = "base44_cli";
100
- const DEFAULT_API_URL = "https://app.base44.com";
101
- function getBase44Dir() {
102
- return join(homedir(), ".base44");
103
- }
104
- function getAuthFilePath() {
105
- return join(getBase44Dir(), "auth", "auth.json");
106
- }
107
- function getProjectConfigPatterns() {
108
- return [
109
- `${PROJECT_SUBDIR}/config.jsonc`,
110
- `${PROJECT_SUBDIR}/config.json`,
111
- "config.jsonc",
112
- "config.json"
113
- ];
114
- }
115
- /**
116
- * Load .env.local from the project root if it exists.
117
- * Values won't override existing process.env variables.
118
- * Safe to call multiple times - only loads once.
119
- */
120
- async function loadProjectEnv(projectRoot) {
121
- const { findProjectRoot: findProjectRoot$1 } = await Promise.resolve().then(() => config_exports);
122
- const found = projectRoot ? { root: projectRoot } : await findProjectRoot$1();
123
- if (!found) return;
124
- config({
125
- path: join(found.root, ".env.local"),
126
- override: false
127
- });
128
- }
129
- /**
130
- * Get the Base44 API URL.
131
- * Priority: process.env.BASE44_API_URL > .env.local > default
132
- */
133
- function getBase44ApiUrl() {
134
- return process.env.BASE44_API_URL || DEFAULT_API_URL;
135
- }
136
- /**
137
- * Get the Base44 Client ID (app ID).
138
- * Priority: process.env.BASE44_CLIENT_ID > .env.local
139
- * Returns undefined if not set.
140
- */
141
- function getBase44ClientId() {
142
- return process.env.BASE44_CLIENT_ID;
143
- }
144
-
145
- //#endregion
146
- //#region src/core/auth/authClient.ts
147
- /**
148
- * Separate ky instance for OAuth endpoints.
149
- * These don't need Authorization headers (they use client_id + tokens in body).
150
- */
151
- const authClient = ky.create({
152
- prefixUrl: getBase44ApiUrl(),
153
- headers: { "User-Agent": "Base44 CLI" }
154
- });
155
- var authClient_default = authClient;
156
-
157
- //#endregion
158
- //#region src/core/auth/api.ts
159
- async function generateDeviceCode() {
160
- const response = await authClient_default.post("oauth/device/code", {
161
- json: {
162
- client_id: AUTH_CLIENT_ID,
163
- scope: "apps:read apps:write"
164
- },
165
- throwHttpErrors: false
166
- });
167
- if (!response.ok) throw new AuthApiError(`Failed to generate device code: ${response.status} ${response.statusText}`);
168
- const result = DeviceCodeResponseSchema.safeParse(await response.json());
169
- if (!result.success) throw new AuthValidationError(`Invalid device code response from server: ${result.error.message}`);
170
- return result.data;
171
- }
172
- async function getTokenFromDeviceCode(deviceCode) {
173
- const searchParams = new URLSearchParams();
174
- searchParams.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
175
- searchParams.set("device_code", deviceCode);
176
- searchParams.set("client_id", AUTH_CLIENT_ID);
177
- const response = await authClient_default.post("oauth/token", {
178
- body: searchParams.toString(),
179
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
180
- throwHttpErrors: false
181
- });
182
- const json = await response.json();
183
- if (!response.ok) {
184
- const errorResult = OAuthErrorSchema.safeParse(json);
185
- if (!errorResult.success) throw new AuthValidationError(`Token request failed: ${errorResult.error.message}`);
186
- const { error, error_description } = errorResult.data;
187
- if (error === "authorization_pending" || error === "slow_down") return null;
188
- throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
189
- }
190
- const result = TokenResponseSchema.safeParse(json);
191
- if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
192
- return result.data;
193
- }
194
- async function renewAccessToken(refreshToken) {
195
- const searchParams = new URLSearchParams();
196
- searchParams.set("grant_type", "refresh_token");
197
- searchParams.set("refresh_token", refreshToken);
198
- searchParams.set("client_id", AUTH_CLIENT_ID);
199
- const response = await authClient_default.post("oauth/token", {
200
- body: searchParams.toString(),
201
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
202
- throwHttpErrors: false
203
- });
204
- const json = await response.json();
205
- if (!response.ok) {
206
- const errorResult = OAuthErrorSchema.safeParse(json);
207
- if (!errorResult.success) throw new AuthApiError(`Token refresh failed: ${response.statusText}`);
208
- const { error, error_description } = errorResult.data;
209
- throw new AuthApiError(error_description ?? `OAuth error: ${error}`);
210
- }
211
- const result = TokenResponseSchema.safeParse(json);
212
- if (!result.success) throw new AuthValidationError(`Invalid token response from server: ${result.error.message}`);
213
- return result.data;
214
- }
215
- async function getUserInfo(accessToken) {
216
- const response = await authClient_default.get("oauth/userinfo", { headers: { Authorization: `Bearer ${accessToken}` } });
217
- if (!response.ok) throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
218
- const result = UserInfoSchema.safeParse(await response.json());
219
- if (!result.success) throw new AuthValidationError(`Invalid UserInfo response from server: ${result.error.message}`);
220
- return result.data;
221
- }
222
-
223
79
  //#endregion
224
80
  //#region src/core/utils/fs.ts
225
81
  async function pathExists(path) {
@@ -235,6 +91,11 @@ async function writeFile$1(filePath, content) {
235
91
  if (!await pathExists(dir)) await mkdir(dir, { recursive: true });
236
92
  await writeFile(filePath, content, "utf-8");
237
93
  }
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);
98
+ }
238
99
  async function readJsonFile(filePath) {
239
100
  if (!await pathExists(filePath)) throw new Error(`File not found: ${filePath}`);
240
101
  try {
@@ -261,6 +122,60 @@ async function deleteFile(filePath) {
261
122
  await unlink(filePath);
262
123
  }
263
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)));
177
+ }
178
+
264
179
  //#endregion
265
180
  //#region src/core/auth/config.ts
266
181
  const TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
@@ -328,260 +243,45 @@ async function refreshAndSaveTokens() {
328
243
  }
329
244
 
330
245
  //#endregion
331
- //#region src/cli/utils/runCommand.ts
332
- const base44Color = chalk.bgHex("#E86B3C");
246
+ //#region src/core/utils/httpClient.ts
247
+ const retriedRequests = /* @__PURE__ */ new WeakSet();
333
248
  /**
334
- * Wraps a command function with the Base44 intro banner.
335
- * All CLI commands should use this utility to ensure consistent branding.
336
- * Also loads .env.local from the project root if available.
337
- *
338
- * @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.
339
251
  */
340
- async function runCommand(commandFn) {
341
- intro(base44Color(" Base 44 "));
342
- await loadProjectEnv();
343
- try {
344
- await commandFn();
345
- } catch (e) {
346
- if (e instanceof Error) log.error(e.stack ?? e.message);
347
- else log.error(String(e));
348
- process.exit(1);
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]
349
278
  }
350
- }
351
-
352
- //#endregion
353
- //#region src/cli/utils/runTask.ts
354
- /**
355
- * Wraps an async operation with automatic spinner management.
356
- * The spinner is automatically started, and stopped on both success and error.
357
- *
358
- * @param startMessage - Message to show when spinner starts
359
- * @param operation - The async operation to execute
360
- * @param options - Optional configuration
361
- * @returns The result of the operation
362
- */
363
- async function runTask(startMessage, operation, options) {
364
- const s = spinner();
365
- s.start(startMessage);
366
- try {
367
- const result = await operation();
368
- s.stop(options?.successMessage || startMessage);
369
- return result;
370
- } catch (error) {
371
- s.stop(options?.errorMessage || "Failed");
372
- throw error;
373
- }
374
- }
375
-
376
- //#endregion
377
- //#region src/cli/utils/prompts.ts
378
- /**
379
- * Handles prompt cancellation by exiting gracefully.
380
- */
381
- function handleCancel(value) {
382
- if (isCancel(value)) {
383
- cancel("Operation cancelled.");
384
- process.exit(0);
385
- }
386
- }
387
- /**
388
- * Wrapper around @clack/prompts text() that handles cancellation automatically.
389
- * Returns the string value directly, exits process if cancelled.
390
- */
391
- async function textPrompt(options) {
392
- const value = await text(options);
393
- handleCancel(value);
394
- return value;
395
- }
396
-
397
- //#endregion
398
- //#region src/cli/utils/banner.ts
399
- const orange = chalk.hex("#E86B3C");
400
- const BANNER = `
401
- ${orange("██████╗ █████╗ ███████╗███████╗ ██╗ ██╗██╗ ██╗")}
402
- ${orange("██╔══██╗██╔══██╗██╔════╝██╔════╝ ██║ ██║██║ ██║")}
403
- ${orange("██████╔╝███████║███████╗█████╗ ███████║███████║")}
404
- ${orange("██╔══██╗██╔══██║╚════██║██╔══╝ ╚════██║╚════██║")}
405
- ${orange("██████╔╝██║ ██║███████║███████╗ ██║ ██║")}
406
- ${orange("╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝")}
407
- `;
408
- function printBanner() {
409
- console.log(BANNER);
410
- }
411
-
412
- //#endregion
413
- //#region src/cli/commands/auth/login.ts
414
- async function generateAndDisplayDeviceCode() {
415
- const deviceCodeResponse = await runTask("Generating device code...", async () => {
416
- return await generateDeviceCode();
417
- }, {
418
- successMessage: "Device code generated",
419
- errorMessage: "Failed to generate device code"
420
- });
421
- log.info(`Your code is: ${chalk.bold(deviceCodeResponse.userCode)}\nPlease visit: ${deviceCodeResponse.verificationUriComplete}`);
422
- return deviceCodeResponse;
423
- }
424
- async function waitForAuthentication(deviceCode, expiresIn, interval) {
425
- let tokenResponse;
426
- try {
427
- await runTask("Waiting for you to complete authentication...", async () => {
428
- await pWaitFor(async () => {
429
- const result = await getTokenFromDeviceCode(deviceCode);
430
- if (result !== null) {
431
- tokenResponse = result;
432
- return true;
433
- }
434
- return false;
435
- }, {
436
- interval: interval * 1e3,
437
- timeout: expiresIn * 1e3
438
- });
439
- }, {
440
- successMessage: "Authentication completed!",
441
- errorMessage: "Authentication failed"
442
- });
443
- } catch (error) {
444
- if (error instanceof Error && error.message.includes("timed out")) throw new Error("Authentication timed out. Please try again.");
445
- throw error;
446
- }
447
- if (tokenResponse === void 0) throw new Error("Failed to retrieve authentication token.");
448
- return tokenResponse;
449
- }
450
- async function saveAuthData(response, userInfo) {
451
- const expiresAt = Date.now() + response.expiresIn * 1e3;
452
- await writeAuth({
453
- accessToken: response.accessToken,
454
- refreshToken: response.refreshToken,
455
- expiresAt,
456
- email: userInfo.email,
457
- name: userInfo.name
458
- });
459
- }
460
- async function login() {
461
- const deviceCodeResponse = await generateAndDisplayDeviceCode();
462
- const token = await waitForAuthentication(deviceCodeResponse.deviceCode, deviceCodeResponse.expiresIn, deviceCodeResponse.interval);
463
- const userInfo = await getUserInfo(token.accessToken);
464
- await saveAuthData(token, userInfo);
465
- log.success(`Successfully logged in as ${chalk.bold(userInfo.email)}`);
466
- }
467
- const loginCommand = new Command("login").description("Authenticate with Base44").action(async () => {
468
- await runCommand(login);
469
- });
470
-
471
- //#endregion
472
- //#region src/cli/commands/auth/whoami.ts
473
- async function whoami() {
474
- const auth = await readAuth();
475
- log.info(`Logged in as: ${auth.name} (${auth.email})`);
476
- }
477
- const whoamiCommand = new Command("whoami").description("Display current authenticated user").action(async () => {
478
- await runCommand(whoami);
479
- });
480
-
481
- //#endregion
482
- //#region src/cli/commands/auth/logout.ts
483
- async function logout() {
484
- await deleteAuth();
485
- log.info("Logged out successfully");
486
- }
487
- const logoutCommand = new Command("logout").description("Logout from current device").action(async () => {
488
- await runCommand(logout);
489
- });
490
-
491
- //#endregion
492
- //#region src/core/resources/entity/schema.ts
493
- const EntityPropertySchema = z.object({
494
- type: z.string(),
495
- description: z.string().optional(),
496
- enum: z.array(z.string()).optional(),
497
- default: z.union([
498
- z.string(),
499
- z.number(),
500
- z.boolean()
501
- ]).optional(),
502
- format: z.string().optional(),
503
- items: z.any().optional(),
504
- relation: z.object({
505
- entity: z.string(),
506
- type: z.string()
507
- }).optional()
508
- });
509
- const EntityPoliciesSchema = z.object({
510
- read: z.string().optional(),
511
- create: z.string().optional(),
512
- update: z.string().optional(),
513
- delete: z.string().optional()
514
- });
515
- const EntitySchema = z.object({
516
- name: z.string().min(1, "Entity name cannot be empty"),
517
- type: z.literal("object"),
518
- properties: z.record(z.string(), EntityPropertySchema),
519
- required: z.array(z.string()).optional(),
520
- policies: EntityPoliciesSchema.optional()
521
- });
522
- const SyncEntitiesResponseSchema = z.object({
523
- created: z.array(z.string()),
524
- updated: z.array(z.string()),
525
- deleted: z.array(z.string())
526
- });
527
-
528
- //#endregion
529
- //#region src/core/resources/entity/config.ts
530
- async function readEntityFile(entityPath) {
531
- const parsed = await readJsonFile(entityPath);
532
- const result = EntitySchema.safeParse(parsed);
533
- if (!result.success) throw new Error(`Invalid entity configuration in ${entityPath}: ${result.error.issues.map((e) => e.message).join(", ")}`);
534
- return result.data;
535
- }
536
- async function readAllEntities(entitiesDir) {
537
- if (!await pathExists(entitiesDir)) return [];
538
- const files = await globby("*.{json,jsonc}", {
539
- cwd: entitiesDir,
540
- absolute: true
541
- });
542
- return await Promise.all(files.map((filePath) => readEntityFile(filePath)));
543
- }
544
-
545
- //#endregion
546
- //#region src/core/utils/httpClient.ts
547
- const retriedRequests = /* @__PURE__ */ new WeakSet();
548
- /**
549
- * Handles 401 responses by refreshing the token and retrying the request.
550
- * Only retries once per request to prevent infinite loops.
551
- */
552
- async function handleUnauthorized(request, _options, response) {
553
- if (response.status !== 401) return;
554
- if (retriedRequests.has(request)) return;
555
- const newAccessToken = await refreshAndSaveTokens();
556
- if (!newAccessToken) return;
557
- retriedRequests.add(request);
558
- return ky(request, { headers: { Authorization: `Bearer ${newAccessToken}` } });
559
- }
560
- const base44Client = ky.create({
561
- prefixUrl: getBase44ApiUrl(),
562
- headers: { "User-Agent": "Base44 CLI" },
563
- hooks: {
564
- beforeRequest: [async (request) => {
565
- try {
566
- const auth = await readAuth();
567
- if (isTokenExpired(auth)) {
568
- const newAccessToken = await refreshAndSaveTokens();
569
- if (newAccessToken) {
570
- request.headers.set("Authorization", `Bearer ${newAccessToken}`);
571
- return;
572
- }
573
- }
574
- request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
575
- } catch {}
576
- }],
577
- afterResponse: [handleUnauthorized]
578
- }
579
- });
580
- /**
581
- * Returns an HTTP client scoped to the current app.
582
- */
583
- function getAppClient() {
584
- return base44Client.extend({ prefixUrl: new URL(`/api/apps/${getBase44ClientId()}/`, getBase44ApiUrl()).href });
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 });
585
285
  }
586
286
 
587
287
  //#endregion
@@ -668,11 +368,6 @@ const functionResource = { readAll: readAllFunctions };
668
368
 
669
369
  //#endregion
670
370
  //#region src/core/project/config.ts
671
- var config_exports = /* @__PURE__ */ __exportAll({
672
- ProjectConfigSchema: () => ProjectConfigSchema,
673
- findProjectRoot: () => findProjectRoot,
674
- readProjectConfig: () => readProjectConfig
675
- });
676
371
  const ProjectConfigSchema = z.looseObject({
677
372
  name: z.string().min(1, "Project name cannot be empty"),
678
373
  entitiesDir: z.string().default("./entities"),
@@ -727,24 +422,15 @@ async function readProjectConfig(projectRoot) {
727
422
  };
728
423
  }
729
424
 
730
- //#endregion
731
- //#region src/cli/commands/project/show-project.ts
732
- async function showProject() {
733
- const projectData = await runTask("Reading project configuration", async () => {
734
- return await readProjectConfig();
735
- }, {
736
- successMessage: "Project configuration loaded",
737
- errorMessage: "Failed to load project configuration"
738
- });
739
- const jsonOutput = JSON.stringify(projectData, null, 2);
740
- log.info(jsonOutput);
741
- }
742
- const showProjectCommand = new Command("show-project").description("Display project configuration, entities, and functions").action(async () => {
743
- await runCommand(showProject);
744
- });
745
-
746
425
  //#endregion
747
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) });
748
434
  const SiteConfigSchema = z.object({
749
435
  buildCommand: z.string().optional(),
750
436
  serveCommand: z.string().optional(),
@@ -771,98 +457,412 @@ async function createProject(projectName, description) {
771
457
  }
772
458
 
773
459
  //#endregion
774
- //#region src/core/project/templates/index.ts
775
- const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), "templates");
776
- const CONFIG_TEMPLATE_PATH = join(TEMPLATES_DIR, "config.jsonc.ejs");
777
- const ENV_TEMPLATE_PATH = join(TEMPLATES_DIR, "env.local.ejs");
778
- async function renderConfigTemplate(data) {
779
- return ejs.renderFile(CONFIG_TEMPLATE_PATH, data);
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;
780
464
  }
781
- async function renderEnvTemplate(data) {
782
- return ejs.renderFile(ENV_TEMPLATE_PATH, data);
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
+ }
783
482
  }
784
483
 
785
484
  //#endregion
786
- //#region src/core/project/init.ts
787
- /**
788
- * Initialize a new Base44 project.
789
- * Creates the base44 directory, config.jsonc, and .env.local files.
790
- */
791
- async function initProject(options) {
792
- const { name, description, path: basePath } = options;
793
- const projectDir = join(basePath, PROJECT_SUBDIR);
794
- const configPath = join(projectDir, "config.jsonc");
795
- const envPath = join(projectDir, ".env.local");
485
+ //#region src/core/project/create.ts
486
+ async function createProjectFiles(options) {
487
+ const { name, description, path: basePath, template } = options;
796
488
  const existingConfigs = await globby(getProjectConfigPatterns(), {
797
489
  cwd: basePath,
798
490
  absolute: true
799
491
  });
800
492
  if (existingConfigs.length > 0) throw new Error(`A Base44 project already exists at ${existingConfigs[0]}. Please choose a different location.`);
801
493
  const { projectId } = await createProject(name, description);
802
- await writeFile$1(configPath, await renderConfigTemplate({
494
+ await renderTemplate(template, basePath, {
803
495
  name,
804
- description
805
- }));
806
- await writeFile$1(envPath, await renderEnvTemplate({ projectId }));
807
- return {
808
- projectId,
809
- projectDir,
810
- configPath,
811
- envPath
812
- };
496
+ description,
497
+ projectId
498
+ });
499
+ return { projectDir: basePath };
813
500
  }
814
501
 
815
502
  //#endregion
816
- //#region src/cli/commands/entities/push.ts
817
- async function pushEntitiesAction() {
818
- const { entities } = await readProjectConfig();
819
- if (entities.length === 0) {
820
- log.warn("No entities found in project");
821
- return;
822
- }
823
- log.info(`Found ${entities.length} entities to push`);
824
- const result = await runTask("Pushing entities to Base44", async () => {
825
- return await pushEntities(entities);
826
- }, {
827
- successMessage: "Entities pushed successfully",
828
- errorMessage: "Failed to push entities"
829
- });
830
- if (result.created.length > 0) log.success(`Created: ${result.created.join(", ")}`);
831
- if (result.updated.length > 0) log.success(`Updated: ${result.updated.join(", ")}`);
832
- if (result.deleted.length > 0) log.warn(`Deleted: ${result.deleted.join(", ")}`);
833
- if (result.created.length === 0 && result.updated.length === 0 && result.deleted.length === 0) log.info("No changes detected");
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
+
784
+ //#endregion
785
+ //#region src/cli/commands/project/show-project.ts
786
+ async function showProject() {
787
+ const projectData = await runTask("Reading project configuration", async () => {
788
+ return await readProjectConfig();
789
+ }, {
790
+ successMessage: "Project configuration loaded",
791
+ errorMessage: "Failed to load project configuration"
792
+ });
793
+ const jsonOutput = JSON.stringify(projectData, null, 2);
794
+ log.info(jsonOutput);
795
+ }
796
+ const showProjectCommand = new Command("show-project").description("Display project configuration, entities, and functions").action(async () => {
797
+ await runCommand(showProject);
798
+ });
799
+
800
+ //#endregion
801
+ //#region src/cli/commands/entities/push.ts
802
+ async function pushEntitiesAction() {
803
+ const { entities } = await readProjectConfig();
804
+ if (entities.length === 0) {
805
+ log.warn("No entities found in project");
806
+ return;
807
+ }
808
+ log.info(`Found ${entities.length} entities to push`);
809
+ const result = await runTask("Pushing entities to Base44", async () => {
810
+ return await pushEntities(entities);
811
+ }, {
812
+ successMessage: "Entities pushed successfully",
813
+ errorMessage: "Failed to push entities"
814
+ });
815
+ if (result.created.length > 0) log.success(`Created: ${result.created.join(", ")}`);
816
+ if (result.updated.length > 0) log.success(`Updated: ${result.updated.join(", ")}`);
817
+ if (result.deleted.length > 0) log.warn(`Deleted: ${result.deleted.join(", ")}`);
818
+ if (result.created.length === 0 && result.updated.length === 0 && result.deleted.length === 0) log.info("No changes detected");
834
819
  }
835
820
  const entitiesPushCommand = new Command("entities").description("Manage project entities").addCommand(new Command("push").description("Push local entities to Base44").action(async () => {
836
821
  await runCommand(pushEntitiesAction);
837
822
  }));
838
823
 
839
824
  //#endregion
840
- //#region src/cli/commands/project/init.ts
841
- async function init() {
825
+ //#region src/cli/commands/project/create.ts
826
+ async function create() {
842
827
  printBanner();
843
828
  await loadProjectEnv();
844
- const name = await textPrompt({
845
- message: "What is the name of your project?",
846
- placeholder: "my-app-backend",
847
- validate: (value) => {
848
- if (!value || value.trim().length === 0) return "Project name is required";
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
+ });
849
857
  }
850
- });
851
- const description = await textPrompt({
852
- message: "Project description (optional)",
853
- placeholder: "A brief description of your project"
854
- });
855
- const defaultPath = "./";
856
- const resolvedPath = resolve(await textPrompt({
857
- message: "Where should we create the base44 folder?",
858
- placeholder: defaultPath,
859
- initialValue: defaultPath
860
- }) || defaultPath);
858
+ }, { onCancel: onPromptCancel });
859
+ const resolvedPath = resolve(projectPath);
861
860
  await runTask("Creating project...", async () => {
862
- return await initProject({
861
+ return await createProjectFiles({
863
862
  name: name.trim(),
864
863
  description: description ? description.trim() : void 0,
865
- path: resolvedPath
864
+ path: resolvedPath,
865
+ template
866
866
  });
867
867
  }, {
868
868
  successMessage: "Project created successfully",
@@ -870,9 +870,9 @@ async function init() {
870
870
  });
871
871
  log.success(`Project ${chalk.bold(name)} has been initialized!`);
872
872
  }
873
- const initCommand = new Command("init").alias("create").description("Initialize a new Base44 project").action(async () => {
873
+ const createCommand = new Command("create").description("Create a new Base44 project").action(async () => {
874
874
  try {
875
- await init();
875
+ await create();
876
876
  } catch (e) {
877
877
  if (e instanceof Error) log.error(e.stack ?? e.message);
878
878
  else log.error(String(e));
@@ -891,10 +891,10 @@ program.name("base44").description("Base44 CLI - Unified interface for managing
891
891
  program.addCommand(loginCommand);
892
892
  program.addCommand(whoamiCommand);
893
893
  program.addCommand(logoutCommand);
894
- program.addCommand(initCommand);
894
+ program.addCommand(createCommand);
895
895
  program.addCommand(showProjectCommand);
896
896
  program.addCommand(entitiesPushCommand);
897
897
  program.parse();
898
898
 
899
899
  //#endregion
900
- export { findProjectRoot as n, readProjectConfig as r, ProjectConfigSchema as t };
900
+ export { };