@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.
- package/dist/cli/index.js +345 -147
- 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/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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(`
|
|
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:
|
|
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(
|
|
369
|
+
async function saveAuthData(response, userInfo) {
|
|
370
|
+
const expiresAt = Date.now() + response.expiresIn * 1e3;
|
|
287
371
|
await writeAuth({
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
297
|
-
|
|
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 = {
|
|
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
|
-
|
|
439
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
},
|