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.
@@ -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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ BUILDX_API_ENDPOINT=https://test-api.example.com
2
+ BUILDX_API_KEY=test-api-key-123
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
+ }