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