@base44-preview/cli 0.0.1-pr.11.ccd1624 → 0.0.1-pr.13.61a06bf

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 (2) hide show
  1. package/dist/cli/index.js +345 -147
  2. package/package.json +2 -1
package/dist/cli/index.js CHANGED
@@ -1,22 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { readFileSync } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- import { dirname, join } from "node:path";
6
- import { intro, log, spinner } from "@clack/prompts";
7
3
  import chalk from "chalk";
4
+ import { intro, log, spinner } from "@clack/prompts";
8
5
  import pWaitFor from "p-wait-for";
9
6
  import { z } from "zod";
7
+ import { dirname, join } from "node:path";
10
8
  import { homedir } from "node:os";
9
+ import ky from "ky";
11
10
  import { access, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
12
11
  import { parse, printParseErrorCode } from "jsonc-parser";
13
12
  import { globby } from "globby";
14
13
 
15
- //#region src/cli/utils/packageVersion.ts
16
- function getPackageVersion() {
17
- const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
18
- return JSON.parse(readFileSync(packageJsonPath, "utf-8")).version;
19
- }
14
+ //#region src/core/auth/schema.ts
15
+ const AuthDataSchema = z.object({
16
+ accessToken: z.string().min(1, "Token cannot be empty"),
17
+ refreshToken: z.string().min(1, "Refresh token cannot be empty"),
18
+ expiresAt: z.number().int().positive("Expires at must be a positive integer"),
19
+ email: z.email(),
20
+ name: z.string().min(1, "Name cannot be empty")
21
+ });
22
+ const DeviceCodeResponseSchema = z.object({
23
+ device_code: z.string().min(1, "Device code cannot be empty"),
24
+ user_code: z.string().min(1, "User code cannot be empty"),
25
+ verification_uri: z.url("Invalid verification URL"),
26
+ verification_uri_complete: z.url("Invalid complete verification URL"),
27
+ expires_in: z.number().int().positive("Expires in must be a positive integer"),
28
+ interval: z.number().int().positive("Interval in must be a positive integer")
29
+ }).transform((data) => ({
30
+ deviceCode: data.device_code,
31
+ userCode: data.user_code,
32
+ verificationUri: data.verification_uri,
33
+ verificationUriComplete: data.verification_uri_complete,
34
+ expiresIn: data.expires_in,
35
+ interval: data.interval
36
+ }));
37
+ const TokenResponseSchema = z.object({
38
+ access_token: z.string().min(1, "Token cannot be empty"),
39
+ token_type: z.string().min(1, "Token type cannot be empty"),
40
+ expires_in: z.number().int().positive("Expires in must be a positive integer"),
41
+ refresh_token: z.string().min(1, "Refresh token cannot be empty"),
42
+ scope: z.string().optional()
43
+ }).transform((data) => ({
44
+ accessToken: data.access_token,
45
+ tokenType: data.token_type,
46
+ expiresIn: data.expires_in,
47
+ refreshToken: data.refresh_token,
48
+ scope: data.scope
49
+ }));
50
+ const OAuthErrorSchema = z.object({
51
+ error: z.string(),
52
+ error_description: z.string().optional()
53
+ });
54
+ const UserInfoSchema = z.object({
55
+ email: z.email(),
56
+ name: z.string()
57
+ });
20
58
 
21
59
  //#endregion
22
60
  //#region src/core/errors.ts
@@ -28,134 +66,12 @@ var AuthApiError = class extends Error {
28
66
  }
29
67
  };
30
68
  var AuthValidationError = class extends Error {
31
- constructor(message, issues) {
69
+ constructor(message) {
32
70
  super(message);
33
- this.issues = issues;
34
71
  this.name = "AuthValidationError";
35
72
  }
36
73
  };
37
74
 
38
- //#endregion
39
- //#region src/cli/utils/runCommand.ts
40
- const base44Color = chalk.bgHex("#E86B3C");
41
- /**
42
- * Wraps a command function with the Base44 intro banner.
43
- * All CLI commands should use this utility to ensure consistent branding.
44
- *
45
- * @param commandFn - The async function to execute as the command
46
- */
47
- async function runCommand(commandFn) {
48
- intro(base44Color(" Base 44 "));
49
- try {
50
- await commandFn();
51
- } catch (e) {
52
- if (e instanceof AuthValidationError) {
53
- const issues = e.issues.map((i) => i.message).join(", ");
54
- log.error(`Invalid response from server: ${issues}`);
55
- } else if (e instanceof AuthApiError || e instanceof Error) log.error(e.message);
56
- else log.error(String(e));
57
- process.exit(1);
58
- }
59
- }
60
-
61
- //#endregion
62
- //#region src/cli/utils/runTask.ts
63
- /**
64
- * Wraps an async operation with automatic spinner management.
65
- * The spinner is automatically started, and stopped on both success and error.
66
- *
67
- * @param startMessage - Message to show when spinner starts
68
- * @param operation - The async operation to execute
69
- * @param options - Optional configuration
70
- * @returns The result of the operation
71
- */
72
- async function runTask(startMessage, operation, options) {
73
- const s = spinner();
74
- s.start(startMessage);
75
- try {
76
- const result = await operation();
77
- s.stop(options?.successMessage || startMessage);
78
- return result;
79
- } catch (error) {
80
- s.stop(options?.errorMessage || "Failed");
81
- throw error;
82
- }
83
- }
84
-
85
- //#endregion
86
- //#region src/core/auth/schema.ts
87
- const AuthDataSchema = z.object({
88
- token: z.string().min(1, "Token cannot be empty"),
89
- email: z.email(),
90
- name: z.string().min(1, "Name cannot be empty")
91
- });
92
- const DeviceCodeResponseSchema = z.object({
93
- deviceCode: z.string().min(1, "Device code cannot be empty"),
94
- userCode: z.string().min(1, "User code cannot be empty"),
95
- verificationUrl: z.url("Invalid verification URL"),
96
- expiresIn: z.number().int().positive("Expires in must be a positive integer")
97
- });
98
- const TokenResponseSchema = z.object({
99
- token: z.string().min(1, "Token cannot be empty"),
100
- email: z.email("Invalid email address"),
101
- name: z.string().min(1, "Name cannot be empty")
102
- });
103
-
104
- //#endregion
105
- //#region src/core/auth/api.ts
106
- async function delay(ms) {
107
- return new Promise((resolve) => setTimeout(resolve, ms));
108
- }
109
- const deviceCodeToTokenMap = /* @__PURE__ */ new Map();
110
- async function generateDeviceCode() {
111
- try {
112
- await delay(1e3);
113
- const deviceCode = `device-code-${Date.now()}`;
114
- deviceCodeToTokenMap.set(deviceCode, {
115
- startTime: Date.now(),
116
- readyAfter: 5e3
117
- });
118
- const mockResponse = {
119
- deviceCode,
120
- userCode: "ABCD-1234",
121
- verificationUrl: "https://app.base44.com/verify",
122
- expiresIn: 600
123
- };
124
- const result = DeviceCodeResponseSchema.safeParse(mockResponse);
125
- if (!result.success) throw new AuthValidationError("Invalid device code response from server", result.error.issues.map((issue) => ({
126
- message: issue.message,
127
- path: issue.path.map(String)
128
- })));
129
- return result.data;
130
- } catch (error) {
131
- if (error instanceof AuthValidationError) throw error;
132
- throw new AuthApiError("Failed to generate device code", error instanceof Error ? error : new Error(String(error)));
133
- }
134
- }
135
- async function getTokenFromDeviceCode(deviceCode) {
136
- try {
137
- await delay(1e3);
138
- const deviceInfo = deviceCodeToTokenMap.get(deviceCode);
139
- if (!deviceInfo) return null;
140
- if (Date.now() - deviceInfo.startTime < deviceInfo.readyAfter) return null;
141
- const mockResponse = {
142
- token: `mock-token-${Date.now()}`,
143
- email: "shahart@base44.com",
144
- name: "Shahar Talmi"
145
- };
146
- const result = TokenResponseSchema.safeParse(mockResponse);
147
- if (!result.success) throw new AuthValidationError("Invalid token response from server", result.error.issues.map((issue) => ({
148
- message: issue.message,
149
- path: issue.path.map(String)
150
- })));
151
- deviceCodeToTokenMap.delete(deviceCode);
152
- return result.data;
153
- } catch (error) {
154
- if (error instanceof AuthValidationError || error instanceof AuthApiError) throw error;
155
- throw new AuthApiError("Failed to retrieve token from device code", error instanceof Error ? error : new Error(String(error)));
156
- }
157
- }
158
-
159
75
  //#endregion
160
76
  //#region src/core/consts.ts
161
77
  const PROJECT_SUBDIR = "base44";
@@ -174,6 +90,94 @@ function getProjectConfigPatterns() {
174
90
  "config.json"
175
91
  ];
176
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}`);
169
+ }
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
+ }
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;
180
+ }
177
181
 
178
182
  //#endregion
179
183
  //#region src/core/utils/fs.ts
@@ -216,6 +220,8 @@ async function deleteFile(filePath) {
216
220
 
217
221
  //#endregion
218
222
  //#region src/core/auth/config.ts
223
+ const TOKEN_REFRESH_BUFFER_MS = 60 * 1e3;
224
+ let refreshPromise = null;
219
225
  async function readAuth() {
220
226
  try {
221
227
  const parsed = await readJsonFile(getAuthFilePath());
@@ -244,6 +250,83 @@ async function deleteAuth() {
244
250
  throw new Error(`Failed to delete authentication file: ${error instanceof Error ? error.message : "Unknown error"}`);
245
251
  }
246
252
  }
253
+ /**
254
+ * Checks if the access token is expired or about to expire.
255
+ */
256
+ function isTokenExpired(auth) {
257
+ return Date.now() >= auth.expiresAt - TOKEN_REFRESH_BUFFER_MS;
258
+ }
259
+ /**
260
+ * Refreshes the access token and saves the new tokens.
261
+ * Returns the new access token, or null if refresh failed.
262
+ * Uses a lock to prevent concurrent refresh requests.
263
+ */
264
+ async function refreshAndSaveTokens() {
265
+ if (refreshPromise) return refreshPromise;
266
+ refreshPromise = (async () => {
267
+ try {
268
+ const auth = await readAuth();
269
+ const tokenResponse = await renewAccessToken(auth.refreshToken);
270
+ await writeAuth({
271
+ ...auth,
272
+ accessToken: tokenResponse.accessToken,
273
+ refreshToken: tokenResponse.refreshToken,
274
+ expiresAt: Date.now() + tokenResponse.expiresIn * 1e3
275
+ });
276
+ return tokenResponse.accessToken;
277
+ } catch {
278
+ await deleteAuth();
279
+ return null;
280
+ } finally {
281
+ refreshPromise = null;
282
+ }
283
+ })();
284
+ return refreshPromise;
285
+ }
286
+
287
+ //#endregion
288
+ //#region src/cli/utils/runCommand.ts
289
+ const base44Color = chalk.bgHex("#E86B3C");
290
+ /**
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
295
+ */
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
+ }
305
+ }
306
+
307
+ //#endregion
308
+ //#region src/cli/utils/runTask.ts
309
+ /**
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
317
+ */
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
+ }
329
+ }
247
330
 
248
331
  //#endregion
249
332
  //#region src/cli/commands/auth/login.ts
@@ -254,10 +337,10 @@ async function generateAndDisplayDeviceCode() {
254
337
  successMessage: "Device code generated",
255
338
  errorMessage: "Failed to generate device code"
256
339
  });
257
- log.info(`Please visit: ${deviceCodeResponse.verificationUrl}\nEnter your device code: ${deviceCodeResponse.userCode}`);
340
+ log.info(`Your code is: ${chalk.bold(deviceCodeResponse.userCode)}\nPlease visit: ${deviceCodeResponse.verificationUriComplete}`);
258
341
  return deviceCodeResponse;
259
342
  }
260
- async function waitForAuthentication(deviceCode, expiresIn) {
343
+ async function waitForAuthentication(deviceCode, expiresIn, interval) {
261
344
  let tokenResponse;
262
345
  try {
263
346
  await runTask("Waiting for you to complete authentication...", async () => {
@@ -269,7 +352,7 @@ async function waitForAuthentication(deviceCode, expiresIn) {
269
352
  }
270
353
  return false;
271
354
  }, {
272
- interval: 2e3,
355
+ interval: interval * 1e3,
273
356
  timeout: expiresIn * 1e3
274
357
  });
275
358
  }, {
@@ -283,18 +366,22 @@ async function waitForAuthentication(deviceCode, expiresIn) {
283
366
  if (tokenResponse === void 0) throw new Error("Failed to retrieve authentication token.");
284
367
  return tokenResponse;
285
368
  }
286
- async function saveAuthData(token) {
369
+ async function saveAuthData(response, userInfo) {
370
+ const expiresAt = Date.now() + response.expiresIn * 1e3;
287
371
  await writeAuth({
288
- token: token.token,
289
- email: token.email,
290
- name: token.name
372
+ accessToken: response.accessToken,
373
+ refreshToken: response.refreshToken,
374
+ expiresAt,
375
+ email: userInfo.email,
376
+ name: userInfo.name
291
377
  });
292
378
  }
293
379
  async function login() {
294
380
  const deviceCodeResponse = await generateAndDisplayDeviceCode();
295
- const token = await waitForAuthentication(deviceCodeResponse.deviceCode, deviceCodeResponse.expiresIn);
296
- await saveAuthData(token);
297
- log.success(`Logged in as ${token.name}`);
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)}`);
298
385
  }
299
386
  const loginCommand = new Command("login").description("Authenticate with Base44").action(async () => {
300
387
  await runCommand(login);
@@ -351,6 +438,11 @@ const EntitySchema = z.object({
351
438
  required: z.array(z.string()).optional(),
352
439
  policies: EntityPoliciesSchema.optional()
353
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
+ });
354
446
 
355
447
  //#endregion
356
448
  //#region src/core/resources/entity/config.ts
@@ -369,9 +461,71 @@ async function readAllEntities(entitiesDir) {
369
461
  return await Promise.all(files.map((filePath) => readEntityFile(filePath)));
370
462
  }
371
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());
521
+ }
522
+
372
523
  //#endregion
373
524
  //#region src/core/resources/entity/resource.ts
374
- const entityResource = { readAll: readAllEntities };
525
+ const entityResource = {
526
+ readAll: readAllEntities,
527
+ push: pushEntities
528
+ };
375
529
 
376
530
  //#endregion
377
531
  //#region src/core/resources/function/schema.ts
@@ -435,8 +589,8 @@ const functionResource = { readAll: readAllFunctions };
435
589
  //#region src/core/config/project.ts
436
590
  const ProjectConfigSchema = z.looseObject({
437
591
  name: z.string().min(1, "Project name cannot be empty"),
438
- entitySrc: z.string().default("./entities"),
439
- functionSrc: z.string().default("./functions")
592
+ entitiesDir: z.string().default("./entities"),
593
+ functionsDir: z.string().default("./functions")
440
594
  });
441
595
  async function findConfigInDir(dir) {
442
596
  return (await globby(getProjectConfigPatterns(), {
@@ -475,7 +629,7 @@ async function readProjectConfig(projectRoot) {
475
629
  }
476
630
  const project = result.data;
477
631
  const configDir = dirname(configPath);
478
- const [entities, functions] = await Promise.all([entityResource.readAll(join(configDir, project.entitySrc)), functionResource.readAll(join(configDir, project.functionSrc))]);
632
+ const [entities, functions] = await Promise.all([entityResource.readAll(join(configDir, project.entitiesDir)), functionResource.readAll(join(configDir, project.functionsDir))]);
479
633
  return {
480
634
  project: {
481
635
  ...project,
@@ -503,14 +657,58 @@ const showProjectCommand = new Command("show-project").description("Display proj
503
657
  await runCommand(showProject);
504
658
  });
505
659
 
660
+ //#endregion
661
+ //#region src/core/config/app.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
+ //#endregion
676
+ //#region src/cli/commands/entities/push.ts
677
+ async function pushEntitiesAction() {
678
+ const { entities } = await readProjectConfig();
679
+ if (entities.length === 0) {
680
+ log.warn("No entities found in project");
681
+ return;
682
+ }
683
+ log.info(`Found ${entities.length} entities to push`);
684
+ const result = await runTask("Pushing entities to Base44", async () => {
685
+ return await pushEntities(entities);
686
+ }, {
687
+ successMessage: "Entities pushed successfully",
688
+ errorMessage: "Failed to push entities"
689
+ });
690
+ if (result.created.length > 0) log.success(`Created: ${result.created.join(", ")}`);
691
+ if (result.updated.length > 0) log.success(`Updated: ${result.updated.join(", ")}`);
692
+ if (result.deleted.length > 0) log.warn(`Deleted: ${result.deleted.join(", ")}`);
693
+ if (result.created.length === 0 && result.updated.length === 0 && result.deleted.length === 0) log.info("No changes detected");
694
+ }
695
+ const entitiesPushCommand = new Command("entities").description("Manage project entities").addCommand(new Command("push").description("Push local entities to Base44").action(async () => {
696
+ await runCommand(pushEntitiesAction);
697
+ }));
698
+
699
+ //#endregion
700
+ //#region package.json
701
+ var version = "0.0.1";
702
+
506
703
  //#endregion
507
704
  //#region src/cli/index.ts
508
705
  const program = new Command();
509
- program.name("base44").description("Base44 CLI - Unified interface for managing Base44 applications").version(getPackageVersion());
706
+ program.name("base44").description("Base44 CLI - Unified interface for managing Base44 applications").version(version);
510
707
  program.addCommand(loginCommand);
511
708
  program.addCommand(whoamiCommand);
512
709
  program.addCommand(logoutCommand);
513
710
  program.addCommand(showProjectCommand);
711
+ program.addCommand(entitiesPushCommand);
514
712
  program.parse();
515
713
 
516
714
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/cli",
3
- "version": "0.0.1-pr.11.ccd1624",
3
+ "version": "0.0.1-pr.13.61a06bf",
4
4
  "description": "Base44 CLI - Unified interface for managing Base44 applications",
5
5
  "type": "module",
6
6
  "main": "./dist/cli/index.js",
@@ -38,6 +38,7 @@
38
38
  "commander": "^12.1.0",
39
39
  "globby": "^16.1.0",
40
40
  "jsonc-parser": "^3.3.1",
41
+ "ky": "^1.14.2",
41
42
  "p-wait-for": "^6.0.0",
42
43
  "zod": "^4.3.5"
43
44
  },