hot-updater 0.32.0 → 0.33.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.
Files changed (2) hide show
  1. package/dist/index.mjs +227 -97
  2. package/package.json +14 -14
package/dist/index.mjs CHANGED
@@ -32,6 +32,7 @@ import { constants as constants$2, createBrotliCompress } from "zlib";
32
32
  import { assertNodeStoragePlugin, getContentAddressedAssetStoragePath } from "@hot-updater/plugin-core";
33
33
  import { createBundleDiff } from "@hot-updater/server";
34
34
  import net from "node:net";
35
+ import { setTimeout as setTimeout$1 } from "timers/promises";
35
36
  import { createJiti } from "jiti";
36
37
  //#region ../../node_modules/.pnpm/commander@14.0.0/node_modules/commander/lib/error.js
37
38
  var require_error = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -30956,6 +30957,8 @@ const LIST_COLUMNS = [
30956
30957
  }
30957
30958
  ];
30958
30959
  const DEFAULT_LIMIT = 20;
30960
+ const DELETE_VERIFY_ATTEMPTS = 12;
30961
+ const DELETE_VERIFY_DELAY_MS = 1e3;
30959
30962
  const formatRow = (bundle) => {
30960
30963
  const out = {};
30961
30964
  for (const field of LIST_FIELDS) {
@@ -31145,7 +31148,7 @@ const handleBundleDelete = async (bundleId, options = {}) => {
31145
31148
  }
31146
31149
  await databasePlugin.deleteBundle(bundle);
31147
31150
  await databasePlugin.commitBundle();
31148
- if (await databasePlugin.getBundleById(bundleId)) {
31151
+ if (!await waitForDeletedBundle(databasePlugin, bundleId)) {
31149
31152
  p.log.error(`Verification failed: ${bundleId} still exists.`);
31150
31153
  process.exit(1);
31151
31154
  }
@@ -31155,6 +31158,13 @@ const handleBundleDelete = async (bundleId, options = {}) => {
31155
31158
  await safeOnUnmount$2(databasePlugin);
31156
31159
  }
31157
31160
  };
31161
+ async function waitForDeletedBundle(databasePlugin, bundleId) {
31162
+ for (let attempt = 0; attempt < DELETE_VERIFY_ATTEMPTS; attempt += 1) {
31163
+ if (!await databasePlugin.getBundleById(bundleId)) return true;
31164
+ if (attempt < DELETE_VERIFY_ATTEMPTS - 1) await setTimeout$1(DELETE_VERIFY_DELAY_MS);
31165
+ }
31166
+ return false;
31167
+ }
31158
31168
  //#endregion
31159
31169
  //#region ../../node_modules/.pnpm/es-toolkit@1.32.0/node_modules/es-toolkit/dist/predicate/isPlainObject.mjs
31160
31170
  function isPlainObject(value) {
@@ -31728,7 +31738,7 @@ var require_subset = /* @__PURE__ */ __commonJSMin(((exports, module) => {
31728
31738
  module.exports = subset;
31729
31739
  }));
31730
31740
  //#endregion
31731
- //#region src/commands/doctor.ts
31741
+ //#region src/commands/doctorInfrastructureTargets.ts
31732
31742
  var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
31733
31743
  const internalRe = require_re();
31734
31744
  const constants = require_constants$1();
@@ -31782,15 +31792,7 @@ var import_semver = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
31782
31792
  rcompareIdentifiers: identifiers.rcompareIdentifiers
31783
31793
  };
31784
31794
  })))(), 1);
31785
- const INFRASTRUCTURE_RECOVERY_COMMANDS = [
31786
- "hot-updater init",
31787
- "hot-updater db migrate",
31788
- "hot-updater db generate"
31789
- ];
31790
- const FINGERPRINT_RECOVERY_COMMANDS = ["npx hot-updater fingerprint create"];
31791
- const EXPORT_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys export-public"];
31792
- const REMOVE_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys remove"];
31793
- const INFRASTRUCTURE_UPDATE_TARGETS = [
31795
+ const UPDATE_TARGETS = [
31794
31796
  {
31795
31797
  version: "0.13.0",
31796
31798
  note: "Initial provider infrastructure migrations"
@@ -31818,35 +31820,51 @@ const INFRASTRUCTURE_UPDATE_TARGETS = [
31818
31820
  {
31819
31821
  version: "0.32.0",
31820
31822
  note: "Content-addressed manifest asset routing"
31823
+ },
31824
+ {
31825
+ version: "0.33.0",
31826
+ note: "provider update checks reuse selected bundles"
31821
31827
  }
31822
31828
  ];
31823
- const getInfrastructureTargetVersionAt = (index) => {
31824
- const target = INFRASTRUCTURE_UPDATE_TARGETS.at(index);
31825
- if (!target) throw new Error("INFRASTRUCTURE_UPDATE_TARGETS must not be empty");
31826
- return target.version;
31829
+ const getTargetAt = ({ index, label, targets }) => {
31830
+ const target = targets.at(index);
31831
+ if (!target) throw new Error(`${label} must not be empty`);
31832
+ return target;
31827
31833
  };
31828
- /**
31829
- * Checks if two versions (or version and range) are compatible.
31830
- * @param versionA - First version or range string.
31831
- * @param versionB - Second version or range string.
31832
- * @returns True if compatible, false otherwise.
31833
- */
31834
- function areVersionsCompatible(versionA, versionB) {
31835
- if (versionA === versionB) return true;
31836
- const comparableVersionA = import_semver.validRange(versionA) ? import_semver.minVersion(versionA) : null;
31837
- const comparableVersionB = import_semver.validRange(versionB) ? import_semver.minVersion(versionB) : null;
31838
- if (comparableVersionA && comparableVersionB && comparableVersionA.prerelease.length === 0 && comparableVersionB.prerelease.length === 0 && comparableVersionA.major === comparableVersionB.major && comparableVersionA.minor === comparableVersionB.minor) return true;
31839
- const options = { includePrerelease: true };
31840
- if (import_semver.valid(versionA) && import_semver.validRange(versionB) && import_semver.satisfies(versionA, versionB, options)) return true;
31841
- if (import_semver.valid(versionB) && import_semver.validRange(versionA) && import_semver.satisfies(versionB, versionA, options)) return true;
31842
- return false;
31843
- }
31844
- function getRequiredInfrastructureVersion(hotUpdaterVersion = getInfrastructureTargetVersionAt(-1)) {
31834
+ const getLatestKnownTargetVersion = () => {
31835
+ return getTargetAt({
31836
+ index: -1,
31837
+ label: "UPDATE_TARGETS",
31838
+ targets: UPDATE_TARGETS
31839
+ }).version;
31840
+ };
31841
+ const getRequiredTarget = ({ hotUpdaterVersion, targets }) => {
31845
31842
  const current = import_semver.coerce(hotUpdaterVersion)?.version;
31846
- if (!current) return getInfrastructureTargetVersionAt(-1);
31847
- let requiredVersion = getInfrastructureTargetVersionAt(0);
31848
- for (const target of INFRASTRUCTURE_UPDATE_TARGETS) if (import_semver.lte(target.version, current)) requiredVersion = target.version;
31849
- return requiredVersion;
31843
+ if (!current) return null;
31844
+ let requiredTarget = null;
31845
+ for (const target of targets) if (import_semver.lte(target.version, current)) requiredTarget = target;
31846
+ return requiredTarget ?? getTargetAt({
31847
+ index: 0,
31848
+ label: "UPDATE_TARGETS",
31849
+ targets
31850
+ });
31851
+ };
31852
+ function getRequiredInfrastructureVersion(hotUpdaterVersion = getTargetAt({
31853
+ index: -1,
31854
+ label: "UPDATE_TARGETS",
31855
+ targets: UPDATE_TARGETS
31856
+ }).version) {
31857
+ return getRequiredUpdateTarget(hotUpdaterVersion).version;
31858
+ }
31859
+ function getRequiredUpdateTarget(hotUpdaterVersion = getLatestKnownTargetVersion()) {
31860
+ return getRequiredTarget({
31861
+ hotUpdaterVersion,
31862
+ targets: UPDATE_TARGETS
31863
+ }) ?? getTargetAt({
31864
+ index: -1,
31865
+ label: "UPDATE_TARGETS",
31866
+ targets: UPDATE_TARGETS
31867
+ });
31850
31868
  }
31851
31869
  function isInfrastructureUpdateRequired({ serverVersion, requiredVersion = getRequiredInfrastructureVersion() }) {
31852
31870
  const normalizedServerVersion = import_semver.valid(serverVersion);
@@ -31854,6 +31872,13 @@ function isInfrastructureUpdateRequired({ serverVersion, requiredVersion = getRe
31854
31872
  if (!normalizedServerVersion || !normalizedRequiredVersion) throw new Error("Invalid infrastructure version");
31855
31873
  return import_semver.lt(normalizedServerVersion, normalizedRequiredVersion);
31856
31874
  }
31875
+ //#endregion
31876
+ //#region src/commands/doctorInfrastructure.ts
31877
+ const INFRASTRUCTURE_RECOVERY_COMMANDS = [
31878
+ "hot-updater init",
31879
+ "hot-updater db migrate",
31880
+ "hot-updater db generate"
31881
+ ];
31857
31882
  function resolveVersionEndpoint(serverBaseUrl) {
31858
31883
  const url = new URL(serverBaseUrl.trim());
31859
31884
  const pathname = url.pathname.replace(/\/+$/, "");
@@ -31862,11 +31887,83 @@ function resolveVersionEndpoint(serverBaseUrl) {
31862
31887
  url.pathname = `${pathname}/version`;
31863
31888
  return url.toString();
31864
31889
  }
31865
- const createInfrastructureRemediation = () => ({
31866
- fixability: "blocked",
31867
- reason: "Server infrastructure changes usually need provider credentials, environment variables, and redeploy access.",
31868
- commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS]
31869
- });
31890
+ const createInfrastructureRemediation = () => {
31891
+ return {
31892
+ fixability: "blocked",
31893
+ reason: "Server infrastructure changes usually need provider credentials, environment variables, and redeploy access.",
31894
+ commands: [...INFRASTRUCTURE_RECOVERY_COMMANDS]
31895
+ };
31896
+ };
31897
+ async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, requiredTarget = getRequiredUpdateTarget() }) {
31898
+ const versionEndpoint = resolveVersionEndpoint(serverBaseUrl);
31899
+ const baseUrl = serverBaseUrl.trim();
31900
+ const requiredVersion = requiredTarget.version;
31901
+ try {
31902
+ const response = await fetchImpl(versionEndpoint, { headers: { Accept: "application/json" } });
31903
+ if (!response.ok) {
31904
+ if (response.status === 404) return {
31905
+ baseUrl,
31906
+ versionEndpoint,
31907
+ requiredVersion,
31908
+ needsUpdate: true,
31909
+ updateReason: "Version endpoint not found"
31910
+ };
31911
+ return {
31912
+ baseUrl,
31913
+ versionEndpoint,
31914
+ requiredVersion,
31915
+ error: `Version endpoint returned ${response.status}`
31916
+ };
31917
+ }
31918
+ const data = await response.json();
31919
+ if (typeof data.version !== "string") return {
31920
+ baseUrl,
31921
+ versionEndpoint,
31922
+ requiredVersion,
31923
+ error: "Version endpoint response must include a string version"
31924
+ };
31925
+ const needsUpdate = isInfrastructureUpdateRequired({
31926
+ serverVersion: data.version,
31927
+ requiredVersion
31928
+ });
31929
+ return {
31930
+ baseUrl,
31931
+ versionEndpoint,
31932
+ serverVersion: data.version,
31933
+ requiredVersion,
31934
+ needsUpdate,
31935
+ updateReason: needsUpdate ? requiredTarget.note : void 0
31936
+ };
31937
+ } catch (error) {
31938
+ return {
31939
+ baseUrl,
31940
+ versionEndpoint,
31941
+ requiredVersion,
31942
+ error: error instanceof Error ? error.message : String(error)
31943
+ };
31944
+ }
31945
+ }
31946
+ //#endregion
31947
+ //#region src/commands/doctor.ts
31948
+ const FINGERPRINT_RECOVERY_COMMANDS = ["npx hot-updater fingerprint create"];
31949
+ const EXPORT_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys export-public"];
31950
+ const REMOVE_PUBLIC_KEY_COMMANDS = ["npx hot-updater keys remove"];
31951
+ /**
31952
+ * Checks if two versions (or version and range) are compatible.
31953
+ * @param versionA - First version or range string.
31954
+ * @param versionB - Second version or range string.
31955
+ * @returns True if compatible, false otherwise.
31956
+ */
31957
+ function areVersionsCompatible(versionA, versionB) {
31958
+ if (versionA === versionB) return true;
31959
+ const comparableVersionA = import_semver.validRange(versionA) ? import_semver.minVersion(versionA) : null;
31960
+ const comparableVersionB = import_semver.validRange(versionB) ? import_semver.minVersion(versionB) : null;
31961
+ if (comparableVersionA && comparableVersionB && comparableVersionA.prerelease.length === 0 && comparableVersionB.prerelease.length === 0 && comparableVersionA.major === comparableVersionB.major && comparableVersionA.minor === comparableVersionB.minor) return true;
31962
+ const options = { includePrerelease: true };
31963
+ if (import_semver.valid(versionA) && import_semver.validRange(versionB) && import_semver.satisfies(versionA, versionB, options)) return true;
31964
+ if (import_semver.valid(versionB) && import_semver.validRange(versionA) && import_semver.satisfies(versionB, versionA, options)) return true;
31965
+ return false;
31966
+ }
31870
31967
  const toRelativePath = (cwd, filePath) => path.relative(cwd, filePath);
31871
31968
  const resolveProjectPath = (cwd, filePath) => path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
31872
31969
  const findNativeFiles = ({ cwd, platform, pattern }) => {
@@ -32125,49 +32222,55 @@ async function checkNativeStatus({ cwd }) {
32125
32222
  issues
32126
32223
  };
32127
32224
  }
32128
- async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, requiredVersion = getRequiredInfrastructureVersion() }) {
32129
- const versionEndpoint = resolveVersionEndpoint(serverBaseUrl);
32130
- const baseUrl = serverBaseUrl.trim();
32225
+ async function checkBundleIndexStatus({ fix }) {
32131
32226
  try {
32132
- const response = await fetchImpl(versionEndpoint, { headers: { Accept: "application/json" } });
32133
- if (!response.ok) {
32134
- if (response.status === 404) return {
32135
- baseUrl,
32136
- versionEndpoint,
32137
- requiredVersion,
32138
- needsUpdate: true,
32139
- updateReason: "Version endpoint not found"
32227
+ const database = await (await loadConfig(null)).database();
32228
+ try {
32229
+ const bundleIndexDiagnostics = database.diagnostics?.bundleIndex;
32230
+ if (!bundleIndexDiagnostics) {
32231
+ if (!fix) return;
32232
+ return {
32233
+ adapterName: database.name,
32234
+ status: "not-applicable"
32235
+ };
32236
+ }
32237
+ const health = await bundleIndexDiagnostics.check();
32238
+ if (health?.status === "ok") return {
32239
+ adapterName: database.name,
32240
+ status: "ok",
32241
+ repairAvailable: bundleIndexDiagnostics.repair !== void 0,
32242
+ health
32243
+ };
32244
+ if (!fix || !bundleIndexDiagnostics.repair) return {
32245
+ adapterName: database.name,
32246
+ status: health.status,
32247
+ repairAvailable: bundleIndexDiagnostics.repair !== void 0,
32248
+ health
32249
+ };
32250
+ const repair = await bundleIndexDiagnostics.repair();
32251
+ const postRepairHealth = await bundleIndexDiagnostics.check();
32252
+ if (postRepairHealth.status !== "ok") return {
32253
+ adapterName: database.name,
32254
+ status: postRepairHealth.status,
32255
+ repairAvailable: true,
32256
+ health,
32257
+ postRepairHealth,
32258
+ repair
32140
32259
  };
32141
32260
  return {
32142
- baseUrl,
32143
- versionEndpoint,
32144
- requiredVersion,
32145
- error: `Version endpoint returned ${response.status}`
32261
+ adapterName: database.name,
32262
+ status: "repaired",
32263
+ repairAvailable: true,
32264
+ health,
32265
+ postRepairHealth,
32266
+ repair
32146
32267
  };
32268
+ } finally {
32269
+ await database.onUnmount?.();
32147
32270
  }
32148
- const data = await response.json();
32149
- if (typeof data.version !== "string") return {
32150
- baseUrl,
32151
- versionEndpoint,
32152
- requiredVersion,
32153
- error: "Version endpoint response must include a string version"
32154
- };
32155
- const needsUpdate = isInfrastructureUpdateRequired({
32156
- serverVersion: data.version,
32157
- requiredVersion
32158
- });
32159
- return {
32160
- baseUrl,
32161
- versionEndpoint,
32162
- serverVersion: data.version,
32163
- requiredVersion,
32164
- needsUpdate
32165
- };
32166
32271
  } catch (error) {
32167
32272
  return {
32168
- baseUrl,
32169
- versionEndpoint,
32170
- requiredVersion,
32273
+ status: "error",
32171
32274
  error: error.message
32172
32275
  };
32173
32276
  }
@@ -32179,7 +32282,7 @@ async function checkInfrastructureStatus({ serverBaseUrl, fetchImpl = fetch, req
32179
32282
  */
32180
32283
  async function doctor(options = {}) {
32181
32284
  try {
32182
- const { cwd = getCwd(), serverBaseUrl, fetch: fetchImpl } = options;
32285
+ const { cwd = getCwd(), serverBaseUrl, fetch: fetchImpl, fix = false } = options;
32183
32286
  const packageResult = await readPackageUp(cwd);
32184
32287
  if (!packageResult) return {
32185
32288
  success: false,
@@ -32213,15 +32316,17 @@ async function doctor(options = {}) {
32213
32316
  details.infrastructure = await checkInfrastructureStatus({
32214
32317
  serverBaseUrl,
32215
32318
  fetchImpl,
32216
- requiredVersion: getRequiredInfrastructureVersion(hotUpdaterVersion)
32319
+ requiredTarget: getRequiredUpdateTarget(hotUpdaterVersion)
32217
32320
  });
32218
32321
  if (details.infrastructure.error !== void 0 || details.infrastructure.needsUpdate === true) details.infrastructure.remediation = createInfrastructureRemediation();
32219
32322
  }
32220
32323
  if (hasReactNativePackage) details.native = await checkNativeStatus({ cwd });
32324
+ details.bundleIndex = await checkBundleIndexStatus({ fix });
32221
32325
  if (versionMismatches.length > 0) details.versionMismatches = versionMismatches;
32222
32326
  const hasInfrastructureIssue = details.infrastructure?.error !== void 0 || details.infrastructure?.needsUpdate === true;
32223
32327
  const hasNativeIssue = details.native?.issues.some((issue) => issue.type === "error") === true;
32224
- if (versionMismatches.length > 0 || hasInfrastructureIssue || hasNativeIssue) return {
32328
+ const hasBundleIndexIssue = details.bundleIndex?.status === "error" || details.bundleIndex?.status === "missing" || details.bundleIndex?.status === "stale";
32329
+ if (versionMismatches.length > 0 || hasInfrastructureIssue || hasNativeIssue || hasBundleIndexIssue) return {
32225
32330
  success: false,
32226
32331
  details
32227
32332
  };
@@ -32233,6 +32338,10 @@ async function doctor(options = {}) {
32233
32338
  success: true,
32234
32339
  details
32235
32340
  };
32341
+ if (details.bundleIndex) return {
32342
+ success: true,
32343
+ details
32344
+ };
32236
32345
  return true;
32237
32346
  } catch (error) {
32238
32347
  return {
@@ -32267,15 +32376,21 @@ const promptServerBaseUrl = async () => {
32267
32376
  const trimmed = serverBaseUrl.trim();
32268
32377
  return trimmed ? trimmed : void 0;
32269
32378
  };
32270
- const handleDoctor = async ({ serverBaseUrl, json = false } = {}) => {
32379
+ const handleDoctor = async ({ serverBaseUrl, json = false, fix = false } = {}) => {
32271
32380
  if (json) {
32272
- const result = normalizeDoctorResult(await doctor({ serverBaseUrl }));
32381
+ const result = normalizeDoctorResult(await doctor({
32382
+ serverBaseUrl,
32383
+ fix
32384
+ }));
32273
32385
  console.log(JSON.stringify(result, null, 2));
32274
32386
  if (!result.success) process.exit(1);
32275
32387
  return;
32276
32388
  }
32277
32389
  p.intro("Hot Updater doctor");
32278
- const result = await doctor({ serverBaseUrl: serverBaseUrl ?? await promptServerBaseUrl() });
32390
+ const result = await doctor({
32391
+ serverBaseUrl: serverBaseUrl ?? await promptServerBaseUrl(),
32392
+ fix
32393
+ });
32279
32394
  if (result === true) {
32280
32395
  p.log.success("All checks passed.");
32281
32396
  p.outro("Healthy.");
@@ -32302,12 +32417,10 @@ const handleDoctor = async ({ serverBaseUrl, json = false } = {}) => {
32302
32417
  if (infrastructure.updateReason) p.log.info(`Reason: ${infrastructure.updateReason}`);
32303
32418
  } else if (infrastructure.error) p.log.error(`Infrastructure check failed: ${infrastructure.error}`);
32304
32419
  else p.log.success("Infrastructure is up to date.");
32305
- 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([
32306
- ui.command("hot-updater db generate"),
32307
- "or",
32308
- ui.command("hot-updater db migrate"),
32309
- "then redeploy server"
32310
- ]))]));
32420
+ if (infrastructure.remediation) {
32421
+ const recoveryLines = infrastructure.remediation.commands.map((command, index) => ui.kv(index === 0 ? "Command" : `Command ${index + 1}`, ui.command(command)));
32422
+ p.log.message(ui.block("Recovery", recoveryLines));
32423
+ }
32311
32424
  }
32312
32425
  if (details?.native) {
32313
32426
  const native = details.native;
@@ -32328,6 +32441,23 @@ const handleDoctor = async ({ serverBaseUrl, json = false } = {}) => {
32328
32441
  p.log.info(issue.resolution);
32329
32442
  }
32330
32443
  }
32444
+ if (details?.bundleIndex) {
32445
+ const bundleIndex = details.bundleIndex;
32446
+ const lines = [ui.kv("Status", bundleIndex.status), ...bundleIndex.adapterName ? [ui.kv("Adapter", bundleIndex.adapterName)] : []];
32447
+ if (bundleIndex.health) lines.push(ui.kv("Canonical", bundleIndex.health.canonicalBundles), ui.kv("Indexed", bundleIndex.health.indexedBundles), ui.kv("Missing", bundleIndex.health.missingBundles), ui.kv("Extra", bundleIndex.health.extraBundles));
32448
+ if (bundleIndex.postRepairHealth) lines.push(ui.kv("Post-repair", bundleIndex.postRepairHealth.status), ui.kv("Post missing", bundleIndex.postRepairHealth.missingBundles), ui.kv("Post extra", bundleIndex.postRepairHealth.extraBundles));
32449
+ if (bundleIndex.repair) lines.push(ui.kv("Written", bundleIndex.repair.indexedBundles), ui.kv("Pages", bundleIndex.repair.pagesWritten), ui.kv("Scopes", bundleIndex.repair.scopesWritten));
32450
+ p.log.message(ui.block("Bundle index", lines));
32451
+ if (bundleIndex.status === "error") p.log.error(`Bundle index check failed: ${bundleIndex.error}`);
32452
+ else if (bundleIndex.status === "repaired") p.log.success("Bundle index repaired.");
32453
+ else if (bundleIndex.status === "ok") p.log.success("Bundle index is healthy.");
32454
+ else if (bundleIndex.status === "not-applicable") p.log.info("Bundle index repair is not supported by this adapter.");
32455
+ else if (bundleIndex.repair) p.log.error("Bundle index repair completed, but the index is still out of sync.");
32456
+ else if (bundleIndex.repairAvailable) {
32457
+ p.log.warn("Bundle index is out of sync.");
32458
+ p.log.info(`Run ${ui.command("hot-updater doctor --fix")} to repair it.`);
32459
+ } else p.log.error("Bundle index is out of sync and repair is not available.");
32460
+ }
32331
32461
  if (details?.versionMismatches && details.versionMismatches.length > 0) {
32332
32462
  p.log.warn("Version mismatches found:");
32333
32463
  for (const mismatch of details.versionMismatches) p.log.error(`${mismatch.packageName}: ${mismatch.currentVersion} (expected ${mismatch.expectedVersion})`);
@@ -53014,17 +53144,17 @@ const DEFAULT_CONFIG_BASENAMES = [
53014
53144
  path.join("src", "hotUpdater"),
53015
53145
  path.join("src", "db")
53016
53146
  ];
53017
- const findDefaultConfigPath = () => {
53147
+ const findDefaultConfigPath = (cwd) => {
53018
53148
  for (const basename of DEFAULT_CONFIG_BASENAMES) for (const ext of SUPPORTED_CONFIG_EXTENSIONS) {
53019
- const candidate = path.resolve(process.cwd(), `${basename}.${ext}`);
53149
+ const candidate = path.resolve(cwd, `${basename}.${ext}`);
53020
53150
  if (existsSync(candidate)) return candidate;
53021
53151
  }
53022
53152
  return null;
53023
53153
  };
53024
- const resolveConfigPath = (configPath) => {
53154
+ const resolveConfigPath = (configPath, cwd) => {
53025
53155
  const trimmedConfigPath = configPath.trim();
53026
- if (trimmedConfigPath) return path.resolve(process.cwd(), trimmedConfigPath);
53027
- const defaultConfigPath = findDefaultConfigPath();
53156
+ if (trimmedConfigPath) return path.resolve(cwd, trimmedConfigPath);
53157
+ const defaultConfigPath = findDefaultConfigPath(cwd);
53028
53158
  if (defaultConfigPath) return defaultConfigPath;
53029
53159
  p.log.error("Could not find a Hot Updater config file.");
53030
53160
  p.log.message(ui.block("Examples", [
@@ -53037,8 +53167,8 @@ const resolveConfigPath = (configPath) => {
53037
53167
  /**
53038
53168
  * Load and validate hotUpdater instance from config file
53039
53169
  */
53040
- async function loadHotUpdater(configPath) {
53041
- const absoluteConfigPath = resolveConfigPath(configPath);
53170
+ async function loadHotUpdater(configPath, options = {}) {
53171
+ const absoluteConfigPath = resolveConfigPath(configPath, options.cwd ?? process.cwd());
53042
53172
  if (!existsSync(absoluteConfigPath)) {
53043
53173
  p.log.error(ui.line(["Config file not found:", ui.path(absoluteConfigPath)]));
53044
53174
  process.exit(1);
@@ -54180,7 +54310,7 @@ const parseRolloutCohortCount = (value) => {
54180
54310
  const program = new Command$1();
54181
54311
  program.name("hot-updater").description(banner(version)).version(version);
54182
54312
  program.command("init").description("Initialize Hot Updater").action(init);
54183
- 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);
54313
+ 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("--fix", "automatically fix safe issues").option("--json", "output machine-readable doctor result").action(handleDoctor);
54184
54314
  const fingerprintCommand = program.command("fingerprint").description("Generate fingerprint");
54185
54315
  fingerprintCommand.action(handleFingerprint);
54186
54316
  fingerprintCommand.command("create").description("Create fingerprint").action(handleCreateFingerprint);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hot-updater",
3
3
  "type": "module",
4
- "version": "0.32.0",
4
+ "version": "0.33.0",
5
5
  "engines": {
6
6
  "node": ">=20.19.0"
7
7
  },
@@ -46,13 +46,13 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "jiti": "2.6.1",
49
- "@hot-updater/android-helper": "0.32.0",
50
- "@hot-updater/apple-helper": "0.32.0",
51
- "@hot-updater/cli-tools": "0.32.0",
52
- "@hot-updater/core": "0.32.0",
53
- "@hot-updater/console": "0.32.0",
54
- "@hot-updater/plugin-core": "0.32.0",
55
- "@hot-updater/server": "0.32.0"
49
+ "@hot-updater/android-helper": "0.33.0",
50
+ "@hot-updater/apple-helper": "0.33.0",
51
+ "@hot-updater/cli-tools": "0.33.0",
52
+ "@hot-updater/console": "0.33.0",
53
+ "@hot-updater/core": "0.33.0",
54
+ "@hot-updater/plugin-core": "0.33.0",
55
+ "@hot-updater/server": "0.33.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@bacons/xcode": "1.0.0-alpha.24",
@@ -75,12 +75,12 @@
75
75
  "tsdown": "0.21.6",
76
76
  "sql-formatter": "15.6.10",
77
77
  "typescript": "6.0.2",
78
- "@hot-updater/aws": "0.32.0",
79
- "@hot-updater/cloudflare": "0.32.0",
80
- "@hot-updater/firebase": "0.32.0",
81
- "@hot-updater/server": "0.32.0",
82
- "@hot-updater/supabase": "0.32.0",
83
- "@hot-updater/test-utils": "0.32.0"
78
+ "@hot-updater/aws": "0.33.0",
79
+ "@hot-updater/cloudflare": "0.33.0",
80
+ "@hot-updater/server": "0.33.0",
81
+ "@hot-updater/test-utils": "0.33.0",
82
+ "@hot-updater/supabase": "0.33.0",
83
+ "@hot-updater/firebase": "0.33.0"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@expo/fingerprint": "*",