@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-beta.9",
3
+ "version": "3.0.0",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -28,6 +28,7 @@
28
28
  "./akanConfig": "./akanConfig/index.ts",
29
29
  "./artifact": "./artifact/index.ts",
30
30
  "./cloud": "./cloud/index.ts",
31
+ "./codeAgent": "./codeAgent/index.ts",
31
32
  "./commandDecorators": "./commandDecorators/index.ts",
32
33
  "./frontendBuild": "./frontendBuild/index.ts",
33
34
  "./incrementalBuilder": "./incrementalBuilder/index.ts",
@@ -38,13 +39,15 @@
38
39
  "./*": "./*.ts"
39
40
  },
40
41
  "dependencies": {
42
+ "@earendil-works/chord": "0.86.1",
43
+ "@earendil-works/pi-agent-core": "0.86.1",
44
+ "@earendil-works/pi-ai": "0.86.1",
45
+ "@earendil-works/pi-coding-agent": "0.86.1",
46
+ "@earendil-works/pi-telemetry": "0.86.1",
47
+ "@earendil-works/pi-tui": "0.86.1",
41
48
  "@inquirer/prompts": "^8.4.3",
42
- "@langchain/core": "^1.1.47",
43
- "@langchain/deepseek": "^1.0.26",
44
- "@langchain/openai": "^1.4.6",
45
49
  "@tailwindcss/node": "^4.3.0",
46
- "@trapezedev/project": "^7.1.4",
47
- "akanjs": "3.0.0-beta.9",
50
+ "akanjs": "3.0.0",
48
51
  "chalk": "^5.6.2",
49
52
  "commander": "^14.0.3",
50
53
  "dayjs": "^1.11.20",
@@ -56,16 +59,13 @@
56
59
  "subset-font": "^2.5.0",
57
60
  "tailwind-scrollbar": "^4.0.2",
58
61
  "tailwindcss": "^4.3.0",
62
+ "typebox": "1.1.38",
59
63
  "typescript": "^6.0.3"
60
64
  },
61
65
  "peerDependencies": {
62
- "@capacitor/cli": "^8.3.4",
63
- "react": "19.2.7"
66
+ "react": "19.3.0"
64
67
  },
65
68
  "peerDependenciesMeta": {
66
- "@capacitor/cli": {
67
- "optional": true
68
- },
69
69
  "react": {
70
70
  "optional": true
71
71
  }
@@ -3,30 +3,17 @@ import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { PackageExportsMap } from "@akanjs/devkit/packageExportsMap";
5
5
 
6
- // Guards the published shape of this package, which the monorepo cannot exercise on its own.
7
- //
8
- // Inside the monorepo Bun resolves `@akanjs/devkit/executors` through the root tsconfig `paths`
9
- // (`@akanjs/devkit/*` -> `pkgs/@akanjs/devkit/*`), and that resolver probes extensions and directory
10
- // indexes. A published consumer has no such mapping: it goes through this package's `exports` map,
11
- // whose targets are matched *exactly*. So `"./*": "./*"` type-checked, built, and passed every test
12
- // here while every subpath import failed at runtime for anyone installing the tarball:
13
- //
14
- // error: Cannot find module '@akanjs/devkit/executors' from
15
- // '<consumer>/node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts'
16
- //
17
- // `PackageRunner.verifyDistPackage` runs the same check against the built dist tree of every
18
- // publishable package at release time; these tests keep this package honest on every run.
6
+ //? The monorepo resolves subpaths through tsconfig `paths`, which probe extensions; a published consumer goes through
7
+ //? the exports map, matched exactly — so `"./*": "./*"` passed every test here and broke every installed subpath.
19
8
 
20
9
  const packageDir = import.meta.dir;
21
10
  const exportsMap = await PackageExportsMap.from(packageDir);
22
11
 
23
- /** Every facet the root barrel re-exports, as the subpath a consumer would import. */
24
12
  const barrelFacets = async (): Promise<string[]> => {
25
13
  const barrel = await Bun.file(path.join(packageDir, "index.ts")).text();
26
14
  return [...barrel.matchAll(/^export (?:type )?\* from "\.\/([^"]+)";$/gm)].map((match) => `./${match[1]}`);
27
15
  };
28
16
 
29
- /** Every `@akanjs/devkit/<subpath>` specifier written anywhere in the two packages that use them. */
30
17
  const importedSubpaths = async (): Promise<string[]> => {
31
18
  const repoRoot = path.resolve(packageDir, "../../..");
32
19
  const glob = new Bun.Glob("pkgs/@akanjs/{cli,devkit}/**/*.{ts,tsx}");
@@ -53,9 +40,7 @@ describe("published exports map", () => {
53
40
  });
54
41
 
55
42
  test("covers both facet shapes and keeps explicit extensions intact", () => {
56
- // A single wildcard cannot serve all three: `./*` -> `./*.ts` reaches bare files, directory
57
- // facets need their own literal entry, and `./*.ts` -> `./*.ts` keeps an already-suffixed
58
- // specifier from becoming `./cloud/cloudApi.ts.ts`.
43
+ // One wildcard cannot serve bare files, directory facets and already-suffixed specifiers (no `.ts.ts`) at once.
59
44
  expect(exportsMap.resolve("./executors")).toBe("./executors.ts");
60
45
  expect(exportsMap.resolve("./frontendBuild")).toBe("./frontendBuild/index.ts");
61
46
  expect(exportsMap.resolve("./cloud/cloudApi.ts")).toBe("./cloud/cloudApi.ts");
@@ -7,17 +7,9 @@ export interface UnreachableSubpath {
7
7
  target: string | null;
8
8
  }
9
9
 
10
- /**
11
- * The subset of Node's `exports` resolution that Bun applies to a published package.
12
- *
13
- * Exports targets are matched **exactly**: no extension is appended and no `index.ts` is probed. That
14
- * is invisible inside this monorepo, where `@akanjs/devkit/*` and `akanjs/*` resolve through the root
15
- * tsconfig `paths` instead — a resolver that *does* probe both. A map of `{"./*": "./*"}` therefore
16
- * type-checks, builds, and passes every test while every subpath import fails for anyone who installs
17
- * the tarball. This class exists so that gap can be asserted against before publishing.
18
- */
10
+ // Bun matches exports targets exactly (no extension, no index.ts), while this monorepo resolves `akanjs/*` through
11
+ // tsconfig paths, which probe both — so a broken map passes every in-repo test and fails for every installer.
19
12
  export class PackageExportsMap {
20
- /** Reads `<packageDir>/package.json` and builds the map from its `exports` field. */
21
13
  static async from(packageDir: string) {
22
14
  const manifest = (await Bun.file(path.join(packageDir, "package.json")).json()) as { exports?: unknown };
23
15
  return new PackageExportsMap(packageDir, manifest.exports);
@@ -60,7 +52,6 @@ export class PackageExportsMap {
60
52
  // wins over `./*`, which is what keeps an already-suffixed specifier from gaining a second `.ts`.
61
53
  this.#patterns.sort((a, b) => b.prefix.length - a.prefix.length || b.suffix.length - a.suffix.length);
62
54
  }
63
- /** Returns the target an `exports` lookup yields, or null when the subpath is unexported. */
64
55
  resolve(subpath: string): string | null {
65
56
  const literal = this.#literals.get(subpath);
66
57
  if (literal) return literal;
@@ -71,20 +62,13 @@ export class PackageExportsMap {
71
62
  }
72
63
  return null;
73
64
  }
74
- /**
75
- * Resolves a subpath and reports whether the target it yields is a readable file.
76
- *
77
- * A directory does not count. `{"./*": "./*"}` maps `./commandDecorators` onto the directory of
78
- * that name, which exists but is not a module — an `existsSync` check here reports such a subpath
79
- * as reachable while the import still fails.
80
- */
65
+ // A directory does not count: `{"./*": "./*"}` maps `./commandDecorators` onto a folder, which is not importable.
81
66
  resolveToFile(subpath: string): { target: string | null; exists: boolean } {
82
67
  const target = this.resolve(subpath);
83
68
  if (!target) return { target: null, exists: false };
84
69
  const stat = statSync(path.join(this.#packageDir, target), { throwIfNoEntry: false });
85
70
  return { target, exists: !!stat?.isFile() };
86
71
  }
87
- /** Returns the given subpaths that no consumer could import, in input order. */
88
72
  findUnreachable(subpaths: Iterable<string>): UnreachableSubpath[] {
89
73
  const unreachable: UnreachableSubpath[] = [];
90
74
  for (const subpath of subpaths) {
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import path from "node:path";
3
+ import type { PackageJson } from "@akanjs/devkit/types";
4
+
5
+ //? A published install reads neither pi's npm-shrinkwrap.json nor our overrides, so PackageRunner re-declares pi's
6
+ //? sibling pins (a floating 0.80.x once broke every global install); this keeps them equal to the shrinkwrap.
7
+
8
+ const repoRoot = path.resolve(import.meta.dir, "../../..");
9
+ const piName = "@earendil-works/pi-coding-agent";
10
+ const piDir = `${repoRoot}/node_modules/${piName}`;
11
+ const publishedPackages = ["pkgs/@akanjs/devkit/package.json", "pkgs/@akanjs/cli/package.json"];
12
+
13
+ interface Shrinkwrap {
14
+ packages: Record<string, { version?: string }>;
15
+ }
16
+
17
+ const readJson = async <T>(file: string) => (await Bun.file(file).json()) as T;
18
+
19
+ const shrinkwrappedPins = async () => {
20
+ const { packages } = await readJson<Shrinkwrap>(`${piDir}/npm-shrinkwrap.json`);
21
+ // Top-level entries only: a nested `node_modules/` key is another package's placement, not ours to pin.
22
+ const entries = Object.entries(packages)
23
+ .filter(([key]) => /^node_modules\/@earendil-works\/[^/]+$/.test(key))
24
+ .map(([key, entry]) => [key.replace(/^node_modules\//, ""), entry.version ?? ""] as const);
25
+ return Object.fromEntries(entries);
26
+ };
27
+
28
+ describe("pi dependency pins", () => {
29
+ test("the shrinkwrap read here belongs to the coding agent we publish", async () => {
30
+ const installed = await readJson<PackageJson>(`${piDir}/package.json`);
31
+ for (const file of publishedPackages) {
32
+ const { dependencies = {} } = await readJson<PackageJson>(`${repoRoot}/${file}`);
33
+ expect({ file, [piName]: dependencies[piName] }).toEqual({ file, [piName]: installed.version });
34
+ }
35
+ });
36
+
37
+ test("every sibling pi package is published at the version the coding agent shrinkwrapped", async () => {
38
+ const pins = await shrinkwrappedPins();
39
+ expect(Object.keys(pins).length).toBeGreaterThan(0);
40
+ for (const file of publishedPackages) {
41
+ const { dependencies = {} } = await readJson<PackageJson>(`${repoRoot}/${file}`);
42
+ const declared = Object.fromEntries(Object.keys(pins).map((name) => [name, dependencies[name]]));
43
+ expect({ file, ...declared }).toEqual({ file, ...pins });
44
+ }
45
+ });
46
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BunTestReport } from "./BunTestReport";
3
+
4
+ describe("BunTestReport", () => {
5
+ test("reads the final summary and attributes each failure to the file header above it", () => {
6
+ const report = new BunTestReport(
7
+ [
8
+ "bun test v1.4.2 (744846f84)",
9
+ "",
10
+ "server\\artifact\\routeSeedIndex.test.ts:",
11
+ "(pass) route seed index > keeps seeds [0.40ms]",
12
+ "(fail) route seed index > restores relative seed paths [1.20ms]",
13
+ "",
14
+ "lib/linter.test.ts:",
15
+ "\u001b[31m(fail) Linter > finds biome [3.00ms]\u001b[0m",
16
+ "",
17
+ "2 tests failed:",
18
+ "(fail) route seed index > restores relative seed paths [1.20ms]",
19
+ "(fail) Linter > finds biome [3.00ms]",
20
+ "",
21
+ " 1 pass",
22
+ " 3 skip",
23
+ " 2 fail",
24
+ " 1 error",
25
+ "Ran 6 tests across 2 files. [120.00ms]",
26
+ ].join("\r\n"),
27
+ );
28
+
29
+ expect(report.hasSummary).toBe(true);
30
+ expect([report.pass, report.fail, report.skip, report.errors, report.files]).toEqual([1, 2, 3, 1, 2]);
31
+ expect(report.failures).toEqual([
32
+ { file: "server/artifact/routeSeedIndex.test.ts", name: "route seed index > restores relative seed paths" },
33
+ { file: "lib/linter.test.ts", name: "Linter > finds biome" },
34
+ ]);
35
+ expect(report.passed).toBe(false);
36
+ });
37
+
38
+ test("a run with no summary is not passed, and counts what it saw before it stopped", () => {
39
+ const report = new BunTestReport(
40
+ [
41
+ "server/akanApp.test.ts:",
42
+ "(pass) AkanApp > boots [1ms]",
43
+ "(pass) AkanApp > relays [2ms]",
44
+ "panic(main thread)",
45
+ ].join("\n"),
46
+ );
47
+
48
+ expect(report.hasSummary).toBe(false);
49
+ expect(report.passed).toBe(false);
50
+ expect(report.pass).toBe(2);
51
+ expect(report.lastFile).toBe("server/akanApp.test.ts");
52
+ });
53
+
54
+ test("a clean summary passes", () => {
55
+ const report = new BunTestReport(" 4 pass\n 0 fail\nRan 4 tests across 1 file. [12.00ms]\n");
56
+
57
+ expect(report.passed).toBe(true);
58
+ expect(report.files).toBe(1);
59
+ });
60
+ });
@@ -0,0 +1,65 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ export interface BunTestFailure {
4
+ file: string | null;
5
+ name: string;
6
+ }
7
+
8
+ export class BunTestReport {
9
+ static readonly #fileHeaderPattern = /^(\S+\.(?:test|spec)\.[cm]?[jt]sx?):$/;
10
+ //? After the last file, bun repeats every failure under an "N tests failed:" line — those belong to no header.
11
+ static readonly #relistPattern = /^\d+ tests? failed:$/;
12
+ static readonly #durationSuffix = /\s+\[[\d.]+m?s\]$/;
13
+
14
+ readonly pass: number;
15
+ readonly fail: number;
16
+ readonly skip: number;
17
+ readonly errors: number;
18
+ readonly files: number;
19
+ readonly hasSummary: boolean;
20
+ readonly failures: BunTestFailure[];
21
+ readonly lastFile: string | null;
22
+
23
+ constructor(output: string) {
24
+ const lines = stripVTControlCharacters(output).split(/\r?\n/);
25
+ let currentFile: string | null = null;
26
+ let lastFile: string | null = null;
27
+ let relisted = false;
28
+ const failures = new Map<string, BunTestFailure>();
29
+ for (const line of lines) {
30
+ if (BunTestReport.#relistPattern.test(line)) relisted = true;
31
+ const header = BunTestReport.#fileHeaderPattern.exec(line);
32
+ if (header?.[1] && !relisted) {
33
+ currentFile = header[1].replaceAll("\\", "/");
34
+ lastFile = currentFile;
35
+ continue;
36
+ }
37
+ if (!line.startsWith("(fail) ")) continue;
38
+ const name = line.slice("(fail) ".length).replace(BunTestReport.#durationSuffix, "");
39
+ if (!failures.has(name)) failures.set(name, { file: relisted ? null : currentFile, name });
40
+ }
41
+ const ran = lines.findLast((line) => /^Ran \d+ tests? across \d+ files?/.test(line));
42
+ this.hasSummary = !!ran;
43
+ this.files = ran ? Number(/across (\d+) files?/.exec(ran)?.[1] ?? 0) : 0;
44
+ this.failures = [...failures.values()];
45
+ this.lastFile = lastFile;
46
+ this.pass = ran
47
+ ? BunTestReport.#lastCount(lines, /^\s*(\d+) pass$/)
48
+ : lines.filter((line) => line.startsWith("(pass) ")).length;
49
+ this.fail = ran ? BunTestReport.#lastCount(lines, /^\s*(\d+) fail$/) : this.failures.length;
50
+ this.skip = ran ? BunTestReport.#lastCount(lines, /^\s*(\d+) skip$/) : 0;
51
+ this.errors = ran ? BunTestReport.#lastCount(lines, /^\s*(\d+) errors?$/) : 0;
52
+ }
53
+
54
+ get passed() {
55
+ return this.hasSummary && this.fail === 0 && this.errors === 0;
56
+ }
57
+
58
+ static #lastCount(lines: string[], pattern: RegExp) {
59
+ for (const line of lines.toReversed()) {
60
+ const count = pattern.exec(line)?.[1];
61
+ if (count) return Number(count);
62
+ }
63
+ return 0;
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
+ import { IdleWatchedProcess } from "./IdleWatchedProcess";
6
+
7
+ const makeRoot = tempDirs("akan-idle-watch-");
8
+ const makeLogPath = async () => path.join(await makeRoot(), "nested", "run.log");
9
+
10
+ describe("IdleWatchedProcess", () => {
11
+ test("tees the output of a command that finishes into the log and reports its exit code", async () => {
12
+ const logPath = await makeLogPath();
13
+ const result = await new IdleWatchedProcess(
14
+ [process.execPath, "--eval", "console.log('out'); console.error('err'); process.exit(3)"],
15
+ { logPath, idleMs: 10_000 },
16
+ ).run();
17
+
18
+ expect(result.exitCode).toBe(3);
19
+ expect(result.hung).toBe(false);
20
+ expect(result.output).toContain("out");
21
+ expect(result.output).toContain("err");
22
+ expect(await readFile(logPath, "utf8")).toContain("out");
23
+ });
24
+
25
+ test("gives up on a command that goes quiet, after asking the caller to clean up", async () => {
26
+ const logPath = await makeLogPath();
27
+ let cleanedUp = false;
28
+ const result = await new IdleWatchedProcess(
29
+ [process.execPath, "--eval", "console.log('started'); setInterval(() => {}, 1000)"],
30
+ {
31
+ logPath,
32
+ idleMs: 400,
33
+ onIdle: async () => {
34
+ cleanedUp = true;
35
+ },
36
+ },
37
+ ).run();
38
+
39
+ expect(result.hung).toBe(true);
40
+ expect(result.exitCode).toBeNull();
41
+ expect(cleanedUp).toBe(true);
42
+ expect(result.output).toContain("started");
43
+ });
44
+
45
+ test("a command that keeps talking is not idle, but still stops at the total budget", async () => {
46
+ const logPath = await makeLogPath();
47
+ const result = await new IdleWatchedProcess(
48
+ [process.execPath, "--eval", "setInterval(() => console.log('tick'), 50)"],
49
+ { logPath, idleMs: 5_000, totalMs: 500 },
50
+ ).run();
51
+
52
+ expect(result.hung).toBe(false);
53
+ expect(result.timedOut).toBe(true);
54
+ expect(result.exitCode).toBeNull();
55
+ });
56
+ });
@@ -0,0 +1,73 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface IdleWatchedResult {
5
+ exitCode: number | null;
6
+ hung: boolean;
7
+ timedOut: boolean;
8
+ output: string;
9
+ ms: number;
10
+ }
11
+
12
+ interface IdleWatchedOptions {
13
+ logPath: string;
14
+ idleMs: number;
15
+ totalMs?: number;
16
+ cwd?: string;
17
+ env?: Record<string, string | undefined>;
18
+ onIdle?: () => Promise<void>;
19
+ onOutput?: (text: string) => void;
20
+ }
21
+
22
+ // Silence is the only reliable hang signal: a runner stuck in a synchronous loop never lets bun's test timeout fire.
23
+ export class IdleWatchedProcess {
24
+ readonly #command: string[];
25
+ readonly #options: IdleWatchedOptions;
26
+
27
+ constructor(command: string[], options: IdleWatchedOptions) {
28
+ this.#command = command;
29
+ this.#options = options;
30
+ }
31
+
32
+ async run(): Promise<IdleWatchedResult> {
33
+ const { logPath, idleMs, totalMs, cwd, env, onIdle, onOutput } = this.#options;
34
+ await mkdir(path.dirname(logPath), { recursive: true });
35
+ const log = Bun.file(logPath).writer();
36
+ const startedAt = performance.now();
37
+ let lastOutputAt = startedAt;
38
+ let output = "";
39
+ let hung = false;
40
+ let timedOut = false;
41
+ const proc = Bun.spawn(this.#command, { cwd, env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
42
+ const pump = async (stream: ReadableStream<Uint8Array>) => {
43
+ const decoder = new TextDecoder();
44
+ for await (const chunk of stream) {
45
+ lastOutputAt = performance.now();
46
+ const text = decoder.decode(chunk, { stream: true });
47
+ output += text;
48
+ log.write(chunk);
49
+ onOutput?.(text);
50
+ }
51
+ };
52
+ const pumping = Promise.all([pump(proc.stdout), pump(proc.stderr)]);
53
+ const watch = setInterval(
54
+ () => {
55
+ const now = performance.now();
56
+ if (now - lastOutputAt >= idleMs) hung = true;
57
+ else if (totalMs !== undefined && now - startedAt >= totalMs) timedOut = true;
58
+ else return;
59
+ clearInterval(watch);
60
+ void (async () => {
61
+ await onIdle?.().catch(() => undefined);
62
+ proc.kill();
63
+ })();
64
+ },
65
+ Math.max(50, Math.min(1000, idleMs / 4)),
66
+ );
67
+ const exitCode = await proc.exited;
68
+ clearInterval(watch);
69
+ await pumping;
70
+ await log.end();
71
+ return { exitCode: hung || timedOut ? null : exitCode, hung, timedOut, output, ms: performance.now() - startedAt };
72
+ }
73
+ }
@@ -0,0 +1,105 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import type { LinuxTestTargetConfig, PlatformGatePolicy } from "../cloud/constants";
4
+ import { IdleWatchedProcess } from "./IdleWatchedProcess";
5
+ import { type PlatformRunContext, PlatformTestTarget } from "./PlatformTestTarget";
6
+
7
+ export class LinuxDockerTarget extends PlatformTestTarget {
8
+ static readonly cacheVolume = "akan-test-bun-cache";
9
+
10
+ readonly platform = "linux";
11
+ readonly policy: PlatformGatePolicy;
12
+ readonly #config: LinuxTestTargetConfig;
13
+ #container: string | null = null;
14
+
15
+ constructor(config: LinuxTestTargetConfig = {}) {
16
+ super();
17
+ this.#config = config;
18
+ this.policy = config.policy ?? "gate";
19
+ }
20
+
21
+ //* No compiler in the image: with one, ssh2 builds its native addon and Bun panics loading it (oven-sh/bun#18546).
22
+ static readonly dockerfile = [
23
+ `FROM oven/bun:${Bun.version}-slim`,
24
+ "RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates procps lsof && rm -rf /var/lib/apt/lists/*",
25
+ ].join("\n");
26
+
27
+ get image() {
28
+ return `akan-test-linux:bun-${Bun.version}-${Bun.hash(LinuxDockerTarget.dockerfile).toString(16).slice(0, 8)}`;
29
+ }
30
+
31
+ protected async prepare(context: PlatformRunContext) {
32
+ if ((await LinuxDockerTarget.spawnText(["docker", "info"])).exitCode !== 0)
33
+ throw new Error("Docker is not running — start Docker Desktop, or leave linux out of --platforms");
34
+ await this.#ensureImage();
35
+ const container = `akan-test-${context.runId}`;
36
+ const cpus = this.#config.cpus ?? Math.max(2, Math.floor(os.availableParallelism() / 2));
37
+ const started = await LinuxDockerTarget.spawnText([
38
+ "docker",
39
+ "run",
40
+ "-d",
41
+ "--rm",
42
+ "--name",
43
+ container,
44
+ "--cpus",
45
+ String(cpus),
46
+ ...(this.#config.memory ? ["--memory", this.#config.memory] : []),
47
+ "-v",
48
+ `${LinuxDockerTarget.cacheVolume}:/root/.bun/install/cache`,
49
+ "-v",
50
+ `${path.dirname(context.snapshot.tarPath)}:/io:ro`,
51
+ "-v",
52
+ `${context.envPath}:/io-env:ro`,
53
+ "-w",
54
+ "/w",
55
+ this.image,
56
+ "sleep",
57
+ "infinity",
58
+ ]);
59
+ if (started.exitCode !== 0) throw new Error(`docker run failed: ${started.stderr.trim()}`);
60
+ this.#container = container;
61
+ const extract = await this.exec(
62
+ `tar -xf /io/${path.basename(context.snapshot.tarPath)} -C /w && cp /io-env /w/.env && git -C /w init -q`,
63
+ path.join(context.logDir, this.platform, "extract.log"),
64
+ PlatformTestTarget.installIdleMs,
65
+ );
66
+ if (extract.exitCode !== 0) throw new Error("extracting the snapshot into the container failed");
67
+ }
68
+
69
+ protected async exec(command: string, logPath: string, idleMs: number) {
70
+ const container = this.#container;
71
+ if (!container) throw new Error("the linux container is not running");
72
+ return await new IdleWatchedProcess(
73
+ [
74
+ "docker",
75
+ "exec",
76
+ ...Object.entries(PlatformTestTarget.gitIdentityEnv).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
77
+ container,
78
+ "sh",
79
+ "-c",
80
+ command,
81
+ ],
82
+ {
83
+ logPath,
84
+ idleMs,
85
+ onIdle: async () => {
86
+ await LinuxDockerTarget.spawnText(["docker", "exec", container, "pkill", "-9", "bun"]);
87
+ },
88
+ },
89
+ ).run();
90
+ }
91
+
92
+ protected async cleanup() {
93
+ if (this.#container) await LinuxDockerTarget.spawnText(["docker", "rm", "-f", this.#container]);
94
+ this.#container = null;
95
+ }
96
+
97
+ async #ensureImage() {
98
+ if ((await LinuxDockerTarget.spawnText(["docker", "image", "inspect", this.image])).exitCode === 0) return;
99
+ const built = await LinuxDockerTarget.spawnText(
100
+ ["docker", "build", "-t", this.image, "-"],
101
+ LinuxDockerTarget.dockerfile,
102
+ );
103
+ if (built.exitCode !== 0) throw new Error(`building ${this.image} failed: ${built.stderr.trim().slice(-400)}`);
104
+ }
105
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tempDirs } from "../testHelpers";
5
+ import { BunTestReport } from "./BunTestReport";
6
+ import { PlatformTestRun } from "./PlatformTestRun";
7
+ import type { PackageTestResult, PlatformTestResult } from "./PlatformTestTarget";
8
+
9
+ const makeRoot = tempDirs("akan-platform-env-");
10
+
11
+ const pkg = (status: PackageTestResult["status"]): PackageTestResult => ({
12
+ pkg: "akanjs",
13
+ status,
14
+ report: new BunTestReport(""),
15
+ ms: 1,
16
+ logPath: "x.log",
17
+ });
18
+ const result = (
19
+ platform: PlatformTestResult["platform"],
20
+ policy: PlatformTestResult["policy"],
21
+ packages: PackageTestResult[],
22
+ infraError: string | null = null,
23
+ ): PlatformTestResult => ({ platform, policy, packages, envWarnings: [], infraError, ms: 1 });
24
+
25
+ describe("PlatformTestRun", () => {
26
+ test("a failing gate blocks, a failing warn only warns, and a setup failure counts as a failure", () => {
27
+ const verdict = PlatformTestRun.verdict([
28
+ result("linux", "gate", [pkg("pass"), pkg("fail")]),
29
+ result("windows", "warn", [pkg("pass")], "cannot reach user@host over ssh"),
30
+ ]);
31
+
32
+ expect(verdict.blocking.map((one) => one.platform)).toEqual(["linux"]);
33
+ expect(verdict.warning.map((one) => one.platform)).toEqual(["windows"]);
34
+ });
35
+
36
+ test("a hung package fails its platform", () => {
37
+ expect(PlatformTestRun.failed(result("windows", "gate", [pkg("pass"), pkg("hung")]))).toBe(true);
38
+ expect(PlatformTestRun.failed(result("windows", "gate", [pkg("pass")]))).toBe(false);
39
+ });
40
+
41
+ test("hands a test host only the env keys the CLI needs, never the secrets beside them", async () => {
42
+ const root = await makeRoot();
43
+ await writeFile(
44
+ path.join(root, ".env"),
45
+ ["USE_AKANJS_PKGS=true", "OPENAI_API_KEY=sk-secret", 'AKAN_PUBLIC_SERVE_DOMAIN="akanjs.com"', "JEV_KEY=x"].join(
46
+ "\n",
47
+ ),
48
+ );
49
+
50
+ expect(await PlatformTestRun.allowlistedEnv(root)).toBe(
51
+ 'USE_AKANJS_PKGS=true\nAKAN_PUBLIC_SERVE_DOMAIN="akanjs.com"\n',
52
+ );
53
+ });
54
+
55
+ test("reads a comma-separated platform list, where none means no remote platform", () => {
56
+ expect(PlatformTestRun.parsePlatforms("linux, Windows,linux")).toEqual(["linux", "windows"]);
57
+ expect(PlatformTestRun.parsePlatforms("none")).toEqual([]);
58
+ expect(() => PlatformTestRun.parsePlatforms("linux,mac")).toThrow("mac");
59
+ });
60
+
61
+ test("run ids sort by time", () => {
62
+ expect(PlatformTestRun.newRunId(new Date(2026, 8, 26, 9, 5, 7))).toBe("20260926-090507");
63
+ });
64
+ });