@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/incrementalBuilder.proc.js +410 -63
- package/index.js +360 -45
- package/package.json +2 -2
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 ??
|
|
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
|
|
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
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
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
|
-
|
|
4838
|
-
}
|
|
4839
|
-
if (shouldRestartBuilderByDevPlan(message)) {
|
|
4898
|
+
this.#pendingRecycle = null;
|
|
4840
4899
|
try {
|
|
4841
|
-
|
|
4900
|
+
if (refreshConfig)
|
|
4901
|
+
await this.#restartDevHost(merged);
|
|
4902
|
+
else
|
|
4903
|
+
await this.#restartDevChildren(merged);
|
|
4842
4904
|
} catch (err) {
|
|
4843
|
-
this.#recordDevHostRestartFailure(
|
|
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
|
|
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
|
|
14118
|
-
|
|
14119
|
-
|
|
14120
|
-
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
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
|
-
|
|
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.
|
|
14200
|
-
const
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
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
|
|
14596
|
-
|
|
14597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}).
|
|
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, {
|
|
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 }).
|
|
21856
|
-
|
|
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.
|
|
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",
|