@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
@@ -1,9 +1,7 @@
1
1
  import path from "node:path";
2
- // Module paths, never a barrel. The `@akanjs/devkit` root re-exports all 41 modules, which would drag
3
- // @trapezedev/project, the @langchain stack, ssh2, ink and the cloud stack in; and `frontendBuild`'s own
4
- // barrel reaches `cssCompiler`/`ssrBaseArtifactBuilder`, which pull tailwindcss + @tailwindcss/node
5
- // (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
6
- // into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
2
+ // Module paths, never a barrel: the devkit and `frontendBuild` barrels would hold tailwindcss (~40MB), ssh2, ink
3
+ // and the cloud stack for the whole dev session (`entryModuleGraph.test.ts` enforces it).
4
+ import { ApplicationBuildReporter } from "@akanjs/devkit/applicationBuildReporter";
7
5
  import { CodegenLock } from "@akanjs/devkit/codegenLock";
8
6
  import type { App } from "@akanjs/devkit/commandDecorators";
9
7
  import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
@@ -41,6 +39,7 @@ interface IncrementalBuilderOptions {
41
39
  }
42
40
 
43
41
  type IncrementalBuilderBootDeps = Pick<IncrementalBuilderOptions, "artifact" | "optimizedFonts" | "discovery">;
42
+ type BatchWork = Pick<BuildBatchRequest, "generation" | "needs" | "changedFiles">;
44
43
 
45
44
  class IncrementalBuilder {
46
45
  #logger = new Logger("IncrementalBuilder");
@@ -84,10 +83,7 @@ class IncrementalBuilder {
84
83
  return `${this.#app.cwdPath}/.akan/artifact`;
85
84
  }
86
85
 
87
- /**
88
- * Build a route and answer it. The reply is part of the work item on purpose: `shutdown` drains the
89
- * work queue before exiting, so folding the flush in here is what makes "drained" mean "answered".
90
- */
86
+ // The reply is part of the work item on purpose: `shutdown` drains the queue, so "drained" must mean "answered".
91
87
  async handleBuildRoute(msg: BuilderReq): Promise<void> {
92
88
  await this.#enqueueWork(`build-route:${msg.routeId}`, async () =>
93
89
  BuilderChannel.send(await this.#handleBuildRoute(msg)),
@@ -100,6 +96,7 @@ class IncrementalBuilder {
100
96
  app: this.#app,
101
97
  routeId: msg.routeId,
102
98
  seeds: msg.seeds,
99
+ graphSeeds: msg.graphSeeds,
103
100
  artifact: this.#artifact,
104
101
  knownEntries: new Set<string>(msg.knownEntries),
105
102
  discovery: this.#discovery,
@@ -122,7 +119,7 @@ class IncrementalBuilder {
122
119
  } as BuildRouteResultPayload,
123
120
  };
124
121
  } catch (err) {
125
- const errMsg = err instanceof Error ? err.message : String(err);
122
+ const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
126
123
  this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
127
124
  this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
128
125
  return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
@@ -159,16 +156,11 @@ class IncrementalBuilder {
159
156
  }
160
157
  }
161
158
 
162
- /** No queued work and no debounced css rebuild, so nothing is lost if the process exits now. */
163
159
  get #idle(): boolean {
164
160
  return this.#inFlight === 0 && this.#cssRebuildTimer === null;
165
161
  }
166
162
 
167
- /**
168
- * Reported only when the builder is idle. The host's only lever against the bundler arenas
169
- * `Bun.build` retains is to recycle this process, and a recycle decided while work is queued would
170
- * either truncate that work or race the shutdown drain — so a busy builder simply says nothing.
171
- */
163
+ // Idle only: the host recycles on these metrics, and a recycle decided mid-work would truncate it or race the drain.
172
164
  #reportMetrics(): void {
173
165
  if (!this.#idle || this.#shuttingDown) return;
174
166
  BuilderChannel.emit({
@@ -177,34 +169,24 @@ class IncrementalBuilder {
177
169
  });
178
170
  }
179
171
 
180
- /**
181
- * Finish queued work, then exit so the OS reclaims the bundler arenas. The host restarts a
182
- * replacement; build requests that arrive during the drain are refused with the same retry error
183
- * the backend already handles for any other builder restart.
184
- */
172
+ // Exits so the OS reclaims the bundler arenas; the host spawns the replacement.
185
173
  async shutdown(reason: string): Promise<void> {
186
174
  if (this.#shuttingDown) return;
187
175
  this.#shuttingDown = true;
188
176
  const started = Date.now();
189
177
  this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
190
- // Stopped before the drain, or a save landing mid-drain would enqueue a batch behind the queue tail
191
- // this method already awaited — and `process.exit(0)` would cut that batch off partway through
192
- // writing its artifacts. The replacement rebuilds every artifact from its boot build anyway, so the
193
- // batch is not lost; a half-written one would be.
178
+ // Before the drain: a save landing mid-drain would queue behind the awaited tail and `process.exit` would cut
179
+ // its artifacts off half-written; the replacement's boot build redoes that work anyway.
194
180
  this.#watcher?.stop();
195
181
  if (this.#cssRebuildTimer) {
196
- // Only reachable if a css batch landed between the idle report and this request: the fresh
197
- // boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
182
+ // Only if css landed after the idle report; the replacement's boot build recompiles css, so nothing is lost.
198
183
  clearTimeout(this.#cssRebuildTimer);
199
184
  this.#cssRebuildTimer = null;
200
185
  this.#pendingCssRebuild = null;
201
186
  }
202
187
  await this.#workQueue.catch(() => undefined);
203
188
  await this.#cssRebuildQueue.catch(() => undefined);
204
- // Drained queues do not mean the host has the results. The events those work items produced are the
205
- // largest messages this process sends, and `process.exit` discards an ipc write that has not
206
- // flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
207
- // anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
189
+ // Drained queues are not delivered results: `process.exit` drops unflushed ipc writes (a relayed `css-updated`).
208
190
  const flushed = await BuilderChannel.drain();
209
191
  this.#logger.debug(
210
192
  `drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
@@ -215,8 +197,7 @@ class IncrementalBuilder {
215
197
  get shuttingDown(): boolean {
216
198
  return this.#shuttingDown;
217
199
  }
218
- //* Watch events name the file's real path, so synced lib pages are matched by `realDir` while their
219
- //* page key still carries the app-relative `(libs)/(<lib>)` prefix.
200
+ //* Watch events carry real paths: synced lib pages match by `realDir`, their keys keep the `(libs)/(<lib>)` prefix.
220
201
  static #matchPageRoot(roots: PageRoot[], abs: string): PageRoot | null {
221
202
  for (const root of roots) {
222
203
  const absRoot = path.resolve(root.realDir);
@@ -244,7 +225,6 @@ class IncrementalBuilder {
244
225
  }
245
226
  return false;
246
227
  }
247
- /** Debounced css-only rebuild; the compile itself runs in a disposable worker like every other build. */
248
228
  scheduleCssRebuild({ generation, changedFiles }: { generation?: number; changedFiles?: string[] }) {
249
229
  this.#pendingCssRebuild = { generation, changedFiles };
250
230
  if (this.#cssRebuildTimer) clearTimeout(this.#cssRebuildTimer);
@@ -279,13 +259,12 @@ class IncrementalBuilder {
279
259
  }, 150);
280
260
  }
281
261
  async installWatcher() {
282
- const artifactDir = this.#artifactDir;
283
262
  const roots = await new WatchRootResolver(this.#app).resolve();
284
263
  const watcher = new HmrWatcher({
285
264
  roots,
286
265
  logger: this.#logger,
287
266
  onBatch: async (batch: ChangeBatch) => {
288
- await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(artifactDir, batch));
267
+ await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(batch));
289
268
  },
290
269
  });
291
270
  await watcher.start();
@@ -293,13 +272,11 @@ class IncrementalBuilder {
293
272
  this.#logger.verbose(`watching ${roots.length} roots`);
294
273
  }
295
274
 
296
- async #handleWatchBatch(artifactDir: string, batch: ChangeBatch) {
275
+ async #handleWatchBatch(batch: ChangeBatch) {
297
276
  const rawKinds = new Set(batch.kinds);
298
277
  if (rawKinds.size === 0) return;
299
278
  const generation = ++this.#generation;
300
- //* Insert framework imports that are used but omitted (e.g. `Int` in *.constant.ts, `fetch` in
301
- //* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
302
- //* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
279
+ //* Auto-import edits touch only this batch's files, so they rebuild in this same generation.
303
280
  const [autoImport, indexSync] = await CodegenLock.run(
304
281
  this.#app.workspace.workspaceRoot,
305
282
  `hmr-batch:${this.#app.name}`,
@@ -311,9 +288,7 @@ class IncrementalBuilder {
311
288
  for (const error of autoImport.errors) this.#logger.error(error);
312
289
  if (autoImport.changedFiles.length > 0)
313
290
  this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
314
- //* Both passes above write source files, and this generation's build consumes what they wrote. Hand
315
- //* them to the watcher so its verification scan does not read them back as a user edit and spend a
316
- //* second generation rebuilding identical content.
291
+ //* Absorbed so the watcher's verification scan does not replay these writes as a user edit next generation.
317
292
  await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
318
293
  const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
319
294
  generation,
@@ -342,8 +317,7 @@ class IncrementalBuilder {
342
317
  }
343
318
  if (indexSync.changedFiles.length > 0) this.#sendBuildStatus("barrel", { generation, ok: true, files });
344
319
 
345
- // Server-only generations (e.g. a .service.ts or srvkit edit) must not rebuild or refresh the
346
- // client: a fresh pages buildId would broadcast rsc-refresh to browsers for no visible change.
320
+ // Server-only generations skip the client: a fresh pages buildId would rsc-refresh browsers for no visible change.
347
321
  const rebuildClient = devPlan.actions.includes("rebuild-client");
348
322
  if (kinds.includes("code") && !rebuildClient) {
349
323
  this.#logger.verbose(`client rebuild skipped; devPlan actions=${devPlan.actions.join(",") || "(none)"}`);
@@ -360,56 +334,36 @@ class IncrementalBuilder {
360
334
 
361
335
  const needs: BuildBatchNeed[] = [];
362
336
  if (kinds.includes("code") && rebuildClient) {
363
- if (this.#shouldRebuildCsr()) needs.push("csr");
337
+ if (this.#csrActive) needs.push("csr");
364
338
  else
365
339
  this.#logger.verbose(
366
340
  `csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`,
367
341
  );
368
342
  needs.push("pages");
369
- // Server-only code edits cannot introduce class names the CSS scanner would pick up; only a
370
- // client rebuild or a direct stylesheet edit can change the compiled CSS. Folded into this
371
- // generation's batch rather than debounced separately: the work queue already serializes
372
- // generations, so by the time a second batch runs the 150ms debounce would have fired anyway,
373
- // and a second worker spawn per save costs more than the coalescing ever saved.
343
+ // In this batch rather than debounced: the queue already serializes generations, and a second worker per save
344
+ // costs more than coalescing would save.
374
345
  needs.push("css");
375
346
  }
376
347
 
377
348
  BuilderChannel.emit(event);
378
349
 
379
350
  if (needs.length > 0) await this.#runBatch({ generation, needs, changedFiles: files });
380
- // A css-only batch keeps its debounce: those arrive in bursts while a stylesheet is edited, and
381
- // without a pages build in front of them there is nothing else to space them out.
351
+ // Css-only batches keep the debounce: they arrive in bursts while a stylesheet is edited.
382
352
  else if (kinds.includes("css")) {
383
353
  this.scheduleCssRebuild({ generation, changedFiles: files });
384
354
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
385
355
  }
386
356
  }
387
357
 
388
- /**
389
- * Run one generation of build work in a process that exits afterwards. The worker streams the
390
- * messages the backend and the HMR overlay consume, which this relays untouched, so the sequence a
391
- * browser observes is the same one the in-process build produced.
392
- */
393
- async #runBatch({
394
- generation,
395
- needs,
396
- changedFiles,
397
- }: {
398
- generation: number;
399
- needs: BuildBatchNeed[];
400
- changedFiles: string[];
401
- }): Promise<BuildBatchResult> {
358
+ async #runBatch(work: BatchWork): Promise<BuildBatchResult> {
359
+ const { generation, needs, changedFiles } = work;
402
360
  const started = Date.now();
403
- const result = await this.#batchRunner.run(await this.#batchRequest({ generation, needs, changedFiles }), (msg) =>
404
- BuilderChannel.emit(msg),
405
- );
361
+ const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
406
362
  if (result.optimizedFonts) this.#optimizedFonts = result.optimizedFonts;
407
363
  if (result.cssAssets) this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
408
- // A worker that died before reporting streamed no build-status of its own, so report one per need
409
- // it was given: the generation must go red rather than look like it silently succeeded.
364
+ // A crashed worker streamed no build-status, so each need goes red here instead of looking silently successful.
410
365
  if (result.crashed) {
411
- // `base` is excluded because it is not a `BuildPhase`: a boot build has no phase board to fail, and
412
- // it never travels through here — `#buildBootDeps` runs it and throws into the degraded-boot path.
366
+ // `base` is not a `BuildPhase` and never comes through here: `#buildBootDeps` throws into the degraded boot.
413
367
  for (const need of needs)
414
368
  if (need !== "base")
415
369
  this.#sendBuildStatus(need, { generation, ok: false, files: changedFiles, message: result.errors[need] });
@@ -418,15 +372,7 @@ class IncrementalBuilder {
418
372
  return result;
419
373
  }
420
374
 
421
- async #batchRequest({
422
- generation,
423
- needs,
424
- changedFiles,
425
- }: {
426
- generation: number;
427
- needs: BuildBatchNeed[];
428
- changedFiles: string[];
429
- }): Promise<BuildBatchRequest> {
375
+ async #batchRequest({ generation, needs, changedFiles }: BatchWork): Promise<BuildBatchRequest> {
430
376
  return {
431
377
  appName: this.#app.name,
432
378
  workspaceRoot: this.#app.workspace.workspaceRoot,
@@ -447,10 +393,7 @@ class IncrementalBuilder {
447
393
  this.#logger.verbose(`ready (watch=${this.#watch})`);
448
394
  }
449
395
 
450
- /**
451
- * After a degraded boot recovers, the backend is still serving the last-good bundle; push a
452
- * fresh pages/css state so connected browsers pick up the fixed code without another edit.
453
- */
396
+ // The backend still serves the last-good bundle after a degraded boot; push fresh pages/css without another edit.
454
397
  async announceRecoveredState(changedFiles: string[]): Promise<void> {
455
398
  const generation = ++this.#generation;
456
399
  await this.#enqueueWork("boot-recovered", async () => {
@@ -458,21 +401,12 @@ class IncrementalBuilder {
458
401
  });
459
402
  }
460
403
 
461
- /**
462
- * Re-announce the state this builder just booted with, after the host recycled the previous one.
463
- *
464
- * The backend reads `base-artifact.json` once at boot and never re-reads it, so a pages or css hash
465
- * that moved while the builder was being replaced would otherwise leave it pointing at the previous
466
- * artifact until the next save. Announced from the boot artifact rather than by rebuilding: the
467
- * bundles are already on disk, and spending another ~200MB of bundler arena in a process that was
468
- * just recycled to reclaim memory would defeat the purpose. The host suppresses the announcement
469
- * when the hashes match, which is the common case, so a clean recycle never reloads a browser.
470
- */
404
+ // The backend reads base-artifact.json once, at its boot. Re-announced from disk rather than rebuilt (this process
405
+ // was recycled to free bundler memory); the host drops it when hashes match, so a clean recycle reloads nothing.
471
406
  async announceBootState(): Promise<void> {
472
407
  const generation = ++this.#generation;
473
408
  const reason = "builder-recycle" as const;
474
- // Awaited rather than emitted: this runs during a recycle, so the host may ask this builder to shut
475
- // down at any moment, and "announced boot state" must mean the announcement left the process.
409
+ // Awaited, not emitted: the host may shut this builder down any moment, so "announced" must mean flushed.
476
410
  await BuilderChannel.send({
477
411
  type: "pages-updated",
478
412
  data: {
@@ -499,16 +433,11 @@ class IncrementalBuilder {
499
433
  this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
500
434
  }
501
435
 
502
- /**
503
- * Build the dev CSR artifact because a request asked for it, and keep it in sync from now on. The
504
- * dev server only serves CSR through the opt-in `/__csr` and `?csr=true` routes — mobile local dev
505
- * points a device WebView at the latter — so nothing needs the artifact until one of them is hit.
506
- */
436
+ // On demand: dev serves CSR only via the opt-in `/__csr` and `?csr=true` routes; once built, it rebuilds every save.
507
437
  async handleBuildCsr(msg: BuilderCsrReq): Promise<void> {
508
438
  await this.#enqueueWork("build-csr", async (): Promise<void> => {
509
439
  const started = Date.now();
510
- // Messages are not relayed: an on-demand CSR build is a request/response, and the phase board
511
- // never carried a csr status for it before. The error travels in the response below.
440
+ // Not relayed: an on-demand CSR build is request/response, and its error travels in the response.
512
441
  const result = await this.#batchRunner.run(
513
442
  await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }),
514
443
  );
@@ -524,26 +453,11 @@ class IncrementalBuilder {
524
453
  });
525
454
  }
526
455
 
527
- #shouldRebuildCsr() {
528
- return this.#csrActive;
529
- }
530
-
531
456
  static #csrArmedByEnv() {
532
457
  return process.env.AKAN_DEV_CSR_REBUILD === "1";
533
458
  }
534
459
 
535
- /**
536
- * Build the boot artifact in a process that exits afterwards, and keep only the serializable result.
537
- *
538
- * This runs in a worker for the same reason every other build does, and it was the largest single
539
- * holdout: measured on `apps/akan`, `SsrBaseArtifactBuilder.build()` retains **+1143 MB** that
540
- * `Bun.gc(true)` cannot touch, which is 65 % of the builder's post-boot RSS. Nothing was lost by
541
- * moving it — the builder only ever kept `artifact` and `optimizedFonts`, both plain data, and the
542
- * artifact is written to `base-artifact.json` regardless.
543
- *
544
- * `GraphClientEntryDiscovery.create` stays here because route builds need it live, and it costs
545
- * nothing to keep: measured at **0 ms and 0 MB**, because it builds its graph lazily on first use.
546
- */
460
+ // Discovery stays in-process because route builds need it live; it is free at boot, building its graph lazily.
547
461
  static async #buildBootDeps(app: App, runner: BuildBatchRunner): Promise<IncrementalBuilderBootDeps> {
548
462
  const result = await runner.run({
549
463
  appName: app.name,
@@ -552,15 +466,12 @@ class IncrementalBuilder {
552
466
  generation: 0,
553
467
  needs: ["base"],
554
468
  changedFiles: [],
555
- // Discovered by the worker: at boot the watcher has no validated keys to seed, and the boot build
556
- // globs them itself anyway.
557
469
  pageKeys: null,
558
470
  optimizedFonts: null,
559
471
  cssAssets: null,
560
472
  artifactDir: path.resolve(`${app.cwdPath}/.akan/artifact`),
561
473
  });
562
- // A failed boot build has to throw, not degrade quietly: `main` catches this to enter the degraded
563
- // watch mode that keeps the dev server alive until the error is fixed.
474
+ // Throws rather than degrading quietly: `main` catches it to enter degraded watch mode.
564
475
  if (result.errors.base) throw new Error(result.errors.base);
565
476
  if (!result.artifact || !result.optimizedFonts)
566
477
  throw new Error("boot build reported success without an artifact; the build worker likely died");
@@ -568,13 +479,7 @@ class IncrementalBuilder {
568
479
  return { artifact: result.artifact, optimizedFonts: result.optimizedFonts, discovery };
569
480
  }
570
481
 
571
- /**
572
- * A session that already armed dev CSR keeps it armed across builder restarts through the env flag,
573
- * so the artifact has to be rebuilt for the replacement. Queued after `builder-ready` and run in a
574
- * disposable worker rather than inline: a full minified browser-target build of every page costs
575
- * ~350MB of bundler arena that an inline build would never give back, and nothing serves CSR until a
576
- * `/__csr` or `?csr=true` request arrives anyway.
577
- */
482
+ // The env flag carries an armed CSR session across builder restarts, so the replacement rebuilds it after ready.
578
483
  async rearmCsrFromEnv(): Promise<void> {
579
484
  if (!IncrementalBuilder.#csrArmedByEnv()) return;
580
485
  this.#csrActive = true;
@@ -587,12 +492,8 @@ class IncrementalBuilder {
587
492
  });
588
493
  }
589
494
 
590
- /**
591
- * A compile error in the boot build must not kill the builder: the builder is the dev server's
592
- * file watcher, so exiting here leaves nothing to notice the fix. Report the failure, emit
593
- * builder-ready so the host keeps the backend serving the last-good artifact, then retry the
594
- * boot build on every file change until it succeeds.
595
- */
495
+ // A boot compile error must not kill the builder, the dev server's only file watcher: report it, emit builder-ready
496
+ // so the backend keeps the last-good artifact, and retry the boot build on every change.
596
497
  static #recoverBoot(
597
498
  app: App,
598
499
  bootError: unknown,
@@ -648,8 +549,7 @@ class IncrementalBuilder {
648
549
  const app = AppExecutor.from(workspace, appName);
649
550
  const watch = process.env.AKAN_WATCH !== "0";
650
551
  let builder: IncrementalBuilder | null = null;
651
- // Registered before the boot build so backend requests get an error response (instead of hanging
652
- // the backend) while the builder is still booting or recovering from a failed build.
552
+ // Registered before the boot build so a request during boot or recovery gets an error instead of hanging.
653
553
  const bootingError = "builder is recovering from a failed boot build; retry after the build error is fixed";
654
554
  const recyclingError = "builder is recycling to release bundler memory; retry after it restarts";
655
555
  process.on("message", (msg: BuilderMessage) => {
@@ -662,34 +562,21 @@ class IncrementalBuilder {
662
562
  void builder.shutdown(msg.reason);
663
563
  return;
664
564
  }
665
- if (msg.type === "build-route") {
565
+ if (msg.type !== "build-route" && msg.type !== "build-csr") return;
566
+ if (!builder || builder.shuttingDown) {
666
567
  const error = builder?.shuttingDown ? recyclingError : bootingError;
667
- if (!builder || builder.shuttingDown) {
668
- BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
669
- return;
670
- }
671
- void builder.handleBuildRoute(msg);
568
+ BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
672
569
  return;
673
570
  }
674
- if (msg.type === "build-csr") {
675
- const error = builder?.shuttingDown ? recyclingError : bootingError;
676
- if (!builder || builder.shuttingDown) {
677
- BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
678
- return;
679
- }
680
- void builder.handleBuildCsr(msg);
681
- }
571
+ if (msg.type === "build-route") void builder.handleBuildRoute(msg);
572
+ else void builder.handleBuildCsr(msg);
682
573
  });
683
- // The IPC channel closes when the dev host dies (including SIGKILL); exit instead of running
684
- // as an orphaned watcher that keeps rebuilding for nobody. Nothing is drained here on purpose —
685
- // there is no longer anyone on the other end to flush to.
574
+ // Closes when the host dies (even by SIGKILL): nothing left to drain, and an orphan would rebuild for nobody.
686
575
  process.on("disconnect", () => {
687
576
  logger.warn("host IPC channel closed; exiting builder");
688
577
  process.exit(0);
689
578
  });
690
579
  let recoveredFiles: string[] | null = null;
691
- // Owned by `main` rather than the instance: the boot build has to run before an instance exists, and
692
- // a degraded boot re-runs it once per file change until it succeeds.
693
580
  const bootRunner = new BuildBatchRunner({ workspaceRoot, cwd: app.cwdPath });
694
581
  try {
695
582
  builder = new IncrementalBuilder({ app, watch, ...(await IncrementalBuilder.#buildBootDeps(app, bootRunner)) });
@@ -706,7 +593,8 @@ class IncrementalBuilder {
706
593
  }
707
594
  }
708
595
 
709
- void IncrementalBuilder.main().catch((err) => {
596
+ // Awaited, not voided: on Windows Bun exits during a missing file's `Bun.file` read unless an entry await is pending.
597
+ await IncrementalBuilder.main().catch((err) => {
710
598
  console.error(err);
711
599
  process.exit(1);
712
600
  });
package/index.ts CHANGED
@@ -1,18 +1,6 @@
1
- // Types only, deliberately. This barrel re-exports all 40 devkit facets, so a single *value* import
2
- // from it loads every one of them — measured at 417MB / 654ms, versus 38MB / 38ms for the one subpath
3
- // the caller actually needed. That is how `commandDecorators/command.ts` silently put ink, typescript,
4
- // ssh2, @trapezedev/project, @langchain/* and the cloud stack into every process that registered a CLI
5
- // command, and it is the single largest regression this package can suffer from a one-line edit.
6
- //
7
- // `export type *` erases at runtime (verified on Bun 1.3.14: the re-exported module is never loaded),
8
- // so `import type { App } from "@akanjs/devkit"` stays free and readable while a value import fails to
9
- // compile with a self-explaining error:
10
- //
11
- // TS1362: 'FileSys' cannot be used as a value because it was exported using 'export type'.
12
- //
13
- // Import values from the owning facet instead: `import { FileSys } from "@akanjs/devkit/fileSys"`.
14
- // Keep this file type-only; do not "fix" a TS1362 by widening an entry back to `export *`.
15
- export type * from "./aiEditor";
1
+ // XXX: types only. A value import from this barrel loads every facet (ink, typescript, ssh2, …) into the process;
2
+ // `export type *` erases at runtime. Import values from the owning facet (`@akanjs/devkit/fileSys`) and never "fix" a
3
+ // TS1362 by widening an entry back to `export *`.
16
4
  export type * from "./akanApp";
17
5
  export type * from "./akanConfig";
18
6
  export type * from "./akanContext";
@@ -24,15 +12,14 @@ export type * from "./applicationTestPreload";
24
12
  export type * from "./artifact";
25
13
  export type * from "./capacitorApp";
26
14
  export type * from "./cloud";
15
+ export type * from "./codeAgent";
27
16
  export type * from "./commandDecorators";
28
17
  export type * from "./createTunnel";
29
18
  export type * from "./dependencyScanner";
30
19
  export type * from "./executors";
31
20
  export type * from "./fileSys";
32
21
  export type * from "./frontendBuild";
33
- export type * from "./getDirname";
34
22
  export type * from "./getRelatedCnsts";
35
- export type * from "./guideline";
36
23
  export type * from "./incrementalBuilder";
37
24
  export type * from "./mobile";
38
25
  export type * from "./packageExportsMap";
@@ -42,6 +29,7 @@ export type * from "./recipeScanner";
42
29
  export type * from "./scanInfo";
43
30
  export type * from "./selectModel";
44
31
  export type * from "./spinner";
32
+ export type * from "./stripAnsi";
45
33
  export type * from "./transforms";
46
34
  export type * from "./typeChecker";
47
35
  export type * from "./types";