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