@comergehq/cli 0.1.5 → 0.1.7
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.js +245 -73
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc10 from "picocolors";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
|
|
8
8
|
// src/commands/login.ts
|
|
@@ -22,7 +22,7 @@ var CliError = class extends Error {
|
|
|
22
22
|
|
|
23
23
|
// src/config/config.ts
|
|
24
24
|
import { z } from "zod";
|
|
25
|
-
var API_URL = "
|
|
25
|
+
var API_URL = "https://api.comerge.ai";
|
|
26
26
|
var SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
|
|
27
27
|
var SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
|
|
28
28
|
function isValidUrl(value) {
|
|
@@ -438,9 +438,9 @@ function createApiClient(config, opts) {
|
|
|
438
438
|
const cfg = config;
|
|
439
439
|
const apiKey = (opts?.apiKey ?? "").trim();
|
|
440
440
|
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
441
|
-
async function request(
|
|
441
|
+
async function request(path13, init) {
|
|
442
442
|
const { token, session, fromEnv } = await getAuthToken(cfg);
|
|
443
|
-
const url = new URL(
|
|
443
|
+
const url = new URL(path13, cfg.apiUrl).toString();
|
|
444
444
|
const doFetch = async (bearer) => {
|
|
445
445
|
const res2 = await fetch(url, {
|
|
446
446
|
...init,
|
|
@@ -484,7 +484,13 @@ function createApiClient(config, opts) {
|
|
|
484
484
|
}),
|
|
485
485
|
getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
|
|
486
486
|
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
487
|
-
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) })
|
|
487
|
+
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
|
|
488
|
+
initiateBundle: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles`, { method: "POST", body: JSON.stringify(payload) }),
|
|
489
|
+
getBundle: (appId, bundleId) => request(`/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}`, { method: "GET" }),
|
|
490
|
+
getBundleDownloadUrl: (appId, bundleId, options) => request(
|
|
491
|
+
`/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/download?redirect=${options?.redirect ?? false}`,
|
|
492
|
+
{ method: "GET" }
|
|
493
|
+
)
|
|
488
494
|
};
|
|
489
495
|
}
|
|
490
496
|
|
|
@@ -584,11 +590,11 @@ function registerWhoamiCommand(program) {
|
|
|
584
590
|
}
|
|
585
591
|
|
|
586
592
|
// src/commands/init.ts
|
|
587
|
-
import
|
|
593
|
+
import pc9 from "picocolors";
|
|
588
594
|
|
|
589
595
|
// src/services/initLocal.ts
|
|
590
596
|
import os5 from "os";
|
|
591
|
-
import
|
|
597
|
+
import pc8 from "picocolors";
|
|
592
598
|
import prompts from "prompts";
|
|
593
599
|
|
|
594
600
|
// src/services/ensureAuth.ts
|
|
@@ -1057,9 +1063,9 @@ async function importLocal(params) {
|
|
|
1057
1063
|
}
|
|
1058
1064
|
|
|
1059
1065
|
// src/commands/shellInit.ts
|
|
1060
|
-
import
|
|
1066
|
+
import path10 from "path";
|
|
1061
1067
|
import os3 from "os";
|
|
1062
|
-
import
|
|
1068
|
+
import pc7 from "picocolors";
|
|
1063
1069
|
import fse4 from "fs-extra";
|
|
1064
1070
|
|
|
1065
1071
|
// src/utils/packageJson.ts
|
|
@@ -1148,16 +1154,25 @@ import { ComergeStudio } from '@comergehq/studio';
|
|
|
1148
1154
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1149
1155
|
// @ts-ignore
|
|
1150
1156
|
import config from '../comerge.config.json';
|
|
1157
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1158
|
+
// @ts-ignore
|
|
1159
|
+
import embeddedMeta from '../assets/comerge/base.meta.json';
|
|
1151
1160
|
|
|
1152
1161
|
export default function Index() {
|
|
1153
1162
|
const appId = String((config as any)?.appId || '');
|
|
1154
1163
|
const appKey = String((config as any)?.appKey || 'MicroMain');
|
|
1155
|
-
const
|
|
1164
|
+
const clientKey = String((config as any)?.clientKey || '');
|
|
1165
|
+
const embeddedBaseBundles = {
|
|
1166
|
+
ios: { module: require('../assets/comerge/base.ios.jsbundle'), meta: (embeddedMeta as any)?.ios },
|
|
1167
|
+
android: { module: require('../assets/comerge/base.android.jsbundle'), meta: (embeddedMeta as any)?.android },
|
|
1168
|
+
};
|
|
1156
1169
|
return (
|
|
1157
1170
|
<>
|
|
1158
1171
|
<Stack.Screen options={{ headerShown: false }} />
|
|
1159
1172
|
<View style={{ flex: 1 }}>
|
|
1160
|
-
{appId ?
|
|
1173
|
+
{appId ? (
|
|
1174
|
+
<ComergeStudio appId={appId} clientKey={clientKey} appKey={appKey} embeddedBaseBundles={embeddedBaseBundles} />
|
|
1175
|
+
) : null}
|
|
1161
1176
|
</View>
|
|
1162
1177
|
</>
|
|
1163
1178
|
);
|
|
@@ -1178,6 +1193,12 @@ function shellMetroConfigJs() {
|
|
|
1178
1193
|
return `const { getDefaultConfig } = require('expo/metro-config');
|
|
1179
1194
|
|
|
1180
1195
|
const config = getDefaultConfig(__dirname);
|
|
1196
|
+
const resolver = config.resolver || {};
|
|
1197
|
+
const assetExts = resolver.assetExts || [];
|
|
1198
|
+
if (!assetExts.includes('jsbundle')) {
|
|
1199
|
+
resolver.assetExts = [...assetExts, 'jsbundle'];
|
|
1200
|
+
}
|
|
1201
|
+
config.resolver = resolver;
|
|
1181
1202
|
|
|
1182
1203
|
module.exports = config;
|
|
1183
1204
|
`;
|
|
@@ -1276,7 +1297,7 @@ async function stripProject(params) {
|
|
|
1276
1297
|
}
|
|
1277
1298
|
|
|
1278
1299
|
// src/commands/shellInit.ts
|
|
1279
|
-
import
|
|
1300
|
+
import fs12 from "fs/promises";
|
|
1280
1301
|
|
|
1281
1302
|
// src/utils/appJsonPatch.ts
|
|
1282
1303
|
import fs9 from "fs/promises";
|
|
@@ -1420,6 +1441,145 @@ function findMatchingBracket(text, openIdx, open, close) {
|
|
|
1420
1441
|
return -1;
|
|
1421
1442
|
}
|
|
1422
1443
|
|
|
1444
|
+
// src/services/bundles.ts
|
|
1445
|
+
import fs11 from "fs";
|
|
1446
|
+
import fsPromises from "fs/promises";
|
|
1447
|
+
import path9 from "path";
|
|
1448
|
+
import { Readable } from "stream";
|
|
1449
|
+
import { pipeline } from "stream/promises";
|
|
1450
|
+
import pc6 from "picocolors";
|
|
1451
|
+
function sleep2(ms) {
|
|
1452
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1453
|
+
}
|
|
1454
|
+
function isRetryableNetworkError(e) {
|
|
1455
|
+
const err = e;
|
|
1456
|
+
const code = typeof err?.code === "string" ? err.code : "";
|
|
1457
|
+
const message = typeof err?.message === "string" ? err.message : "";
|
|
1458
|
+
if (code === "ERR_NETWORK" || code === "ECONNABORTED" || code === "ETIMEDOUT") return true;
|
|
1459
|
+
if (message.toLowerCase().includes("network error")) return true;
|
|
1460
|
+
if (message.toLowerCase().includes("timeout")) return true;
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
async function withRetry(fn, opts) {
|
|
1464
|
+
let lastErr = null;
|
|
1465
|
+
for (let attempt = 1; attempt <= opts.attempts; attempt += 1) {
|
|
1466
|
+
try {
|
|
1467
|
+
return await fn();
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
lastErr = e;
|
|
1470
|
+
const retryable = isRetryableNetworkError(e);
|
|
1471
|
+
if (!retryable || attempt >= opts.attempts) {
|
|
1472
|
+
throw e;
|
|
1473
|
+
}
|
|
1474
|
+
const exp = Math.min(opts.maxDelayMs, opts.baseDelayMs * Math.pow(2, attempt - 1));
|
|
1475
|
+
const jitter = Math.floor(Math.random() * 250);
|
|
1476
|
+
await sleep2(exp + jitter);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
throw lastErr;
|
|
1480
|
+
}
|
|
1481
|
+
function unwrapResponseObject(resp, label) {
|
|
1482
|
+
const obj = resp?.responseObject;
|
|
1483
|
+
if (obj === void 0 || obj === null) {
|
|
1484
|
+
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
1485
|
+
throw new CliError(message, { exitCode: 1, hint: resp ? JSON.stringify(resp, null, 2) : null });
|
|
1486
|
+
}
|
|
1487
|
+
return obj;
|
|
1488
|
+
}
|
|
1489
|
+
async function pollBundle(api, appId, bundleId, opts) {
|
|
1490
|
+
const start = Date.now();
|
|
1491
|
+
while (true) {
|
|
1492
|
+
try {
|
|
1493
|
+
const bundleResp = await api.getBundle(appId, bundleId);
|
|
1494
|
+
const bundle = unwrapResponseObject(bundleResp, "bundle");
|
|
1495
|
+
if (bundle.status === "succeeded" || bundle.status === "failed") return bundle;
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
if (!isRetryableNetworkError(e)) {
|
|
1498
|
+
throw e;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (Date.now() - start > opts.timeoutMs) {
|
|
1502
|
+
throw new Error("Bundle build timed out.");
|
|
1503
|
+
}
|
|
1504
|
+
await sleep2(opts.intervalMs);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async function downloadToFile(url, targetPath) {
|
|
1508
|
+
const dir = path9.dirname(targetPath);
|
|
1509
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
1510
|
+
const tmpPath = `${targetPath}.tmp-${Date.now()}`;
|
|
1511
|
+
await withRetry(
|
|
1512
|
+
async () => {
|
|
1513
|
+
const res = await fetch(url);
|
|
1514
|
+
if (!res.ok) {
|
|
1515
|
+
throw new Error(`Download failed (status ${res.status})`);
|
|
1516
|
+
}
|
|
1517
|
+
if (!res.body) {
|
|
1518
|
+
throw new Error("Download response has no body.");
|
|
1519
|
+
}
|
|
1520
|
+
const body = Readable.fromWeb(res.body);
|
|
1521
|
+
const out = fs11.createWriteStream(tmpPath);
|
|
1522
|
+
await pipeline(body, out);
|
|
1523
|
+
},
|
|
1524
|
+
{ attempts: 3, baseDelayMs: 500, maxDelayMs: 4e3 }
|
|
1525
|
+
);
|
|
1526
|
+
const stat = await fsPromises.stat(tmpPath);
|
|
1527
|
+
if (!stat.size || stat.size <= 0) {
|
|
1528
|
+
await fsPromises.unlink(tmpPath).catch(() => {
|
|
1529
|
+
});
|
|
1530
|
+
throw new Error("Downloaded bundle is empty.");
|
|
1531
|
+
}
|
|
1532
|
+
await fsPromises.rename(tmpPath, targetPath);
|
|
1533
|
+
}
|
|
1534
|
+
async function downloadBundle(api, appId, platform, targetPath) {
|
|
1535
|
+
const initiateResp = await api.initiateBundle(appId, {
|
|
1536
|
+
platform,
|
|
1537
|
+
idempotencyKey: `${appId}:head:${platform}`
|
|
1538
|
+
});
|
|
1539
|
+
const initiate = unwrapResponseObject(initiateResp, "bundle initiate");
|
|
1540
|
+
const finalBundle = initiate.status === "succeeded" || initiate.status === "failed" ? initiate : await pollBundle(api, appId, initiate.id, { timeoutMs: 3 * 60 * 1e3, intervalMs: 1200 });
|
|
1541
|
+
if (finalBundle.status === "failed") {
|
|
1542
|
+
throw new Error("Bundle build failed.");
|
|
1543
|
+
}
|
|
1544
|
+
const signedResp = await api.getBundleDownloadUrl(appId, finalBundle.id, { redirect: false });
|
|
1545
|
+
const signed = unwrapResponseObject(signedResp, "bundle download url");
|
|
1546
|
+
const url = String(signed.url || "").trim();
|
|
1547
|
+
if (!url) throw new Error("Download URL is missing.");
|
|
1548
|
+
await downloadToFile(url, targetPath);
|
|
1549
|
+
const fingerprint = finalBundle.checksumSha256 ?? `id:${finalBundle.id}`;
|
|
1550
|
+
const meta = {
|
|
1551
|
+
fingerprint,
|
|
1552
|
+
bundleId: finalBundle.id,
|
|
1553
|
+
checksumSha256: finalBundle.checksumSha256 ?? null,
|
|
1554
|
+
size: finalBundle.size ?? null,
|
|
1555
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1556
|
+
};
|
|
1557
|
+
return { platform, bundlePath: targetPath, meta };
|
|
1558
|
+
}
|
|
1559
|
+
async function downloadEmbeddedBundles(params) {
|
|
1560
|
+
const log = params.log ?? (() => {
|
|
1561
|
+
});
|
|
1562
|
+
const baseDir = path9.join(params.outRoot, "assets", "comerge");
|
|
1563
|
+
const iosPath = path9.join(baseDir, "base.ios.jsbundle");
|
|
1564
|
+
const androidPath = path9.join(baseDir, "base.android.jsbundle");
|
|
1565
|
+
log(pc6.dim("Downloading base bundles for shell..."));
|
|
1566
|
+
const results = await Promise.allSettled([
|
|
1567
|
+
downloadBundle(params.api, params.appId, "ios", iosPath),
|
|
1568
|
+
downloadBundle(params.api, params.appId, "android", androidPath)
|
|
1569
|
+
]);
|
|
1570
|
+
const meta = {};
|
|
1571
|
+
for (const res of results) {
|
|
1572
|
+
if (res.status === "fulfilled") {
|
|
1573
|
+
meta[res.value.platform] = res.value.meta;
|
|
1574
|
+
} else {
|
|
1575
|
+
log(pc6.yellow(`Warning: failed to download ${res.reason?.message ?? res.reason}`));
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
await fsPromises.mkdir(baseDir, { recursive: true });
|
|
1579
|
+
await fsPromises.writeFile(path9.join(baseDir, "base.meta.json"), JSON.stringify(meta, null, 2) + "\n", "utf8");
|
|
1580
|
+
return meta;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1423
1583
|
// src/commands/shellInit.ts
|
|
1424
1584
|
async function shellInit(params) {
|
|
1425
1585
|
const projectRoot = await findExpoProjectRoot(process.cwd());
|
|
@@ -1442,17 +1602,17 @@ async function shellInit(params) {
|
|
|
1442
1602
|
exitCode: 2
|
|
1443
1603
|
});
|
|
1444
1604
|
}
|
|
1445
|
-
const requestedOutputPath =
|
|
1605
|
+
const requestedOutputPath = path10.resolve(projectRoot, outDir);
|
|
1446
1606
|
const outputPath = await findAvailableDirPath(requestedOutputPath);
|
|
1447
|
-
console.log(
|
|
1607
|
+
console.log(pc7.dim(`Project: ${projectRoot}`));
|
|
1448
1608
|
if (outputPath !== requestedOutputPath) {
|
|
1449
|
-
console.log(
|
|
1450
|
-
console.log(
|
|
1609
|
+
console.log(pc7.dim(`Output: ${requestedOutputPath} (already exists)`));
|
|
1610
|
+
console.log(pc7.dim(`Using: ${outputPath}`));
|
|
1451
1611
|
} else {
|
|
1452
|
-
console.log(
|
|
1612
|
+
console.log(pc7.dim(`Output: ${outputPath}`));
|
|
1453
1613
|
}
|
|
1454
|
-
const outputIsInsideProject = outputPath === projectRoot || outputPath.startsWith(projectRoot +
|
|
1455
|
-
const stagingPath = outputIsInsideProject ? await
|
|
1614
|
+
const outputIsInsideProject = outputPath === projectRoot || outputPath.startsWith(projectRoot + path10.sep);
|
|
1615
|
+
const stagingPath = outputIsInsideProject ? await fs12.mkdtemp(path10.join(os3.tmpdir(), "comerge-shell-")) : outputPath;
|
|
1456
1616
|
let movedToFinal = false;
|
|
1457
1617
|
try {
|
|
1458
1618
|
if (!outputIsInsideProject) {
|
|
@@ -1461,28 +1621,40 @@ async function shellInit(params) {
|
|
|
1461
1621
|
await copyProject({ projectRoot, outRoot: stagingPath });
|
|
1462
1622
|
await stripProject({ outRoot: stagingPath });
|
|
1463
1623
|
try {
|
|
1464
|
-
const didPatch = await ensureComergeShellPlugins(
|
|
1624
|
+
const didPatch = await ensureComergeShellPlugins(path10.join(stagingPath, "app.json"));
|
|
1465
1625
|
if (didPatch) {
|
|
1466
|
-
console.log(
|
|
1626
|
+
console.log(pc7.dim("Updated app.json with required Comerge shell plugins."));
|
|
1467
1627
|
}
|
|
1468
1628
|
} catch {
|
|
1469
1629
|
}
|
|
1470
|
-
await writeJsonAtomic(
|
|
1630
|
+
await writeJsonAtomic(path10.join(stagingPath, "comerge.config.json"), {
|
|
1471
1631
|
appId,
|
|
1472
1632
|
appKey: appKey || "MicroMain",
|
|
1473
|
-
apiKey
|
|
1633
|
+
clientKey: apiKey
|
|
1474
1634
|
});
|
|
1475
|
-
await writeTextAtomic(path9.join(stagingPath, "app/_layout.tsx"), shellLayoutTsx());
|
|
1476
|
-
await writeTextAtomic(path9.join(stagingPath, "app/index.tsx"), shellIndexTsx());
|
|
1477
|
-
await ensureTextFile(path9.join(stagingPath, "babel.config.js"), shellBabelConfigJs());
|
|
1478
|
-
await ensureTextFile(path9.join(stagingPath, "metro.config.js"), shellMetroConfigJs());
|
|
1479
1635
|
try {
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1636
|
+
const cfg = await resolveConfig({ yes: true });
|
|
1637
|
+
const api = createApiClient(cfg, { apiKey });
|
|
1638
|
+
await downloadEmbeddedBundles({
|
|
1639
|
+
api,
|
|
1640
|
+
appId,
|
|
1641
|
+
outRoot: stagingPath,
|
|
1642
|
+
log: (msg) => console.log(msg)
|
|
1643
|
+
});
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
console.log(pc7.yellow(`Warning: failed to embed base bundles: ${e instanceof Error ? e.message : String(e)}`));
|
|
1646
|
+
}
|
|
1647
|
+
await writeTextAtomic(path10.join(stagingPath, "app/_layout.tsx"), shellLayoutTsx());
|
|
1648
|
+
await writeTextAtomic(path10.join(stagingPath, "app/index.tsx"), shellIndexTsx());
|
|
1649
|
+
await ensureTextFile(path10.join(stagingPath, "babel.config.js"), shellBabelConfigJs());
|
|
1650
|
+
await ensureTextFile(path10.join(stagingPath, "metro.config.js"), shellMetroConfigJs());
|
|
1651
|
+
try {
|
|
1652
|
+
const didPatch = await ensureReanimatedBabelPlugin(path10.join(stagingPath, "babel.config.js"));
|
|
1653
|
+
if (didPatch) console.log(pc7.dim("Updated babel.config.js to include react-native-reanimated/plugin."));
|
|
1482
1654
|
} catch {
|
|
1483
1655
|
}
|
|
1484
1656
|
await ensureTextFile(
|
|
1485
|
-
|
|
1657
|
+
path10.join(stagingPath, "tsconfig.json"),
|
|
1486
1658
|
JSON.stringify(
|
|
1487
1659
|
{
|
|
1488
1660
|
extends: "expo/tsconfig.base",
|
|
@@ -1496,9 +1668,9 @@ async function shellInit(params) {
|
|
|
1496
1668
|
const { pkg: shellPkg, warnings, info } = buildShellPackageJson({
|
|
1497
1669
|
original: originalPkg
|
|
1498
1670
|
});
|
|
1499
|
-
await writeJsonAtomic(
|
|
1500
|
-
for (const w of warnings) console.log(
|
|
1501
|
-
for (const i of info) console.log(
|
|
1671
|
+
await writeJsonAtomic(path10.join(stagingPath, "package.json"), shellPkg);
|
|
1672
|
+
for (const w of warnings) console.log(pc7.yellow(`Warning: ${w}`));
|
|
1673
|
+
for (const i of info) console.log(pc7.dim(`Info: ${i}`));
|
|
1502
1674
|
if (outputIsInsideProject) {
|
|
1503
1675
|
await fse4.move(stagingPath, outputPath, { overwrite: false });
|
|
1504
1676
|
movedToFinal = true;
|
|
@@ -1506,18 +1678,18 @@ async function shellInit(params) {
|
|
|
1506
1678
|
const finalPath = outputIsInsideProject ? outputPath : stagingPath;
|
|
1507
1679
|
const pm = params.packageManager ?? await detectPackageManager(projectRoot);
|
|
1508
1680
|
const { cmd, args } = installCommand(pm);
|
|
1509
|
-
console.log(
|
|
1681
|
+
console.log(pc7.dim(`Installing dependencies with ${cmd}...`));
|
|
1510
1682
|
const res = await execa3(cmd, args, { cwd: finalPath, stdio: "inherit" });
|
|
1511
1683
|
if (res.exitCode !== 0) {
|
|
1512
1684
|
throw new CliError("Dependency installation failed.", { exitCode: res.exitCode ?? 1 });
|
|
1513
1685
|
}
|
|
1514
|
-
console.log(
|
|
1686
|
+
console.log(pc7.dim("Running Expo prebuild..."));
|
|
1515
1687
|
const prebuild = await execa3("npx", ["expo", "prebuild"], { cwd: finalPath, stdio: "inherit" });
|
|
1516
1688
|
if (prebuild.exitCode !== 0) {
|
|
1517
1689
|
throw new CliError("Expo prebuild failed.", { exitCode: prebuild.exitCode ?? 1 });
|
|
1518
1690
|
}
|
|
1519
|
-
console.log(
|
|
1520
|
-
console.log(
|
|
1691
|
+
console.log(pc7.green("Comerge has been installed successfully."));
|
|
1692
|
+
console.log(pc7.dim("You can now open the app from the My apps page in the Comerge app."));
|
|
1521
1693
|
} finally {
|
|
1522
1694
|
if (outputIsInsideProject && !movedToFinal) {
|
|
1523
1695
|
try {
|
|
@@ -1529,7 +1701,7 @@ async function shellInit(params) {
|
|
|
1529
1701
|
}
|
|
1530
1702
|
async function ensureTextFile(filePath, contents) {
|
|
1531
1703
|
try {
|
|
1532
|
-
await
|
|
1704
|
+
await fs12.access(filePath);
|
|
1533
1705
|
return;
|
|
1534
1706
|
} catch {
|
|
1535
1707
|
}
|
|
@@ -1537,8 +1709,8 @@ async function ensureTextFile(filePath, contents) {
|
|
|
1537
1709
|
}
|
|
1538
1710
|
|
|
1539
1711
|
// src/utils/appDefaults.ts
|
|
1540
|
-
import
|
|
1541
|
-
import
|
|
1712
|
+
import fs13 from "fs/promises";
|
|
1713
|
+
import path11 from "path";
|
|
1542
1714
|
import fg2 from "fast-glob";
|
|
1543
1715
|
import { execa as execa4 } from "execa";
|
|
1544
1716
|
function normalizeString(value) {
|
|
@@ -1571,8 +1743,8 @@ async function loadExpoConfig(projectRoot) {
|
|
|
1571
1743
|
return parsed;
|
|
1572
1744
|
} catch {
|
|
1573
1745
|
}
|
|
1574
|
-
const appJsonPath =
|
|
1575
|
-
const rawAppJson = await
|
|
1746
|
+
const appJsonPath = path11.join(projectRoot, "app.json");
|
|
1747
|
+
const rawAppJson = await fs13.readFile(appJsonPath, "utf8").catch(() => null);
|
|
1576
1748
|
if (!rawAppJson) return {};
|
|
1577
1749
|
try {
|
|
1578
1750
|
const parsed = JSON.parse(rawAppJson);
|
|
@@ -1590,7 +1762,7 @@ async function findIosBundleId(projectRoot) {
|
|
|
1590
1762
|
suppressErrors: true
|
|
1591
1763
|
});
|
|
1592
1764
|
for (const file of pbxprojMatches) {
|
|
1593
|
-
const raw = await
|
|
1765
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1594
1766
|
if (!raw) continue;
|
|
1595
1767
|
const match = raw.match(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([A-Za-z0-9.\-]+)\s*;/);
|
|
1596
1768
|
if (match && match[1]) return match[1];
|
|
@@ -1601,7 +1773,7 @@ async function findIosBundleId(projectRoot) {
|
|
|
1601
1773
|
suppressErrors: true
|
|
1602
1774
|
});
|
|
1603
1775
|
for (const file of plistMatches) {
|
|
1604
|
-
const raw = await
|
|
1776
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1605
1777
|
if (!raw) continue;
|
|
1606
1778
|
const match = raw.match(/<key>CFBundleIdentifier<\/key>\s*<string>([^<]+)<\/string>/);
|
|
1607
1779
|
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
@@ -1611,8 +1783,8 @@ async function findIosBundleId(projectRoot) {
|
|
|
1611
1783
|
async function findAndroidPackageName(projectRoot) {
|
|
1612
1784
|
const gradlePaths = ["android/app/build.gradle", "android/app/build.gradle.kts"];
|
|
1613
1785
|
for (const rel of gradlePaths) {
|
|
1614
|
-
const file =
|
|
1615
|
-
const raw = await
|
|
1786
|
+
const file = path11.join(projectRoot, rel);
|
|
1787
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1616
1788
|
if (!raw) continue;
|
|
1617
1789
|
const match = raw.match(/applicationId\s+["']([^"']+)["']/) || raw.match(/applicationId\s*=\s*["']([^"']+)["']/);
|
|
1618
1790
|
if (match && match[1]) return match[1];
|
|
@@ -1623,7 +1795,7 @@ async function findAndroidPackageName(projectRoot) {
|
|
|
1623
1795
|
suppressErrors: true
|
|
1624
1796
|
});
|
|
1625
1797
|
for (const file of manifestMatches) {
|
|
1626
|
-
const raw = await
|
|
1798
|
+
const raw = await fs13.readFile(file, "utf8").catch(() => null);
|
|
1627
1799
|
if (!raw) continue;
|
|
1628
1800
|
const match = raw.match(/<manifest[^>]*\spackage="([^"]+)"/);
|
|
1629
1801
|
if (match && match[1] && !match[1].includes("$")) return match[1];
|
|
@@ -1643,9 +1815,9 @@ async function inferExpoAppDefaults(projectRoot) {
|
|
|
1643
1815
|
fallbackName = normalizeString(pkg?.name ?? null);
|
|
1644
1816
|
}
|
|
1645
1817
|
if (!fallbackName) {
|
|
1646
|
-
fallbackName = normalizeString(
|
|
1818
|
+
fallbackName = normalizeString(path11.basename(projectRoot));
|
|
1647
1819
|
}
|
|
1648
|
-
const resolvedSlug = slug ?? normalizeSlug(fallbackName ?? null) ?? normalizeSlug(
|
|
1820
|
+
const resolvedSlug = slug ?? normalizeSlug(fallbackName ?? null) ?? normalizeSlug(path11.basename(projectRoot));
|
|
1649
1821
|
const suggestedBundleId = suggestBundleId(resolvedSlug);
|
|
1650
1822
|
const nativeIosBundleId = iosBundleId ?? await findIosBundleId(projectRoot);
|
|
1651
1823
|
const nativeAndroidPackage = androidPackageName ?? await findAndroidPackageName(projectRoot);
|
|
@@ -1661,27 +1833,27 @@ async function inferExpoAppDefaults(projectRoot) {
|
|
|
1661
1833
|
}
|
|
1662
1834
|
|
|
1663
1835
|
// src/services/apiKeyStore.ts
|
|
1664
|
-
import
|
|
1836
|
+
import fs14 from "fs/promises";
|
|
1665
1837
|
import os4 from "os";
|
|
1666
|
-
import
|
|
1838
|
+
import path12 from "path";
|
|
1667
1839
|
var KEYTAR_SERVICE2 = "comerge-cli-api-keys";
|
|
1668
1840
|
function xdgConfigHome2() {
|
|
1669
1841
|
const v = process.env.XDG_CONFIG_HOME;
|
|
1670
1842
|
if (typeof v === "string" && v.trim().length > 0) return v;
|
|
1671
|
-
return
|
|
1843
|
+
return path12.join(os4.homedir(), ".config");
|
|
1672
1844
|
}
|
|
1673
1845
|
function apiKeyFilePath() {
|
|
1674
|
-
return
|
|
1846
|
+
return path12.join(xdgConfigHome2(), "comerge", "api-keys.json");
|
|
1675
1847
|
}
|
|
1676
1848
|
async function ensurePathPermissions2(filePath) {
|
|
1677
|
-
const dir =
|
|
1678
|
-
await
|
|
1849
|
+
const dir = path12.dirname(filePath);
|
|
1850
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
1679
1851
|
try {
|
|
1680
|
-
await
|
|
1852
|
+
await fs14.chmod(dir, 448);
|
|
1681
1853
|
} catch {
|
|
1682
1854
|
}
|
|
1683
1855
|
try {
|
|
1684
|
-
await
|
|
1856
|
+
await fs14.chmod(filePath, 384);
|
|
1685
1857
|
} catch {
|
|
1686
1858
|
}
|
|
1687
1859
|
}
|
|
@@ -1704,7 +1876,7 @@ function keyId(orgId, clientAppId) {
|
|
|
1704
1876
|
}
|
|
1705
1877
|
async function readKeyFile() {
|
|
1706
1878
|
const fp = apiKeyFilePath();
|
|
1707
|
-
const raw = await
|
|
1879
|
+
const raw = await fs14.readFile(fp, "utf8").catch(() => null);
|
|
1708
1880
|
if (!raw) return {};
|
|
1709
1881
|
try {
|
|
1710
1882
|
const parsed = JSON.parse(raw);
|
|
@@ -1744,7 +1916,7 @@ async function setStoredApiKey(orgId, clientAppId, apiKey) {
|
|
|
1744
1916
|
}
|
|
1745
1917
|
|
|
1746
1918
|
// src/services/initLocal.ts
|
|
1747
|
-
function
|
|
1919
|
+
function unwrapResponseObject2(resp, label) {
|
|
1748
1920
|
const obj = resp?.responseObject;
|
|
1749
1921
|
if (obj === void 0 || obj === null) {
|
|
1750
1922
|
const message = typeof resp?.message === "string" && resp.message.trim().length > 0 ? resp.message : `Missing ${label} response`;
|
|
@@ -1788,7 +1960,7 @@ async function selectClientApp(apps, yes) {
|
|
|
1788
1960
|
return { app: null };
|
|
1789
1961
|
}
|
|
1790
1962
|
console.log(
|
|
1791
|
-
|
|
1963
|
+
pc8.dim(
|
|
1792
1964
|
"No apps found. Create one here or in the Comerge dashboard at dashboard.comerge.ai."
|
|
1793
1965
|
)
|
|
1794
1966
|
);
|
|
@@ -1914,9 +2086,9 @@ async function initLocal(params) {
|
|
|
1914
2086
|
await api.autoEnableDeveloper();
|
|
1915
2087
|
} catch {
|
|
1916
2088
|
}
|
|
1917
|
-
const orgs =
|
|
2089
|
+
const orgs = unwrapResponseObject2(await api.listOrganizations(), "organizations");
|
|
1918
2090
|
const selectedOrg = await selectOrganization(orgs, params.yes);
|
|
1919
|
-
const apps =
|
|
2091
|
+
const apps = unwrapResponseObject2(
|
|
1920
2092
|
await api.listClientApps({ orgId: selectedOrg.id }),
|
|
1921
2093
|
"client apps"
|
|
1922
2094
|
);
|
|
@@ -1935,7 +2107,7 @@ async function initLocal(params) {
|
|
|
1935
2107
|
iosBundleId: details.iosEnabled ? details.iosBundleId ?? void 0 : void 0,
|
|
1936
2108
|
androidPackageName: details.androidEnabled ? details.androidPackageName ?? void 0 : void 0
|
|
1937
2109
|
});
|
|
1938
|
-
clientApp =
|
|
2110
|
+
clientApp = unwrapResponseObject2(createdResp, "client app");
|
|
1939
2111
|
importAppName = details.name;
|
|
1940
2112
|
}
|
|
1941
2113
|
const clientAppId = String(clientApp.id || "").trim();
|
|
@@ -1944,17 +2116,17 @@ async function initLocal(params) {
|
|
|
1944
2116
|
}
|
|
1945
2117
|
let apiKey = await getStoredApiKey(selectedOrg.id, clientAppId);
|
|
1946
2118
|
if (!apiKey) {
|
|
1947
|
-
const me =
|
|
2119
|
+
const me = unwrapResponseObject2(await api.getMe(), "user");
|
|
1948
2120
|
const keyName = buildKeyName({ email: me.email ?? null, id: me.id ?? null });
|
|
1949
2121
|
const createdKeyResp = await api.createClientAppKey(clientAppId, { name: keyName });
|
|
1950
|
-
const keyPayload =
|
|
2122
|
+
const keyPayload = unwrapResponseObject2(createdKeyResp, "client key");
|
|
1951
2123
|
apiKey = String(keyPayload.key || "").trim();
|
|
1952
2124
|
if (!apiKey) {
|
|
1953
2125
|
throw new CliError("Server did not return a client key.", { exitCode: 1 });
|
|
1954
2126
|
}
|
|
1955
2127
|
await setStoredApiKey(selectedOrg.id, clientAppId, apiKey);
|
|
1956
2128
|
}
|
|
1957
|
-
if (!params.json) console.log(
|
|
2129
|
+
if (!params.json) console.log(pc8.dim("Importing project..."));
|
|
1958
2130
|
const imported = await importLocal({
|
|
1959
2131
|
apiKey,
|
|
1960
2132
|
yes: params.yes,
|
|
@@ -1964,7 +2136,7 @@ async function initLocal(params) {
|
|
|
1964
2136
|
includeDotenv: params.includeDotenv,
|
|
1965
2137
|
dryRun: false
|
|
1966
2138
|
});
|
|
1967
|
-
if (!params.json) console.log(
|
|
2139
|
+
if (!params.json) console.log(pc8.dim("Generating shell app..."));
|
|
1968
2140
|
await shellInit({
|
|
1969
2141
|
outDir: params.outDir,
|
|
1970
2142
|
appId: imported.appId,
|
|
@@ -1999,8 +2171,8 @@ function registerInitCommands(program) {
|
|
|
1999
2171
|
console.log(JSON.stringify(res, null, 2));
|
|
2000
2172
|
return;
|
|
2001
2173
|
}
|
|
2002
|
-
console.log(
|
|
2003
|
-
if (res.status) console.log(
|
|
2174
|
+
console.log(pc9.green(`Completed. appId=${res.appId}`));
|
|
2175
|
+
if (res.status) console.log(pc9.dim(`Status: ${res.status}`));
|
|
2004
2176
|
});
|
|
2005
2177
|
}
|
|
2006
2178
|
|
|
@@ -2015,19 +2187,19 @@ async function main(argv) {
|
|
|
2015
2187
|
registerLogoutCommand(program);
|
|
2016
2188
|
registerInitCommands(program);
|
|
2017
2189
|
program.configureOutput({
|
|
2018
|
-
outputError: (str, write) => write(
|
|
2190
|
+
outputError: (str, write) => write(pc10.red(str))
|
|
2019
2191
|
});
|
|
2020
2192
|
try {
|
|
2021
2193
|
await program.parseAsync(argv);
|
|
2022
2194
|
} catch (err) {
|
|
2023
2195
|
const e = err;
|
|
2024
2196
|
if (e instanceof CliError) {
|
|
2025
|
-
console.error(
|
|
2026
|
-
if (e.hint) console.error(
|
|
2197
|
+
console.error(pc10.red(`Error: ${e.message}`));
|
|
2198
|
+
if (e.hint) console.error(pc10.dim(e.hint));
|
|
2027
2199
|
process.exitCode = e.exitCode;
|
|
2028
2200
|
return;
|
|
2029
2201
|
}
|
|
2030
|
-
console.error(
|
|
2202
|
+
console.error(pc10.red(`Unexpected error: ${e?.message ?? String(e)}`));
|
|
2031
2203
|
process.exitCode = 1;
|
|
2032
2204
|
}
|
|
2033
2205
|
}
|