@akanjs/cli 2.4.0-rc.8 → 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
@@ -776,6 +776,13 @@ var normalizeStringList = (values) => {
776
776
  const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
777
777
  return normalized.length > 0 ? [...new Set(normalized)] : undefined;
778
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)}`;
779
786
  var normalizeDeepLinkDomain = (domain) => {
780
787
  const normalized = domain.trim();
781
788
  if (!normalized)
@@ -816,6 +823,7 @@ class AkanAppConfig {
816
823
  i18n;
817
824
  publicEnv;
818
825
  mobile;
826
+ hasMobileConfig;
819
827
  secrets;
820
828
  baseDevEnv;
821
829
  libs;
@@ -846,6 +854,7 @@ class AkanAppConfig {
846
854
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
847
855
  this.publicEnv = config?.publicEnv ?? [];
848
856
  this.secrets = config?.secrets ?? [];
857
+ this.hasMobileConfig = Boolean(config.mobile);
849
858
  this.mobile = this.#resolveMobileConfig(config.mobile);
850
859
  this.docker = this.#makeDockerContent(config?.docker ?? {});
851
860
  }
@@ -856,7 +865,7 @@ class AkanAppConfig {
856
865
  ...rawMobile
857
866
  } = mobile ?? {};
858
867
  const appName = rawMobile.appName ?? this.app.name;
859
- const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
868
+ const appId = rawMobile.appId ?? deriveDefaultAppId(this.baseDevEnv.repoName, this.app.name);
860
869
  const version = rawMobile.version ?? "0.0.1";
861
870
  const buildNum = rawMobile.buildNum ?? 1;
862
871
  const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
@@ -4322,6 +4331,8 @@ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4322
4331
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4323
4332
  var BUILDER_READY_TIMEOUT_MS = 150000;
4324
4333
  var BUILDER_START_MAX_ATTEMPTS = 3;
4334
+ var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
4335
+ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
4325
4336
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
4326
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;
4327
4338
  var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
@@ -4398,6 +4409,43 @@ var mergeBackendRestartReasons = (current, next) => ({
4398
4409
  });
4399
4410
  var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
4400
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
+ };
4401
4449
  var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
4402
4450
 
4403
4451
  class BackendImportGraph {
@@ -4520,8 +4568,12 @@ class AkanAppHost {
4520
4568
  #restartTimer = null;
4521
4569
  #backendRecoveryTimer = null;
4522
4570
  #backendRecoveryAttempts = 0;
4571
+ #backendGaveUp = false;
4523
4572
  #backendLifecycleState = "stopped";
4524
4573
  #pendingRestartReason = null;
4574
+ #pendingRecycle = null;
4575
+ #builderRecoveryTimer = null;
4576
+ #builderRecoveryAttempts = 0;
4525
4577
  #backendStartStatus = null;
4526
4578
  #backendBuildStatusGeneration = 0;
4527
4579
  #backendStderrTail = [];
@@ -4559,6 +4611,10 @@ class AkanAppHost {
4559
4611
  clearTimeout(this.#backendRecoveryTimer);
4560
4612
  this.#backendRecoveryTimer = null;
4561
4613
  }
4614
+ if (this.#builderRecoveryTimer) {
4615
+ clearTimeout(this.#builderRecoveryTimer);
4616
+ this.#builderRecoveryTimer = null;
4617
+ }
4562
4618
  await this.#stopBackend();
4563
4619
  this.#stopBuilder();
4564
4620
  return this;
@@ -4574,6 +4630,7 @@ class AkanAppHost {
4574
4630
  }
4575
4631
  #startBackend(startStatus = null) {
4576
4632
  this.#backendStartStatus = startStatus;
4633
+ this.#backendGaveUp = false;
4577
4634
  this.#setBackendLifecycleState("starting");
4578
4635
  this.#backendReady = false;
4579
4636
  this.#backendStderrTail = [];
@@ -4765,7 +4822,8 @@ class AkanAppHost {
4765
4822
  if (this.#backendRecoveryTimer || this.#backend)
4766
4823
  return;
4767
4824
  if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4768
- 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;
4769
4827
  this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4770
4828
  this.logger.error(`[backend-recovery] ${message}`);
4771
4829
  if (this.#backendStderrTail.length > 0) {
@@ -4812,6 +4870,7 @@ ${this.#backendStderrTail.join(`
4812
4870
  if (message.type === "build-status") {
4813
4871
  this.#recordBuildStatus(message.data);
4814
4872
  this.#sendOrQueueBuildStatus(message.data);
4873
+ this.#reviveBackendAfterGreenBuild(message.data);
4815
4874
  return;
4816
4875
  }
4817
4876
  if (message.type === "pages-updated")
@@ -4826,19 +4885,25 @@ ${this.#backendStderrTail.join(`
4826
4885
  }
4827
4886
  async#handleInvalidate(message) {
4828
4887
  this.#logDevPlan(message);
4829
- if (shouldRestartDevHostByDevPlan(message)) {
4830
- try {
4831
- await this.#restartDevHost(message);
4832
- } catch (err) {
4833
- 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;
4834
4897
  }
4835
- return;
4836
- }
4837
- if (shouldRestartBuilderByDevPlan(message)) {
4898
+ this.#pendingRecycle = null;
4838
4899
  try {
4839
- await this.#restartDevChildren(message);
4900
+ if (refreshConfig)
4901
+ await this.#restartDevHost(merged);
4902
+ else
4903
+ await this.#restartDevChildren(merged);
4840
4904
  } catch (err) {
4841
- this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4905
+ this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
4906
+ this.#resurrectDevChildren(merged);
4842
4907
  }
4843
4908
  return;
4844
4909
  }
@@ -4848,6 +4913,70 @@ ${this.#backendStderrTail.join(`
4848
4913
  }
4849
4914
  this.#sendToBackend(message);
4850
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
+ }
4851
4980
  async#restartDevChildren(message) {
4852
4981
  const generation = message.devPlan?.generation ?? message.generation;
4853
4982
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
@@ -4868,6 +4997,11 @@ ${this.#backendStderrTail.join(`
4868
4997
  clearTimeout(this.#backendRecoveryTimer);
4869
4998
  this.#backendRecoveryTimer = null;
4870
4999
  }
5000
+ if (this.#builderRecoveryTimer) {
5001
+ clearTimeout(this.#builderRecoveryTimer);
5002
+ this.#builderRecoveryTimer = null;
5003
+ }
5004
+ this.#builderRecoveryAttempts = 0;
4871
5005
  this.#pendingRestartReason = null;
4872
5006
  this.#lastGoodFrontend = {};
4873
5007
  this.#buildStatusByPhase.clear();
@@ -4905,7 +5039,7 @@ ${this.#backendStderrTail.join(`
4905
5039
  phase: "scan",
4906
5040
  ok: false,
4907
5041
  files: message.files,
4908
- message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
5042
+ message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
4909
5043
  };
4910
5044
  this.#recordBuildStatus(status);
4911
5045
  this.#sendOrQueueBuildStatus(status);
@@ -14075,6 +14209,7 @@ var resolveMobilePath = (target, pathname) => {
14075
14209
  };
14076
14210
  var targetHtmlFilename = (target) => target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
14077
14211
  // pkgs/@akanjs/devkit/capacitorApp.ts
14212
+ var SWIFTUICORE_MIN_IOS_MAJOR = 18;
14078
14213
  var iosNativeBlockedEnvKeys = new Set([
14079
14214
  "AR",
14080
14215
  "AS",
@@ -14112,17 +14247,56 @@ async function writeRootCapacitorConfig(appRoot, content) {
14112
14247
  await clearRootCapacitorConfigs(appRoot);
14113
14248
  await Bun.write(path42.join(appRoot, "capacitor.config.json"), content);
14114
14249
  }
14115
- var getLocalIP = () => {
14116
- const interfaces = os.networkInterfaces();
14117
- for (const iface of Object.values(interfaces)) {
14118
- if (!iface)
14119
- continue;
14120
- for (const alias of iface) {
14121
- if (alias.family === "IPv4" && !alias.internal)
14122
- 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 });
14123
14293
  }
14124
14294
  }
14125
- 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 };
14126
14300
  };
14127
14301
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14128
14302
  var asString = (value) => typeof value === "string" ? value : undefined;
@@ -14142,6 +14316,30 @@ var dedupeIosRunTargets = (targets) => {
14142
14316
  }
14143
14317
  return [...byKey.values()];
14144
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));
14145
14343
  function walkRecords(value, visit) {
14146
14344
  if (Array.isArray(value)) {
14147
14345
  for (const item of value)
@@ -14194,21 +14392,38 @@ function parseSimctlDevices(output) {
14194
14392
  try {
14195
14393
  const json = JSON.parse(output);
14196
14394
  const devices = json.devices ?? {};
14197
- return Object.values(devices).flat().filter(isRecord2).flatMap((device) => {
14198
- const id = firstString(device.udid, device.UDID, device.identifier);
14199
- const name = firstString(device.name, device.displayName);
14200
- const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
14201
- if (!id || !name || !isAvailable)
14202
- return [];
14203
- 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
+ });
14204
14413
  });
14205
14414
  } catch {
14206
14415
  const targets = [];
14416
+ let runtime;
14207
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
+ }
14208
14423
  const match = line.match(/^\s*(.+?)\s+\(([0-9A-Fa-f-]{20,})\)\s+\(([^)]+)\)/);
14209
14424
  if (!match)
14210
14425
  continue;
14211
- 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 });
14212
14427
  }
14213
14428
  return targets;
14214
14429
  }
@@ -14243,6 +14458,13 @@ function buildIosNativeRunCommand({
14243
14458
  }
14244
14459
  function classifyIosRunFailure(log) {
14245
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
+ }
14246
14468
  if (lower.includes("unknown argument: '-index-store-path'") || lower.includes("compiler was not recognized")) {
14247
14469
  return {
14248
14470
  kind: "compiler-toolchain",
@@ -14340,6 +14562,21 @@ function formatIosRunFailureMessage(input2) {
14340
14562
  function sanitizeIosNativeRunEnv(env) {
14341
14563
  return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
14342
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
+ };
14343
14580
  var androidReleaseSigningKeys = [
14344
14581
  "MYAPP_RELEASE_STORE_FILE",
14345
14582
  "MYAPP_RELEASE_STORE_PASSWORD",
@@ -14588,23 +14825,37 @@ class CapacitorApp {
14588
14825
  await this.#runIosPhysicalDevice({ operation, env, runTarget, noAllowProvisioningUpdates });
14589
14826
  }
14590
14827
  async#selectIosRunTarget(deviceId) {
14591
- const targets = await this.#loadIosRunTargets();
14828
+ const targets = sortIosRunTargets(await this.#loadIosRunTargets());
14592
14829
  if (deviceId) {
14593
- const found = targets.find((target) => target.id === deviceId);
14594
- if (!found)
14595
- 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);
14596
14837
  return found;
14597
14838
  }
14598
14839
  if (targets.length === 0) {
14599
14840
  throw new Error("No iOS run targets found. Open Simulator or connect an iPhone, then retry.");
14600
14841
  }
14601
- return await select2({
14842
+ const selected = await select2({
14602
14843
  message: "Select iOS run target",
14603
14844
  choices: targets.map((target) => ({
14604
- name: `[${target.kind}] ${target.name}${target.state ? ` (${target.state})` : ""}`,
14845
+ name: `[${target.kind}] ${target.name}${target.runtime ? ` \u2014 ${target.runtime}` : ""}${target.state ? ` (${target.state})` : ""}`,
14605
14846
  value: target
14606
14847
  }))
14607
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.`);
14608
14859
  }
14609
14860
  async#loadIosRunTargets() {
14610
14861
  const devices = await this.#loadPhysicalIosDevices();
@@ -14851,7 +15102,13 @@ ${error.message}`;
14851
15102
  }
14852
15103
  async#writeCapacitorConfig({ operation }, commandEnv) {
14853
15104
  await mkdir11(this.targetRoot, { recursive: true });
14854
- 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
+ }
14855
15112
  const config = materializeCapacitorConfig(this.target, {
14856
15113
  operation,
14857
15114
  localIp,
@@ -14862,6 +15119,17 @@ ${error.message}`;
14862
15119
  await Bun.write(path42.join(this.targetRoot, "capacitor.config.json"), content);
14863
15120
  return content;
14864
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
+ }
14865
15133
  async#prepareTargetAssets() {
14866
15134
  if (!this.target.assets)
14867
15135
  return;
@@ -17536,6 +17804,7 @@ try {
17536
17804
  operation = "local",
17537
17805
  env = "local",
17538
17806
  target,
17807
+ device,
17539
17808
  regenerate = false,
17540
17809
  noAllowProvisioningUpdates = false
17541
17810
  } = {}) {
@@ -17544,7 +17813,7 @@ try {
17544
17813
  await this.#buildMobileCsr(app, env);
17545
17814
  await this.#runMobileTargets(targets, async (mobileTarget2) => {
17546
17815
  const capacitorApp2 = new CapacitorApp(app, mobileTarget2.config);
17547
- await capacitorApp2.runIos({ operation, env, regenerate, noAllowProvisioningUpdates });
17816
+ await capacitorApp2.runIos({ operation, env, regenerate, noAllowProvisioningUpdates, iosDeviceId: device });
17548
17817
  if (open)
17549
17818
  await capacitorApp2.openIos();
17550
17819
  });
@@ -17935,6 +18204,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17935
18204
  env = "local",
17936
18205
  write = true,
17937
18206
  target,
18207
+ device,
17938
18208
  regenerate = false,
17939
18209
  noAllowProvisioningUpdates = false
17940
18210
  } = {}) {
@@ -17947,6 +18217,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
17947
18217
  operation,
17948
18218
  env,
17949
18219
  target,
18220
+ device,
17950
18221
  regenerate,
17951
18222
  noAllowProvisioningUpdates
17952
18223
  });
@@ -18081,7 +18352,10 @@ class ApplicationCommand extends command("application", [ApplicationScript], ({
18081
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, {
18082
18353
  desc: "disable automatic iOS provisioning updates for physical devices",
18083
18354
  default: false
18084
- }).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) {
18085
18359
  await this.applicationScript.startIos(app, {
18086
18360
  target: target2,
18087
18361
  env: asMobileEnv(env),
@@ -18089,7 +18363,8 @@ class ApplicationCommand extends command("application", [ApplicationScript], ({
18089
18363
  operation: release ? "release" : "local",
18090
18364
  write,
18091
18365
  regenerate,
18092
- noAllowProvisioningUpdates
18366
+ noAllowProvisioningUpdates,
18367
+ device: device || undefined
18093
18368
  });
18094
18369
  }),
18095
18370
  startAndroid: target({ short: true, desc: "Start Android app in emulator or device" }).with(App).option("target", String, mobileTargetOption).option("env", String, {
@@ -21450,10 +21725,49 @@ class ContextRunner extends runner("context") {
21450
21725
  });
21451
21726
  return format === "json" ? jsonText(context) : AkanContextAnalyzer.renderMarkdown(context, { module });
21452
21727
  }
21453
- 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);
21454
21735
  const result = await AkanContextAnalyzer.doctor(workspace, { strict });
21455
21736
  return format === "json" ? jsonText(result) : renderDoctorText(result);
21456
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
+ }
21457
21771
  async getGuidelineResource(name) {
21458
21772
  return await Prompter.getInstruction(name);
21459
21773
  }
@@ -21850,8 +22164,11 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
21850
22164
  desc: "output format",
21851
22165
  default: "text",
21852
22166
  enum: ["text", "json"]
21853
- }).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
21854
- 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 });
21855
22172
  }),
21856
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, {
21857
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.8",
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.8",
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",