buildx-cli 1.0.8 → 1.0.10
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/.github/workflows/auto-publish.yml +254 -0
- package/.github/workflows/create-pr.yml +182 -0
- package/.prettierrc +8 -0
- package/README.md +316 -36
- package/eslint.config.mjs +115 -0
- package/jest.config.cjs +16 -0
- package/package.json +23 -1
- package/rollup.config.mjs +64 -0
- package/scripts/prepare-publish.js +12 -0
- package/src/__tests__/config.test.ts +102 -0
- package/src/__tests__/schema-types-convert.test.ts +147 -0
- package/src/commands/auth/login.ts +148 -0
- package/src/commands/auth/logout.ts +16 -0
- package/src/commands/auth/status.ts +52 -0
- package/src/commands/config/clear.ts +16 -0
- package/src/commands/config/index.ts +14 -0
- package/src/commands/config/setup.ts +108 -0
- package/src/commands/config/show.ts +96 -0
- package/src/commands/functions.ts +703 -0
- package/src/commands/projects/current.ts +36 -0
- package/src/commands/projects/list.ts +61 -0
- package/src/commands/projects/set-default.ts +59 -0
- package/src/commands/sync.ts +778 -0
- package/src/config/index.ts +169 -0
- package/src/index.ts +62 -0
- package/src/services/api.ts +198 -0
- package/src/services/schema-generator.ts +132 -0
- package/src/services/schema-types-convert.ts +361 -0
- package/src/types/index.ts +91 -0
- package/src/utils/env.ts +117 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/sync.ts +70 -0
- package/test.env +2 -0
- package/tsconfig.json +29 -0
- package/index.cjs +0 -21
- package/index.d.ts +0 -1
- package/index.js +0 -21
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { matchByFilters } from "../utils/sync";
|
|
2
|
+
|
|
3
|
+
type BxFieldAnnotations = {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
ref?: string;
|
|
8
|
+
choices?: Array<{ value: string; label: string }>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function splitTopLevelMembers(objectBody: string): string[] {
|
|
12
|
+
const members: string[] = [];
|
|
13
|
+
let current = "";
|
|
14
|
+
let braceDepth = 0;
|
|
15
|
+
let bracketDepth = 0;
|
|
16
|
+
let parenDepth = 0;
|
|
17
|
+
let angleDepth = 0;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < objectBody.length; i++) {
|
|
20
|
+
const ch = objectBody[i];
|
|
21
|
+
if (ch === "{") braceDepth++;
|
|
22
|
+
if (ch === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
23
|
+
if (ch === "[") bracketDepth++;
|
|
24
|
+
if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
25
|
+
if (ch === "(") parenDepth++;
|
|
26
|
+
if (ch === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
27
|
+
if (ch === "<") angleDepth++;
|
|
28
|
+
if (ch === ">") angleDepth = Math.max(0, angleDepth - 1);
|
|
29
|
+
|
|
30
|
+
if (ch === ";" && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && angleDepth === 0) {
|
|
31
|
+
if (current.trim()) members.push(current.trim());
|
|
32
|
+
current = "";
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
current += ch;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (current.trim()) {
|
|
39
|
+
members.push(current.trim());
|
|
40
|
+
}
|
|
41
|
+
return members;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseCollectionIdEnum(source: string): Map<string, string> {
|
|
45
|
+
const map = new Map<string, string>();
|
|
46
|
+
const enumMatch = source.match(/export\s+enum\s+BxCollectionId\s*{([\s\S]*?)}/m);
|
|
47
|
+
if (!enumMatch) return map;
|
|
48
|
+
|
|
49
|
+
const body = enumMatch[1];
|
|
50
|
+
const entryRegex = /([A-Za-z0-9_]+)\s*=\s*"([^"]+)"/g;
|
|
51
|
+
let m: RegExpExecArray | null;
|
|
52
|
+
while ((m = entryRegex.exec(body)) !== null) {
|
|
53
|
+
map.set(m[1], m[2]);
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseExportedObjectTypes(source: string): Array<{ typeName: string; body: string }> {
|
|
59
|
+
const results: Array<{ typeName: string; body: string }> = [];
|
|
60
|
+
const regex = /export\s+type\s+([A-Za-z0-9_]+)\s*=\s*{/g;
|
|
61
|
+
let match: RegExpExecArray | null;
|
|
62
|
+
while ((match = regex.exec(source)) !== null) {
|
|
63
|
+
const typeName = match[1];
|
|
64
|
+
if (typeName.startsWith("Bx")) continue;
|
|
65
|
+
|
|
66
|
+
const braceStart = source.indexOf("{", match.index);
|
|
67
|
+
if (braceStart < 0) continue;
|
|
68
|
+
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let end = -1;
|
|
71
|
+
for (let i = braceStart; i < source.length; i++) {
|
|
72
|
+
const ch = source[i];
|
|
73
|
+
if (ch === "{") depth++;
|
|
74
|
+
if (ch === "}") {
|
|
75
|
+
depth--;
|
|
76
|
+
if (depth === 0) {
|
|
77
|
+
end = i;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (end < 0) continue;
|
|
83
|
+
|
|
84
|
+
results.push({
|
|
85
|
+
typeName,
|
|
86
|
+
body: source.slice(braceStart + 1, end)
|
|
87
|
+
});
|
|
88
|
+
regex.lastIndex = end;
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveCollectionId(typeName: string, enumMap: Map<string, string>): string {
|
|
94
|
+
const mapped = enumMap.get(typeName);
|
|
95
|
+
if (mapped) return mapped;
|
|
96
|
+
return typeName
|
|
97
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
98
|
+
.replace(/-/g, "_")
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function mapScalarTypeToFieldType(typeText: string): string {
|
|
103
|
+
const normalized = typeText.trim().replace(/^\((.*)\)$/, "$1");
|
|
104
|
+
if (["string", "BxText", "BxEmail", "BxUrl", "BxObjectId", "BxAuth", "BxPhoneNumber", "BxSequence"].includes(normalized)) return "Text";
|
|
105
|
+
if (["BxLongText", "BxRichText"].includes(normalized)) return "LongText";
|
|
106
|
+
if (["number", "BxNumber", "BxInteger"].includes(normalized)) return "Number";
|
|
107
|
+
if (["boolean", "BxBoolean"].includes(normalized)) return "Toggle";
|
|
108
|
+
if (["BxCheckbox"].includes(normalized)) return "Checkbox";
|
|
109
|
+
if (["Date", "BxDate"].includes(normalized)) return "Date";
|
|
110
|
+
if (["BxDateTime"].includes(normalized)) return "DateTime";
|
|
111
|
+
if (["BxChoices"].includes(normalized)) return "Choices";
|
|
112
|
+
if (["BxMultipleChoices"].includes(normalized)) return "MultipleChoices";
|
|
113
|
+
if (["BxUser"].includes(normalized)) return "User";
|
|
114
|
+
if (["BxUsers"].includes(normalized)) return "Users";
|
|
115
|
+
if (["BxFiles"].includes(normalized)) return "Files";
|
|
116
|
+
if (["BxImages"].includes(normalized)) return "Images";
|
|
117
|
+
if (["BxObject"].includes(normalized)) return "Object";
|
|
118
|
+
if (["BxList"].includes(normalized)) return "List";
|
|
119
|
+
if (["BxContent"].includes(normalized)) return "Content";
|
|
120
|
+
return "Text";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function convertTypeFieldToSchemaField(typeText: string, optional: boolean): any {
|
|
124
|
+
const required = !optional;
|
|
125
|
+
const requiredPart = required ? { required: true } : {};
|
|
126
|
+
const raw = typeText.trim();
|
|
127
|
+
const compact = raw.replace(/\s+/g, "");
|
|
128
|
+
const unionParts = raw.split("|").map((part) => part.trim()).filter(Boolean);
|
|
129
|
+
const nonNullable = unionParts.filter((part) => part !== "null" && part !== "undefined");
|
|
130
|
+
const primary = nonNullable[0] || raw;
|
|
131
|
+
|
|
132
|
+
const singleRefMatch = compact.match(/^(?:Partial<([A-Za-z0-9_]+)>\|BxDataObject|BxDataObject\|Partial<([A-Za-z0-9_]+)>)$/);
|
|
133
|
+
const singleRef = singleRefMatch?.[1] || singleRefMatch?.[2];
|
|
134
|
+
if (singleRef) {
|
|
135
|
+
return {
|
|
136
|
+
type: "DataObject",
|
|
137
|
+
...requiredPart,
|
|
138
|
+
propertiesScheme: { ref: singleRef }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const manyRefMatch = compact.match(/^(?:Partial<([A-Za-z0-9_]+)>\[\]\|BxDataObject\[\]|BxDataObject\[\]\|Partial<([A-Za-z0-9_]+)>\[\])$/);
|
|
142
|
+
const manyRef = manyRefMatch?.[1] || manyRefMatch?.[2];
|
|
143
|
+
if (manyRef) {
|
|
144
|
+
return {
|
|
145
|
+
type: "DataObjects",
|
|
146
|
+
...requiredPart,
|
|
147
|
+
propertiesScheme: { ref: manyRef }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (primary.endsWith("[]")) {
|
|
152
|
+
const inner = primary.slice(0, -2).trim().replace(/^\((.*)\)$/, "$1");
|
|
153
|
+
return {
|
|
154
|
+
type: "List",
|
|
155
|
+
...requiredPart,
|
|
156
|
+
propertiesScheme: {
|
|
157
|
+
children: [{ name: "item", type: mapScalarTypeToFieldType(inner) }]
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (primary.startsWith("{") && primary.endsWith("}")) {
|
|
163
|
+
return {
|
|
164
|
+
type: "Object",
|
|
165
|
+
...requiredPart,
|
|
166
|
+
propertiesScheme: { children: [] }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
type: mapScalarTypeToFieldType(primary),
|
|
172
|
+
...requiredPart
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseBxFieldAnnotations(memberText: string): BxFieldAnnotations {
|
|
177
|
+
const annotations: BxFieldAnnotations = {};
|
|
178
|
+
const blocks = memberText.match(/\/\*\*[\s\S]*?\*\//g) || [];
|
|
179
|
+
for (const block of blocks) {
|
|
180
|
+
const content = block
|
|
181
|
+
.replace(/^\/\*\*?/, "")
|
|
182
|
+
.replace(/\*\/$/, "")
|
|
183
|
+
.split("\n")
|
|
184
|
+
.map((line) => line.replace(/^\s*\*\s?/, ""))
|
|
185
|
+
.join("\n");
|
|
186
|
+
const tagRegex = /@bx\.([a-z_]+)\s*([\s\S]*?)(?=@bx\.|$)/gi;
|
|
187
|
+
let match: RegExpExecArray | null;
|
|
188
|
+
while ((match = tagRegex.exec(content)) !== null) {
|
|
189
|
+
const [, keyRaw, valueRaw] = match;
|
|
190
|
+
const key = keyRaw.toLowerCase();
|
|
191
|
+
const value = valueRaw.trim();
|
|
192
|
+
if (key === "title" && value) annotations.title = value;
|
|
193
|
+
if (key === "description" && value) annotations.description = value;
|
|
194
|
+
if (key === "ref" && value) annotations.ref = value;
|
|
195
|
+
if (key === "required") {
|
|
196
|
+
if (!value) {
|
|
197
|
+
annotations.required = true;
|
|
198
|
+
} else {
|
|
199
|
+
annotations.required = !["false", "0", "no"].includes(value.toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (key === "choices" && value) {
|
|
203
|
+
annotations.choices = value
|
|
204
|
+
.split("|")
|
|
205
|
+
.map((part) => part.trim())
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.map((part) => {
|
|
208
|
+
const separator = part.indexOf(":");
|
|
209
|
+
if (separator < 0) {
|
|
210
|
+
return { value: part, label: part };
|
|
211
|
+
}
|
|
212
|
+
const choiceValue = part.slice(0, separator).trim();
|
|
213
|
+
const choiceLabel = part.slice(separator + 1).trim();
|
|
214
|
+
return {
|
|
215
|
+
value: choiceValue,
|
|
216
|
+
label: choiceLabel
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return annotations;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stripCommentsForFieldParse(memberText: string): string {
|
|
226
|
+
return memberText
|
|
227
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
228
|
+
.replace(/\/\/.*$/gm, " ")
|
|
229
|
+
.trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applyFieldAnnotations(fieldSchema: any, annotations: BxFieldAnnotations, warnings: string[], fieldPath: string): any {
|
|
233
|
+
const next = { ...fieldSchema };
|
|
234
|
+
if (annotations.title) next.title = annotations.title;
|
|
235
|
+
if (annotations.description) next.description = annotations.description;
|
|
236
|
+
if (annotations.required !== undefined) {
|
|
237
|
+
if (annotations.required) {
|
|
238
|
+
next.required = true;
|
|
239
|
+
} else {
|
|
240
|
+
delete next.required;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (annotations.ref) {
|
|
244
|
+
next.propertiesScheme = {
|
|
245
|
+
...(next.propertiesScheme || {}),
|
|
246
|
+
ref: annotations.ref
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (annotations.choices) {
|
|
250
|
+
if (next.type === "Choices" || next.type === "MultipleChoices") {
|
|
251
|
+
next.propertiesScheme = {
|
|
252
|
+
...(next.propertiesScheme || {}),
|
|
253
|
+
choices: annotations.choices
|
|
254
|
+
};
|
|
255
|
+
} else {
|
|
256
|
+
warnings.push(`Field "${fieldPath}" has @bx.choices but inferred type is "${next.type}"`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return next;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function buildCollectionsFromTypes(typeContent: string, filters: string[]): { collections: any[]; warnings: string[] } {
|
|
263
|
+
const enumMap = parseCollectionIdEnum(typeContent);
|
|
264
|
+
const typeBlocks = parseExportedObjectTypes(typeContent);
|
|
265
|
+
const warnings: string[] = [];
|
|
266
|
+
const collections: any[] = [];
|
|
267
|
+
|
|
268
|
+
for (const block of typeBlocks) {
|
|
269
|
+
const collectionId = resolveCollectionId(block.typeName, enumMap);
|
|
270
|
+
if (!matchByFilters(collectionId, filters)) continue;
|
|
271
|
+
|
|
272
|
+
const members = splitTopLevelMembers(block.body);
|
|
273
|
+
const formSchema: Record<string, any> = {};
|
|
274
|
+
for (const member of members) {
|
|
275
|
+
const annotations = parseBxFieldAnnotations(member);
|
|
276
|
+
const trimmed = stripCommentsForFieldParse(member);
|
|
277
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
278
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\??\s*:\s*([\s\S]+)$/);
|
|
279
|
+
if (!match) continue;
|
|
280
|
+
const [, fieldName, typeTextRaw] = match;
|
|
281
|
+
const optional = /\?\s*:/.test(trimmed);
|
|
282
|
+
if (["_id", "__bxstate"].includes(fieldName)) continue;
|
|
283
|
+
|
|
284
|
+
const fieldSchema = convertTypeFieldToSchemaField(typeTextRaw.trim(), optional);
|
|
285
|
+
if (fieldSchema.type === "Text" && !/^(string|BxText|BxEmail|BxUrl|BxObjectId|BxAuth|BxUser|BxPhoneNumber|BxSequence)\b/.test(typeTextRaw.trim())) {
|
|
286
|
+
warnings.push(`Field "${collectionId}.${fieldName}" used fallback Text for type "${typeTextRaw.trim()}"`);
|
|
287
|
+
}
|
|
288
|
+
if (fieldSchema.propertiesScheme?.ref && enumMap.has(fieldSchema.propertiesScheme.ref)) {
|
|
289
|
+
fieldSchema.propertiesScheme.ref = enumMap.get(fieldSchema.propertiesScheme.ref);
|
|
290
|
+
}
|
|
291
|
+
const annotatedField = applyFieldAnnotations(fieldSchema, annotations, warnings, `${collectionId}.${fieldName}`);
|
|
292
|
+
if (annotatedField.propertiesScheme?.ref && enumMap.has(annotatedField.propertiesScheme.ref)) {
|
|
293
|
+
annotatedField.propertiesScheme.ref = enumMap.get(annotatedField.propertiesScheme.ref);
|
|
294
|
+
}
|
|
295
|
+
formSchema[fieldName] = annotatedField;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
collections.push({
|
|
299
|
+
collection_id: collectionId,
|
|
300
|
+
form_schema: formSchema
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return { collections, warnings };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function mergeSchemaField(baseField: any, inferredField: any): any {
|
|
307
|
+
if (!baseField) return inferredField;
|
|
308
|
+
if (!inferredField) return baseField;
|
|
309
|
+
|
|
310
|
+
const merged: any = {
|
|
311
|
+
...baseField,
|
|
312
|
+
...inferredField
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const fieldType = String(inferredField.type || merged.type || "");
|
|
316
|
+
const baseProperties = baseField.propertiesScheme || {};
|
|
317
|
+
const inferredProperties = inferredField.propertiesScheme || {};
|
|
318
|
+
|
|
319
|
+
if (fieldType === "DataObject" || fieldType === "DataObjects") {
|
|
320
|
+
merged.propertiesScheme = {
|
|
321
|
+
...(baseProperties.ref ? { ref: baseProperties.ref } : {}),
|
|
322
|
+
...(baseProperties.local_key ? { local_key: baseProperties.local_key } : {}),
|
|
323
|
+
...(baseProperties.foreign_key ? { foreign_key: baseProperties.foreign_key } : {}),
|
|
324
|
+
...(baseProperties.schema ? { schema: baseProperties.schema } : {}),
|
|
325
|
+
...(inferredProperties.ref ? { ref: inferredProperties.ref } : {}),
|
|
326
|
+
...(inferredProperties.local_key ? { local_key: inferredProperties.local_key } : {}),
|
|
327
|
+
...(inferredProperties.foreign_key ? { foreign_key: inferredProperties.foreign_key } : {}),
|
|
328
|
+
...(inferredProperties.schema ? { schema: inferredProperties.schema } : {})
|
|
329
|
+
};
|
|
330
|
+
} else if (baseField.propertiesScheme || inferredField.propertiesScheme) {
|
|
331
|
+
merged.propertiesScheme = {
|
|
332
|
+
...baseProperties,
|
|
333
|
+
...inferredProperties
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
return merged;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function mergeCollectionWithBase(baseCollection: any, inferredCollection: any): any {
|
|
340
|
+
const baseSchema = baseCollection?.form_schema && typeof baseCollection.form_schema === "object"
|
|
341
|
+
? baseCollection.form_schema
|
|
342
|
+
: {};
|
|
343
|
+
const inferredSchema = inferredCollection?.form_schema && typeof inferredCollection.form_schema === "object"
|
|
344
|
+
? inferredCollection.form_schema
|
|
345
|
+
: {};
|
|
346
|
+
|
|
347
|
+
const mergedSchema: Record<string, any> = {};
|
|
348
|
+
const keys = new Set<string>([
|
|
349
|
+
...Object.keys(baseSchema),
|
|
350
|
+
...Object.keys(inferredSchema)
|
|
351
|
+
]);
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
mergedSchema[key] = mergeSchemaField(baseSchema[key], inferredSchema[key]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
...inferredCollection,
|
|
358
|
+
...baseCollection,
|
|
359
|
+
form_schema: mergedSchema
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface AuthConfig {
|
|
2
|
+
token: string;
|
|
3
|
+
expiresAt?: string;
|
|
4
|
+
username?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LoginCredentials {
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LoginResponse {
|
|
13
|
+
token: string;
|
|
14
|
+
expiresAt?: string;
|
|
15
|
+
user?: {
|
|
16
|
+
id: string;
|
|
17
|
+
username: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApiConfig {
|
|
23
|
+
endpoint: string;
|
|
24
|
+
apiKey: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Project {
|
|
28
|
+
project_id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
apiUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProjectsConfig {
|
|
34
|
+
default?: string;
|
|
35
|
+
list: Project[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SyncConfig {
|
|
39
|
+
outputPath: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface GlobalConfig {
|
|
43
|
+
api?: ApiConfig;
|
|
44
|
+
auth?: AuthConfig;
|
|
45
|
+
projects?: ProjectsConfig;
|
|
46
|
+
sync?: SyncConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ApiResponse<T = any> {
|
|
50
|
+
data: T;
|
|
51
|
+
status: number;
|
|
52
|
+
message?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SchemaField {
|
|
56
|
+
name: string;
|
|
57
|
+
type: string;
|
|
58
|
+
required?: boolean;
|
|
59
|
+
description?: string;
|
|
60
|
+
defaultValue?: any;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SchemaCollection {
|
|
64
|
+
name: string;
|
|
65
|
+
fields: SchemaField[];
|
|
66
|
+
description?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface SchemaResponse {
|
|
70
|
+
collections: SchemaCollection[];
|
|
71
|
+
version: string;
|
|
72
|
+
generatedAt: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LoginOptions {
|
|
76
|
+
token?: string;
|
|
77
|
+
username?: string;
|
|
78
|
+
password?: string;
|
|
79
|
+
interactive?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SyncOptions {
|
|
83
|
+
projectId: string;
|
|
84
|
+
output?: string;
|
|
85
|
+
apiUrl?: string;
|
|
86
|
+
force?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ProjectOptions {
|
|
90
|
+
projectId: string;
|
|
91
|
+
}
|
package/src/utils/env.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export interface EnvConfig {
|
|
5
|
+
BUILDX_API_ENDPOINT?: string;
|
|
6
|
+
BUILDX_API_KEY?: string;
|
|
7
|
+
BUILDX_PROJECT_ID?: string;
|
|
8
|
+
_sources?: {
|
|
9
|
+
endpointKey?: string;
|
|
10
|
+
apiKeyKey?: string;
|
|
11
|
+
projectIdKey?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadEnvConfig(): EnvConfig {
|
|
16
|
+
const envConfig: EnvConfig = {};
|
|
17
|
+
const fileEnv: Record<string, string> = {};
|
|
18
|
+
|
|
19
|
+
const endpointCandidates = [
|
|
20
|
+
"BUILDX_API_ENDPOINT",
|
|
21
|
+
"NEXT_PUBLIC_BUILDX_API_ENDPOINT",
|
|
22
|
+
"NEXT_PUBLIC_API_ENDPOINT"
|
|
23
|
+
];
|
|
24
|
+
const apiKeyCandidates = [
|
|
25
|
+
"BUILDX_API_KEY",
|
|
26
|
+
"NEXT_PUBLIC_BUILDX_API_KEY",
|
|
27
|
+
"NEXT_PUBLIC_API_KEY"
|
|
28
|
+
];
|
|
29
|
+
const projectIdCandidates = [
|
|
30
|
+
"BUILDX_PROJECT_ID",
|
|
31
|
+
"NEXT_PUBLIC_BUILDX_PROJECT_ID"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Try to load .env file
|
|
35
|
+
const envPath = path.resolve(process.cwd(), ".env");
|
|
36
|
+
if (fs.existsSync(envPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const envContent = fs.readFileSync(envPath, "utf8");
|
|
39
|
+
const envLines = envContent.split("\n");
|
|
40
|
+
|
|
41
|
+
for (const line of envLines) {
|
|
42
|
+
const trimmedLine = line.trim();
|
|
43
|
+
if (trimmedLine && !trimmedLine.startsWith("#")) {
|
|
44
|
+
const [key, ...valueParts] = trimmedLine.split("=");
|
|
45
|
+
if (key && valueParts.length > 0) {
|
|
46
|
+
const value = valueParts.join("=").replace(/^["']|["']$/g, ""); // Remove quotes
|
|
47
|
+
if (
|
|
48
|
+
endpointCandidates.includes(key) ||
|
|
49
|
+
apiKeyCandidates.includes(key) ||
|
|
50
|
+
projectIdCandidates.includes(key)
|
|
51
|
+
) {
|
|
52
|
+
fileEnv[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn("Warning: Could not read .env file:", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const envSources = {
|
|
63
|
+
...fileEnv,
|
|
64
|
+
...Object.fromEntries(
|
|
65
|
+
Object.entries(process.env)
|
|
66
|
+
.filter(([key, value]) => {
|
|
67
|
+
if (!value) return false;
|
|
68
|
+
return (
|
|
69
|
+
endpointCandidates.includes(key) ||
|
|
70
|
+
apiKeyCandidates.includes(key) ||
|
|
71
|
+
projectIdCandidates.includes(key)
|
|
72
|
+
);
|
|
73
|
+
})
|
|
74
|
+
.map(([key, value]) => [key, String(value)])
|
|
75
|
+
)
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (const key of endpointCandidates) {
|
|
79
|
+
if (envSources[key]) {
|
|
80
|
+
envConfig.BUILDX_API_ENDPOINT = envSources[key];
|
|
81
|
+
envConfig._sources = {
|
|
82
|
+
...(envConfig._sources || {}),
|
|
83
|
+
endpointKey: key
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const key of apiKeyCandidates) {
|
|
90
|
+
if (envSources[key]) {
|
|
91
|
+
envConfig.BUILDX_API_KEY = envSources[key];
|
|
92
|
+
envConfig._sources = {
|
|
93
|
+
...(envConfig._sources || {}),
|
|
94
|
+
apiKeyKey: key
|
|
95
|
+
};
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key of projectIdCandidates) {
|
|
101
|
+
if (envSources[key]) {
|
|
102
|
+
envConfig.BUILDX_PROJECT_ID = envSources[key];
|
|
103
|
+
envConfig._sources = {
|
|
104
|
+
...(envConfig._sources || {}),
|
|
105
|
+
projectIdKey: key
|
|
106
|
+
};
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return envConfig;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function hasEnvConfig(): boolean {
|
|
115
|
+
const envConfig = loadEnvConfig();
|
|
116
|
+
return !!(envConfig.BUILDX_API_ENDPOINT && envConfig.BUILDX_API_KEY);
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export class Logger {
|
|
4
|
+
static info(message: string): void {
|
|
5
|
+
console.log(chalk.blue("ℹ"), message);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static success(message: string): void {
|
|
9
|
+
console.log(chalk.green("✓"), message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static warning(message: string): void {
|
|
13
|
+
console.log(chalk.yellow("⚠"), message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static error(message: string): void {
|
|
17
|
+
console.error(chalk.red("✗"), message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static debug(message: string): void {
|
|
21
|
+
if (process.env.DEBUG) {
|
|
22
|
+
console.log(chalk.gray("🐛"), message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static log(message: string): void {
|
|
27
|
+
console.log(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
|
|
5
|
+
export function sha256(content: string): string {
|
|
6
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function deepSort(value: any): any {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return value.map(deepSort);
|
|
12
|
+
}
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const sorted: Record<string, any> = {};
|
|
15
|
+
for (const key of Object.keys(value).sort()) {
|
|
16
|
+
sorted[key] = deepSort(value[key]);
|
|
17
|
+
}
|
|
18
|
+
return sorted;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stableStringify(value: any): string {
|
|
24
|
+
return JSON.stringify(deepSort(value));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function objectChecksum(value: any): string {
|
|
28
|
+
return sha256(stableStringify(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function toAbsolutePath(filePath: string): string {
|
|
32
|
+
if (path.isAbsolute(filePath)) {
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
return path.resolve(process.cwd(), filePath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readJsonIfExists<T = any>(filePath: string): Promise<T | null> {
|
|
39
|
+
if (!(await fs.pathExists(filePath))) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return fs.readJson(filePath) as Promise<T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sanitizeFileName(name: string): string {
|
|
46
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseFilters(filterInput?: string[] | string): string[] {
|
|
50
|
+
if (!filterInput) return [];
|
|
51
|
+
const items = Array.isArray(filterInput) ? filterInput : [filterInput];
|
|
52
|
+
return items
|
|
53
|
+
.flatMap((item) => String(item).split(","))
|
|
54
|
+
.map((item) => item.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function wildcardToRegExp(pattern: string): RegExp {
|
|
59
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
60
|
+
const regexText = `^${escaped.replace(/\*/g, ".*")}$`;
|
|
61
|
+
return new RegExp(regexText, "i");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function matchByFilters(value: string, filters: string[]): boolean {
|
|
65
|
+
if (!filters.length) return true;
|
|
66
|
+
return filters.some((filter) => {
|
|
67
|
+
const regExp = wildcardToRegExp(filter);
|
|
68
|
+
return regExp.test(value);
|
|
69
|
+
});
|
|
70
|
+
}
|
package/test.env
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"moduleResolution": "node",
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"emitDecoratorMetadata": true
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
"dist",
|
|
27
|
+
"**/*.test.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|