@akanjs/cli 2.4.0-rc.7 → 2.4.0-rc.9

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/index.js CHANGED
@@ -720,7 +720,6 @@ var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
720
720
  var NATIVE_RUNTIME_PACKAGES = ["sharp"];
721
721
  var DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"];
722
722
  var MOBILE_RUNTIME_PACKAGES = [
723
- "firebase",
724
723
  "@capacitor/cli",
725
724
  "@capacitor/core",
726
725
  "@capacitor/ios",
@@ -728,7 +727,6 @@ var MOBILE_RUNTIME_PACKAGES = [
728
727
  "@capacitor/assets"
729
728
  ];
730
729
  var MOBILE_APP_CAPACITOR_PLUGINS = [
731
- "@capacitor-community/fcm",
732
730
  "@capacitor/app",
733
731
  "@capacitor/browser",
734
732
  "@capacitor/camera",
@@ -778,6 +776,13 @@ var normalizeStringList = (values) => {
778
776
  const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
779
777
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
780
778
  };
779
+ var sanitizeAppIdSegment = (value) => {
780
+ const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
781
+ if (!cleaned)
782
+ return "app";
783
+ return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
784
+ };
785
+ var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
781
786
  var normalizeDeepLinkDomain = (domain) => {
782
787
  const normalized = domain.trim();
783
788
  if (!normalized)
@@ -818,6 +823,7 @@ class AkanAppConfig {
818
823
  i18n;
819
824
  publicEnv;
820
825
  mobile;
826
+ hasMobileConfig;
821
827
  secrets;
822
828
  baseDevEnv;
823
829
  libs;
@@ -848,6 +854,7 @@ class AkanAppConfig {
848
854
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
849
855
  this.publicEnv = config?.publicEnv ?? [];
850
856
  this.secrets = config?.secrets ?? [];
857
+ this.hasMobileConfig = Boolean(config.mobile);
851
858
  this.mobile = this.#resolveMobileConfig(config.mobile);
852
859
  this.docker = this.#makeDockerContent(config?.docker ?? {});
853
860
  }
@@ -858,7 +865,7 @@ class AkanAppConfig {
858
865
  ...rawMobile
859
866
  } = mobile ?? {};
860
867
  const appName = rawMobile.appName ?? this.app.name;
861
- const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
868
+ const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
862
869
  const version = rawMobile.version ?? "0.0.1";
863
870
  const buildNum = rawMobile.buildNum ?? 1;
864
871
  const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
@@ -4324,6 +4331,8 @@ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4324
4331
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4325
4332
  var BUILDER_READY_TIMEOUT_MS = 150000;
4326
4333
  var BUILDER_START_MAX_ATTEMPTS = 3;
4334
+ var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
4335
+ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
4327
4336
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
4328
4337
  var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
4329
4338
  var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
@@ -4400,6 +4409,43 @@ var mergeBackendRestartReasons = (current, next) => ({
4400
4409
  });
4401
4410
  var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
4402
4411
  var shouldQueueBuildStatusReplay = (backendReady, pendingReplayCount) => !backendReady || pendingReplayCount > 0;
4412
+ var hasBuildFailureForGeneration = (statusByPhase, generation) => {
4413
+ if (typeof generation !== "number")
4414
+ return false;
4415
+ for (const status of statusByPhase.values()) {
4416
+ if (!status.ok && status.generation === generation)
4417
+ return true;
4418
+ }
4419
+ return false;
4420
+ };
4421
+ var mergeDevPlans = (current, next) => {
4422
+ if (!current)
4423
+ return next;
4424
+ if (!next)
4425
+ return current;
4426
+ const reasonByFile = { ...current.reasonByFile };
4427
+ for (const [file, reasons] of Object.entries(next.reasonByFile)) {
4428
+ reasonByFile[file] = [...new Set([...reasonByFile[file] ?? [], ...reasons])].sort();
4429
+ }
4430
+ return {
4431
+ generation: Math.max(current.generation, next.generation),
4432
+ files: [...new Set([...current.files, ...next.files])].sort(),
4433
+ generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
4434
+ roles: [...new Set([...current.roles, ...next.roles])].sort(),
4435
+ actions: [...new Set([...current.actions, ...next.actions])].sort(),
4436
+ reasonByFile
4437
+ };
4438
+ };
4439
+ var mergeInvalidateMessages = (current, next) => {
4440
+ const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
4441
+ return {
4442
+ type: "invalidate",
4443
+ kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
4444
+ files: [...new Set([...current.files, ...next.files])].sort(),
4445
+ generation: generation >= 0 ? generation : undefined,
4446
+ devPlan: mergeDevPlans(current.devPlan, next.devPlan)
4447
+ };
4448
+ };
4403
4449
  var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
4404
4450
 
4405
4451
  class BackendImportGraph {
@@ -4522,8 +4568,12 @@ class AkanAppHost {
4522
4568
  #restartTimer = null;
4523
4569
  #backendRecoveryTimer = null;
4524
4570
  #backendRecoveryAttempts = 0;
4571
+ #backendGaveUp = false;
4525
4572
  #backendLifecycleState = "stopped";
4526
4573
  #pendingRestartReason = null;
4574
+ #pendingRecycle = null;
4575
+ #builderRecoveryTimer = null;
4576
+ #builderRecoveryAttempts = 0;
4527
4577
  #backendStartStatus = null;
4528
4578
  #backendBuildStatusGeneration = 0;
4529
4579
  #backendStderrTail = [];
@@ -4561,6 +4611,10 @@ class AkanAppHost {
4561
4611
  clearTimeout(this.#backendRecoveryTimer);
4562
4612
  this.#backendRecoveryTimer = null;
4563
4613
  }
4614
+ if (this.#builderRecoveryTimer) {
4615
+ clearTimeout(this.#builderRecoveryTimer);
4616
+ this.#builderRecoveryTimer = null;
4617
+ }
4564
4618
  await this.#stopBackend();
4565
4619
  this.#stopBuilder();
4566
4620
  return this;
@@ -4576,6 +4630,7 @@ class AkanAppHost {
4576
4630
  }
4577
4631
  #startBackend(startStatus = null) {
4578
4632
  this.#backendStartStatus = startStatus;
4633
+ this.#backendGaveUp = false;
4579
4634
  this.#setBackendLifecycleState("starting");
4580
4635
  this.#backendReady = false;
4581
4636
  this.#backendStderrTail = [];
@@ -4767,7 +4822,8 @@ class AkanAppHost {
4767
4822
  if (this.#backendRecoveryTimer || this.#backend)
4768
4823
  return;
4769
4824
  if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4770
- const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for a server-side edit to retry.`;
4825
+ const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for an edit or a green build to retry.`;
4826
+ this.#backendGaveUp = true;
4771
4827
  this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4772
4828
  this.logger.error(`[backend-recovery] ${message}`);
4773
4829
  if (this.#backendStderrTail.length > 0) {
@@ -4814,6 +4870,7 @@ ${this.#backendStderrTail.join(`
4814
4870
  if (message.type === "build-status") {
4815
4871
  this.#recordBuildStatus(message.data);
4816
4872
  this.#sendOrQueueBuildStatus(message.data);
4873
+ this.#reviveBackendAfterGreenBuild(message.data);
4817
4874
  return;
4818
4875
  }
4819
4876
  if (message.type === "pages-updated")
@@ -4828,19 +4885,25 @@ ${this.#backendStderrTail.join(`
4828
4885
  }
4829
4886
  async#handleInvalidate(message) {
4830
4887
  this.#logDevPlan(message);
4831
- if (shouldRestartDevHostByDevPlan(message)) {
4832
- try {
4833
- await this.#restartDevHost(message);
4834
- } catch (err) {
4835
- this.#recordDevHostRestartFailure(message, err, "Config");
4888
+ const wantsDevHostRestart = shouldRestartDevHostByDevPlan(message);
4889
+ const pending = this.#pendingRecycle;
4890
+ if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || pending && message.kinds.includes("code")) {
4891
+ const refreshConfig = wantsDevHostRestart || (pending?.refreshConfig ?? false);
4892
+ const merged = pending ? mergeInvalidateMessages(pending.message, message) : message;
4893
+ const generation = message.devPlan?.generation ?? message.generation;
4894
+ if (hasBuildFailureForGeneration(this.#buildStatusByPhase, generation)) {
4895
+ this.#deferRecycle(merged, { refreshConfig, generation });
4896
+ return;
4836
4897
  }
4837
- return;
4838
- }
4839
- if (shouldRestartBuilderByDevPlan(message)) {
4898
+ this.#pendingRecycle = null;
4840
4899
  try {
4841
- await this.#restartDevChildren(message);
4900
+ if (refreshConfig)
4901
+ await this.#restartDevHost(merged);
4902
+ else
4903
+ await this.#restartDevChildren(merged);
4842
4904
  } catch (err) {
4843
- this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4905
+ this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
4906
+ this.#resurrectDevChildren(merged);
4844
4907
  }
4845
4908
  return;
4846
4909
  }
@@ -4850,6 +4913,70 @@ ${this.#backendStderrTail.join(`
4850
4913
  }
4851
4914
  this.#sendToBackend(message);
4852
4915
  }
4916
+ #deferRecycle(message, { refreshConfig, generation }) {
4917
+ this.#pendingRecycle = { message, refreshConfig };
4918
+ const kind = refreshConfig ? "Config" : "Runtime metadata";
4919
+ this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart deferred generation=${generation ?? "(unknown)"}; keeping the running dev server until the build error is fixed`);
4920
+ const status = {
4921
+ generation: generation ?? this.#nextBackendBuildStatusGeneration(),
4922
+ phase: "scan",
4923
+ ok: false,
4924
+ files: message.files,
4925
+ message: `${kind} change is on hold while the build is failing; it will apply automatically once the error is fixed.`
4926
+ };
4927
+ this.#recordBuildStatus(status);
4928
+ this.#sendOrQueueBuildStatus(status);
4929
+ }
4930
+ #resurrectDevChildren(message) {
4931
+ const generation = message.devPlan?.generation ?? message.generation;
4932
+ if (!this.#backend)
4933
+ this.#startBackend({ generation, files: message.files });
4934
+ this.#scheduleBuilderRecovery({ generation, files: message.files });
4935
+ }
4936
+ #scheduleBuilderRecovery(reason) {
4937
+ if (this.#builderRecoveryTimer || this.#builder)
4938
+ return;
4939
+ const attempt = this.#builderRecoveryAttempts;
4940
+ const delay = Math.min(BUILDER_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BUILDER_RECOVERY_MAX_DELAY_MS);
4941
+ this.#builderRecoveryAttempts = attempt + 1;
4942
+ this.logger.warn(`[builder-recovery] builder is down; retrying start in ${delay}ms (attempt ${this.#builderRecoveryAttempts})`);
4943
+ this.#builderRecoveryTimer = setTimeout(() => {
4944
+ this.#builderRecoveryTimer = null;
4945
+ if (this.#builder)
4946
+ return;
4947
+ this.#recoverBuilder(reason);
4948
+ }, delay);
4949
+ }
4950
+ async#recoverBuilder(reason) {
4951
+ try {
4952
+ await this.#startBuilder();
4953
+ } catch (err) {
4954
+ this.logger.warn(`[builder-recovery] builder start failed: ${err instanceof Error ? err.message : String(err)}`);
4955
+ this.#scheduleBuilderRecovery(reason);
4956
+ return;
4957
+ }
4958
+ this.#builderRecoveryAttempts = 0;
4959
+ this.logger.info("[builder-recovery] builder recovered");
4960
+ const status = {
4961
+ generation: reason.generation ?? this.#nextBackendBuildStatusGeneration(),
4962
+ phase: "scan",
4963
+ ok: true,
4964
+ files: reason.files,
4965
+ message: "Builder recovered"
4966
+ };
4967
+ this.#recordBuildStatus(status);
4968
+ this.#sendOrQueueBuildStatus(status);
4969
+ if (!this.#backend && !this.#backendRecoveryTimer) {
4970
+ this.#startBackend({ generation: reason.generation, files: reason.files });
4971
+ }
4972
+ }
4973
+ #reviveBackendAfterGreenBuild(status) {
4974
+ if (!status.ok || !this.#backendGaveUp || this.#backend || this.#backendRecoveryTimer)
4975
+ return;
4976
+ this.logger.info(`[backend-recovery] build went green (generation=${status.generation}); retrying backend`);
4977
+ this.#backendRecoveryAttempts = 0;
4978
+ this.#startBackend({ generation: status.generation, files: status.files });
4979
+ }
4853
4980
  async#restartDevChildren(message) {
4854
4981
  const generation = message.devPlan?.generation ?? message.generation;
4855
4982
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
@@ -4870,6 +4997,11 @@ ${this.#backendStderrTail.join(`
4870
4997
  clearTimeout(this.#backendRecoveryTimer);
4871
4998
  this.#backendRecoveryTimer = null;
4872
4999
  }
5000
+ if (this.#builderRecoveryTimer) {
5001
+ clearTimeout(this.#builderRecoveryTimer);
5002
+ this.#builderRecoveryTimer = null;
5003
+ }
5004
+ this.#builderRecoveryAttempts = 0;
4873
5005
  this.#pendingRestartReason = null;
4874
5006
  this.#lastGoodFrontend = {};
4875
5007
  this.#buildStatusByPhase.clear();
@@ -4907,7 +5039,7 @@ ${this.#backendStderrTail.join(`
4907
5039
  phase: "scan",
4908
5040
  ok: false,
4909
5041
  files: message.files,
4910
- message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
5042
+ message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
4911
5043
  };
4912
5044
  this.#recordBuildStatus(status);
4913
5045
  this.#sendOrQueueBuildStatus(status);
@@ -14077,6 +14209,7 @@ var resolveMobilePath = (target, pathname) => {
14077
14209
  };
14078
14210
  var targetHtmlFilename = (target) => target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
14079
14211
  // pkgs/@akanjs/devkit/capacitorApp.ts
14212
+ var SWIFTUICORE_MIN_IOS_MAJOR = 18;
14080
14213
  var iosNativeBlockedEnvKeys = new Set([
14081
14214
  "AR",
14082
14215
  "AS",
@@ -14114,17 +14247,56 @@ async function writeRootCapacitorConfig(appRoot, content) {
14114
14247
  await clearRootCapacitorConfigs(appRoot);
14115
14248
  await Bun.write(path42.join(appRoot, "capacitor.config.json"), content);
14116
14249
  }
14117
- var getLocalIP = () => {
14118
- const interfaces = os.networkInterfaces();
14119
- for (const iface of Object.values(interfaces)) {
14120
- if (!iface)
14121
- continue;
14122
- for (const alias of iface) {
14123
- if (alias.family === "IPv4" && !alias.internal)
14124
- return alias.address;
14250
+ var virtualInterfacePrefixes = [
14251
+ "bridge",
14252
+ "utun",
14253
+ "llw",
14254
+ "awdl",
14255
+ "ap",
14256
+ "vmnet",
14257
+ "vnic",
14258
+ "tap",
14259
+ "tun",
14260
+ "docker",
14261
+ "veth",
14262
+ "vboxnet",
14263
+ "gif",
14264
+ "stf"
14265
+ ];
14266
+ var physicalInterfacePrefixes = ["en", "eth", "wlan", "wlp", "enp", "eno", "wlo"];
14267
+ var isPrivateLanIpv4 = (address) => {
14268
+ if (address.startsWith("10.") || address.startsWith("192.168."))
14269
+ return true;
14270
+ const secondOctet = Number(address.match(/^172\.(\d+)\./)?.[1]);
14271
+ return Number.isFinite(secondOctet) && secondOctet >= 16 && secondOctet <= 31;
14272
+ };
14273
+ var scoreDevHostCandidate = (name, address) => {
14274
+ const lowerName = name.toLowerCase();
14275
+ let score = 0;
14276
+ if (address.startsWith("169.254."))
14277
+ score -= 1000;
14278
+ if (virtualInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix)))
14279
+ score -= 100;
14280
+ else if (physicalInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix)))
14281
+ score += 100;
14282
+ if (isPrivateLanIpv4(address))
14283
+ score += 10;
14284
+ return score;
14285
+ };
14286
+ var selectLocalDevHost = (interfaces, { override } = {}) => {
14287
+ const candidates = [];
14288
+ for (const [name, aliases] of Object.entries(interfaces)) {
14289
+ for (const alias of aliases ?? []) {
14290
+ if (alias.family !== "IPv4" || alias.internal)
14291
+ continue;
14292
+ candidates.push({ name, address: alias.address });
14125
14293
  }
14126
14294
  }
14127
- return "127.0.0.1";
14295
+ const trimmedOverride = override?.trim();
14296
+ if (trimmedOverride)
14297
+ return { host: trimmedOverride, source: "override", candidates };
14298
+ const [best] = [...candidates].sort((a, b) => scoreDevHostCandidate(b.name, b.address) - scoreDevHostCandidate(a.name, a.address) || a.name.localeCompare(b.name) || a.address.localeCompare(b.address));
14299
+ return best ? { host: best.address, source: "detected", candidates } : { host: "127.0.0.1", source: "loopback", candidates };
14128
14300
  };
14129
14301
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14130
14302
  var asString = (value) => typeof value === "string" ? value : undefined;
@@ -14144,6 +14316,30 @@ var dedupeIosRunTargets = (targets) => {
14144
14316
  }
14145
14317
  return [...byKey.values()];
14146
14318
  };
14319
+ var formatSimctlRuntime = (key) => {
14320
+ if (!key)
14321
+ return;
14322
+ if (/^(iOS|watchOS|tvOS|visionOS)\s+[\d.]+$/i.test(key))
14323
+ return key;
14324
+ const match = key.match(/(iOS|watchOS|tvOS|visionOS)-(\d+)(?:-(\d+))?/i);
14325
+ if (!match)
14326
+ return;
14327
+ return `${match[1]} ${match[2]}${match[3] ? `.${match[3]}` : ""}`;
14328
+ };
14329
+ var parseIosRuntimeMajor = (runtime) => {
14330
+ const major = runtime?.match(/(\d+)/)?.[1];
14331
+ if (major === undefined)
14332
+ return;
14333
+ const parsed = Number.parseInt(major, 10);
14334
+ return Number.isNaN(parsed) ? undefined : parsed;
14335
+ };
14336
+ var iosRunTargetRank = (target) => {
14337
+ const state = target.state?.toLowerCase() ?? "";
14338
+ const ready = state.includes("booted") || state.includes("connected") || state.includes("available") ? 1000 : 0;
14339
+ const device = target.kind === "device" ? 500 : 0;
14340
+ return ready + device + (parseIosRuntimeMajor(target.runtime) ?? 0);
14341
+ };
14342
+ var sortIosRunTargets = (targets) => [...targets].sort((a, b) => iosRunTargetRank(b) - iosRunTargetRank(a) || a.name.localeCompare(b.name));
14147
14343
  function walkRecords(value, visit) {
14148
14344
  if (Array.isArray(value)) {
14149
14345
  for (const item of value)
@@ -14196,21 +14392,38 @@ function parseSimctlDevices(output) {
14196
14392
  try {
14197
14393
  const json = JSON.parse(output);
14198
14394
  const devices = json.devices ?? {};
14199
- return Object.values(devices).flat().filter(isRecord2).flatMap((device) => {
14200
- const id = firstString(device.udid, device.UDID, device.identifier);
14201
- const name = firstString(device.name, device.displayName);
14202
- const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14203
- if (!id || !name || !isAvailable)
14204
- return [];
14205
- return [{ id, name, kind: "simulator", state: asString(device.state) }];
14395
+ return Object.entries(devices).flatMap(([runtimeKey, list]) => {
14396
+ const runtime = formatSimctlRuntime(runtimeKey);
14397
+ return (Array.isArray(list) ? list : []).filter(isRecord2).flatMap((device) => {
14398
+ const id = firstString(device.udid, device.UDID, device.identifier);
14399
+ const name = firstString(device.name, device.displayName);
14400
+ const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14401
+ if (!id || !name || !isAvailable)
14402
+ return [];
14403
+ return [
14404
+ {
14405
+ id,
14406
+ name,
14407
+ kind: "simulator",
14408
+ state: asString(device.state),
14409
+ runtime: runtime ?? formatSimctlRuntime(asString(device.runtimeIdentifier))
14410
+ }
14411
+ ];
14412
+ });
14206
14413
  });
14207
14414
  } catch {
14208
14415
  const targets = [];
14416
+ let runtime;
14209
14417
  for (const line of output.split(/\r?\n/)) {
14418
+ const header = line.match(/^--\s*(.+?)\s*--\s*$/);
14419
+ if (header) {
14420
+ runtime = formatSimctlRuntime(header[1]);
14421
+ continue;
14422
+ }
14210
14423
  const match = line.match(/^\s*(.+?)\s+\(([0-9A-Fa-f-]{20,})\)\s+\(([^)]+)\)/);
14211
14424
  if (!match)
14212
14425
  continue;
14213
- targets.push({ id: match[2], name: match[1].trim(), kind: "simulator", state: match[3] });
14426
+ targets.push({ id: match[2], name: match[1].trim(), kind: "simulator", state: match[3], runtime });
14214
14427
  }
14215
14428
  return targets;
14216
14429
  }
@@ -14245,6 +14458,13 @@ function buildIosNativeRunCommand({
14245
14458
  }
14246
14459
  function classifyIosRunFailure(log) {
14247
14460
  const lower = log.toLowerCase();
14461
+ if (lower.includes("swiftuicore") && (lower.includes("library not loaded") || lower.includes("library missing") || lower.includes("dyld"))) {
14462
+ return {
14463
+ kind: "simulator-runtime",
14464
+ title: "App crashed loading SwiftUICore \u2014 the iOS runtime is too old.",
14465
+ detail: `The build links SwiftUICore, which only exists on iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+. Rerun on an iOS ${SWIFTUICORE_MIN_IOS_MAJOR} or newer simulator/device (pass --device to pick one non-interactively).`
14466
+ };
14467
+ }
14248
14468
  if (lower.includes("unknown argument: '-index-store-path'") || lower.includes("compiler was not recognized")) {
14249
14469
  return {
14250
14470
  kind: "compiler-toolchain",
@@ -14342,6 +14562,21 @@ function formatIosRunFailureMessage(input2) {
14342
14562
  function sanitizeIosNativeRunEnv(env) {
14343
14563
  return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
14344
14564
  }
14565
+ var PLACEHOLDER_APP_IDS = [
14566
+ "com.myapp.app",
14567
+ "com.myorg.myapp",
14568
+ "com.example.app",
14569
+ "com.example.myapp"
14570
+ ];
14571
+ var placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
14572
+ var isPlaceholderAppId = (appId) => {
14573
+ const normalized = appId?.trim().toLowerCase() ?? "";
14574
+ if (!normalized)
14575
+ return true;
14576
+ if (PLACEHOLDER_APP_IDS.includes(normalized))
14577
+ return true;
14578
+ return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
14579
+ };
14345
14580
  var androidReleaseSigningKeys = [
14346
14581
  "MYAPP_RELEASE_STORE_FILE",
14347
14582
  "MYAPP_RELEASE_STORE_PASSWORD",
@@ -14590,23 +14825,37 @@ class CapacitorApp {
14590
14825
  await this.#runIosPhysicalDevice({ operation, env, runTarget, noAllowProvisioningUpdates });
14591
14826
  }
14592
14827
  async#selectIosRunTarget(deviceId) {
14593
- const targets = await this.#loadIosRunTargets();
14828
+ const targets = sortIosRunTargets(await this.#loadIosRunTargets());
14594
14829
  if (deviceId) {
14595
- const found = targets.find((target) => target.id === deviceId);
14596
- if (!found)
14597
- throw new Error(`iOS run target '${deviceId}' was not found.`);
14830
+ const needle = deviceId.toLowerCase();
14831
+ const found = targets.find((target) => target.id === deviceId) ?? targets.find((target) => target.name.toLowerCase() === needle) ?? targets.find((target) => target.name.toLowerCase().includes(needle) || target.id.toLowerCase().includes(needle) || (target.runtime?.toLowerCase().includes(needle) ?? false));
14832
+ if (!found) {
14833
+ const available = targets.map((t) => `${t.name}${t.runtime ? ` (${t.runtime})` : ""}`).join(", ") || "none";
14834
+ throw new Error(`iOS run target '${deviceId}' was not found. Available: ${available}`);
14835
+ }
14836
+ this.#warnIfLegacySimulatorRuntime(found);
14598
14837
  return found;
14599
14838
  }
14600
14839
  if (targets.length === 0) {
14601
14840
  throw new Error("No iOS run targets found. Open Simulator or connect an iPhone, then retry.");
14602
14841
  }
14603
- return await select2({
14842
+ const selected = await select2({
14604
14843
  message: "Select iOS run target",
14605
14844
  choices: targets.map((target) => ({
14606
- name: `[${target.kind}] ${target.name}${target.state ? ` (${target.state})` : ""}`,
14845
+ name: `[${target.kind}] ${target.name}${target.runtime ? ` \u2014 ${target.runtime}` : ""}${target.state ? ` (${target.state})` : ""}`,
14607
14846
  value: target
14608
14847
  }))
14609
14848
  });
14849
+ this.#warnIfLegacySimulatorRuntime(selected);
14850
+ return selected;
14851
+ }
14852
+ #warnIfLegacySimulatorRuntime(target) {
14853
+ if (target.kind !== "simulator")
14854
+ return;
14855
+ const major = parseIosRuntimeMajor(target.runtime);
14856
+ if (major === undefined || major >= SWIFTUICORE_MIN_IOS_MAJOR)
14857
+ return;
14858
+ this.app.logger.warn(`Selected simulator runs ${target.runtime ?? "an older iOS"}. Recent SDK builds link SwiftUICore and require iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+; if the app crashes at launch with a "Library not loaded: SwiftUICore" dyld error, pick an iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+ simulator instead.`);
14610
14859
  }
14611
14860
  async#loadIosRunTargets() {
14612
14861
  const devices = await this.#loadPhysicalIosDevices();
@@ -14853,7 +15102,13 @@ ${error.message}`;
14853
15102
  }
14854
15103
  async#writeCapacitorConfig({ operation }, commandEnv) {
14855
15104
  await mkdir11(this.targetRoot, { recursive: true });
14856
- const localIp = operation === "local" ? getLocalIP() : undefined;
15105
+ let localIp;
15106
+ if (operation === "local") {
15107
+ const override = commandEnv.AKAN_PUBLIC_CLIENT_HOST ?? process.env.AKAN_PUBLIC_CLIENT_HOST;
15108
+ const resolution = selectLocalDevHost(os.networkInterfaces(), { override });
15109
+ localIp = resolution.host;
15110
+ this.#logDevHostResolution(resolution, commandEnv);
15111
+ }
14857
15112
  const config = materializeCapacitorConfig(this.target, {
14858
15113
  operation,
14859
15114
  localIp,
@@ -14864,6 +15119,17 @@ ${error.message}`;
14864
15119
  await Bun.write(path42.join(this.targetRoot, "capacitor.config.json"), content);
14865
15120
  return content;
14866
15121
  }
15122
+ #logDevHostResolution(resolution, commandEnv) {
15123
+ this.app.log(`Mobile live-reload server: ${this.#localCsrUrl(resolution.host, commandEnv)}`);
15124
+ if (resolution.source === "override")
15125
+ return;
15126
+ const suspicious = resolution.host === "127.0.0.1" || resolution.host.startsWith("169.254.");
15127
+ const alternatives = resolution.candidates.filter((candidate) => candidate.address !== resolution.host);
15128
+ if (!suspicious && alternatives.length === 0)
15129
+ return;
15130
+ const alternativeText = alternatives.length ? ` Other interfaces: ${alternatives.map((candidate) => `${candidate.address} (${candidate.name})`).join(", ")}.` : "";
15131
+ this.app.logger.warn(`A physical device must reach ${resolution.host} on your LAN.${suspicious ? " That address looks non-routable." : ""}${alternativeText} If the device shows a blank screen, pin the right one with AKAN_PUBLIC_CLIENT_HOST=<ip>.`);
15132
+ }
14867
15133
  async#prepareTargetAssets() {
14868
15134
  if (!this.target.assets)
14869
15135
  return;
@@ -17538,6 +17804,7 @@ try {
17538
17804
  operation = "local",
17539
17805
  env = "local",
17540
17806
  target,
17807
+ device,
17541
17808
  regenerate = false,
17542
17809
  noAllowProvisioningUpdates = false
17543
17810
  } = {}) {
@@ -17546,7 +17813,7 @@ try {
17546
17813
  await this.#buildMobileCsr(app, env);
17547
17814
  await this.#runMobileTargets(targets, async (mobileTarget2) => {
17548
17815
  const capacitorApp2 = new CapacitorApp(app, mobileTarget2.config);
17549
- await capacitorApp2.runIos({ operation, env, regenerate, noAllowProvisioningUpdates });
17816
+ await capacitorApp2.runIos({ operation, env, regenerate, noAllowProvisioningUpdates, iosDeviceId: device });
17550
17817
  if (open)
17551
17818
  await capacitorApp2.openIos();
17552
17819
  });
@@ -17937,6 +18204,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17937
18204
  env = "local",
17938
18205
  write = true,
17939
18206
  target,
18207
+ device,
17940
18208
  regenerate = false,
17941
18209
  noAllowProvisioningUpdates = false
17942
18210
  } = {}) {
@@ -17949,6 +18217,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17949
18217
  operation,
17950
18218
  env,
17951
18219
  target,
18220
+ device,
17952
18221
  regenerate,
17953
18222
  noAllowProvisioningUpdates
17954
18223
  });
@@ -18083,7 +18352,10 @@ class ApplicationCommand extends command("application", [ApplicationScript], ({
18083
18352
  }).option("open", Boolean, { desc: "open ios simulator", default: false }).option("release", Boolean, { desc: "release mode", default: false }).option("write", Boolean, { desc: "write code generation", default: true }).option("regenerate", Boolean, { flag: "g", desc: "delete and regenerate native project", default: false }).option("noAllowProvisioningUpdates", Boolean, {
18084
18353
  desc: "disable automatic iOS provisioning updates for physical devices",
18085
18354
  default: false
18086
- }).exec(async function(app, target2, env, open, release, write, regenerate, noAllowProvisioningUpdates) {
18355
+ }).option("device", String, {
18356
+ desc: "run target to select non-interactively: udid, device name, or runtime (e.g. 'iPhone 16' or 'iOS 18')",
18357
+ default: ""
18358
+ }).exec(async function(app, target2, env, open, release, write, regenerate, noAllowProvisioningUpdates, device) {
18087
18359
  await this.applicationScript.startIos(app, {
18088
18360
  target: target2,
18089
18361
  env: asMobileEnv(env),
@@ -18091,7 +18363,8 @@ class ApplicationCommand extends command("application", [ApplicationScript], ({
18091
18363
  operation: release ? "release" : "local",
18092
18364
  write,
18093
18365
  regenerate,
18094
- noAllowProvisioningUpdates
18366
+ noAllowProvisioningUpdates,
18367
+ device: device || undefined
18095
18368
  });
18096
18369
  }),
18097
18370
  startAndroid: target({ short: true, desc: "Start Android app in emulator or device" }).with(App).option("target", String, mobileTargetOption).option("env", String, {
@@ -21452,10 +21725,49 @@ class ContextRunner extends runner("context") {
21452
21725
  });
21453
21726
  return format === "json" ? jsonText(context) : AkanContextAnalyzer.renderMarkdown(context, { module });
21454
21727
  }
21455
- async doctor(workspace, { format = "text", strict = false } = {}) {
21728
+ async doctor(workspace, {
21729
+ format = "text",
21730
+ strict = false,
21731
+ ios = false
21732
+ } = {}) {
21733
+ if (ios)
21734
+ return await this.#doctorIos(workspace, format);
21456
21735
  const result = await AkanContextAnalyzer.doctor(workspace, { strict });
21457
21736
  return format === "json" ? jsonText(result) : renderDoctorText(result);
21458
21737
  }
21738
+ async#doctorIos(workspace, format) {
21739
+ const appNames = await workspace.getApps();
21740
+ const diagnostics = [];
21741
+ for (const appName of appNames) {
21742
+ const app = AppExecutor.from(workspace, appName);
21743
+ const config = await app.getConfig();
21744
+ if (!config.hasMobileConfig)
21745
+ continue;
21746
+ for (const { name, config: target } of await getMobileTargets(app)) {
21747
+ if (!isPlaceholderAppId(target.appId))
21748
+ continue;
21749
+ diagnostics.push({
21750
+ severity: "warning",
21751
+ code: "mobile-appid-placeholder",
21752
+ path: `apps/${appName}/akan.config.ts`,
21753
+ message: `Mobile target '${name}' uses placeholder bundle id '${target.appId}'. Apple's developer portal almost always already claims it, so signing to a physical device fails with "cannot be registered to your development team". Set a unique mobile.appId (reverse-DNS of your org).`
21754
+ });
21755
+ }
21756
+ }
21757
+ const status = diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed";
21758
+ if (format === "json")
21759
+ return jsonText({ schemaVersion: 1, kind: "ios", status, diagnostics });
21760
+ const lines = [`Akan iOS diagnostics for ${workspace.repoName}`];
21761
+ if (diagnostics.length === 0)
21762
+ lines.push(" No mobile configuration issues found.");
21763
+ else
21764
+ for (const diagnostic of diagnostics) {
21765
+ lines.push(` [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
21766
+ lines.push(` ${diagnostic.path}`);
21767
+ }
21768
+ return lines.join(`
21769
+ `);
21770
+ }
21459
21771
  async getGuidelineResource(name) {
21460
21772
  return await Prompter.getInstruction(name);
21461
21773
  }
@@ -21852,8 +22164,11 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
21852
22164
  desc: "output format",
21853
22165
  default: "text",
21854
22166
  enum: ["text", "json"]
21855
- }).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
21856
- await this.contextScript.doctor(workspace, { format, strict });
22167
+ }).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).option("ios", Boolean, {
22168
+ desc: "report iOS/mobile config diagnostics (placeholder bundle ids, etc.)",
22169
+ default: false
22170
+ }).with(Workspace).exec(async function(format, strict, ios, workspace) {
22171
+ await this.contextScript.doctor(workspace, { format, strict, ios });
21857
22172
  }),
21858
22173
  mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor, Claude Code, and Codex" }).arg("target", String, { desc: "cursor, claude, codex, or all", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
21859
22174
  desc: "MCP permission mode",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.4.0-rc.7",
3
+ "version": "2.4.0-rc.9",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.4.0-rc.7",
37
+ "akanjs": "2.4.0-rc.9",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",