@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
package/linter.ts CHANGED
@@ -58,16 +58,8 @@ interface LintResponse {
58
58
  warnings: LintMessage[];
59
59
  }
60
60
 
61
- /**
62
- * Extracts the `--reporter=json` report from Biome's output.
63
- *
64
- * Biome mixes configuration and IO diagnostics into the same stream as the report, so the output is not
65
- * always JSON alone. Slicing from the first `{` to the last `}` mis-parses as soon as that leading text
66
- * carries a brace of its own, which is not hypothetical: a config error printed ahead of the report made
67
- * one unchanged command alternate between 2 and 10 diagnostics. So every `{` is tried as a start, its
68
- * match is found by depth while string literals are skipped, and the first candidate that both parses and
69
- * carries a report field wins — a brace in prose parses as nothing, and `{"a":1}` in prose is not a report.
70
- */
61
+ // Biome mixes config and IO diagnostics into the report stream, and that text can carry braces of its own, so every
62
+ // `{` is tried as a start (string literals skipped) and the first candidate that parses and carries a report field wins.
71
63
  export const parseBiomeReport = (output: string): BiomeReport => {
72
64
  for (let start = output.indexOf("{"); start !== -1; start = output.indexOf("{", start + 1)) {
73
65
  const end = objectEndAt(output, start);
@@ -111,7 +103,19 @@ const asBiomeReport = (candidate: string): BiomeReport | null => {
111
103
  /** Biome reads `biome.json` first and `biome.jsonc` second; only the latter may carry comments. */
112
104
  const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
113
105
 
106
+ const emptyResult = (filePath: string): LintResult => ({
107
+ filePath,
108
+ messages: [],
109
+ errorCount: 0,
110
+ warningCount: 0,
111
+ fixableErrorCount: 0,
112
+ fixableWarningCount: 0,
113
+ });
114
+
114
115
  export class Linter {
116
+ //? Bun links a package bin as `<name>.exe` (plus a `.bunx` stub) on Windows, and a bare `<name>` elsewhere.
117
+ static readonly biomeBinName = process.platform === "win32" ? "biome.exe" : "biome";
118
+
115
119
  lintRoot: string;
116
120
  configPath: string;
117
121
  #biomeBin: string;
@@ -119,7 +123,7 @@ export class Linter {
119
123
  constructor(cwdPath: string) {
120
124
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
121
125
  this.configPath = Linter.#configPathIn(this.lintRoot) ?? path.join(this.lintRoot, "biome.json");
122
- const localBiomeBin = path.join(this.lintRoot, "node_modules/.bin/biome");
126
+ const localBiomeBin = path.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
123
127
  this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
124
128
  }
125
129
 
@@ -199,16 +203,7 @@ export class Linter {
199
203
  for (const diagnostic of report.diagnostics ?? []) {
200
204
  if (diagnostic.severity !== "error" && diagnostic.severity !== "warning") continue;
201
205
  const diagnosticFilePath = this.#diagnosticFilePath(diagnostic, filePath);
202
- const result =
203
- resultsByPath.get(diagnosticFilePath) ??
204
- ({
205
- filePath: diagnosticFilePath,
206
- messages: [],
207
- errorCount: 0,
208
- warningCount: 0,
209
- fixableErrorCount: 0,
210
- fixableWarningCount: 0,
211
- } satisfies LintResult);
206
+ const result = resultsByPath.get(diagnosticFilePath) ?? emptyResult(diagnosticFilePath);
212
207
  const message = this.#createLintMessage(diagnostic);
213
208
  result.messages.push(message);
214
209
  if (message.severity === 2) result.errorCount += 1;
@@ -217,15 +212,7 @@ export class Linter {
217
212
  }
218
213
 
219
214
  return [
220
- resultsByPath.get(filePath) ??
221
- ({
222
- filePath,
223
- messages: [],
224
- errorCount: 0,
225
- warningCount: 0,
226
- fixableErrorCount: 0,
227
- fixableWarningCount: 0,
228
- } satisfies LintResult),
215
+ resultsByPath.get(filePath) ?? emptyResult(filePath),
229
216
  ...[...resultsByPath.entries()].filter(([resultPath]) => resultPath !== filePath).map(([, result]) => result),
230
217
  ];
231
218
  }
@@ -269,22 +256,12 @@ export class Linter {
269
256
  return await this.lintFile(filePath);
270
257
  }
271
258
 
272
- /**
273
- * Lint a single file using Biome.
274
- * @param filePath - Path to the file to lint
275
- * @returns Array of Biome results in the legacy lint result shape
276
- */
277
259
  async lintFile(filePath: string): Promise<LintResponse> {
278
260
  const resolvedFilePath = this.#resolveFilePath(filePath);
279
261
  if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
280
262
  return await this.#checkFile(resolvedFilePath);
281
263
  }
282
264
 
283
- /**
284
- * Format lint results for console output
285
- * @param results - Array of Biome results
286
- * @returns Formatted string
287
- */
288
265
  formatLintResults(results: LintResult[]): string {
289
266
  if (results.length === 0) return "No files to lint";
290
267
 
@@ -305,7 +282,7 @@ export class Linter {
305
282
  const sourceContent = readFileSync(result.filePath, "utf8");
306
283
  sourceLines = sourceContent.split("\n");
307
284
  } catch {
308
- // Ignore read errors
285
+ //? An unreadable source only loses its code frame.
309
286
  }
310
287
  }
311
288
 
@@ -318,14 +295,12 @@ export class Linter {
318
295
  output.push(`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
319
296
  output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
320
297
 
321
- // Show source line with underline
322
298
  if (sourceLines.length > 0 && message.line <= sourceLines.length) {
323
299
  const sourceLine = sourceLines[message.line - 1];
324
300
  const lineNumber = message.line.toString().padStart(5, " ");
325
301
 
326
302
  output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
327
303
 
328
- // Create underline
329
304
  const underlinePrefix = " ".repeat(message.column - 1);
330
305
  const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
331
306
  const underline = "^".repeat(Math.max(1, underlineLength));
@@ -345,27 +320,7 @@ export class Linter {
345
320
  return summary.concat(output).join("\n");
346
321
  }
347
322
 
348
- /**
349
- * Get detailed lint information
350
- * @param filePath - Path to the file to lint
351
- * @returns Object containing detailed lint information
352
- */
353
- async getDetailedLintInfo(filePath: string): Promise<{
354
- results: LintResult[];
355
- details: {
356
- line: number;
357
- column: number;
358
- message: string;
359
- ruleId: string | null;
360
- severity: "error" | "warning";
361
- }[];
362
- stats: {
363
- errorCount: number;
364
- warningCount: number;
365
- fixableErrorCount: number;
366
- fixableWarningCount: number;
367
- };
368
- }> {
323
+ async getDetailedLintInfo(filePath: string) {
369
324
  const { results } = await this.lintFile(filePath);
370
325
 
371
326
  const details = results.flatMap((result) =>
@@ -385,22 +340,13 @@ export class Linter {
385
340
  fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
386
341
  fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount,
387
342
  }),
388
- {
389
- errorCount: 0,
390
- warningCount: 0,
391
- fixableErrorCount: 0,
392
- fixableWarningCount: 0,
393
- },
343
+ { errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 },
394
344
  );
395
345
 
396
346
  return { results, details, stats };
397
347
  }
398
348
 
399
- /**
400
- * Check if a file has lint errors
401
- * @param filePath - Path to the file to check
402
- * @returns true if there are no errors, false otherwise
403
- */
349
+ /** Also false when the file cannot be linted at all. */
404
350
  async hasNoLintErrors(filePath: string): Promise<boolean> {
405
351
  try {
406
352
  const { results } = await this.lintFile(filePath);
@@ -410,32 +356,17 @@ export class Linter {
410
356
  }
411
357
  }
412
358
 
413
- /**
414
- * Get only error messages (excluding warnings)
415
- * @param filePath - Path to the file to lint
416
- * @returns Array of error messages
417
- */
418
359
  async getErrors(filePath: string): Promise<LintMessage[]> {
419
360
  const { results } = await this.lintFile(filePath);
420
361
  return results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
421
362
  }
422
363
 
423
- /**
424
- * Get only warning messages
425
- * @param filePath - Path to the file to lint
426
- * @returns Array of warning messages
427
- */
428
364
  async getWarnings(filePath: string): Promise<LintMessage[]> {
429
365
  const { results } = await this.lintFile(filePath);
430
366
  return results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
431
367
  }
432
368
 
433
- /**
434
- * Fix lint errors automatically
435
- * @param filePath - Path to the file to fix
436
- * @param dryRun - If true, returns the fixed content without writing to file
437
- * @returns Fixed content and remaining issues
438
- */
369
+ /** `dryRun` returns the fixed content in `output` without writing the file. */
439
370
  async fixFile(filePath: string, dryRun = false): Promise<LintResponse> {
440
371
  const resolvedFilePath = this.#resolveFilePath(filePath);
441
372
  if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
@@ -451,19 +382,8 @@ export class Linter {
451
382
  return { ...lintResult, fixed: stdout !== source, output: stdout };
452
383
  }
453
384
 
454
- /**
455
- * Get Biome configuration for a file
456
- * @param filePath - Path to the file
457
- * @returns Biome configuration object
458
- */
459
- /**
460
- * Applies Biome's safe fixes to many files in one process.
461
- *
462
- * One spawn, not one per file: Biome's startup dominates a small file, so the per-file path turns a
463
- * fifteen-file scaffold into three seconds of process launches. Paths that do not exist are dropped
464
- * rather than thrown on, because the caller is usually reporting what a template just wrote and a
465
- * template is allowed to decline a file.
466
- */
385
+ // One spawn for all files: Biome's startup dominates a small file. Missing paths are dropped (a template may
386
+ // decline a file).
467
387
  async fixFiles(filePaths: string[]): Promise<{ fixed: string[] }> {
468
388
  const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
469
389
  if (resolved.length === 0) return { fixed: [] };
@@ -489,21 +409,11 @@ export class Linter {
489
409
  return JSON.parse(readFileSync(this.configPath, "utf8")) as unknown;
490
410
  }
491
411
 
492
- /**
493
- * Get rules that are causing errors in a file
494
- * @param filePath - Path to the file to check
495
- * @returns Object mapping rule IDs to their error counts
496
- */
497
412
  async getProblematicRules(filePath: string): Promise<Record<string, number>> {
498
413
  const { results } = await this.lintFile(filePath);
499
414
  const ruleCounts: Record<string, number> = {};
500
-
501
- results.forEach((result) => {
502
- result.messages.forEach((message) => {
503
- if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
504
- });
505
- });
506
-
415
+ for (const message of results.flatMap((result) => result.messages))
416
+ if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
507
417
  return ruleCounts;
508
418
  }
509
419
  }
@@ -11,6 +11,8 @@ export interface ResolvedMobileTarget {
11
11
 
12
12
  export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
13
13
 
14
+ const trimSlashes = (value: string) => value.replace(/^\/+|\/+$/g, "");
15
+
14
16
  export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
15
17
  const config = await app.getConfig();
16
18
  return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
@@ -20,26 +22,23 @@ export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
20
22
  const config = await app.getConfig();
21
23
  const targetNames = Object.keys(config.mobile.targets);
22
24
  if (targetNames.length > 0) return targetNames;
23
- const basePaths = [...config.basePaths];
24
- return basePaths;
25
+ return [...config.basePaths];
25
26
  };
26
27
 
27
28
  const resolveMobileTargetByBasePath = (
28
29
  targets: ResolvedMobileTarget[],
29
30
  basePath: string,
30
31
  ): ResolvedMobileTarget | undefined => {
31
- const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
32
- const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
32
+ const normalizedBasePath = trimSlashes(basePath);
33
+ const byBasePath = targets.find(
34
+ ({ config }) => config.basePath !== undefined && trimSlashes(config.basePath) === normalizedBasePath,
35
+ );
33
36
  if (byBasePath) return byBasePath;
34
37
  const [template] = targets;
35
38
  if (!template) return undefined;
36
39
  return {
37
40
  name: normalizedBasePath,
38
- config: {
39
- ...template.config,
40
- name: normalizedBasePath,
41
- basePath: normalizedBasePath,
42
- },
41
+ config: { ...template.config, name: normalizedBasePath, basePath: normalizedBasePath },
43
42
  };
44
43
  };
45
44
 
@@ -70,18 +69,20 @@ export const resolveMobileTargets = async (
70
69
  const target = targets.find((candidate) => candidate.name === selection);
71
70
  if (target) return [target];
72
71
  const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
73
- if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
72
+ if (basePathTarget && config.basePaths.has(trimSlashes(selection))) return [basePathTarget];
74
73
  const choices = await getMobileTargetChoices(app);
75
74
  throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
76
75
  };
77
76
 
78
77
  export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
79
- const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
78
+ const basePath = trimSlashes(target.basePath ?? "");
80
79
  const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
81
80
  if (!basePath) return normalizedPath;
82
81
  if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
83
82
  return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
84
83
  };
85
84
 
86
- export const targetHtmlFilename = (target: AkanMobileTargetConfig) =>
87
- target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
85
+ export const targetHtmlFilename = (target: AkanMobileTargetConfig) => {
86
+ const basePath = trimSlashes(target.basePath ?? "");
87
+ return basePath ? `${basePath}.html` : "index.html";
88
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-beta.2",
3
+ "version": "3.0.0-beta.20",
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,16 @@
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
50
  "@trapezedev/project": "^7.1.4",
47
- "akanjs": "3.0.0-beta.2",
51
+ "akanjs": "3.0.0-beta.20",
48
52
  "chalk": "^5.6.2",
49
53
  "commander": "^14.0.3",
50
54
  "dayjs": "^1.11.20",
@@ -56,6 +60,7 @@
56
60
  "subset-font": "^2.5.0",
57
61
  "tailwind-scrollbar": "^4.0.2",
58
62
  "tailwindcss": "^4.3.0",
63
+ "typebox": "1.1.38",
59
64
  "typescript": "^6.0.3"
60
65
  },
61
66
  "peerDependencies": {
@@ -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
+ }