hot-updater 0.30.12 → 0.31.1

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.
Files changed (2) hide show
  1. package/dist/index.mjs +667 -25
  2. package/package.json +14 -14
package/dist/index.mjs CHANGED
@@ -25,8 +25,11 @@ import { finished } from "node:stream/promises";
25
25
  import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
26
26
  import { Buffer as Buffer$1 } from "node:buffer";
27
27
  import { SourceSkips, createFingerprintAsync, diffFingerprintChangesAsync } from "@expo/fingerprint";
28
+ import { brotliCompress, constants as constants$1 } from "zlib";
29
+ import { assertNodeStoragePlugin } from "@hot-updater/plugin-core";
30
+ import { createBundleDiff } from "@hot-updater/server";
28
31
  import net from "node:net";
29
- import fs$3, { constants as constants$1 } from "node:fs/promises";
32
+ import fs$3, { constants as constants$2 } from "node:fs/promises";
30
33
  import crypto$1 from "node:crypto";
31
34
  import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from "kysely";
32
35
  import { format } from "sql-formatter";
@@ -47497,7 +47500,7 @@ const getWslDrivesMountPoint = (() => {
47497
47500
  const configFilePath = "/etc/wsl.conf";
47498
47501
  let isConfigFileExists = false;
47499
47502
  try {
47500
- await fs$3.access(configFilePath, constants$1.F_OK);
47503
+ await fs$3.access(configFilePath, constants$2.F_OK);
47501
47504
  isConfigFileExists = true;
47502
47505
  } catch {}
47503
47506
  if (!isConfigFileExists) return defaultMountPoint;
@@ -47599,7 +47602,7 @@ const baseOpen = async (options) => {
47599
47602
  const isBundled = !__dirname || __dirname === "/";
47600
47603
  let exeLocalXdgOpen = false;
47601
47604
  try {
47602
- await fs$3.access(localXdgOpenPath, constants$1.X_OK);
47605
+ await fs$3.access(localXdgOpenPath, constants$2.X_OK);
47603
47606
  exeLocalXdgOpen = true;
47604
47607
  } catch {}
47605
47608
  command = process$1.versions.electron ?? (platform$1 === "android" || isBundled || !exeLocalXdgOpen) ? "xdg-open" : localXdgOpenPath;
@@ -47759,15 +47762,23 @@ const getFileHashFromFile = async (filepath) => {
47759
47762
  };
47760
47763
  //#endregion
47761
47764
  //#region src/utils/bundleManifest.ts
47762
- const createBundleManifest = async ({ bundleId, targetFiles }) => {
47765
+ const createBundleManifest = async ({ bundleId, signFileHash, targetFiles }) => {
47763
47766
  return {
47764
47767
  bundleId,
47765
- assets: Object.fromEntries(await Promise.all([...targetFiles].sort((left, right) => left.name.localeCompare(right.name)).map(async (target) => [target.name, { fileHash: await getFileHashFromFile(target.path) }])))
47768
+ assets: Object.fromEntries(await Promise.all([...targetFiles].sort((left, right) => left.name.localeCompare(right.name)).map(async (target) => {
47769
+ const fileHash = await getFileHashFromFile(target.path);
47770
+ const signature = signFileHash ? await signFileHash(fileHash) : void 0;
47771
+ return [target.name, {
47772
+ fileHash,
47773
+ ...signature ? { signature } : {}
47774
+ }];
47775
+ })))
47766
47776
  };
47767
47777
  };
47768
- const writeBundleManifest = async ({ buildPath, bundleId, targetFiles }) => {
47778
+ const writeBundleManifest = async ({ buildPath, bundleId, signFileHash, targetFiles }) => {
47769
47779
  const manifest = await createBundleManifest({
47770
47780
  bundleId,
47781
+ signFileHash,
47771
47782
  targetFiles
47772
47783
  });
47773
47784
  const manifestPath = path$1.join(buildPath, "manifest.json");
@@ -48035,6 +48046,7 @@ const getDefaultTargetAppVersion = async (platform) => {
48035
48046
  };
48036
48047
  //#endregion
48037
48048
  //#region src/commands/deploy.ts
48049
+ const compressBrotli = promisify(brotliCompress);
48038
48050
  const normalizeRolloutPercentage = (rollout) => {
48039
48051
  if (rollout === void 0) return 100;
48040
48052
  const parsedRollout = typeof rollout === "number" ? rollout : Number(rollout);
@@ -48044,6 +48056,64 @@ const normalizeRolloutPercentage = (rollout) => {
48044
48056
  const getRolloutCohortCountFromPercentage = (rolloutPercentage) => {
48045
48057
  return rolloutPercentage * 10;
48046
48058
  };
48059
+ const normalizePatchMaxBaseBundles = (maxBaseBundles) => {
48060
+ if (maxBaseBundles === void 0) return 3;
48061
+ if (!Number.isInteger(maxBaseBundles) || maxBaseBundles < 1) throw new Error("Patch maxBaseBundles must be a positive integer");
48062
+ return maxBaseBundles;
48063
+ };
48064
+ const getPatchBaseBundles = async ({ bundleId, channel, databasePlugin, maxBaseBundles, platform, target }) => {
48065
+ const where = {
48066
+ channel,
48067
+ enabled: true,
48068
+ id: { lt: bundleId },
48069
+ platform,
48070
+ ...target.fingerprintHash ? { fingerprintHash: target.fingerprintHash } : {
48071
+ targetAppVersion: target.appVersion,
48072
+ targetAppVersionNotNull: true
48073
+ }
48074
+ };
48075
+ const { data } = await databasePlugin.getBundles({
48076
+ limit: maxBaseBundles,
48077
+ orderBy: {
48078
+ direction: "desc",
48079
+ field: "id"
48080
+ },
48081
+ where
48082
+ });
48083
+ return data.filter((bundle) => bundle.id !== bundleId).slice(0, maxBaseBundles);
48084
+ };
48085
+ const createAutoPatches = async ({ bundleId, channel, databasePlugin, maxBaseBundles, platform, storagePlugin, target }) => {
48086
+ const baseBundles = await getPatchBaseBundles({
48087
+ bundleId,
48088
+ channel,
48089
+ databasePlugin,
48090
+ maxBaseBundles,
48091
+ platform,
48092
+ target
48093
+ });
48094
+ const failures = [];
48095
+ let createdCount = 0;
48096
+ for (const baseBundle of baseBundles) try {
48097
+ await createBundleDiff({
48098
+ baseBundleId: baseBundle.id,
48099
+ bundleId
48100
+ }, {
48101
+ databasePlugin,
48102
+ storagePlugin
48103
+ }, { makePrimary: createdCount === 0 });
48104
+ createdCount += 1;
48105
+ } catch (error) {
48106
+ failures.push({
48107
+ baseBundleId: baseBundle.id,
48108
+ message: error instanceof Error ? error.message : "Unknown patch error"
48109
+ });
48110
+ }
48111
+ return {
48112
+ candidateCount: baseBundles.length,
48113
+ createdCount,
48114
+ failures
48115
+ };
48116
+ };
48047
48117
  const getExtensionFromCompressStrategy = (compressStrategy) => {
48048
48118
  switch (compressStrategy) {
48049
48119
  case "tar.br": return ".tar.br";
@@ -48052,6 +48122,33 @@ const getExtensionFromCompressStrategy = (compressStrategy) => {
48052
48122
  default: throw new Error(`Unsupported compress strategy: ${compressStrategy}`);
48053
48123
  }
48054
48124
  };
48125
+ const getRelativeStorageDir = (relativePath) => {
48126
+ const normalized = relativePath.replace(/\\/g, "/");
48127
+ const dirname = path$1.posix.dirname(normalized);
48128
+ return dirname === "." ? "" : dirname;
48129
+ };
48130
+ const isBrotliManifestBundleAsset = (relativePath) => /(^|\/)index\.[^/]+\.bundle$/.test(relativePath.replace(/\\/g, "/"));
48131
+ const replaceStorageUriLeaf = (storageUri, nextLeaf) => {
48132
+ const storageUrl = new URL(storageUri);
48133
+ const normalizedPath = storageUrl.pathname.replace(/\/+$/, "");
48134
+ const lastSlashIndex = normalizedPath.lastIndexOf("/");
48135
+ storageUrl.pathname = `${lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ""}/${nextLeaf}`;
48136
+ return storageUrl.toString();
48137
+ };
48138
+ const ensureUploadSourcePath = async ({ outputPath, targetFile }) => {
48139
+ const uploadName = isBrotliManifestBundleAsset(targetFile.name) ? `${targetFile.name}.br` : targetFile.name;
48140
+ const expectedFilename = path$1.posix.basename(uploadName);
48141
+ const actualFilename = path$1.basename(targetFile.path);
48142
+ if (uploadName === targetFile.name && expectedFilename === actualFilename) return targetFile.path;
48143
+ const aliasDir = path$1.join(outputPath, "upload-artifacts", getRelativeStorageDir(uploadName));
48144
+ await fs$2.promises.mkdir(aliasDir, { recursive: true });
48145
+ const aliasPath = path$1.join(aliasDir, expectedFilename);
48146
+ if (uploadName !== targetFile.name) {
48147
+ const source = await fs$2.promises.readFile(targetFile.path);
48148
+ await fs$2.promises.writeFile(aliasPath, await compressBrotli(source, { params: { [constants$1.BROTLI_PARAM_QUALITY]: 11 } }));
48149
+ } else await fs$2.promises.copyFile(targetFile.path, aliasPath);
48150
+ return aliasPath;
48151
+ };
48055
48152
  const getPlatformName = (platform) => platform === "ios" ? "iOS" : "Android";
48056
48153
  const getDeployPlatforms = async (options) => {
48057
48154
  if (options.platform) return [options.platform];
@@ -48099,6 +48196,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48099
48196
  console.error("No config found. Please run `hot-updater init` first.");
48100
48197
  process.exit(1);
48101
48198
  }
48199
+ const maxPatchBaseBundles = config.patch.enabled ? normalizePatchMaxBaseBundles(config.patch.maxBaseBundles) : 0;
48102
48200
  const signingValidation = await validateSigningConfig(config);
48103
48201
  if (signingValidation.issues.length > 0) {
48104
48202
  const errors = signingValidation.issues.filter((i) => i.type === "error");
@@ -48180,6 +48278,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48180
48278
  const outputPath = options.bundleOutputPath ?? HotUpdateDirUtil.getDefaultOutputPath({ cwd });
48181
48279
  let bundleId = null;
48182
48280
  let fileHash;
48281
+ let manifestFileHash = null;
48183
48282
  const platformName = getPlatformName(platform);
48184
48283
  const outputRoot = getBundleOutputRoot({
48185
48284
  cwd,
@@ -48204,9 +48303,14 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48204
48303
  config.storage(),
48205
48304
  config.database()
48206
48305
  ]);
48306
+ assertNodeStoragePlugin(storagePlugin);
48207
48307
  try {
48208
48308
  const taskRef = {
48209
48309
  buildResult: null,
48310
+ targetFiles: [],
48311
+ manifestPath: null,
48312
+ manifestStorageUri: null,
48313
+ assetBaseStorageUri: null,
48210
48314
  storageUri: null
48211
48315
  };
48212
48316
  await p.tasks([{
@@ -48222,12 +48326,15 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48222
48326
  const { manifestPath } = await writeBundleManifest({
48223
48327
  buildPath,
48224
48328
  bundleId: currentBundleId,
48329
+ signFileHash: config.signing?.enabled && config.signing.privateKeyPath ? (assetFileHash) => signBundle(assetFileHash, config.signing.privateKeyPath) : void 0,
48225
48330
  targetFiles
48226
48331
  });
48227
48332
  const bundleTargetFiles = [...targetFiles, {
48228
48333
  path: manifestPath,
48229
48334
  name: "manifest.json"
48230
48335
  }];
48336
+ taskRef.targetFiles = targetFiles;
48337
+ taskRef.manifestPath = manifestPath;
48231
48338
  switch (compressStrategy) {
48232
48339
  case "tar.br":
48233
48340
  await createTarBrTargetFiles({
@@ -48260,6 +48367,11 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48260
48367
  throw error;
48261
48368
  }
48262
48369
  }
48370
+ manifestFileHash = await getFileHashFromFile(manifestPath);
48371
+ if (config.signing?.enabled) {
48372
+ if (!config.signing.privateKeyPath) throw new Error("privateKeyPath is required when signing is enabled. Please provide a valid path to your RSA private key in hot-updater.config.ts");
48373
+ manifestFileHash = createSignedFileHash(await signBundle(manifestFileHash, config.signing.privateKeyPath));
48374
+ }
48263
48375
  return `✅ Build Complete (${buildPlugin.name})`;
48264
48376
  }
48265
48377
  }]);
@@ -48270,8 +48382,25 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48270
48382
  task: async () => {
48271
48383
  if (!bundleId) throw new Error("Bundle ID not found");
48272
48384
  try {
48273
- const { storageUri } = await storagePlugin.upload(bundleId, bundlePath);
48385
+ const { storageUri } = await storagePlugin.profiles.node.upload(bundleId, bundlePath);
48274
48386
  taskRef.storageUri = storageUri;
48387
+ if (!taskRef.manifestPath) throw new Error("Manifest path not found");
48388
+ const manifestUpload = await storagePlugin.profiles.node.upload(bundleId, taskRef.manifestPath);
48389
+ taskRef.manifestStorageUri = manifestUpload.storageUri;
48390
+ taskRef.assetBaseStorageUri = replaceStorageUriLeaf(manifestUpload.storageUri, "files");
48391
+ await Promise.all(taskRef.targetFiles.map(async (targetFile) => {
48392
+ const relativeDir = getRelativeStorageDir(isBrotliManifestBundleAsset(targetFile.name) ? `${targetFile.name}.br` : targetFile.name);
48393
+ const uploadKey = [
48394
+ bundleId,
48395
+ "files",
48396
+ relativeDir
48397
+ ].filter(Boolean).join("/");
48398
+ const uploadSourcePath = await ensureUploadSourcePath({
48399
+ outputPath: outputRoot,
48400
+ targetFile
48401
+ });
48402
+ return storagePlugin.profiles.node.upload(uploadKey, uploadSourcePath);
48403
+ }));
48275
48404
  } catch (e) {
48276
48405
  if (e instanceof Error) p.log.error(e.message);
48277
48406
  throw new Error("Failed to upload bundle to storage");
@@ -48283,6 +48412,7 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48283
48412
  task: async () => {
48284
48413
  if (!bundleId) throw new Error("Bundle ID not found");
48285
48414
  if (!taskRef.storageUri) throw new Error("Storage URI not found");
48415
+ if (!manifestFileHash) throw new Error("Manifest file hash not found");
48286
48416
  const appVersion = await getNativeAppVersion(platform);
48287
48417
  try {
48288
48418
  await databasePlugin.appendBundle({
@@ -48298,6 +48428,9 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48298
48428
  fingerprintHash: target.fingerprintHash,
48299
48429
  storageUri: taskRef.storageUri,
48300
48430
  metadata: appVersion ? { app_version: appVersion } : {},
48431
+ assetBaseStorageUri: taskRef.assetBaseStorageUri,
48432
+ manifestFileHash,
48433
+ manifestStorageUri: taskRef.manifestStorageUri,
48301
48434
  rolloutCohortCount
48302
48435
  });
48303
48436
  await databasePlugin.commitBundle();
@@ -48305,18 +48438,53 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48305
48438
  if (e instanceof Error) p.log.error(e.message);
48306
48439
  throw e;
48307
48440
  }
48308
- await databasePlugin.onUnmount?.();
48309
48441
  return `✅ Update Complete (${databasePlugin.name})`;
48310
48442
  }
48311
48443
  }]);
48312
48444
  if (!bundleId) throw new Error("Bundle ID not found");
48445
+ const confirmedBundleId = bundleId;
48446
+ if (config.patch.enabled) {
48447
+ let patchSummary = {
48448
+ candidateCount: 0,
48449
+ createdCount: 0,
48450
+ failures: []
48451
+ };
48452
+ await p.tasks([{
48453
+ title: "⚡ Optimizing Delivery",
48454
+ task: async () => {
48455
+ try {
48456
+ patchSummary = await createAutoPatches({
48457
+ bundleId: confirmedBundleId,
48458
+ channel,
48459
+ databasePlugin,
48460
+ maxBaseBundles: maxPatchBaseBundles,
48461
+ platform,
48462
+ storagePlugin,
48463
+ target
48464
+ });
48465
+ } catch (error) {
48466
+ const message = error instanceof Error ? error.message : "Unknown patch optimization error";
48467
+ p.log.warn(`Partial updates unavailable: ${message}`);
48468
+ patchSummary = {
48469
+ candidateCount: 0,
48470
+ createdCount: 0,
48471
+ failures: []
48472
+ };
48473
+ }
48474
+ if (!patchSummary.candidateCount) return "Skipped (no compatible base bundles)";
48475
+ if (!patchSummary.createdCount) return "Skipped (no patch artifacts created)";
48476
+ return `✅ Prepared ${patchSummary.createdCount} partial update path(s)`;
48477
+ }
48478
+ }]);
48479
+ for (const failure of patchSummary.failures) p.log.warn(`Partial update skipped for ${failure.baseBundleId.slice(0, 8)}: ${failure.message}`);
48480
+ }
48313
48481
  if (options.interactive) {
48314
48482
  const port = await getConsolePort(config);
48315
48483
  const isConsoleOpen = await isPortReachable(port, { host: "localhost" });
48316
48484
  const openUrl = new URL(`http://localhost:${port}`);
48317
48485
  openUrl.searchParams.set("channel", channel);
48318
48486
  openUrl.searchParams.set("platform", platform);
48319
- openUrl.searchParams.set("bundleId", bundleId);
48487
+ openUrl.searchParams.set("bundleId", confirmedBundleId);
48320
48488
  const url = openUrl.toString();
48321
48489
  const note = `Console: ${url}`;
48322
48490
  if (!isConsoleOpen) {
@@ -48331,15 +48499,15 @@ const deployPlatform = async ({ options, platform, platformIndex, platformCount
48331
48499
  p.note(note);
48332
48500
  }
48333
48501
  if (multiPlatform) {
48334
- p.log.success(`✅ ${platformName} Deployment Successful (${bundleId})`);
48502
+ p.log.success(`✅ ${platformName} Deployment Successful (${confirmedBundleId})`);
48335
48503
  return {
48336
- bundleId,
48504
+ bundleId: confirmedBundleId,
48337
48505
  platform
48338
48506
  };
48339
48507
  }
48340
- p.outro(`🚀 Deployment Successful (${bundleId})`);
48508
+ p.outro(`🚀 Deployment Successful (${confirmedBundleId})`);
48341
48509
  return {
48342
- bundleId,
48510
+ bundleId: confirmedBundleId,
48343
48511
  platform
48344
48512
  };
48345
48513
  } catch (e) {
@@ -48512,6 +48680,43 @@ const init = async () => {
48512
48680
  ] })) p.log.info(".gitignore has been modified to include hot-updater entries");
48513
48681
  };
48514
48682
  //#endregion
48683
+ //#region src/commands/patch.ts
48684
+ const createPatch = async (options) => {
48685
+ printBanner$1();
48686
+ const platform = options.platform ?? (options.interactive ? await getPlatform("Which platform does this patch target?") : null);
48687
+ if (p.isCancel(platform)) return;
48688
+ if (!platform) {
48689
+ p.log.error("Platform not found. -p <ios | android> or --platform <ios | android>");
48690
+ return;
48691
+ }
48692
+ const config = await loadConfig({
48693
+ channel: options.channel,
48694
+ platform
48695
+ });
48696
+ const [databasePlugin, storagePlugin] = await Promise.all([config.database(), config.storage()]);
48697
+ try {
48698
+ p.note([
48699
+ `Channel: ${options.channel}`,
48700
+ `Platform: ${platform === "ios" ? "iOS" : "Android"}`,
48701
+ `Base bundle: ${options.baseBundleId}`,
48702
+ `Target bundle: ${options.bundleId}`
48703
+ ].join("\n"), "Patch");
48704
+ const updatedBundle = await createBundleDiff({
48705
+ baseBundleId: options.baseBundleId,
48706
+ bundleId: options.bundleId
48707
+ }, {
48708
+ databasePlugin,
48709
+ storagePlugin
48710
+ }, { makePrimary: true });
48711
+ p.outro(`⚡ Patch Ready (${updatedBundle.id})`);
48712
+ } catch (error) {
48713
+ console.error(error);
48714
+ process.exit(1);
48715
+ } finally {
48716
+ await databasePlugin.onUnmount?.();
48717
+ }
48718
+ };
48719
+ //#endregion
48515
48720
  //#region src/commands/runNative.ts
48516
48721
  const runNativeInternal = async ({ options, platform }) => {
48517
48722
  printBanner$1();
@@ -48695,6 +48900,10 @@ const formatBundleSummary = (bundle, nextEnabled) => {
48695
48900
  ].filter((line) => line !== null);
48696
48901
  return ui.block("Bundle", lines);
48697
48902
  };
48903
+ const parseTargetCohorts = (value) => {
48904
+ if (value === void 0) return null;
48905
+ return value.split(",").map((cohort) => cohort.trim()).filter(Boolean);
48906
+ };
48698
48907
  const refuseNonInteractiveMutation = (action) => {
48699
48908
  p.log.error(`Cannot ${action} a bundle without confirmation in a non-interactive shell. Re-run with -y, or use a TTY.`);
48700
48909
  process.exit(1);
@@ -48723,6 +48932,24 @@ const handleBundleList = async (options = {}) => {
48723
48932
  await safeOnUnmount$2(databasePlugin);
48724
48933
  }
48725
48934
  };
48935
+ const handleBundleShow = async (bundleId, options = {}) => {
48936
+ if (!options.json) printBanner$1();
48937
+ const databasePlugin = await (await loadConfig(null)).database();
48938
+ try {
48939
+ const bundle = await databasePlugin.getBundleById(bundleId);
48940
+ if (!bundle) {
48941
+ p.log.error(`No bundle with id ${bundleId}.`);
48942
+ process.exit(1);
48943
+ }
48944
+ if (options.json) {
48945
+ console.log(JSON.stringify(bundle, null, 2));
48946
+ return;
48947
+ }
48948
+ p.log.message(formatBundleSummary(bundle));
48949
+ } finally {
48950
+ await safeOnUnmount$2(databasePlugin);
48951
+ }
48952
+ };
48726
48953
  const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
48727
48954
  const action = nextEnabled ? "enable" : "disable";
48728
48955
  printBanner$1();
@@ -48764,6 +48991,87 @@ const handleBundleSetEnabled = async (bundleId, nextEnabled, options = {}) => {
48764
48991
  await safeOnUnmount$2(databasePlugin);
48765
48992
  }
48766
48993
  };
48994
+ const handleBundleUpdate = async (bundleId, options = {}) => {
48995
+ if (!options.json) printBanner$1();
48996
+ const targetCohorts = parseTargetCohorts(options.targetCohorts);
48997
+ const patch = {};
48998
+ if (options.rolloutCohortCount !== void 0) patch.rolloutCohortCount = options.rolloutCohortCount;
48999
+ if (options.forceUpdate !== void 0) patch.shouldForceUpdate = options.forceUpdate;
49000
+ if (targetCohorts !== null) patch.targetCohorts = targetCohorts;
49001
+ else if (options.clearTargetCohorts) patch.targetCohorts = null;
49002
+ if (Object.keys(patch).length === 0) {
49003
+ p.log.error("No bundle update fields were provided.");
49004
+ process.exit(1);
49005
+ }
49006
+ const databasePlugin = await (await loadConfig(null)).database();
49007
+ try {
49008
+ const bundle = await databasePlugin.getBundleById(bundleId);
49009
+ if (!bundle) {
49010
+ p.log.error(`No bundle with id ${bundleId}.`);
49011
+ process.exit(1);
49012
+ }
49013
+ if (!options.json) p.log.message(formatBundleSummary(bundle));
49014
+ if (!options.yes) {
49015
+ if (!process.stdin.isTTY) refuseNonInteractiveMutation("update");
49016
+ const confirmed = await p.confirm({
49017
+ message: "Update this bundle?",
49018
+ initialValue: false
49019
+ });
49020
+ if (p.isCancel(confirmed) || !confirmed) {
49021
+ p.log.info("Aborted.");
49022
+ process.exit(2);
49023
+ }
49024
+ }
49025
+ await databasePlugin.updateBundle(bundleId, patch);
49026
+ await databasePlugin.commitBundle();
49027
+ const refetched = await databasePlugin.getBundleById(bundleId);
49028
+ if (!refetched) {
49029
+ p.log.error(`Verification failed: ${bundleId} is missing after update.`);
49030
+ process.exit(1);
49031
+ }
49032
+ if (options.json) {
49033
+ console.log(JSON.stringify(refetched, null, 2));
49034
+ return;
49035
+ }
49036
+ p.log.success("Updated bundle.");
49037
+ p.log.info(` ${ui.id(bundleId)}`);
49038
+ } finally {
49039
+ await safeOnUnmount$2(databasePlugin);
49040
+ }
49041
+ };
49042
+ const handleBundleDelete = async (bundleId, options = {}) => {
49043
+ printBanner$1();
49044
+ const databasePlugin = await (await loadConfig(null)).database();
49045
+ try {
49046
+ const bundle = await databasePlugin.getBundleById(bundleId);
49047
+ if (!bundle) {
49048
+ p.log.info(`No bundle with id ${bundleId}. No changes.`);
49049
+ return;
49050
+ }
49051
+ p.log.message(formatBundleSummary(bundle));
49052
+ if (!options.yes) {
49053
+ if (!process.stdin.isTTY) refuseNonInteractiveMutation("delete");
49054
+ const confirmed = await p.confirm({
49055
+ message: "Delete this bundle record?",
49056
+ initialValue: false
49057
+ });
49058
+ if (p.isCancel(confirmed) || !confirmed) {
49059
+ p.log.info("Aborted.");
49060
+ process.exit(2);
49061
+ }
49062
+ }
49063
+ await databasePlugin.deleteBundle(bundle);
49064
+ await databasePlugin.commitBundle();
49065
+ if (await databasePlugin.getBundleById(bundleId)) {
49066
+ p.log.error(`Verification failed: ${bundleId} still exists.`);
49067
+ process.exit(1);
49068
+ }
49069
+ p.log.success("Deleted bundle record.");
49070
+ p.log.info(` ${ui.id(bundleId)}`);
49071
+ } finally {
49072
+ await safeOnUnmount$2(databasePlugin);
49073
+ }
49074
+ };
48767
49075
  //#endregion
48768
49076
  //#region ../../node_modules/.pnpm/es-toolkit@1.32.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
48769
49077
  function isPlainObject(value) {
@@ -49402,6 +49710,14 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
49402
49710
  rcompareIdentifiers: identifiers.rcompareIdentifiers
49403
49711
  };
49404
49712
  })))(), 1);
49713
+ const INFRASTRUCTURE_RECOVERY_COMMANDS = [
49714
+ "hot-updater init",
49715
+ "hot-updater db migrate",
49716
+ "hot-updater db generate"
49717
+ ];
49718
+ const FINGERPRINT_RECOVERY_COMMANDS = ["npx hot-updater fingerprint create"];
49719
+ const EXPORT_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys export-public"];
49720
+ const REMOVE_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys remove"];
49405
49721
  const INFRASTRUCTURE_UPDATE_TARGETS = [
49406
49722
  {
49407
49723
  version: "0.13.0",
@@ -49422,6 +49738,10 @@ const INFRASTRUCTURE_UPDATE_TARGETS = [
49422
49738
  {
49423
49739
  version: "0.30.0",
49424
49740
  note: "Target cohort rollout behavior"
49741
+ },
49742
+ {
49743
+ version: "0.31.0",
49744
+ note: "Bundle artifact storage fields"
49425
49745
  }
49426
49746
  ];
49427
49747
  const getInfrastructureTargetVersionAt = (index) => {
@@ -49463,6 +49783,267 @@ function resolveVersionEndpoint(serverBaseUrl) {
49463
49783
  url.pathname = `${pathname}/version`;
49464
49784
  return url.toString();
49465
49785
  }
49786
+ const createInfrastructureRemediation = () => ({
49787
+ fixability: "blocked",
49788
+ reason: "Server infrastructure changes usually need provider credentials, environment variables, and redeploy access.",
49789
+ commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS]
49790
+ });
49791
+ const toRelativePath = (cwd, filePath) => path$1.relative(cwd, filePath);
49792
+ const resolveProjectPath = (cwd, filePath) => path$1.isAbsolute(filePath) ? filePath : path$1.join(cwd, filePath);
49793
+ const findNativeFiles = ({ cwd, platform, pattern }) => {
49794
+ const platformRoot = path$1.join(cwd, platform);
49795
+ if (!fs$2.existsSync(platformRoot)) return [];
49796
+ return import_out.default.sync(pattern, {
49797
+ cwd: platformRoot,
49798
+ absolute: true,
49799
+ onlyFiles: true,
49800
+ ignore: [
49801
+ "**/Pods/**",
49802
+ "**/build/**",
49803
+ "**/Build/**",
49804
+ "**/*.app/**",
49805
+ "**/*.xcarchive/**"
49806
+ ]
49807
+ }).map((filePath) => toRelativePath(cwd, filePath)).sort();
49808
+ };
49809
+ const findFirstMatchingFile = async ({ cwd, files, patterns }) => {
49810
+ for (const filePath of files) {
49811
+ const absolutePath = resolveProjectPath(cwd, filePath);
49812
+ const content = await fs$2.promises.readFile(absolutePath, "utf-8");
49813
+ if (patterns.some((pattern) => pattern.test(content))) return filePath;
49814
+ }
49815
+ return null;
49816
+ };
49817
+ const readLocalFingerprintFile = async (cwd) => {
49818
+ const fingerprintJsonPath = path$1.join(cwd, "fingerprint.json");
49819
+ try {
49820
+ const content = await fs$2.promises.readFile(fingerprintJsonPath, "utf-8");
49821
+ return {
49822
+ path: "fingerprint.json",
49823
+ value: JSON.parse(content)
49824
+ };
49825
+ } catch {
49826
+ return null;
49827
+ }
49828
+ };
49829
+ const checkIosNativeStatus = async ({ cwd, config, requireFingerprint, expectedFingerprintHash }) => {
49830
+ const configuredPaths = config.platform.ios.infoPlistPaths;
49831
+ if (!(fs$2.existsSync(path$1.join(cwd, "ios")) || configuredPaths.length > 0)) return { issues: [] };
49832
+ const iosParser = new IosConfigParser(configuredPaths);
49833
+ const files = configuredPaths.filter((filePath) => fs$2.existsSync(resolveProjectPath(cwd, filePath)));
49834
+ const issues = [];
49835
+ if (!await iosParser.exists()) issues.push({
49836
+ type: "error",
49837
+ platform: "ios",
49838
+ code: "NATIVE_FILES_NOT_FOUND",
49839
+ message: "iOS Info.plist files were not found.",
49840
+ resolution: "Check platform.ios.infoPlistPaths in hot-updater.config.ts or run iOS prebuild first.",
49841
+ fixability: "auto",
49842
+ paths: configuredPaths
49843
+ });
49844
+ const channel = await iosParser.get("HOT_UPDATER_CHANNEL");
49845
+ const fingerprintHash = requireFingerprint ? await iosParser.get("HOT_UPDATER_FINGERPRINT_HASH") : void 0;
49846
+ if (requireFingerprint && !fingerprintHash?.value) issues.push({
49847
+ type: "error",
49848
+ platform: "ios",
49849
+ code: "MISSING_FINGERPRINT_HASH",
49850
+ message: "HOT_UPDATER_FINGERPRINT_HASH is missing from Info.plist.",
49851
+ resolution: "Run `npx hot-updater fingerprint create` or rebuild through the Expo config plugin.",
49852
+ fixability: "command",
49853
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49854
+ paths: fingerprintHash?.paths.length ? fingerprintHash.paths : files
49855
+ });
49856
+ else if (requireFingerprint && expectedFingerprintHash && fingerprintHash?.value !== expectedFingerprintHash) issues.push({
49857
+ type: "error",
49858
+ platform: "ios",
49859
+ code: "FINGERPRINT_HASH_MISMATCH",
49860
+ message: "HOT_UPDATER_FINGERPRINT_HASH does not match fingerprint.json.",
49861
+ resolution: "Run `npx hot-updater fingerprint create` and rebuild your iOS app.",
49862
+ fixability: "command",
49863
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49864
+ paths: fingerprintHash?.paths ?? files
49865
+ });
49866
+ const appDelegateFiles = findNativeFiles({
49867
+ cwd,
49868
+ platform: "ios",
49869
+ pattern: "**/AppDelegate.{swift,mm,m}"
49870
+ });
49871
+ let bundleProviderConfigured = false;
49872
+ if (appDelegateFiles.length === 0) issues.push({
49873
+ type: "error",
49874
+ platform: "ios",
49875
+ code: "APP_DELEGATE_NOT_FOUND",
49876
+ message: "iOS AppDelegate file was not found.",
49877
+ resolution: "Add HotUpdater.bundleURL() to the app's iOS bundleURL provider.",
49878
+ fixability: "auto"
49879
+ });
49880
+ else {
49881
+ bundleProviderConfigured = await findFirstMatchingFile({
49882
+ cwd,
49883
+ files: appDelegateFiles,
49884
+ patterns: [/HotUpdater\.bundleURL\s*\(/, /\[HotUpdater\s+bundleURL(?:WithBundle)?:?/]
49885
+ }) !== null;
49886
+ if (!bundleProviderConfigured) issues.push({
49887
+ type: "error",
49888
+ platform: "ios",
49889
+ code: "MISSING_IOS_BUNDLE_PROVIDER",
49890
+ message: "iOS AppDelegate does not use HotUpdater.bundleURL().",
49891
+ resolution: "Replace the release JS bundle URL provider with HotUpdater.bundleURL().",
49892
+ fixability: "auto",
49893
+ paths: appDelegateFiles
49894
+ });
49895
+ }
49896
+ return {
49897
+ status: {
49898
+ detected: true,
49899
+ files: [...files, ...appDelegateFiles],
49900
+ channel: channel.value ?? void 0,
49901
+ fingerprintHash: fingerprintHash?.value ?? void 0,
49902
+ bundleProviderConfigured
49903
+ },
49904
+ issues
49905
+ };
49906
+ };
49907
+ const checkAndroidNativeStatus = async ({ cwd, config, requireFingerprint, expectedFingerprintHash }) => {
49908
+ const configuredPaths = config.platform.android.stringResourcePaths;
49909
+ if (!(fs$2.existsSync(path$1.join(cwd, "android")) || configuredPaths.length > 0)) return { issues: [] };
49910
+ const androidParser = new AndroidConfigParser(configuredPaths);
49911
+ const files = configuredPaths.filter((filePath) => fs$2.existsSync(resolveProjectPath(cwd, filePath)));
49912
+ const issues = [];
49913
+ if (!await androidParser.exists()) issues.push({
49914
+ type: "error",
49915
+ platform: "android",
49916
+ code: "NATIVE_FILES_NOT_FOUND",
49917
+ message: "Android strings.xml files were not found.",
49918
+ resolution: "Check platform.android.stringResourcePaths in hot-updater.config.ts or run Android prebuild first.",
49919
+ fixability: "auto",
49920
+ paths: configuredPaths
49921
+ });
49922
+ const channel = await androidParser.get("hot_updater_channel");
49923
+ const fingerprintHash = requireFingerprint ? await androidParser.get("hot_updater_fingerprint_hash") : void 0;
49924
+ if (requireFingerprint && !fingerprintHash?.value) issues.push({
49925
+ type: "error",
49926
+ platform: "android",
49927
+ code: "MISSING_FINGERPRINT_HASH",
49928
+ message: "hot_updater_fingerprint_hash is missing from strings.xml.",
49929
+ resolution: "Run `npx hot-updater fingerprint create` or rebuild through the Expo config plugin.",
49930
+ fixability: "command",
49931
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49932
+ paths: fingerprintHash?.paths.length ? fingerprintHash.paths : files
49933
+ });
49934
+ else if (requireFingerprint && expectedFingerprintHash && fingerprintHash?.value !== expectedFingerprintHash) issues.push({
49935
+ type: "error",
49936
+ platform: "android",
49937
+ code: "FINGERPRINT_HASH_MISMATCH",
49938
+ message: "hot_updater_fingerprint_hash does not match fingerprint.json.",
49939
+ resolution: "Run `npx hot-updater fingerprint create` and rebuild your Android app.",
49940
+ fixability: "command",
49941
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
49942
+ paths: fingerprintHash?.paths ?? files
49943
+ });
49944
+ const mainApplicationFiles = findNativeFiles({
49945
+ cwd,
49946
+ platform: "android",
49947
+ pattern: "**/MainApplication.{kt,java}"
49948
+ });
49949
+ let bundleProviderConfigured = false;
49950
+ if (mainApplicationFiles.length === 0) issues.push({
49951
+ type: "error",
49952
+ platform: "android",
49953
+ code: "MAIN_APPLICATION_NOT_FOUND",
49954
+ message: "Android MainApplication file was not found.",
49955
+ resolution: "Add HotUpdater.getJSBundleFile(applicationContext) to the Android host configuration.",
49956
+ fixability: "auto"
49957
+ });
49958
+ else {
49959
+ bundleProviderConfigured = await findFirstMatchingFile({
49960
+ cwd,
49961
+ files: mainApplicationFiles,
49962
+ patterns: [/HotUpdater\s*(?:\.\s*Companion\s*)?\.\s*getJSBundleFile\s*\(/]
49963
+ }) !== null;
49964
+ if (!bundleProviderConfigured) issues.push({
49965
+ type: "error",
49966
+ platform: "android",
49967
+ code: "MISSING_ANDROID_BUNDLE_PROVIDER",
49968
+ message: "Android MainApplication does not use HotUpdater.getJSBundleFile().",
49969
+ resolution: "Pass HotUpdater.getJSBundleFile(applicationContext) to React Native's JS bundle provider.",
49970
+ fixability: "auto",
49971
+ paths: mainApplicationFiles
49972
+ });
49973
+ }
49974
+ return {
49975
+ status: {
49976
+ detected: true,
49977
+ files: [...files, ...mainApplicationFiles],
49978
+ channel: channel.value ?? void 0,
49979
+ fingerprintHash: fingerprintHash?.value ?? void 0,
49980
+ bundleProviderConfigured
49981
+ },
49982
+ issues
49983
+ };
49984
+ };
49985
+ const toNativeIssue = (issue) => {
49986
+ if (issue.code === "NATIVE_FILES_NOT_FOUND") return {
49987
+ type: issue.type,
49988
+ platform: issue.platform,
49989
+ code: issue.code,
49990
+ message: issue.message,
49991
+ resolution: issue.resolution,
49992
+ fixability: "auto"
49993
+ };
49994
+ return {
49995
+ type: issue.type,
49996
+ platform: issue.platform,
49997
+ code: issue.code,
49998
+ message: issue.message,
49999
+ resolution: issue.resolution,
50000
+ fixability: "command",
50001
+ commands: issue.code === "ORPHAN_PUBLIC_KEY" ? [...REMOVE_PUBLIC_KEY_COMMANDS] : [...EXPORT_PUBLIC_KEY_COMMANDS]
50002
+ };
50003
+ };
50004
+ async function checkNativeStatus({ cwd }) {
50005
+ if (!(fs$2.existsSync(path$1.join(cwd, "ios")) || fs$2.existsSync(path$1.join(cwd, "android")))) return;
50006
+ const config = await loadConfig(null);
50007
+ const localFingerprint = await readLocalFingerprintFile(cwd);
50008
+ const requireFingerprint = config.updateStrategy === "fingerprint";
50009
+ const [ios, android, signing] = await Promise.all([
50010
+ checkIosNativeStatus({
50011
+ cwd,
50012
+ config,
50013
+ requireFingerprint,
50014
+ expectedFingerprintHash: localFingerprint?.value.ios?.hash
50015
+ }),
50016
+ checkAndroidNativeStatus({
50017
+ cwd,
50018
+ config,
50019
+ requireFingerprint,
50020
+ expectedFingerprintHash: localFingerprint?.value.android?.hash
50021
+ }),
50022
+ validateSigningConfig(config)
50023
+ ]);
50024
+ const issues = [
50025
+ ...ios.issues,
50026
+ ...android.issues,
50027
+ ...signing.issues.map(toNativeIssue)
50028
+ ];
50029
+ if (requireFingerprint && !localFingerprint) issues.push({
50030
+ type: "error",
50031
+ platform: "project",
50032
+ code: "MISSING_FINGERPRINT_JSON",
50033
+ message: "fingerprint.json is missing for fingerprint update strategy.",
50034
+ resolution: "Run `npx hot-updater fingerprint create`.",
50035
+ fixability: "command",
50036
+ commands: [...FINGERPRINT_RECOVERY_COMMANDS],
50037
+ paths: ["fingerprint.json"]
50038
+ });
50039
+ return {
50040
+ updateStrategy: config.updateStrategy,
50041
+ fingerprintJsonPath: localFingerprint?.path,
50042
+ ios: ios.status,
50043
+ android: android.status,
50044
+ issues
50045
+ };
50046
+ }
49466
50047
  async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, requiredVersion = getRequiredInfrastructureVersion() }) {
49467
50048
  const versionEndpoint = resolveVersionEndpoint(serverBaseUrl);
49468
50049
  const baseUrl = serverBaseUrl.trim();
@@ -49532,6 +50113,7 @@ async function doctor(options = {}) {
49532
50113
  error: "hot-updater CLI not found. Please install it first."
49533
50114
  };
49534
50115
  const hotUpdaterPackages = Object.keys(allDependencies).filter((key) => key.startsWith("@hot-updater/"));
50116
+ const hasReactNativePackage = allDependencies["@hot-updater/react-native"] !== void 0;
49535
50117
  const versionMismatches = [];
49536
50118
  for (const packageName of hotUpdaterPackages) {
49537
50119
  const currentVersion = allDependencies[packageName];
@@ -49546,14 +50128,19 @@ async function doctor(options = {}) {
49546
50128
  packageJsonPath,
49547
50129
  installedHotUpdaterPackages: hotUpdaterPackages
49548
50130
  };
49549
- if (serverBaseUrl) details.infrastructure = await checkInfrastructureStatus({
49550
- serverBaseUrl,
49551
- fetchImpl,
49552
- requiredVersion: getRequiredInfrastructureVersion(hotUpdaterVersion)
49553
- });
50131
+ if (serverBaseUrl) {
50132
+ details.infrastructure = await checkInfrastructureStatus({
50133
+ serverBaseUrl,
50134
+ fetchImpl,
50135
+ requiredVersion: getRequiredInfrastructureVersion(hotUpdaterVersion)
50136
+ });
50137
+ if (details.infrastructure.error !== void 0 || details.infrastructure.needsUpdate === true) details.infrastructure.remediation = createInfrastructureRemediation();
50138
+ }
50139
+ if (hasReactNativePackage) details.native = await checkNativeStatus({ cwd });
49554
50140
  if (versionMismatches.length > 0) details.versionMismatches = versionMismatches;
49555
50141
  const hasInfrastructureIssue = details.infrastructure?.error !== void 0 || details.infrastructure?.needsUpdate === true;
49556
- if (versionMismatches.length > 0 || hasInfrastructureIssue) return {
50142
+ const hasNativeIssue = details.native?.issues.some((issue) => issue.type === "error") === true;
50143
+ if (versionMismatches.length > 0 || hasInfrastructureIssue || hasNativeIssue) return {
49557
50144
  success: false,
49558
50145
  details
49559
50146
  };
@@ -49561,6 +50148,10 @@ async function doctor(options = {}) {
49561
50148
  success: true,
49562
50149
  details
49563
50150
  };
50151
+ if (details.native) return {
50152
+ success: true,
50153
+ details
50154
+ };
49564
50155
  return true;
49565
50156
  } catch (error) {
49566
50157
  return {
@@ -49569,6 +50160,10 @@ async function doctor(options = {}) {
49569
50160
  };
49570
50161
  }
49571
50162
  }
50163
+ const normalizeDoctorResult = (result) => {
50164
+ if (result === true) return { success: true };
50165
+ return result;
50166
+ };
49572
50167
  const promptServerBaseUrl = async () => {
49573
50168
  if (!process.stdin.isTTY || !process.stdout.isTTY) return;
49574
50169
  const serverBaseUrl = await p.text({
@@ -49591,7 +50186,13 @@ const promptServerBaseUrl = async () => {
49591
50186
  const trimmed = serverBaseUrl.trim();
49592
50187
  return trimmed ? trimmed : void 0;
49593
50188
  };
49594
- const handleDoctor = async ({ serverBaseUrl } = {}) => {
50189
+ const handleDoctor = async ({ serverBaseUrl, json = false } = {}) => {
50190
+ if (json) {
50191
+ const result = normalizeDoctorResult(await doctor({ serverBaseUrl }));
50192
+ console.log(JSON.stringify(result, null, 2));
50193
+ if (!result.success) process.exit(1);
50194
+ return;
50195
+ }
49595
50196
  p.intro("Hot Updater doctor");
49596
50197
  const result = await doctor({ serverBaseUrl: serverBaseUrl ?? await promptServerBaseUrl() });
49597
50198
  if (result === true) {
@@ -49620,6 +50221,31 @@ const handleDoctor = async ({ serverBaseUrl } = {}) => {
49620
50221
  if (infrastructure.updateReason) p.log.info(`Reason: ${infrastructure.updateReason}`);
49621
50222
  } else if (infrastructure.error) p.log.error(`Infrastructure check failed: ${infrastructure.error}`);
49622
50223
  else p.log.success("Infrastructure is up to date.");
50224
+ if (infrastructure.remediation) p.log.message(ui.block("Recovery", [ui.kv("Managed", ui.command("hot-updater init")), ui.kv("@hot-updater/server (self-hosted)", ui.line([
50225
+ ui.command("hot-updater db generate"),
50226
+ "or",
50227
+ ui.command("hot-updater db migrate"),
50228
+ "then redeploy server"
50229
+ ]))]));
50230
+ }
50231
+ if (details?.native) {
50232
+ const native = details.native;
50233
+ const lines = [ui.kv("Strategy", native.updateStrategy)];
50234
+ if (native.ios?.detected) {
50235
+ lines.push(ui.kv("iOS", native.ios.bundleProviderConfigured ? ui.status(true) : ui.status(false)));
50236
+ if (native.ios.channel) lines.push(ui.kv("iOS channel", ui.channel(native.ios.channel)));
50237
+ }
50238
+ if (native.android?.detected) {
50239
+ lines.push(ui.kv("Android", native.android.bundleProviderConfigured ? ui.status(true) : ui.status(false)));
50240
+ if (native.android.channel) lines.push(ui.kv("Android channel", ui.channel(native.android.channel)));
50241
+ }
50242
+ p.log.message(ui.block("Native", lines));
50243
+ for (const issue of native.issues) {
50244
+ const message = `${issue.platform}: ${issue.message}`;
50245
+ if (issue.type === "error") p.log.error(message);
50246
+ else p.log.warn(message);
50247
+ p.log.info(issue.resolution);
50248
+ }
49623
50249
  }
49624
50250
  if (details?.versionMismatches && details.versionMismatches.length > 0) {
49625
50251
  p.log.warn("Version mismatches found:");
@@ -50209,12 +50835,11 @@ async function generateStandaloneSQL(options) {
50209
50835
  const s = p.spinner();
50210
50836
  s.start("Generating SQL from database schema");
50211
50837
  const db = new Kysely({ dialect: createDialect(dbType) });
50212
- const [{ HotUpdaterDB }, { kyselyAdapter }] = await Promise.all([import("@hot-updater/server"), import("@hot-updater/server/adapters/kysely")]);
50213
- const adapter = kyselyAdapter({
50838
+ const [{ createHotUpdater }, { kyselyAdapter }] = await Promise.all([import("@hot-updater/server"), import("@hot-updater/server/adapters/kysely")]);
50839
+ const result = await createHotUpdater({ database: kyselyAdapter({
50214
50840
  db,
50215
50841
  provider: dbType
50216
- });
50217
- const result = await HotUpdaterDB.client(adapter).createMigrator().migrateToLatest({
50842
+ }) }).createMigrator().migrateToLatest({
50218
50843
  mode: "from-schema",
50219
50844
  updateSettings: false
50220
50845
  });
@@ -50734,6 +51359,7 @@ const handlePromote = async (bundleId, options) => {
50734
51359
  let storagePlugin = null;
50735
51360
  try {
50736
51361
  storagePlugin = await config.storage();
51362
+ assertNodeStoragePlugin(storagePlugin);
50737
51363
  } catch {
50738
51364
  storagePlugin = null;
50739
51365
  }
@@ -50910,10 +51536,20 @@ const handleRollback = async (channel, options = {}) => {
50910
51536
  //#endregion
50911
51537
  //#region src/index.ts
50912
51538
  const DEFAULT_CHANNEL = "production";
51539
+ const parseBooleanOption = (value) => {
51540
+ if (value === "true") return true;
51541
+ if (value === "false") return false;
51542
+ throw new InvalidArgumentError("must be true or false");
51543
+ };
51544
+ const parseRolloutCohortCount = (value) => {
51545
+ const count = Number.parseInt(value, 10);
51546
+ if (!Number.isInteger(count) || count < 0 || count > 1e3) throw new InvalidArgumentError("must be an integer between 0 and 1000");
51547
+ return count;
51548
+ };
50913
51549
  const program = new Command();
50914
51550
  program.name("hot-updater").description(banner(version)).version(version);
50915
51551
  program.command("init").description("Initialize Hot Updater").action(init);
50916
- program.command("doctor").description("Check the health of Hot Updater").option("--server-base-url <url>", "server base URL used by update checks (doctor appends /version)").action(handleDoctor);
51552
+ program.command("doctor").description("Check the health of Hot Updater").option("--server-base-url <url>", "server base URL used by update checks (doctor appends /version)").option("--json", "output machine-readable doctor result").action(handleDoctor);
50917
51553
  const fingerprintCommand = program.command("fingerprint").description("Generate fingerprint");
50918
51554
  fingerprintCommand.action(handleFingerprint);
50919
51555
  fingerprintCommand.command("create").description("Create fingerprint").action(handleCreateFingerprint);
@@ -50926,8 +51562,11 @@ bundleCommand.command("list").description("List bundles, most recent first").opt
50926
51562
  if (!Number.isInteger(n) || n <= 0) throw new InvalidArgumentError("must be a positive integer");
50927
51563
  return n;
50928
51564
  }, 20).action(handleBundleList);
51565
+ bundleCommand.command("show").description("Show one bundle by id").argument("<bundle-id>", "the bundle id to show").option("--json", "output raw bundle data as JSON").action((bundleId, options) => handleBundleShow(bundleId, options));
50929
51566
  bundleCommand.command("disable").description("Disable a bundle by id").argument("<bundle-id>", "the id of the bundle to disable").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleSetEnabled(bundleId, false, options));
50930
51567
  bundleCommand.command("enable").description("Re-enable a previously disabled bundle by id").argument("<bundle-id>", "the id of the bundle to enable").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleSetEnabled(bundleId, true, options));
51568
+ bundleCommand.command("update").description("Update bundle rollout and targeting metadata").argument("<bundle-id>", "the bundle id to update").option("--rollout-cohort-count <count>", "rollout cohort count from 0 to 1000", parseRolloutCohortCount).option("--force-update <value>", "set force update flag (true or false)", parseBooleanOption).option("--target-cohorts <cohorts>", "comma-separated target cohorts").option("--clear-target-cohorts", "clear target cohorts").option("--json", "output the updated bundle as JSON").option("-y, --yes", "skip confirmation prompt").action(handleBundleUpdate);
51569
+ bundleCommand.command("delete").description("Delete a bundle record by id").argument("<bundle-id>", "the bundle id to delete").option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handleBundleDelete(bundleId, options));
50931
51570
  bundleCommand.command("promote").description("Move or copy a bundle to a different channel").argument("<bundle-id>", "the id of the bundle to promote").requiredOption("-t, --target <channel>", "channel to promote the bundle to").addOption(new Option("-a, --action <action>", "promote action (copy creates a new bundle id; move keeps the id)").choices(["copy", "move"]).default("copy")).option("-y, --yes", "skip confirmation prompt").action((bundleId, options) => handlePromote(bundleId, options));
50932
51571
  const keysCommand = program.command("keys").description("Code signing key management");
50933
51572
  keysCommand.command("generate").description("Generate RSA key pair for code signing").option("-o, --output <dir>", "output directory for keys", "./keys").option("-k, --key-size <size>", "key size (2048 or 4096)", (value) => {
@@ -50954,6 +51593,9 @@ program.command("deploy").description("deploy a new version").addOption(platform
50954
51593
  process.exit(1);
50955
51594
  }
50956
51595
  }).default(100)).addOption(interactiveCommandOption).addOption(new Option("-c, --channel <channel>", "specify the channel to deploy").default(DEFAULT_CHANNEL)).addOption(new Option("-m, --message <message>", "Specify a custom message for this deployment. If not provided, the latest git commit message will be used as the deployment message")).action(async (options) => deploy(options));
51596
+ program.command("patch").description("create patch artifacts for a deployed bundle").requiredOption("-b, --bundle-id <bundleId>", "target bundle id that should receive the patch artifact").requiredOption("--base-bundle-id <baseBundleId>", "older bundle id to use as the patch base").addOption(platformCommandOption).addOption(interactiveCommandOption).addOption(new Option("-c, --channel <channel>", "specify the channel used to load config").default(DEFAULT_CHANNEL)).action(async (options) => {
51597
+ await createPatch(options);
51598
+ });
50957
51599
  program.command("rollback").description("Disable the most recent enabled bundle on a channel").argument("<channel>", "the channel to roll back").addOption(platformCommandOption).option("-y, --yes", "skip confirmation prompt").option("--target <bundle-id>", "scope rollback to exactly this bundle id (use to retry a failed rollback)").action((channel, options) => handleRollback(channel, options));
50958
51600
  program.command("console").description("open the console").action(async () => {
50959
51601
  printBanner$1();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hot-updater",
3
3
  "type": "module",
4
- "version": "0.30.12",
4
+ "version": "0.31.1",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
7
7
  },
@@ -49,13 +49,13 @@
49
49
  "jiti": "2.6.1",
50
50
  "kysely": "0.28.16",
51
51
  "sql-formatter": "15.6.10",
52
- "@hot-updater/cli-tools": "0.30.12",
53
- "@hot-updater/apple-helper": "0.30.12",
54
- "@hot-updater/console": "0.30.12",
55
- "@hot-updater/core": "0.30.12",
56
- "@hot-updater/android-helper": "0.30.12",
57
- "@hot-updater/plugin-core": "0.30.12",
58
- "@hot-updater/server": "0.30.12"
52
+ "@hot-updater/android-helper": "0.31.1",
53
+ "@hot-updater/apple-helper": "0.31.1",
54
+ "@hot-updater/cli-tools": "0.31.1",
55
+ "@hot-updater/plugin-core": "0.31.1",
56
+ "@hot-updater/core": "0.31.1",
57
+ "@hot-updater/console": "0.31.1",
58
+ "@hot-updater/server": "0.31.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@bacons/xcode": "1.0.0-alpha.24",
@@ -74,12 +74,12 @@
74
74
  "plist": "^3.1.0",
75
75
  "semver": "^7.6.3",
76
76
  "typescript": "6.0.2",
77
- "@hot-updater/aws": "0.30.12",
78
- "@hot-updater/firebase": "0.30.12",
79
- "@hot-updater/server": "0.30.12",
80
- "@hot-updater/test-utils": "0.30.12",
81
- "@hot-updater/cloudflare": "0.30.12",
82
- "@hot-updater/supabase": "0.30.12"
77
+ "@hot-updater/aws": "0.31.1",
78
+ "@hot-updater/cloudflare": "0.31.1",
79
+ "@hot-updater/firebase": "0.31.1",
80
+ "@hot-updater/server": "0.31.1",
81
+ "@hot-updater/supabase": "0.31.1",
82
+ "@hot-updater/test-utils": "0.31.1"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@hot-updater/aws": "*",