@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,454 @@
1
+ import path from "node:path";
2
+ import type { AkanNativeConfig } from "@akanjs/native/config";
3
+ import type { NativeEnv } from "akanjs";
4
+ import type { App } from "../commandDecorators";
5
+ import { Executor } from "../executors";
6
+ import { type DesktopArch, DesktopBin } from "./desktopBin";
7
+ import type { DesktopServerBundle } from "./desktopServerStage";
8
+ import {
9
+ type DesktopPlatform,
10
+ type MobilePlatform,
11
+ type NativePlatform,
12
+ type ResolvedMobileTarget,
13
+ targetHtmlFilename,
14
+ } from "./mobileTarget";
15
+ import { NativeApi, type NativeBuildApiModule } from "./nativeApi";
16
+ import { NativeAppLine, type NativeAppLineRead } from "./nativeAppLine";
17
+ import { NativeConfig } from "./nativeConfig";
18
+ import { NativePluginFolders } from "./nativePluginFolders";
19
+ import { NativeWebDir } from "./nativeWebDir";
20
+
21
+ type TaskOptions = Parameters<NativeBuildApiModule["build"]>[0];
22
+ type DeviceSelector = NonNullable<Parameters<NativeBuildApiModule["run"]>[0]["device"]>;
23
+ type ReleaseOptions = Parameters<NativeBuildApiModule["release"]>[0];
24
+ type AndroidSigning = Extract<ReleaseOptions, { platform: "android" }>["signing"];
25
+
26
+ export interface NativeRunOptions {
27
+ device?: DeviceSelector;
28
+ /** Narrows the iPhone signing to one Apple team. */
29
+ teamId?: string;
30
+ }
31
+
32
+ interface NativeDevBoot {
33
+ steps: string[];
34
+ lines: NativeAppLineRead[];
35
+ }
36
+
37
+ export interface NativeBuildOptions {
38
+ profile?: "debug" | "release";
39
+ /** The server a desktop app carries (the target's `desktop.server`), staged by DesktopServerStage. */
40
+ server?: DesktopServerBundle;
41
+ /** What a person downloads beside the app: a Windows NSIS setup program, a macOS dmg, a Linux AppImage. */
42
+ installer?: boolean;
43
+ /** The CPU the desktop app runs on, of the same OS; a macOS app is arm64 only. Default this computer's. */
44
+ arch?: DesktopArch;
45
+ }
46
+
47
+ export interface NativeDevOptions extends NativeRunOptions {
48
+ /** The akan dev server the gateway proxies. */
49
+ upstream: string;
50
+ lang: string;
51
+ }
52
+
53
+ /** One native target of an app on the native runtime (a phone, or this computer as a desktop app): where it builds,
54
+ * what it ships, and the calls into the API. */
55
+ export class NativeApp {
56
+ readonly targetRoot: string;
57
+ readonly web: NativeWebDir;
58
+ //? A dev boot holds its steps and the app's first lines back: one line says it is up, all of it when it failed.
59
+ #boot: NativeDevBoot | null = null;
60
+
61
+ constructor(
62
+ readonly app: App,
63
+ readonly target: ResolvedMobileTarget,
64
+ readonly env?: NativeEnv,
65
+ ) {
66
+ this.targetRoot = path.join(app.cwdPath, ".akan", "native", target.name);
67
+ this.web = new NativeWebDir(path.join(this.targetRoot, "web"));
68
+ }
69
+
70
+ outDir(platform: NativePlatform) {
71
+ return path.join(this.targetRoot, "build", platform);
72
+ }
73
+
74
+ //? Apart from the builds: a desktop build empties its folder first, which would take a release and its installer.
75
+ devOutDir(platform: NativePlatform) {
76
+ return path.join(this.targetRoot, "dev", platform);
77
+ }
78
+
79
+ /** Where the app starts on `platform`: its platform section's `indexPath`, else the target's. */
80
+ indexPath(platform: NativePlatform) {
81
+ const { config } = this.target;
82
+ const section = platform === "ios" ? config.ios : platform === "android" ? config.android : config.desktop;
83
+ return section?.indexPath ?? config.indexPath;
84
+ }
85
+
86
+ /** The web root, from the production build the native commands run first. */
87
+ async assembleWeb(platform: NativePlatform) {
88
+ const dist = this.app.dist.cwdPath;
89
+ return await this.web.assemble(
90
+ { ...this.target.config, indexPath: this.indexPath(platform) },
91
+ {
92
+ html: path.join(dist, "csr", targetHtmlFilename(this.target.config)),
93
+ publicDir: path.join(dist, "public"),
94
+ fontsDir: path.join(dist, ".akan", "artifact", "fonts"),
95
+ },
96
+ );
97
+ }
98
+
99
+ /** Where a desktop build stages the executables it carries (akan.config.ts `bin`). */
100
+ get binDir() {
101
+ return path.join(this.targetRoot, "bin");
102
+ }
103
+
104
+ /** `stageBin: false` for a config nothing is built from (the update key's): staging downloads the `bin` sources. */
105
+ async config({
106
+ server,
107
+ platform,
108
+ stageBin = true,
109
+ arch,
110
+ }: {
111
+ server?: DesktopServerBundle;
112
+ platform: NativePlatform;
113
+ stageBin?: boolean;
114
+ arch?: DesktopArch;
115
+ }) {
116
+ const [appConfig, plugins, nativePlugins] = await Promise.all([
117
+ this.app.getConfig(),
118
+ this.app.collectPlugins(),
119
+ NativePluginFolders.of(this.app),
120
+ ]);
121
+ const desktop = platform === "macos" || platform === "windows" || platform === "linux";
122
+ const carried = desktop && stageBin ? await new DesktopBin(this.app, appConfig).stage(this.binDir, arch) : [];
123
+ return NativeConfig.build({
124
+ appPath: this.app.cwdPath,
125
+ target: this.target.config,
126
+ webDir: this.web.dir,
127
+ contributions: plugins.flatMap((plugin) => (plugin.native ? [plugin.native] : [])),
128
+ locales: appConfig.i18n.locales,
129
+ env: this.env,
130
+ platform,
131
+ nativePlugins,
132
+ ...(server ? { desktopServer: server } : {}),
133
+ ...(carried.length ? { desktopBin: this.binDir } : {}),
134
+ });
135
+ }
136
+
137
+ /** The API and the config it is about to build, refused here when the runtime would refuse it later. */
138
+ async prepare(platform: NativePlatform, server?: DesktopServerBundle, arch?: DesktopArch) {
139
+ const [api, { config, warnings }] = await Promise.all([
140
+ NativeApi.load(this.app.cwdPath),
141
+ this.config({ platform, ...(server ? { server } : {}), ...(arch ? { arch } : {}) }),
142
+ ]);
143
+ for (const warning of warnings) this.app.logger.warn(warning);
144
+ this.#assertValid(api, config);
145
+ return { api, config };
146
+ }
147
+
148
+ #assertValid(api: NativeBuildApiModule, config: AkanNativeConfig) {
149
+ const problems = api.validateConfig(config, { appDir: this.app.cwdPath });
150
+ if (problems.length)
151
+ throw new Error(
152
+ `Native target '${this.target.name}' makes an invalid native config:\n- ${problems.join("\n- ")}`,
153
+ );
154
+ }
155
+
156
+ //? The app's own output: its console (mirrored by a dev build), the simulator log stream or logcat.
157
+ #appLine = (line: string) => {
158
+ const read = NativeAppLine.read(line, { verbose: Executor.verbose });
159
+ if (this.#boot) this.#boot.lines.push(read);
160
+ else this.app.logger[read.level](read.message);
161
+ };
162
+
163
+ //* `AKAN_PUBLIC_*` is already inlined into the CSR bundle, so the runtime reads no .env file of its own.
164
+ #task(platform: NativePlatform, config: AkanNativeConfig, outDir = this.outDir(platform)): TaskOptions {
165
+ return {
166
+ appDir: this.app.cwdPath,
167
+ config,
168
+ platform,
169
+ outDir,
170
+ envFiles: false,
171
+ skipWebBuild: true,
172
+ log: (event) => {
173
+ if (event.level === "error") this.app.logger.error(event.message);
174
+ else if (event.level === "warn") this.app.logger.warn(event.message);
175
+ else if (this.#boot) this.#boot.steps.push(event.message);
176
+ else if (event.level === "tool") this.app.logger.verbose(event.message);
177
+ else this.app.logger.info(event.message);
178
+ },
179
+ };
180
+ }
181
+
182
+ async build(
183
+ platform: NativePlatform,
184
+ { profile = "release", server, installer = false, arch }: NativeBuildOptions = {},
185
+ ) {
186
+ NativeApp.#assertServerPlatform(platform, server);
187
+ NativeApp.assertInstaller(platform, installer);
188
+ await this.assembleWeb(platform);
189
+ const { api, config } = await this.prepare(platform, server, arch);
190
+ return await api.build({
191
+ ...this.#task(platform, config),
192
+ profile,
193
+ ...(arch ? { arch } : {}),
194
+ ...(platform === "windows"
195
+ ? {
196
+ windows: {
197
+ ...(installer ? { installer } : {}),
198
+ ...(profile === "release" ? NativeApp.#windowsSigning(api) : {}),
199
+ },
200
+ }
201
+ : {}),
202
+ ...(platform === "macos"
203
+ ? {
204
+ macos: {
205
+ ...(profile === "release" ? api.macosDistributionFromEnv(process.env) : {}),
206
+ ...(installer ? { dmg: true } : {}),
207
+ },
208
+ }
209
+ : {}),
210
+ ...(platform === "linux" && installer ? { linux: { appImage: true } } : {}),
211
+ });
212
+ }
213
+
214
+ async run(
215
+ platform: NativePlatform,
216
+ { device, teamId, profile = "debug", server }: NativeRunOptions & NativeBuildOptions = {},
217
+ ) {
218
+ NativeApp.#assertServerPlatform(platform, server);
219
+ await this.assembleWeb(platform);
220
+ const { api, config } = await this.prepare(platform, server);
221
+ return await api.run({
222
+ ...this.#task(platform, config),
223
+ profile,
224
+ onLine: this.#appLine,
225
+ ...(device ? { device } : {}),
226
+ ...(teamId ? { ios: { signing: { teamId } } } : {}),
227
+ });
228
+ }
229
+
230
+ //* The gateway serves the page, its HMR socket and its API calls on the app origin. The dev server's own port is
231
+ //* reversed too: a browser the app hands off to (auth-session) opens it directly (baseEnv getServerOrigin).
232
+ async dev(platform: NativePlatform, { upstream, lang, device, teamId }: NativeDevOptions) {
233
+ const startedAt = performance.now();
234
+ const boot: NativeDevBoot = { steps: [], lines: [] };
235
+ this.#boot = boot;
236
+ try {
237
+ //? The runtime bundles an index.html it finds here as the dev build's page, and a release build leaves one.
238
+ await this.web.clear();
239
+ const [{ api, config }, { api: routes }] = await Promise.all([this.prepare(platform), this.app.getConfig()]);
240
+ const session = await api.dev({
241
+ ...this.#task(platform, config, this.devOutDir(platform)),
242
+ upstream,
243
+ hmrPath: "/_akan/hmr",
244
+ //? A dev page calls its own origin (akanjs baseEnv), so the gateway relays the socket it opens for the API too.
245
+ wsPaths: [`${routes.prefix}${routes.websocketPrefix}`],
246
+ reversePorts: [Number(new URL(upstream).port)],
247
+ onLine: this.#appLine,
248
+ startPath: this.startPath(lang, platform),
249
+ ...(device ? { device } : {}),
250
+ ...(teamId ? { ios: { signing: { teamId } } } : {}),
251
+ });
252
+ this.#boot = null;
253
+ this.app.logger.info(this.readyLine(platform, { upstream, device: session.device, startedAt }));
254
+ for (const step of [...boot.steps, `pages through ${session.gateway}`]) this.app.logger.debug(step);
255
+ for (const line of boot.lines) this.app.logger[line.level](line.message);
256
+ return session;
257
+ } catch (error) {
258
+ this.#boot = null;
259
+ for (const step of boot.steps) this.app.logger.info(step);
260
+ for (const line of boot.lines) this.app.logger[line.level === "debug" ? "info" : line.level](line.message);
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ /** What a dev boot that came up says, and all it says: `cmdc ios ready · iPhone 16 · http://localhost:8283 · 12.4s`. */
266
+ readyLine(
267
+ platform: NativePlatform,
268
+ { upstream, device, startedAt }: { upstream: string; device?: { name: string }; startedAt: number },
269
+ ) {
270
+ const label = platform === "ios" || platform === "android" ? platform : "desktop";
271
+ const target = this.target.name === "default" ? "" : `/${this.target.name}`;
272
+ const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);
273
+ return [`${this.app.name}${target} ${label} ready`, ...(device ? [device.name] : []), upstream, `${seconds}s`].join(
274
+ " · ",
275
+ );
276
+ }
277
+
278
+ async releaseIos({ teamId, adHoc = false }: { teamId?: string; adHoc?: boolean } = {}) {
279
+ await this.assembleWeb("ios");
280
+ const { api, config } = await this.prepare("ios");
281
+ return await api.release({
282
+ ...this.#task("ios", config),
283
+ platform: "ios",
284
+ signing: { ...(teamId ? { teamId } : {}), ...(adHoc ? { distribution: "ad-hoc" as const } : {}) },
285
+ });
286
+ }
287
+
288
+ /** Where publishUpdate writes: upload this folder as the target's updates.url. */
289
+ get updatesDir() {
290
+ return path.join(this.targetRoot, "updates");
291
+ }
292
+
293
+ /** A signed release installed apps take through the updates plugin, from a release build of this bundle. */
294
+ async publishUpdate(
295
+ platform: NativePlatform,
296
+ { channel, server }: { channel?: string; server?: DesktopServerBundle } = {},
297
+ ) {
298
+ NativeApp.#assertServerPlatform(platform, server);
299
+ this.#assertUpdates();
300
+ await this.assembleWeb(platform);
301
+ const { api, config } = await this.prepare(platform, server);
302
+ return await api.publishUpdate({
303
+ ...this.#task(platform, config),
304
+ platform,
305
+ out: this.updatesDir,
306
+ ...(channel ? { channel } : {}),
307
+ //? The updater keeps the installed app's signature, so a release is signed as the downloaded app was.
308
+ ...(platform === "macos" ? { macos: api.macosDistributionFromEnv(process.env) } : {}),
309
+ ...(platform === "windows" ? { windows: NativeApp.#windowsSigning(api) } : {}),
310
+ });
311
+ }
312
+
313
+ /**
314
+ * What publishing refuses once the release is built: no updates settings, an invalid config, no signing key or
315
+ * another app's, and a desktop release whose server presence is not its channel's previous release's.
316
+ */
317
+ async assertPublishable(
318
+ platform: NativePlatform,
319
+ { channel, server = false }: { channel?: string; server?: boolean } = {},
320
+ ) {
321
+ this.#assertUpdates();
322
+ const [api, { config }] = await Promise.all([
323
+ NativeApi.load(this.app.cwdPath),
324
+ this.config({ platform, stageBin: false }),
325
+ ]);
326
+ this.#assertValid(api, config);
327
+ try {
328
+ api.checkPublishUpdate({
329
+ appDir: this.app.cwdPath,
330
+ config,
331
+ platform,
332
+ out: this.updatesDir,
333
+ server,
334
+ ...(channel ? { channel } : {}),
335
+ });
336
+ } catch (error) {
337
+ throw new Error(
338
+ `Native target '${this.target.name}': ${error instanceof Error ? error.message : String(error)}`,
339
+ {
340
+ cause: error,
341
+ },
342
+ );
343
+ }
344
+ }
345
+
346
+ #assertUpdates() {
347
+ if (!this.target.config.updates)
348
+ throw new Error(
349
+ `Native target '${this.target.name}' has no updates: add native.updates: { url, publicKey } to akan.config.ts; \`akan update-keygen ${this.app.name}\` prints the key.`,
350
+ );
351
+ }
352
+
353
+ /** The key update releases of this target's app id on `platform` are signed with: made once, then read. */
354
+ async updateKeygen(platform: NativePlatform) {
355
+ const [api, { config }] = await Promise.all([
356
+ NativeApi.load(this.app.cwdPath),
357
+ this.config({ platform, stageBin: false }),
358
+ ]);
359
+ return api.updateKeygen({ config });
360
+ }
361
+
362
+ async releaseAndroid({ formats = ["aab"] }: { formats?: ("aab" | "apk")[] } = {}) {
363
+ const signing = NativeApp.androidSigning();
364
+ await this.assembleWeb("android");
365
+ const { api, config } = await this.prepare("android");
366
+ return await api.release({ ...this.#task("android", config), platform: "android", signing, formats });
367
+ }
368
+
369
+ /** An unsigned OTA update of this target's web bundle for `platform`, and the channel its binary follows. */
370
+ async packUpdate(platform: MobilePlatform, { out }: { out?: string } = {}) {
371
+ await this.assembleWeb(platform);
372
+ const { api, config } = await this.prepare(platform);
373
+ if (!config.updates)
374
+ throw new Error(
375
+ `Native target '${this.target.name}' takes no updates: add native.updates: { url, publicKey } to akan.config.ts first.`,
376
+ );
377
+ const packed = await api.packUpdate({
378
+ ...this.#task(platform, config),
379
+ platform,
380
+ out: out ?? path.join(this.targetRoot, "updates", platform),
381
+ });
382
+ return { ...packed, channel: config.updates.channel ?? "production" };
383
+ }
384
+
385
+ static async compareBundles(appDir: string, shipped: string, bundle: string) {
386
+ return (await NativeApi.load(appDir)).compareBundles(shipped, bundle);
387
+ }
388
+
389
+ //* The names the Gradle build read, so a CI that already holds these secrets keeps working.
390
+ static androidSigning(env: Record<string, string | undefined> = process.env): AndroidSigning {
391
+ const keystore = env.MYAPP_RELEASE_STORE_FILE;
392
+ const storePassword = env.MYAPP_RELEASE_STORE_PASSWORD;
393
+ const alias = env.MYAPP_RELEASE_KEY_ALIAS;
394
+ const keyPassword = env.MYAPP_RELEASE_KEY_PASSWORD;
395
+ if (!keystore || !storePassword || !alias) {
396
+ const missing = Object.entries({
397
+ MYAPP_RELEASE_STORE_FILE: keystore,
398
+ MYAPP_RELEASE_STORE_PASSWORD: storePassword,
399
+ MYAPP_RELEASE_KEY_ALIAS: alias,
400
+ }).flatMap(([key, value]) => (value ? [] : [key]));
401
+ throw new Error(
402
+ `An Android release is signed with the upload key; set ${missing.join(", ")} in the environment.`,
403
+ );
404
+ }
405
+ return { keystore, storePassword, alias, ...(keyPassword ? { keyPassword } : {}) };
406
+ }
407
+
408
+ /** The first page of a dev build, the target's home as a release bundle opens it; the dev server answers the CSR
409
+ * shell only for `?csr=true`. */
410
+ startPath(lang: string, platform: NativePlatform) {
411
+ const basePath = this.target.config.basePath?.replace(/^\/+|\/+$/g, "");
412
+ const indexPath = this.indexPath(platform);
413
+ const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
414
+ if (basePath) params.set("akanMobileBasePath", basePath);
415
+ if (indexPath) params.set("akanMobileIndexPath", indexPath);
416
+ const home = [lang, basePath, indexPath]
417
+ .flatMap((part) => (part ?? "").split("/"))
418
+ .filter((segment) => segment.length > 0);
419
+ return `/${home.join("/")}?${params}`;
420
+ }
421
+
422
+ static #windowsSigning(api: NativeBuildApiModule) {
423
+ const signing = api.windowsSigningFromEnv(process.env);
424
+ return signing ? { signing } : {};
425
+ }
426
+
427
+ //* `installer` is the file a person downloads: a setup program on Windows, a disk image on macOS, an AppImage on Linux.
428
+ static assertInstaller(platform: NativePlatform, installer: boolean) {
429
+ if (installer && (platform === "ios" || platform === "android"))
430
+ throw new Error(`An installer is built for a desktop app (Windows, macOS, Linux), not for ${platform}.`);
431
+ }
432
+
433
+ //? A phone runs no Bun, so only a desktop app can carry the server.
434
+ static #assertServerPlatform(platform: NativePlatform, server: DesktopServerBundle | undefined) {
435
+ if (server && (platform === "ios" || platform === "android"))
436
+ throw new Error(`Only a desktop app carries its server; ${platform} cannot run one.`);
437
+ }
438
+
439
+ //* A desktop app builds only on its own OS, so the platform is this computer's.
440
+ static desktopPlatform(host: NodeJS.Platform = process.platform): DesktopPlatform {
441
+ if (host === "darwin") return "macos";
442
+ if (host === "win32") return "windows";
443
+ if (host === "linux") return "linux";
444
+ throw new Error(`A desktop app builds on macOS, Windows or Linux, not on ${host}.`);
445
+ }
446
+
447
+ static async devices(appDir: string, platform: MobilePlatform) {
448
+ return await (await NativeApi.load(appDir)).devices({ platform });
449
+ }
450
+
451
+ static async doctor(appDir: string, platforms: NativePlatform[]) {
452
+ return await (await NativeApi.load(appDir)).doctor({ platforms });
453
+ }
454
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { NativeAppLine } from "./nativeAppLine";
3
+
4
+ describe("NativeAppLine", () => {
5
+ test("a page logger line keeps its level and its logger's name", () => {
6
+ expect(NativeAppLine.read("[page warn] [WsClient] WebSocket message process failed")).toEqual({
7
+ level: "warn",
8
+ message: "[page:WsClient] WebSocket message process failed",
9
+ });
10
+ });
11
+
12
+ test("a bare console call keeps its level, `log` reading as info", () => {
13
+ expect(NativeAppLine.read("[page log] hello")).toEqual({ level: "info", message: "[page] hello" });
14
+ expect(NativeAppLine.read("[page debug] [akan:frame:x:1] csr.render {}")).toEqual({
15
+ level: "debug",
16
+ message: "[page:akan:frame:x:1] csr.render {}",
17
+ });
18
+ });
19
+
20
+ test("a continuation line follows its message's level without a prefix of its own", () => {
21
+ expect(NativeAppLine.read("[page+ error] at run (app.js:1:2)")).toEqual({
22
+ level: "error",
23
+ message: " at run (app.js:1:2)",
24
+ });
25
+ });
26
+
27
+ test("a carried server's line keeps the level its logger wrote, and one without a level stays visible", () => {
28
+ const line = "[server] [minimal] 81 - 09/30/2026, 12:30:06 PM ERROR boom +2ms";
29
+ expect(NativeAppLine.read(line)).toEqual({ level: "error", message: line });
30
+ expect(NativeAppLine.read("[server] \u001b[32m[minimal] 81 -\u001b[39m WARN slow").level).toBe("warn");
31
+ expect(NativeAppLine.read("[server] at run (main.js:1:2)").level).toBe("info");
32
+ });
33
+
34
+ test("a second window is named, the first is not", () => {
35
+ expect(NativeAppLine.read("[page#2 info] [App] ready")).toEqual({ level: "info", message: "[page#2:App] ready" });
36
+ });
37
+
38
+ test("logcat lines read by tag and priority", () => {
39
+ expect(NativeAppLine.read("W/AkanNativeConsole( 1234): [WsClient] failed")).toEqual({
40
+ level: "warn",
41
+ message: "[page:WsClient] failed",
42
+ });
43
+ expect(NativeAppLine.read("E/AndroidRuntime( 1234): FATAL EXCEPTION: main")).toEqual({
44
+ level: "error",
45
+ message: "[AndroidRuntime] FATAL EXCEPTION: main",
46
+ });
47
+ expect(
48
+ NativeAppLine.read("I/AkanNative( 1234): pages from http://10.0.2.2:5000 (akan-native dev --hmr)").level,
49
+ ).toBe("debug");
50
+ });
51
+
52
+ test("the host's own lines stay visible but for where its pages come from", () => {
53
+ expect(NativeAppLine.read("[akan-native] opened in the browser: https://x.test")).toEqual({
54
+ level: "info",
55
+ message: "[akan-native] opened in the browser: https://x.test",
56
+ });
57
+ expect(
58
+ NativeAppLine.read("[akan-native native] pages from http://127.0.0.1:6000 (akan-native dev --hmr)").level,
59
+ ).toBe("debug");
60
+ });
61
+
62
+ test("a line no page or host wrote is kept for debug, or for info when verbose", () => {
63
+ expect(NativeAppLine.read("WebContent[123] Could not signal service")).toEqual({
64
+ level: "debug",
65
+ message: "WebContent[123] Could not signal service",
66
+ });
67
+ expect(NativeAppLine.read("WebContent[123] Could not signal service", { verbose: true }).level).toBe("info");
68
+ });
69
+ });
@@ -0,0 +1,71 @@
1
+ import type { LogLevel } from "akanjs/common";
2
+
3
+ export interface NativeAppLineRead {
4
+ level: LogLevel;
5
+ message: string;
6
+ }
7
+
8
+ //* A line a native dev build prints — the page console its host mirrors, the simulator's console, logcat — read back
9
+ //* at the level the page logged it, so the terminal shows it once, stamped by the app's own logger.
10
+ export class NativeAppLine {
11
+ //? The host's grammar (desktop host.ts, iOS AkanNativeShell.swift): `[page<+><#window> <level>] text`, where `+`
12
+ //? marks a continuation line of a multi-line message and `#n` any window but the first.
13
+ static readonly #page = /^\[page(\+)?(?:#(\d+))? (trace|verbose|debug|log|info|warn|error)\] ?(.*)$/;
14
+ static readonly #logcat = /^([VDIWEFA])\/([^(]+?)\s*\(\s*\d+\):\s?(.*)$/;
15
+ static readonly #named = /^\[([^\]\s]+)\] ([\s\S]*)$/;
16
+ //? A server the desktop app carries (host server.ts): `[server] <its own log line>`, whose level word the akan logger
17
+ //? wrote; a line without one (a crash's stack) is still the app's and stays visible.
18
+ static readonly #server = /^\[server\] /;
19
+ static readonly #serverLevel = /\b(TRACE|VERBOSE|DEBUG|INFO|WARN|ERROR)\b/;
20
+ static readonly #logcatLevels: { [priority: string]: LogLevel } = {
21
+ V: "verbose",
22
+ D: "debug",
23
+ I: "info",
24
+ W: "warn",
25
+ E: "error",
26
+ F: "error",
27
+ A: "error",
28
+ };
29
+
30
+ /** `verbose` keeps the lines no page or host wrote (WebKit, the simulator) at info instead of debug. */
31
+ static read(line: string, { verbose = false }: { verbose?: boolean } = {}): NativeAppLineRead {
32
+ const logcat = NativeAppLine.#logcat.exec(line);
33
+ if (logcat) {
34
+ const [, priority = "I", tag = "", text = ""] = logcat;
35
+ const level = NativeAppLine.#logcatLevels[priority] ?? "info";
36
+ if (tag === "AkanNativeConsole") return NativeAppLine.#pageLine(level, text, { continuation: false });
37
+ if (tag === "AkanNative") return NativeAppLine.#hostLine(`[akan-native] ${text}`, level);
38
+ return { level, message: `[${tag}] ${text}` };
39
+ }
40
+ const page = NativeAppLine.#page.exec(line);
41
+ if (page) {
42
+ const [, continuation, window, level = "info", text = ""] = page;
43
+ return NativeAppLine.#pageLine(level === "log" ? "info" : (level as LogLevel), text, {
44
+ continuation: continuation === "+",
45
+ window,
46
+ });
47
+ }
48
+ if (line.startsWith("[akan-native")) return NativeAppLine.#hostLine(line, "info");
49
+ if (NativeAppLine.#server.test(line)) {
50
+ const word = NativeAppLine.#serverLevel.exec(Bun.stripANSI(line))?.[1];
51
+ return { level: word ? (word.toLowerCase() as LogLevel) : "info", message: line };
52
+ }
53
+ return { level: verbose ? "info" : "debug", message: line };
54
+ }
55
+
56
+ static #pageLine(
57
+ level: LogLevel,
58
+ text: string,
59
+ { continuation, window }: { continuation: boolean; window?: string },
60
+ ): NativeAppLineRead {
61
+ if (continuation) return { level, message: ` ${text}` };
62
+ const named = NativeAppLine.#named.exec(text);
63
+ const scope = `page${window ? `#${window}` : ""}${named ? `:${named[1]}` : ""}`;
64
+ return { level, message: `[${scope}] ${named ? named[2] : text}` };
65
+ }
66
+
67
+ //? The host's own lines are about the host, not the app: where its pages come from is boot detail.
68
+ static #hostLine(line: string, level: LogLevel): NativeAppLineRead {
69
+ return { level: /\bpages from\b/.test(line) ? "debug" : level, message: line };
70
+ }
71
+ }