@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/incrementalBuilder.proc.js +410 -61
- package/index.js +360 -43
- package/package.json +2 -2
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 ??
|
|
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
|
|
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
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
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
|
-
|
|
4836
|
-
}
|
|
4837
|
-
if (shouldRestartBuilderByDevPlan(message)) {
|
|
4898
|
+
this.#pendingRecycle = null;
|
|
4838
4899
|
try {
|
|
4839
|
-
|
|
4900
|
+
if (refreshConfig)
|
|
4901
|
+
await this.#restartDevHost(merged);
|
|
4902
|
+
else
|
|
4903
|
+
await this.#restartDevChildren(merged);
|
|
4840
4904
|
} catch (err) {
|
|
4841
|
-
this.#recordDevHostRestartFailure(
|
|
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
|
|
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
|
|
14116
|
-
|
|
14117
|
-
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
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
|
-
|
|
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.
|
|
14198
|
-
const
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
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
|
|
14594
|
-
|
|
14595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}).
|
|
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, {
|
|
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 }).
|
|
21854
|
-
|
|
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.
|
|
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.
|
|
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",
|