@dsi18n/airtable-sync 0.1.0

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/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @dsi18n/airtable-sync
2
+
3
+ **Build-time** tool: pull translation rows from [Airtable](https://airtable.com) and write **dsi18n** locale files (`manifest.json`, `en.json`, `es.json`, …).
4
+
5
+ Use with **`@dsi18n/dsi18n`** at runtime (no Airtable calls in production).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -D @dsi18n/airtable-sync
11
+ ```
12
+
13
+ Requires **Node.js 18+**.
14
+
15
+ ## CLI
16
+
17
+ Create a `.env` in your project root:
18
+
19
+ ```env
20
+ AIRTABLE_API_KEY=pat_...
21
+ AIRTABLE_BASE_ID=app...
22
+ AIRTABLE_TABLE=Translations
23
+ I18N_OUT_DIR=./locales
24
+ ```
25
+
26
+ Run:
27
+
28
+ ```bash
29
+ npx dsi18n-sync
30
+ ```
31
+
32
+ `dsi18n-sync` loads `.env` from the current directory and parent folders (nested overrides win), then runs the build.
33
+
34
+ ### Common env vars
35
+
36
+ | Variable | Description |
37
+ |----------|-------------|
38
+ | `AIRTABLE_API_KEY` | Personal access token |
39
+ | `AIRTABLE_BASE_ID` | Base id (`app…`) |
40
+ | `AIRTABLE_TABLE` | Table name (or use `I18N_TABLES` comma-separated) |
41
+ | `I18N_OUT_DIR` | Output directory (default: `./locales`) |
42
+ | `I18N_KEY_FIELD` | Key column name (default: `key`) |
43
+ | `I18N_DEFAULT_LOCALE` | Default locale tag (default: `en`) |
44
+ | `I18N_LOCALES` | Force locale list, e.g. `en,es` |
45
+ | `I18N_TYPES_OUT_FILE` | Optional path to generate `TranslationKey` TypeScript |
46
+ | `I18N_VERSION` | Bundle version string in manifest |
47
+ | `I18N_DEBUG` | `1` or `true` for verbose logs |
48
+
49
+ ## Programmatic API
50
+
51
+ ```js
52
+ import {
53
+ loadBuildConfigFromEnv,
54
+ loadProjectEnv,
55
+ runBuild,
56
+ } from "@dsi18n/airtable-sync";
57
+
58
+ loadProjectEnv();
59
+ process.env.I18N_OUT_DIR ??= "./locales";
60
+
61
+ await runBuild(loadBuildConfigFromEnv());
62
+ ```
63
+
64
+ ## `package.json` script
65
+
66
+ ```json
67
+ {
68
+ "scripts": {
69
+ "i18n:sync": "dsi18n-sync"
70
+ },
71
+ "devDependencies": {
72
+ "@dsi18n/airtable-sync": "^0.1.0"
73
+ }
74
+ }
75
+ ```
76
+
77
+ Commit the generated `locales/` folder (or generate in CI before deploy), then load them with `@dsi18n/dsi18n`.
78
+
79
+ ## Output layout
80
+
81
+ ```text
82
+ locales/
83
+ manifest.json
84
+ en.json
85
+ es.json
86
+ ```
87
+
88
+ ## Related
89
+
90
+ - Runtime: [`@dsi18n/dsi18n`](https://www.npmjs.com/package/@dsi18n/dsi18n) — `createI18n`, `t()`, `loadLocalesFromDirectory`.
91
+
92
+ ## License
93
+
94
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,537 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir as mkdir$1, writeFile } from 'fs/promises';
3
+ import path, { resolve, dirname as dirname$1, join } from 'path';
4
+ import { existsSync } from 'fs';
5
+ import { config } from 'dotenv';
6
+
7
+ // src/config.ts
8
+ function parseBooleanEnv(value) {
9
+ return value === "1" || value === "true";
10
+ }
11
+ function parseNumberEnv(value) {
12
+ if (!value) {
13
+ return void 0;
14
+ }
15
+ const parsed = Number(value);
16
+ return Number.isFinite(parsed) ? parsed : void 0;
17
+ }
18
+ function loadBuildConfigFromEnv() {
19
+ return {
20
+ airtableApiKey: process.env.AIRTABLE_API_KEY,
21
+ baseId: process.env.AIRTABLE_BASE_ID,
22
+ table: process.env.AIRTABLE_TABLE,
23
+ tablesCsv: process.env.I18N_TABLES,
24
+ outDir: process.env.I18N_OUT_DIR,
25
+ version: process.env.I18N_VERSION,
26
+ keyField: process.env.I18N_KEY_FIELD,
27
+ defaultLocale: process.env.I18N_DEFAULT_LOCALE,
28
+ localesCsv: process.env.I18N_LOCALES,
29
+ typesOutFile: process.env.I18N_TYPES_OUT_FILE,
30
+ debug: parseBooleanEnv(process.env.I18N_DEBUG),
31
+ rateLimitDelayMs: parseNumberEnv(process.env.I18N_RATE_LIMIT_DELAY_MS)
32
+ };
33
+ }
34
+ async function mkdir(pathValue) {
35
+ await mkdir$1(pathValue, { recursive: true });
36
+ }
37
+ async function writeUtf8File(pathValue, content) {
38
+ await writeFile(pathValue, content, "utf8");
39
+ }
40
+ function joinPath(...parts) {
41
+ return path.join(...parts);
42
+ }
43
+ function dirname(pathValue) {
44
+ return path.dirname(pathValue);
45
+ }
46
+
47
+ // src/core/fetch.ts
48
+ var RETRY_ATTEMPTS = 3;
49
+ var RETRY_BASE_DELAY_MS = 300;
50
+ var DEFAULT_RATE_LIMIT_DELAY_MS = 200;
51
+ function sleep(ms) {
52
+ return new Promise((resolve2) => {
53
+ setTimeout(resolve2, ms);
54
+ });
55
+ }
56
+ function isRetryableStatus(status) {
57
+ return status === 429 || status >= 500;
58
+ }
59
+ async function fetchWithRetry(url, init, fetchFn, debug) {
60
+ let lastError = null;
61
+ for (let attempt = 0; attempt < RETRY_ATTEMPTS; attempt += 1) {
62
+ try {
63
+ if (debug) {
64
+ console.log(`[fetch] attempt=${attempt + 1} url=${url.toString()}`);
65
+ }
66
+ const response = await fetchFn(url, init);
67
+ if (!isRetryableStatus(response.status)) {
68
+ return response;
69
+ }
70
+ if (debug) {
71
+ console.warn(
72
+ `[fetch] retryable_status=${response.status} attempt=${attempt + 1} url=${url.toString()}`
73
+ );
74
+ }
75
+ const body = await response.text();
76
+ lastError = new Error(
77
+ `Retryable Airtable response (${response.status}): ${response.statusText}
78
+ ${body}`
79
+ );
80
+ } catch (error) {
81
+ if (debug) {
82
+ console.warn(
83
+ `[fetch] network_error attempt=${attempt + 1} url=${url.toString()}`
84
+ );
85
+ }
86
+ lastError = error instanceof Error ? error : new Error("Unknown fetch error");
87
+ }
88
+ if (attempt < RETRY_ATTEMPTS - 1) {
89
+ await sleep(RETRY_BASE_DELAY_MS * (attempt + 1));
90
+ }
91
+ }
92
+ throw lastError ?? new Error("Fetch failed after retries");
93
+ }
94
+ async function fetchAirtablePage(params) {
95
+ const encodedTable = encodeURIComponent(params.table);
96
+ const url = new URL(
97
+ `https://api.airtable.com/v0/${params.baseId}/${encodedTable}`
98
+ );
99
+ if (params.offset) {
100
+ url.searchParams.set("offset", params.offset);
101
+ }
102
+ const headers = {};
103
+ if (params.apiKey) {
104
+ headers.Authorization = `Bearer ${params.apiKey}`;
105
+ }
106
+ const fetchFn = params.fetchFn ?? fetch;
107
+ if (params.debug) {
108
+ const offsetValue = params.offset ?? "none";
109
+ console.log(`[fetch] table=${params.table} offset=${offsetValue}`);
110
+ }
111
+ const response = await fetchWithRetry(url, { headers }, fetchFn, params.debug);
112
+ if (!response.ok) {
113
+ const body = await response.text();
114
+ throw new Error(
115
+ `Airtable request failed (${response.status}): ${response.statusText}
116
+ ${body}`
117
+ );
118
+ }
119
+ return await response.json();
120
+ }
121
+ async function fetchAllRecords(params) {
122
+ const records = [];
123
+ let offset;
124
+ const rateLimitDelayMs = params.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS;
125
+ do {
126
+ const page = await fetchAirtablePage({
127
+ apiKey: params.apiKey,
128
+ baseId: params.baseId,
129
+ table: params.table,
130
+ offset,
131
+ fetchFn: params.fetchFn,
132
+ debug: params.debug
133
+ });
134
+ records.push(...page.records);
135
+ offset = page.offset;
136
+ if (offset) {
137
+ await sleep(rateLimitDelayMs);
138
+ }
139
+ } while (offset);
140
+ return records;
141
+ }
142
+ async function listBaseTables(params) {
143
+ const url = new URL(`https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`);
144
+ const fetchFn = params.fetchFn ?? fetch;
145
+ const response = await fetchWithRetry(
146
+ url,
147
+ {
148
+ headers: {
149
+ Authorization: `Bearer ${params.apiKey}`
150
+ }
151
+ },
152
+ fetchFn,
153
+ params.debug
154
+ );
155
+ if (!response.ok) {
156
+ const body = await response.text();
157
+ throw new Error(
158
+ `Airtable metadata request failed (${response.status}): ${response.statusText}
159
+ ${body}`
160
+ );
161
+ }
162
+ const json = await response.json();
163
+ return json.tables.map((table) => table.name);
164
+ }
165
+
166
+ // src/core/normalize.ts
167
+ function parseLocalesFromEnv(value) {
168
+ if (!value) {
169
+ return [];
170
+ }
171
+ return value.split(",").map((locale) => locale.trim()).filter(Boolean);
172
+ }
173
+ function inferLocalesFromRecords(records, keyField) {
174
+ const locales = /* @__PURE__ */ new Set();
175
+ for (const record of records) {
176
+ const fields = Object.keys(record.fields);
177
+ for (const fieldName of fields) {
178
+ if (fieldName === keyField) {
179
+ continue;
180
+ }
181
+ const value = record.fields[fieldName];
182
+ if (typeof value === "string") {
183
+ locales.add(fieldName);
184
+ }
185
+ }
186
+ }
187
+ return [...locales].sort();
188
+ }
189
+ function normalizeRecords(records, locales, keyField, options) {
190
+ const normalizedEntries = records.map((record) => {
191
+ const keyValue = record.fields[keyField];
192
+ if (typeof keyValue !== "string" || keyValue.trim().length === 0) {
193
+ if (options?.debug) {
194
+ console.warn(`Skipping record ${record.id}: invalid key`);
195
+ }
196
+ return null;
197
+ }
198
+ const key = keyValue.trim();
199
+ const translations = Object.fromEntries(
200
+ locales.map((locale) => {
201
+ const value = record.fields[locale];
202
+ if (typeof value !== "string") {
203
+ return null;
204
+ }
205
+ const trimmed = value.trim();
206
+ return trimmed.length > 0 ? [locale, trimmed] : null;
207
+ }).filter((item) => item !== null)
208
+ );
209
+ return { key, translations };
210
+ });
211
+ return normalizedEntries.filter(
212
+ (entry) => entry !== null
213
+ );
214
+ }
215
+
216
+ // src/core/validate.ts
217
+ function extractPlaceholders(text) {
218
+ const matches = text.match(/\{([a-zA-Z0-9_]+)\}/g) ?? [];
219
+ const cleaned = matches.map((token) => token.replace(/[{}]/g, ""));
220
+ return [...new Set(cleaned)];
221
+ }
222
+ function detectDuplicateKeys(entries) {
223
+ const seen = /* @__PURE__ */ new Set();
224
+ const duplicates = /* @__PURE__ */ new Set();
225
+ for (const entry of entries) {
226
+ if (seen.has(entry.key)) {
227
+ duplicates.add(entry.key);
228
+ continue;
229
+ }
230
+ seen.add(entry.key);
231
+ }
232
+ return [...duplicates].sort();
233
+ }
234
+ function validateMissingTranslations(entries, locales) {
235
+ return entries.flatMap(
236
+ (entry) => locales.map((locale) => ({
237
+ key: entry.key,
238
+ locale,
239
+ value: entry.translations[locale]
240
+ }))
241
+ ).filter((item) => item.value == null || item.value === "").map((item) => ({
242
+ type: "missing_translation",
243
+ key: item.key,
244
+ locale: item.locale
245
+ }));
246
+ }
247
+ function validatePlaceholdersConsistency(entries, locales, baseLocale) {
248
+ const otherLocales = locales.filter((locale) => locale !== baseLocale);
249
+ return entries.flatMap((entry) => {
250
+ const baseTranslation = entry.translations[baseLocale];
251
+ if (!baseTranslation) {
252
+ return [];
253
+ }
254
+ const basePlaceholders = extractPlaceholders(baseTranslation);
255
+ return otherLocales.map((locale) => {
256
+ const translation = entry.translations[locale];
257
+ if (!translation) {
258
+ return null;
259
+ }
260
+ const placeholders = extractPlaceholders(translation);
261
+ const missingIssues = basePlaceholders.filter((placeholder) => !placeholders.includes(placeholder)).map((placeholder) => ({
262
+ type: "missing_placeholder",
263
+ key: entry.key,
264
+ locale,
265
+ baseLocale,
266
+ placeholder
267
+ }));
268
+ const extraIssues = placeholders.filter((placeholder) => !basePlaceholders.includes(placeholder)).map((placeholder) => ({
269
+ type: "extra_placeholder",
270
+ key: entry.key,
271
+ locale,
272
+ baseLocale,
273
+ placeholder
274
+ }));
275
+ return [...missingIssues, ...extraIssues];
276
+ }).flat().filter((item) => item !== null);
277
+ });
278
+ }
279
+ function validateDefaultLocale(locales, defaultLocale) {
280
+ if (!locales.includes(defaultLocale)) {
281
+ throw new Error(
282
+ `Default locale "${defaultLocale}" is not present in locales: ${locales.join(", ")}`
283
+ );
284
+ }
285
+ }
286
+
287
+ // src/core/run-build.ts
288
+ var DICTIONARY_SCHEMA_REF = "../schema/dictionary.schema.json";
289
+ var MANIFEST_SCHEMA_REF = "../schema/manifest.schema.json";
290
+ var DEFAULT_OUT_DIR = "packages/locales";
291
+ var DEFAULT_VERSION = "0.1.0";
292
+ var DEFAULT_KEY_FIELD = "key";
293
+ var DEFAULT_LOCALE = "en";
294
+ var DEFAULT_TYPES_OUT_FILE = "packages/i18n-core/src/generated/translation-keys.ts";
295
+ var DEFAULT_RATE_LIMIT_DELAY_MS2 = 200;
296
+ function sleep2(ms) {
297
+ return new Promise((resolve2) => {
298
+ setTimeout(resolve2, ms);
299
+ });
300
+ }
301
+ function formatValidationIssue(issue) {
302
+ if (issue.type === "missing_translation") {
303
+ return `Missing ${issue.locale} for key: ${issue.key}`;
304
+ }
305
+ if (issue.type === "missing_placeholder") {
306
+ return `Missing placeholder "${issue.placeholder}" for key "${issue.key}" (${issue.locale}) using base locale "${issue.baseLocale}"`;
307
+ }
308
+ return `Extra placeholder "${issue.placeholder}" for key "${issue.key}" (${issue.locale}) using base locale "${issue.baseLocale}"`;
309
+ }
310
+ function parseTablesFromEnv(value) {
311
+ if (!value) {
312
+ return [];
313
+ }
314
+ return value.split(",").map((table) => table.trim()).filter(Boolean);
315
+ }
316
+ function extractPlaceholders2(text) {
317
+ const matches = text.match(/\{([a-zA-Z0-9_]+)\}/g) ?? [];
318
+ const cleaned = matches.map((token) => token.replace(/[{}]/g, ""));
319
+ return [...new Set(cleaned)];
320
+ }
321
+ function buildDictionaries(entries, locales, version) {
322
+ const dictionaries = Object.fromEntries(
323
+ locales.map((locale) => [
324
+ locale,
325
+ {
326
+ locale,
327
+ version,
328
+ entries: {}
329
+ }
330
+ ])
331
+ );
332
+ entries.forEach((entry) => {
333
+ locales.forEach((locale) => {
334
+ const value = entry.translations[locale];
335
+ if (!value) {
336
+ return;
337
+ }
338
+ dictionaries[locale].entries[entry.key] = {
339
+ value,
340
+ placeholders: extractPlaceholders2(value)
341
+ };
342
+ });
343
+ });
344
+ return dictionaries;
345
+ }
346
+ async function writeOutputs(params) {
347
+ await mkdir(params.outDir);
348
+ const dictionaryEntries = Object.entries(params.dictionaries);
349
+ await Promise.all(
350
+ dictionaryEntries.map(async ([locale, dictionary]) => {
351
+ const filePath = joinPath(params.outDir, `${locale}.json`);
352
+ const payload = {
353
+ $schema: DICTIONARY_SCHEMA_REF,
354
+ locale: dictionary.locale,
355
+ version: dictionary.version,
356
+ entries: dictionary.entries
357
+ };
358
+ await writeUtf8File(filePath, `${JSON.stringify(payload, null, 2)}
359
+ `);
360
+ })
361
+ );
362
+ const manifestPath = joinPath(params.outDir, "manifest.json");
363
+ const manifestPayload = {
364
+ ...params.manifest,
365
+ $schema: MANIFEST_SCHEMA_REF
366
+ };
367
+ await writeUtf8File(
368
+ manifestPath,
369
+ `${JSON.stringify(manifestPayload, null, 2)}
370
+ `
371
+ );
372
+ }
373
+ function buildTypeDefinitions(keys) {
374
+ const sortedKeys = [...keys].sort();
375
+ const keyUnion = sortedKeys.length > 0 ? sortedKeys.map((key) => ` | "${key}"`).join("\n") : ' | "__no_translation_keys__"';
376
+ return [
377
+ "// AUTO-GENERATED FILE. DO NOT EDIT.",
378
+ "// Generated by packages/airtable-sync/src/core/run-build.ts",
379
+ "",
380
+ "export type TranslationKey =",
381
+ keyUnion + ";",
382
+ "",
383
+ "export const translationKeys = [",
384
+ ...sortedKeys.map((key) => ` "${key}",`),
385
+ "] as const;",
386
+ ""
387
+ ].join("\n");
388
+ }
389
+ async function writeTypeDefinitions(typeOutputPath, entries) {
390
+ const keys = entries.map((entry) => entry.key);
391
+ const typeContent = buildTypeDefinitions(keys);
392
+ await mkdir(dirname(typeOutputPath));
393
+ await writeUtf8File(typeOutputPath, typeContent);
394
+ }
395
+ async function runBuild(config) {
396
+ const baseId = config.baseId;
397
+ const airtableApiKey = config.airtableApiKey;
398
+ if (!baseId) {
399
+ throw new Error("Missing required config: baseId");
400
+ }
401
+ if (!airtableApiKey) {
402
+ throw new Error("Missing airtableApiKey");
403
+ }
404
+ const outDir = config.outDir ?? DEFAULT_OUT_DIR;
405
+ const version = config.version ?? DEFAULT_VERSION;
406
+ const keyField = config.keyField ?? DEFAULT_KEY_FIELD;
407
+ const defaultLocale = config.defaultLocale ?? DEFAULT_LOCALE;
408
+ const typesOutFile = config.typesOutFile ?? DEFAULT_TYPES_OUT_FILE;
409
+ const rateLimitDelayMs = config.rateLimitDelayMs ?? DEFAULT_RATE_LIMIT_DELAY_MS2;
410
+ const tablesFromCsv = parseTablesFromEnv(config.tablesCsv);
411
+ const selectedTables = config.table && config.table.trim().length > 0 ? [config.table.trim()] : tablesFromCsv;
412
+ const tables = selectedTables.length > 0 ? selectedTables : await (async () => {
413
+ if (!config.airtableApiKey) {
414
+ throw new Error(
415
+ "AIRTABLE_TABLE or I18N_TABLES is required unless AIRTABLE_API_KEY can access Airtable metadata."
416
+ );
417
+ }
418
+ return listBaseTables({
419
+ apiKey: airtableApiKey,
420
+ baseId,
421
+ fetchFn: config.fetchFn,
422
+ debug: config.debug
423
+ });
424
+ })();
425
+ if (tables.length === 0) {
426
+ throw new Error("No tables found to read from Airtable base.");
427
+ }
428
+ const records = await tables.reduce(
429
+ async (accPromise, table, index) => {
430
+ const acc = await accPromise;
431
+ if (index > 0) {
432
+ await sleep2(rateLimitDelayMs);
433
+ }
434
+ const tableRecords = await fetchAllRecords({
435
+ apiKey: airtableApiKey,
436
+ baseId,
437
+ table,
438
+ fetchFn: config.fetchFn,
439
+ debug: config.debug,
440
+ rateLimitDelayMs
441
+ });
442
+ return [...acc, ...tableRecords];
443
+ },
444
+ Promise.resolve([])
445
+ );
446
+ const configuredLocales = parseLocalesFromEnv(config.localesCsv);
447
+ const inferredLocales = inferLocalesFromRecords(records, keyField);
448
+ const locales = configuredLocales.length > 0 ? configuredLocales : inferredLocales;
449
+ if (locales.length === 0) {
450
+ throw new Error(
451
+ "No locales found. Set I18N_LOCALES or provide language columns in Airtable."
452
+ );
453
+ }
454
+ const normalized = normalizeRecords(records, locales, keyField, {
455
+ debug: config.debug
456
+ });
457
+ const duplicateKeys = detectDuplicateKeys(normalized);
458
+ if (duplicateKeys.length > 0) {
459
+ throw new Error(
460
+ `Duplicate translation keys found: ${duplicateKeys.join(", ")}`
461
+ );
462
+ }
463
+ const missingTranslations = validateMissingTranslations(normalized, locales);
464
+ if (missingTranslations.length > 0) {
465
+ console.warn("Missing translations:");
466
+ missingTranslations.forEach(
467
+ (issue) => console.warn(` - ${formatValidationIssue(issue)}`)
468
+ );
469
+ }
470
+ validateDefaultLocale(locales, defaultLocale);
471
+ const placeholderMismatches = validatePlaceholdersConsistency(
472
+ normalized,
473
+ locales,
474
+ defaultLocale
475
+ );
476
+ if (placeholderMismatches.length > 0) {
477
+ console.warn("Placeholder consistency issues:");
478
+ placeholderMismatches.forEach(
479
+ (issue) => console.warn(` - ${formatValidationIssue(issue)}`)
480
+ );
481
+ }
482
+ const dictionaries = buildDictionaries(normalized, locales, version);
483
+ const manifest = {
484
+ version,
485
+ defaultLocale,
486
+ fallbackChain: [defaultLocale],
487
+ locales,
488
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
489
+ source: {
490
+ type: "airtable",
491
+ baseId,
492
+ tables
493
+ }
494
+ };
495
+ await writeOutputs({
496
+ outDir,
497
+ dictionaries,
498
+ manifest
499
+ });
500
+ await writeTypeDefinitions(typesOutFile, normalized);
501
+ console.log(
502
+ `Generated locales: ${locales.join(", ")} | records: ${normalized.length} | out: ${outDir} | types: ${typesOutFile}`
503
+ );
504
+ }
505
+ function loadProjectEnv(startDir = process.cwd()) {
506
+ const chain = [];
507
+ let dir = resolve(startDir);
508
+ for (let i = 0; i < 6; i += 1) {
509
+ chain.unshift(dir);
510
+ const parent = dirname$1(dir);
511
+ if (parent === dir) {
512
+ break;
513
+ }
514
+ dir = parent;
515
+ }
516
+ for (const folder of chain) {
517
+ const path2 = join(folder, ".env");
518
+ if (existsSync(path2)) {
519
+ config({ path: path2 });
520
+ }
521
+ }
522
+ }
523
+
524
+ // src/index.ts
525
+ async function main() {
526
+ await runBuild(loadBuildConfigFromEnv());
527
+ }
528
+
529
+ // src/cli.ts
530
+ loadProjectEnv();
531
+ main().catch((error) => {
532
+ const message = error instanceof Error ? error.message : String(error);
533
+ console.error(message);
534
+ process.exit(1);
535
+ });
536
+ //# sourceMappingURL=cli.js.map
537
+ //# sourceMappingURL=cli.js.map