@akanjs/devkit 3.0.0-beta.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/DEV_RUNTIME_KNOBS.md +44 -26
  2. package/README.ko.md +21 -7
  3. package/README.md +17 -8
  4. package/abstractDoc.test.ts +6 -15
  5. package/abstractDoc.ts +6 -8
  6. package/agentsIndex.test.ts +4 -7
  7. package/agentsIndex.ts +6 -22
  8. package/akanApp/BackendImportGraph.test.ts +8 -22
  9. package/akanApp/BackendImportGraph.ts +1 -20
  10. package/akanApp/akanApp.host.test.ts +56 -3
  11. package/akanApp/akanApp.host.ts +218 -278
  12. package/akanApp/devBootLatch.test.ts +39 -0
  13. package/akanApp/devBootLatch.ts +39 -0
  14. package/akanApp/devHostPolicy.test.ts +81 -35
  15. package/akanApp/devHostPolicy.ts +41 -102
  16. package/akanConfig/akanBin.test.ts +62 -0
  17. package/akanConfig/akanBin.ts +78 -0
  18. package/akanConfig/akanConfig.test.ts +439 -141
  19. package/akanConfig/akanConfig.ts +359 -275
  20. package/akanConfig/index.ts +1 -0
  21. package/akanConfig/types.ts +22 -8
  22. package/akanContext.ts +110 -160
  23. package/akanMcpContract.ts +28 -73
  24. package/appSelectionMemory.ts +1 -4
  25. package/applicationBuildReporter.ts +37 -5
  26. package/applicationBuildRunner.test.ts +41 -5
  27. package/applicationBuildRunner.ts +71 -63
  28. package/applicationTestPreload.ts +3 -12
  29. package/artifact/implicitRootLayout.test.ts +32 -30
  30. package/artifact/implicitRootLayout.ts +58 -106
  31. package/artifact/routeSeedIndex.test.ts +7 -6
  32. package/artifact/routeSeedIndex.ts +11 -44
  33. package/biome.base.json +35 -2
  34. package/biomeBase.ts +2 -5
  35. package/cloud/cloudApi.test.ts +202 -0
  36. package/cloud/cloudApi.ts +77 -39
  37. package/cloud/configLock.test.ts +64 -0
  38. package/cloud/configLock.ts +96 -0
  39. package/cloud/constants.ts +26 -5
  40. package/cloud/globalConfig.ts +71 -32
  41. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  42. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  43. package/codeAgent/agent/CodeAgent.ts +551 -0
  44. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  45. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  46. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  47. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  48. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  49. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  50. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  51. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  52. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  53. package/codeAgent/agent/CodeMailbox.ts +148 -0
  54. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  55. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  56. package/codeAgent/agent/SubagentPool.ts +214 -0
  57. package/codeAgent/agent/akanCodeModel.ts +47 -0
  58. package/codeAgent/agent/akanCodePaths.ts +31 -0
  59. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  60. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  61. package/codeAgent/agent/codeAgent.test.ts +178 -0
  62. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  63. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  64. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  65. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  66. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  67. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  68. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  69. package/codeAgent/agent/consoleToStderr.ts +8 -0
  70. package/codeAgent/agent/jsonLines.ts +29 -0
  71. package/codeAgent/agent/stringArg.ts +5 -0
  72. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  73. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  74. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  75. package/codeAgent/feedback/PreviewView.ts +95 -0
  76. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  77. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  78. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  79. package/codeAgent/index.ts +34 -0
  80. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  81. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  82. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  83. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  84. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  85. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  86. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  87. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  88. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  89. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  90. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  91. package/codeAgent/tools/AkanCli.ts +50 -0
  92. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  93. package/codeAgent/tools/AkanEditScope.ts +48 -0
  94. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  95. package/codeAgent/tools/AkanToolPack.ts +92 -0
  96. package/codeAgent/tools/AkanVerifier.ts +48 -0
  97. package/codeAgent/tools/AskToolPack.ts +99 -0
  98. package/codeAgent/tools/MailToolPack.ts +76 -0
  99. package/codeAgent/tools/McpClient.ts +187 -0
  100. package/codeAgent/tools/McpOAuth.ts +233 -0
  101. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  102. package/codeAgent/tools/McpServerConfig.ts +129 -0
  103. package/codeAgent/tools/McpSignIn.ts +158 -0
  104. package/codeAgent/tools/McpTokenStore.ts +81 -0
  105. package/codeAgent/tools/McpToolPack.ts +199 -0
  106. package/codeAgent/tools/SessionToolPack.ts +91 -0
  107. package/codeAgent/tools/WebToolPack.ts +147 -0
  108. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  109. package/codeAgent/tools/askToolPack.test.ts +116 -0
  110. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  111. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  112. package/codegenLock.test.ts +4 -13
  113. package/codegenLock.ts +3 -17
  114. package/commandDecorators/argMeta.ts +1 -5
  115. package/commandDecorators/camelToKebabCase.ts +1 -0
  116. package/commandDecorators/command.ts +46 -102
  117. package/commandDecorators/commandBuilder.ts +30 -61
  118. package/commandDecorators/commandDecorators.test.ts +2 -10
  119. package/commandDecorators/dependencyBuilder.ts +10 -1
  120. package/commandDecorators/helpFormatter.ts +68 -145
  121. package/commandDecorators/index.ts +0 -2
  122. package/commandDecorators/targetMeta.ts +9 -7
  123. package/createTunnel.ts +7 -14
  124. package/csrE2e/csrE2eHarness.fixture.ts +505 -0
  125. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  126. package/csrE2e/csrPageStack.e2e.test.ts +252 -0
  127. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  128. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  129. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  130. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  131. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  132. package/dependencyScanner.test.ts +4 -16
  133. package/dependencyScanner.ts +31 -80
  134. package/devkitUtils.test.ts +48 -20
  135. package/executors.linkFallback.test.ts +103 -0
  136. package/executors.test.ts +176 -106
  137. package/executors.ts +212 -498
  138. package/fileEditor.ts +9 -37
  139. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  140. package/fileSys.ts +22 -9
  141. package/formSetterScanner.ts +3 -16
  142. package/frontendBuild/allRoutesBuilder.ts +7 -21
  143. package/frontendBuild/autoImportSync.test.ts +27 -38
  144. package/frontendBuild/autoImportSync.ts +31 -65
  145. package/frontendBuild/buildRouteClient.test.ts +123 -37
  146. package/frontendBuild/bundleDefine.test.ts +12 -0
  147. package/frontendBuild/bundleDefine.ts +16 -0
  148. package/frontendBuild/clientBuildTypes.ts +5 -36
  149. package/frontendBuild/clientEntriesBundler.ts +9 -40
  150. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  151. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  152. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  153. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  154. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  155. package/frontendBuild/csrDevBundler.test.ts +66 -0
  156. package/frontendBuild/csrDevBundler.ts +95 -0
  157. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  158. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  159. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  160. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  161. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  162. package/frontendBuild/csrDevPatcher.ts +548 -0
  163. package/frontendBuild/csrDevPaths.ts +145 -0
  164. package/frontendBuild/csrDevResolver.test.ts +239 -0
  165. package/frontendBuild/csrDevResolver.ts +363 -0
  166. package/frontendBuild/csrDevTypes.ts +61 -0
  167. package/frontendBuild/csrEntryFiles.ts +24 -0
  168. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  169. package/frontendBuild/cssCandidateCache.ts +6 -41
  170. package/frontendBuild/cssCompiler.ts +31 -82
  171. package/frontendBuild/cssImportResolver.ts +6 -19
  172. package/frontendBuild/devChangePlanner.ts +7 -10
  173. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  174. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  175. package/frontendBuild/devRegistryBundler.ts +192 -0
  176. package/frontendBuild/fontOptimizer.test.ts +6 -14
  177. package/frontendBuild/fontOptimizer.ts +15 -72
  178. package/frontendBuild/fontPruner.test.ts +5 -10
  179. package/frontendBuild/fontPruner.ts +5 -20
  180. package/frontendBuild/frontendBuild.test.ts +57 -70
  181. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  182. package/frontendBuild/hmrWatcher.test.ts +59 -32
  183. package/frontendBuild/hmrWatcher.ts +31 -74
  184. package/frontendBuild/index.ts +4 -0
  185. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  186. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  187. package/frontendBuild/precompressArtifacts.ts +5 -18
  188. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  189. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  190. package/frontendBuild/routeClientBuilder.ts +100 -62
  191. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  192. package/frontendBuild/serverGraphFile.test.ts +77 -0
  193. package/frontendBuild/serverGraphFile.ts +93 -0
  194. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  195. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  196. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  197. package/frontendBuild/ssrDevBundler.ts +85 -0
  198. package/frontendBuild/styleContract.ts +1 -8
  199. package/frontendBuild/themeValidator.ts +4 -26
  200. package/frontendBuild/watchRootResolver.test.ts +23 -42
  201. package/frontendBuild/watchRootResolver.ts +5 -27
  202. package/getRelatedCnsts.ts +12 -92
  203. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  204. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  205. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  206. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  207. package/incrementalBuilder/builderChannel.test.ts +24 -49
  208. package/incrementalBuilder/builderChannel.ts +6 -37
  209. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  210. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  211. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  212. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  213. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  214. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  215. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  216. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  217. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  218. package/index.ts +5 -19
  219. package/integration/devResourceProbe.ts +50 -56
  220. package/integration/devStability.integration.test.ts +105 -199
  221. package/integration/devStabilityHarness.test.ts +13 -28
  222. package/integration/devStabilityHarness.ts +65 -330
  223. package/integration/ssrMemoryProbe.ts +41 -167
  224. package/libSource.test.ts +4 -15
  225. package/libSource.ts +19 -31
  226. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  227. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  228. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  229. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  230. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  231. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  232. package/lint/gritRules.test.ts +8 -24
  233. package/lint/no-deep-internal-import.grit +2 -2
  234. package/lint/no-document-cookie.grit +29 -0
  235. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  236. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  237. package/lint/no-web-storage.grit +30 -0
  238. package/linter.test.ts +1 -2
  239. package/linter.ts +26 -116
  240. package/mobile/desktopBin.test.ts +182 -0
  241. package/mobile/desktopBin.ts +144 -0
  242. package/mobile/desktopServerStage.test.ts +252 -0
  243. package/mobile/desktopServerStage.ts +178 -0
  244. package/mobile/index.ts +9 -0
  245. package/mobile/mobileTarget.test.ts +57 -8
  246. package/mobile/mobileTarget.ts +45 -26
  247. package/mobile/nativeApi.test.ts +30 -0
  248. package/mobile/nativeApi.ts +40 -0
  249. package/mobile/nativeApp.test.ts +184 -0
  250. package/mobile/nativeApp.ts +454 -0
  251. package/mobile/nativeAppLine.test.ts +69 -0
  252. package/mobile/nativeAppLine.ts +71 -0
  253. package/mobile/nativeConfig.test.ts +438 -0
  254. package/mobile/nativeConfig.ts +294 -0
  255. package/mobile/nativePluginFolders.test.ts +81 -0
  256. package/mobile/nativePluginFolders.ts +60 -0
  257. package/mobile/nativeWebDir.test.ts +80 -0
  258. package/mobile/nativeWebDir.ts +66 -0
  259. package/mobile/placeholderAppId.test.ts +13 -0
  260. package/mobile/placeholderAppId.ts +16 -0
  261. package/mobile/usageDescriptions.test.ts +27 -0
  262. package/mobile/usageDescriptions.ts +24 -0
  263. package/package.json +11 -11
  264. package/packageExports.test.ts +3 -18
  265. package/packageExportsMap.ts +3 -19
  266. package/piDependencyPins.test.ts +46 -0
  267. package/platformTest/BunTestReport.test.ts +60 -0
  268. package/platformTest/BunTestReport.ts +65 -0
  269. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  270. package/platformTest/IdleWatchedProcess.ts +73 -0
  271. package/platformTest/LinuxDockerTarget.ts +105 -0
  272. package/platformTest/PlatformTestRun.test.ts +64 -0
  273. package/platformTest/PlatformTestRun.ts +141 -0
  274. package/platformTest/PlatformTestTarget.ts +142 -0
  275. package/platformTest/TestSnapshot.test.ts +72 -0
  276. package/platformTest/TestSnapshot.ts +103 -0
  277. package/platformTest/WindowsSshTarget.ts +151 -0
  278. package/prompter.ts +3 -13
  279. package/qualityScanner.test.ts +9 -26
  280. package/qualityScanner.ts +200 -286
  281. package/recipeScanner.test.ts +7 -12
  282. package/recipeScanner.ts +9 -38
  283. package/repoIdentity.ts +2 -9
  284. package/routeSourceValidator.test.ts +0 -1
  285. package/routeSourceValidator.ts +7 -42
  286. package/scanInfo.ts +91 -225
  287. package/selectModel.ts +1 -2
  288. package/semver.ts +3 -5
  289. package/slicePlanner.test.ts +6 -24
  290. package/slicePlanner.ts +11 -32
  291. package/spinner.test.ts +3 -8
  292. package/spinner.ts +4 -16
  293. package/sshTunnel.ts +1 -6
  294. package/ssrScanner.test.ts +1 -6
  295. package/ssrScanner.ts +103 -113
  296. package/stripAnsi.ts +3 -0
  297. package/subspace.test.ts +9 -25
  298. package/subspace.ts +48 -115
  299. package/subspaceConfig.ts +3 -11
  300. package/testHelpers.ts +194 -0
  301. package/transforms/asyncDefaultExportDetector.ts +3 -11
  302. package/transforms/barrelAnalyzer.ts +43 -135
  303. package/transforms/barrelImportsPlugin.ts +38 -175
  304. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  305. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  306. package/transforms/moduleSyntax.ts +32 -0
  307. package/transforms/rscUseClientTransform.ts +42 -20
  308. package/transforms/serverEnvPlugin.test.ts +103 -0
  309. package/transforms/serverEnvPlugin.ts +41 -0
  310. package/transforms/transforms.test.ts +166 -44
  311. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  312. package/transforms/useClientBundlePlugin.ts +6 -23
  313. package/typeChecker.ts +7 -63
  314. package/types.ts +2 -0
  315. package/uploadRelease.ts +10 -25
  316. package/workflow/artifacts.ts +18 -0
  317. package/workflow/executor.test.ts +1 -12
  318. package/workflow/executor.ts +101 -110
  319. package/workflow/index.ts +0 -1
  320. package/workflow/moduleIndex.test.ts +4 -15
  321. package/workflow/moduleIndex.ts +9 -49
  322. package/workflow/plan.ts +18 -25
  323. package/workflow/render.ts +80 -96
  324. package/workflow/rolloutGate.test.ts +1 -1
  325. package/workflow/source.ts +52 -69
  326. package/workflow/sourceAst.ts +50 -0
  327. package/workspaceLayout.test.ts +18 -3
  328. package/workspaceLayout.ts +22 -23
  329. package/abstractCompactor.ts +0 -112
  330. package/aiEditor.test.ts +0 -60
  331. package/aiEditor.ts +0 -387
  332. package/applicationReleasePackager.ts +0 -209
  333. package/capacitorApp.test.ts +0 -372
  334. package/capacitorApp.ts +0 -1648
  335. package/commandDecorators/commandMeta.ts +0 -7
  336. package/commandDecorators/types.ts +0 -10
  337. package/getDirname.ts +0 -4
  338. package/guideline.ts +0 -19
  339. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  340. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  341. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,212 @@
1
+ import { afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { akanCodePaths } from "../agent/akanCodePaths";
6
+ import { McpServerConfig, type McpServerScope } from "./McpServerConfig";
7
+
8
+ const roots: string[] = [];
9
+
10
+ // Without this every add() writes into the real ~/.akan/code/mcp.json of whoever runs the suite.
11
+ beforeAll(() => {
12
+ const home = mkdtempSync(path.join(tmpdir(), "akan-home-"));
13
+ roots.push(home);
14
+ process.env.AKAN_CODE_HOME = home;
15
+ expect(akanCodePaths.globalMcpFile().startsWith(home)).toBe(true);
16
+ });
17
+
18
+ const globalFile = (file: string) => {
19
+ const target = akanCodePaths.globalMcpFile();
20
+ mkdirSync(path.dirname(target), { recursive: true });
21
+ writeFileSync(target, file);
22
+ };
23
+
24
+ const workspace = (file?: string) => {
25
+ const root = mkdtempSync(path.join(tmpdir(), "akan-mcp-"));
26
+ roots.push(root);
27
+ if (file === undefined) return root;
28
+ const target = McpServerConfig.file(root);
29
+ mkdirSync(path.dirname(target), { recursive: true });
30
+ writeFileSync(target, file);
31
+ return root;
32
+ };
33
+
34
+ const written = (root: string) =>
35
+ JSON.parse(readFileSync(McpServerConfig.file(root), "utf8")) as Record<string, object>;
36
+
37
+ afterEach(() => {
38
+ rmSync(akanCodePaths.globalMcpFile(), { force: true });
39
+ for (const root of roots.slice(1)) rmSync(root, { recursive: true, force: true });
40
+ roots.splice(1);
41
+ });
42
+
43
+ describe("MCP server config", () => {
44
+ test("an editor's own block is read as it is written", () => {
45
+ const root = workspace(
46
+ JSON.stringify({
47
+ mcpServers: {
48
+ github: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
49
+ linear: { url: "https://mcp.linear.app/sse" },
50
+ },
51
+ }),
52
+ );
53
+ expect(McpServerConfig.read(root)).toEqual([
54
+ { name: "github", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
55
+ { name: "linear", transport: "http", url: "https://mcp.linear.app/sse" },
56
+ ]);
57
+ });
58
+
59
+ test("a static token and a hand-issued oauth client travel from the file", () => {
60
+ const root = workspace(
61
+ JSON.stringify({
62
+ mcpServers: {
63
+ sentry: {
64
+ url: "https://mcp.sentry.dev/mcp",
65
+ headers: { "x-api-key": "from-the-file" },
66
+ oauth: { clientId: "abc", scope: "project:read" },
67
+ },
68
+ },
69
+ }),
70
+ );
71
+ expect(McpServerConfig.read(root)[0]).toMatchObject({
72
+ headers: { "x-api-key": "from-the-file" },
73
+ oauth: { clientId: "abc", scope: "project:read" },
74
+ });
75
+ });
76
+
77
+ test("a disabled server is declared but never connected", () => {
78
+ const root = workspace(JSON.stringify({ mcpServers: { off: { command: "x", disabled: true } } }));
79
+ expect(McpServerConfig.read(root)).toEqual([]);
80
+ expect(McpServerConfig.declared(root)).toEqual([
81
+ { ref: { name: "off", transport: "stdio", command: "x" }, disabled: true, scope: "workspace" },
82
+ ]);
83
+ });
84
+
85
+ test("a url is an http server and anything else is a command with its argv", () => {
86
+ const root = workspace();
87
+ expect(McpServerConfig.add(root, "linear", ["https://mcp.linear.app/sse"], "workspace")).toEqual({
88
+ name: "linear",
89
+ transport: "http",
90
+ url: "https://mcp.linear.app/sse",
91
+ });
92
+ expect(McpServerConfig.add(root, "github", ["npx", "-y", "server-github"], "workspace")).toEqual({
93
+ name: "github",
94
+ transport: "stdio",
95
+ command: "npx",
96
+ args: ["-y", "server-github"],
97
+ });
98
+ expect(Object.keys(written(root).mcpServers ?? {})).toEqual(["linear", "github"]);
99
+ });
100
+
101
+ test("a VS Code file keeps its own spelling through a write", () => {
102
+ const root = workspace(JSON.stringify({ servers: { a: { command: "a" } } }));
103
+ McpServerConfig.add(root, "b", ["b"], "workspace");
104
+ const file = written(root);
105
+ expect(Object.keys(file)).toEqual(["servers"]);
106
+ expect(Object.keys(file.servers ?? {})).toEqual(["a", "b"]);
107
+ });
108
+
109
+ test("adding the same name twice replaces the entry rather than doubling it", () => {
110
+ const root = workspace();
111
+ McpServerConfig.add(root, "a", ["old"], "workspace");
112
+ McpServerConfig.add(root, "a", ["new", "--flag"], "workspace");
113
+ expect(McpServerConfig.read(root)).toEqual([{ name: "a", transport: "stdio", command: "new", args: ["--flag"] }]);
114
+ });
115
+
116
+ test("removing says whether it removed anything", () => {
117
+ const root = workspace(JSON.stringify({ mcpServers: { a: { command: "a" } } }));
118
+ expect(McpServerConfig.remove(root, "nope")).toBe(false);
119
+ expect(McpServerConfig.remove(root, "a")).toBe("workspace");
120
+ expect(McpServerConfig.read(root)).toEqual([]);
121
+ });
122
+
123
+ test("a server with nothing to reach it by is refused", () => {
124
+ expect(() => McpServerConfig.add(workspace(), "a", [], "workspace")).toThrow();
125
+ });
126
+
127
+ test("a malformed file costs its servers and is named rather than thrown", () => {
128
+ const root = workspace("{ not json");
129
+ expect(McpServerConfig.read(root)).toEqual([]);
130
+ expect(McpServerConfig.problem(root)).toContain("not valid JSON");
131
+ });
132
+
133
+ test("no file at all is not a problem", () => {
134
+ const root = workspace();
135
+ expect(McpServerConfig.read(root)).toEqual([]);
136
+ expect(McpServerConfig.problem(root)).toBeUndefined();
137
+ });
138
+
139
+ test("a server declared in the home file is read from a workspace that declares nothing", () => {
140
+ globalFile(JSON.stringify({ mcpServers: { office: { url: "https://office.example/mcp" } } }));
141
+ const root = workspace();
142
+ expect(McpServerConfig.read(root)).toEqual([
143
+ { name: "office", transport: "http", url: "https://office.example/mcp" },
144
+ ]);
145
+ expect(McpServerConfig.declared(root)[0]?.scope).toBe("global");
146
+ });
147
+
148
+ test("both files are merged, and the workspace wins the name they share", () => {
149
+ globalFile(JSON.stringify({ mcpServers: { office: { url: "https://home/mcp" }, shared: { command: "home" } } }));
150
+ const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
151
+ expect(McpServerConfig.declared(root).map((entry) => [entry.ref.name, entry.scope])).toEqual([
152
+ ["office", "global"],
153
+ ["shared", "workspace"],
154
+ ]);
155
+ expect(McpServerConfig.read(root).find((ref) => ref.name === "shared")?.command).toBe("repo");
156
+ });
157
+
158
+ test("a write goes to the scope it was asked for, and leaves the other file alone", () => {
159
+ const root = workspace();
160
+ McpServerConfig.add(root, "office", ["https://office.example/mcp"]);
161
+ expect(McpServerConfig.declared(root)).toEqual([
162
+ {
163
+ ref: { name: "office", transport: "http", url: "https://office.example/mcp" },
164
+ disabled: false,
165
+ scope: "global",
166
+ },
167
+ ]);
168
+ expect(existsSync(McpServerConfig.file(root, "workspace"))).toBe(false);
169
+ });
170
+
171
+ test("removing finds the file the server is actually in", () => {
172
+ globalFile(JSON.stringify({ mcpServers: { office: { command: "x" } } }));
173
+ const root = workspace(JSON.stringify({ mcpServers: { repobot: { command: "y" } } }));
174
+ expect(McpServerConfig.remove(root, "office")).toBe("global");
175
+ expect(McpServerConfig.remove(root, "repobot")).toBe("workspace");
176
+ expect(McpServerConfig.read(root)).toEqual([]);
177
+ });
178
+
179
+ test("removing a name both files declare takes the workspace's, and the home file's then applies", () => {
180
+ globalFile(JSON.stringify({ mcpServers: { shared: { command: "home" } } }));
181
+ const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
182
+ expect(McpServerConfig.remove(root, "shared")).toBe("workspace");
183
+ expect(McpServerConfig.declared(root)).toEqual([
184
+ { ref: { name: "shared", transport: "stdio", command: "home" }, disabled: false, scope: "global" },
185
+ ]);
186
+ });
187
+
188
+ test("a remove that names a scope touches only that file", () => {
189
+ globalFile(JSON.stringify({ mcpServers: { office: { command: "x" }, shared: { command: "home" } } }));
190
+ const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
191
+ expect(McpServerConfig.remove(root, "office", "workspace")).toBe(false);
192
+ expect(McpServerConfig.remove(root, "shared", "global")).toBe("global");
193
+ expect(McpServerConfig.declared(root).map((entry) => [entry.ref.name, entry.scope])).toEqual([
194
+ ["office", "global"],
195
+ ["shared", "workspace"],
196
+ ]);
197
+ });
198
+
199
+ test("a scope is what a caller names, never guessed from the shape", () => {
200
+ const root = workspace();
201
+ const scopes: McpServerScope[] = ["global", "workspace"];
202
+ for (const scope of scopes) McpServerConfig.add(root, `in-${scope}`, ["x"], scope);
203
+ expect(McpServerConfig.declared(root).map((entry) => entry.scope)).toEqual(["global", "workspace"]);
204
+ });
205
+
206
+ test("a server reads back as the one line it is reached at", () => {
207
+ expect(McpServerConfig.targetOf({ name: "a", transport: "stdio", command: "npx", args: ["-y", "x"] })).toBe(
208
+ "npx -y x",
209
+ );
210
+ expect(McpServerConfig.targetOf({ name: "a", transport: "http", url: "https://x" })).toBe("https://x");
211
+ });
212
+ });
@@ -1,15 +1,10 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, readFile, rm, stat, utimes, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFile, stat, utimes, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { CodegenLock } from "./codegenLock";
5
+ import { tempDirs } from "./testHelpers";
6
6
 
7
- const roots: string[] = [];
8
- const makeRoot = async () => {
9
- const root = await mkdtemp(path.join(tmpdir(), "akan-codegen-lock-"));
10
- roots.push(root);
11
- return root;
12
- };
7
+ const makeRoot = tempDirs("akan-codegen-lock-");
13
8
  const seedHolder = async (root: string, holder: unknown) => {
14
9
  const lockPath = CodegenLock.pathIn(root);
15
10
  await Bun.write(lockPath, typeof holder === "string" ? holder : JSON.stringify(holder));
@@ -18,10 +13,6 @@ const seedHolder = async (root: string, holder: unknown) => {
18
13
  /** A pid that cannot be alive: `kill(0)` on it is ESRCH on every platform this runs on. */
19
14
  const deadPid = 0x7ffffff;
20
15
 
21
- afterEach(async () => {
22
- for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
23
- });
24
-
25
16
  describe("CodegenLock", () => {
26
17
  test("serializes concurrent callers in the same process", async () => {
27
18
  const root = await makeRoot();
package/codegenLock.ts CHANGED
@@ -8,26 +8,12 @@ interface LockHolder {
8
8
  label: string;
9
9
  }
10
10
 
11
- /**
12
- * A workspace-wide mutex over the generated source files every dev server in the workspace rewrites.
13
- *
14
- * `WatchRootResolver` narrows each dev server to its own app and its own lib dependencies, but two apps
15
- * that share a lib still both watch it, so a save there reaches both builders and both regenerate the
16
- * same barrel. Whichever watcher is mid-scan then reads a half-written file back as a user edit, which is
17
- * a rebuild per rewrite. `scanSync` writes the same files at boot for every mounting app.
18
- *
19
- * A wait that expires proceeds *without* the lock rather than failing: this sits on the dev server's
20
- * hot path, and stalling the file watcher is worse than the torn read `FileSys.writeTextAtomic` already
21
- * prevents on its own.
22
- */
11
+ // Two dev servers sharing a lib both regenerate its barrels, and a mid-scan watcher reads a half-written one as a user
12
+ // edit. An expired wait proceeds unlocked: stalling the watcher is worse than the torn read `writeTextAtomic` prevents.
23
13
  export class CodegenLock {
24
14
  static readonly fileName = "codegen.lock";
25
15
  static readonly waitTimeoutMs = 10_000;
26
- /**
27
- * How long an unreadable lock file is respected. It covers the window between the exclusive create
28
- * and the holder write, where the file exists but names no pid yet — a young one is somebody else
29
- * mid-acquire, not a corpse.
30
- */
16
+ /** A holder-less lock file younger than this is someone mid-acquire (between the exclusive create and the write). */
31
17
  static readonly unknownHolderStaleMs = 60_000;
32
18
  static readonly #pollMs = 25;
33
19
  static readonly #logger = new Logger("CodegenLock");
@@ -83,11 +83,7 @@ export const normalizePrimitiveArgType = (type: PrimitiveArgType): NormalizedPri
83
83
  export const App = createInternalArgToken<AppExecutor, "App">("App");
84
84
  export type App = AppExecutor;
85
85
 
86
- /**
87
- * One or more apps, from a variadic positional (`akan start a b`, `akan start a,b`, `akan start all`)
88
- * or a checkbox when none is named. Reach for it only where running several is meaningful — every other
89
- * command takes `App`, whose single-select is unchanged.
90
- */
86
+ /** One or more apps: `akan start a b`, `a,b` or `all`, or a checkbox when none is named. */
91
87
  export const Apps = createInternalArgToken<AppExecutor[], "Apps">("Apps");
92
88
  export type Apps = AppExecutor[];
93
89
 
@@ -0,0 +1 @@
1
+ export const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
@@ -10,11 +10,8 @@ import chalk from "chalk";
10
10
  import { type Command, program } from "commander";
11
11
  import { AppSelectionMemory } from "../appSelectionMemory";
12
12
  import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
13
- // Import the owning modules directly, never the root barrel: `..` re-exports all 41 devkit modules,
14
- // so a barrel import here drags ink, @trapezedev/project, ssh2, @langchain/* and the cloud stack into
15
- // every process that registers a command (measured: 236MB vs 3MB).
16
- import { FileSys } from "../fileSys";
17
- import { getDirname } from "../getDirname";
13
+ // Never the root barrel: it drags ink, ssh2 and the cloud stack into every command process.
14
+ import { FileSys, getDirname } from "../fileSys";
18
15
  import type { PackageJson } from "../types";
19
16
  import {
20
17
  type ArgMeta,
@@ -24,11 +21,11 @@ import {
24
21
  getArgMetas,
25
22
  type InternalArgMeta,
26
23
  } from "./argMeta";
24
+ import { camelToKebabCase } from "./camelToKebabCase";
27
25
  import { CommandContainer } from "./dependencyBuilder";
28
26
  import { formatCommandHelp, formatHelp } from "./helpFormatter";
29
27
  import { type CommandCls, getTargetCommandNames, getTargetMetas } from "./targetMeta";
30
28
 
31
- const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
32
29
  const loggedCliErrorObjects = new WeakSet<object>();
33
30
  const loggedCliErrorMessages = new Set<string>();
34
31
 
@@ -71,6 +68,8 @@ const handleOption = (programCommand: Command, argMeta: ArgMeta) => {
71
68
  `-${flag}, --${kebabName}${type === "boolean" ? " [boolean]" : ` <${kebabName}>`}`,
72
69
  `${desc}${ask ? ` (${ask})` : ""}${example ? ` (example: ${example})` : ""}${choices ? ` (choices: ${choices.map((choice) => choice.name).join(", ")})` : ""}`,
73
70
  );
71
+ if (type === "boolean" && argMeta.argsOption.default === true)
72
+ programCommand.option(`--no-${kebabName}`, `turn off --${kebabName}`);
74
73
  return programCommand;
75
74
  };
76
75
  const handleArgument = (programCommand: Command, argMeta: ArgMeta) => {
@@ -95,30 +94,16 @@ const normalizeEnumChoices = (enumChoices: EnumChoices) =>
95
94
  : { value: choice, name: choice.toString() },
96
95
  );
97
96
 
98
- /**
99
- * The interactive prompt stack, loaded on the first prompt instead of at import.
100
- *
101
- * `runCommands` lives in this module, so a static import put `@inquirer/prompts` (~24MB) in the CLI
102
- * entry's chunk closure — and `akan start` holds that process for the whole dev session while never
103
- * asking a question, because every argument it needs is already on the command line.
104
- *
105
- * The wrappers are typed from the real prompts so no call site changes, and `import type` leaves no
106
- * runtime edge for the bundler to follow (`entryModuleGraph.test.ts` asserts that).
107
- */
97
+ // Loaded on the first prompt: a static import keeps @inquirer/prompts (~24MB) resident for a whole `akan start`.
98
+ // The types come through `import type`, which leaves no runtime edge (entryModuleGraph.test.ts asserts that).
108
99
  const prompts = async () => await import("@inquirer/prompts");
109
100
  const select = ((config, context) => prompts().then((m) => m.select(config, context))) as typeof inquirerSelect;
110
101
  const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
111
102
  const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
112
103
  const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
113
104
 
114
- /**
115
- * Rejects a value that is not one of the declared choices, in commander's own wording.
116
- *
117
- * Only a static choice list can be checked: a `DynamicEnum` resolves against the command context, which
118
- * is not populated until the internal args are resolved, and it is the interactive `select` that consumes
119
- * it. Comparison is stringly on purpose — the value still carries commander's raw string here, while a
120
- * numeric choice list holds numbers.
121
- */
105
+ // A DynamicEnum is not checked: it resolves against a context the internal args have not populated yet.
106
+ // Compared as strings: the value is still commander's raw string while a numeric choice list holds numbers.
122
107
  const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
123
108
  const enumChoices = argMeta.argsOption.enum;
124
109
  if (!enumChoices || typeof enumChoices === "function") return;
@@ -131,17 +116,13 @@ const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
131
116
  );
132
117
  };
133
118
 
134
- const resolveEnumChoices = async (argMeta: ArgMeta, context: CommandContext) => {
135
- const enumChoices = argMeta.argsOption.enum;
136
- if (!enumChoices) return null;
137
- if (typeof enumChoices === "function") return await enumChoices(context);
138
- return enumChoices;
139
- };
119
+ const inputMessageOf = ({ name, argsOption: { desc, example, ask } }: ArgMeta) =>
120
+ ask ? `${ask}: ` : desc ? `${desc}: ` : `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
140
121
 
141
122
  export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
142
123
  const {
143
124
  name,
144
- argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable, example, ask },
125
+ argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable, ask },
145
126
  } = argMeta;
146
127
  if (opt[argMeta.name] !== undefined) {
147
128
  assertEnumChoice(argMeta, opt[argMeta.name]);
@@ -149,42 +130,26 @@ export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unkno
149
130
  } else if (defaultValue !== undefined) return defaultValue;
150
131
 
151
132
  if (enumChoices) {
152
- const choices = normalizeEnumChoices((await resolveEnumChoices(argMeta, context)) ?? []);
133
+ const choices = normalizeEnumChoices(
134
+ (typeof enumChoices === "function" ? await enumChoices(context) : enumChoices) ?? [],
135
+ );
153
136
  if (choices.length === 1) return choices[0]?.value;
154
- const choice = await select({ message: ask ?? desc ?? `Select the ${name} value`, choices });
155
- return choice;
137
+ return await select({ message: ask ?? desc ?? `Select the ${name} value`, choices });
156
138
  } else if (nullable) return null;
157
139
  else if (type === "boolean") {
158
140
  const message = ask ?? desc ?? `Do you want to set ${name}? ${desc ? ` (${desc})` : ""}: `;
159
141
  return await confirm({ message });
160
- } else {
161
- const message = ask
162
- ? `${ask}: `
163
- : desc
164
- ? `${desc}: `
165
- : `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
166
- if (argMeta.argsOption.nullable) return await input({ message });
167
- else return convertArgValue(await input({ message }), type ?? "string");
168
- }
142
+ } else return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
169
143
  };
170
144
 
171
145
  export const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
172
- const {
173
- name,
174
- argsOption: { default: defaultValue, type, desc, nullable, example, ask },
175
- } = argMeta;
146
+ const { default: defaultValue, type, nullable } = argMeta.argsOption;
176
147
  if (value !== undefined) {
177
148
  assertEnumChoice(argMeta, value);
178
149
  return convertArgValue(value, type ?? "string");
179
150
  } else if (defaultValue !== undefined) return defaultValue;
180
151
  else if (nullable) return null;
181
-
182
- const message = ask
183
- ? `${ask}: `
184
- : desc
185
- ? `${desc}: `
186
- : `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
187
- return convertArgValue(await input({ message }), type ?? "string");
152
+ return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
188
153
  };
189
154
 
190
155
  const assignCommandContext = (context: CommandContext, argMeta: ArgMeta | InternalArgMeta, value: unknown) => {
@@ -215,18 +180,13 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
215
180
  );
216
181
  };
217
182
 
218
- /** A variadic positional arrives as an array, and each entry may itself be a comma-separated list. */
219
183
  const parseAppNameList = (value: string | string[] | undefined): string[] =>
220
184
  (Array.isArray(value) ? value : value === undefined ? [] : [value])
221
185
  .flatMap((entry) => entry.split(","))
222
186
  .map((entry) => entry.trim())
223
187
  .filter(Boolean);
224
188
 
225
- /**
226
- * The `Apps` token: named apps, `all`, or a checkbox when the command line names none. Kept out of
227
- * `getInternalArgumentValue` because it is the only internal arg whose positional is variadic, so it is
228
- * the only one whose raw value is an array.
229
- */
189
+ // Kept out of getInternalArgumentValue: Apps is the only internal arg whose positional is variadic (an array).
230
190
  export const getAppsArgumentValue = async (
231
191
  value: string | string[] | undefined,
232
192
  workspace: WorkspaceExecutor,
@@ -263,32 +223,23 @@ export const getInternalArgumentValue = async (
263
223
  if (argMeta.type === "Workspace") return workspace;
264
224
  const sysType = argMeta.type.toLowerCase();
265
225
  const [appNames, libNames, pkgNames] = await workspace.getExecs();
266
- if (sysType === "sys") {
267
- if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
268
- else if (value && libNames.includes(value)) return LibExecutor.from(workspace, value);
269
- else {
270
- const sysName = await select<string>({
271
- message: `Select the App or Lib name`,
272
- choices: [...appNames, ...libNames],
273
- });
274
- if (appNames.includes(sysName)) return AppExecutor.from(workspace, sysName);
275
- else if (libNames.includes(sysName)) return LibExecutor.from(workspace, sysName);
276
- else throw new Error(`Invalid system name: ${sysName}`);
277
- }
278
- } else if (sysType === "exec") {
279
- if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
280
- else if (value && libNames.includes(value)) return LibExecutor.from(workspace, value);
281
- else if (value && pkgNames.includes(value)) return PkgExecutor.from(workspace, value);
282
- else {
283
- const execName = await select<string>({
284
- message: `Select the App or Lib or Pkg name`,
285
- choices: [...appNames, ...libNames, ...pkgNames],
286
- });
287
- if (appNames.includes(execName)) return AppExecutor.from(workspace, execName);
288
- else if (libNames.includes(execName)) return LibExecutor.from(workspace, execName);
289
- else if (pkgNames.includes(execName)) return PkgExecutor.from(workspace, execName);
290
- else throw new Error(`Invalid system name: ${execName}`);
291
- }
226
+ if (sysType === "sys" || sysType === "exec") {
227
+ const pkgChoices = sysType === "exec" ? pkgNames : [];
228
+ const execOf = (name: string) => {
229
+ if (appNames.includes(name)) return AppExecutor.from(workspace, name);
230
+ if (libNames.includes(name)) return LibExecutor.from(workspace, name);
231
+ if (pkgChoices.includes(name)) return PkgExecutor.from(workspace, name);
232
+ return null;
233
+ };
234
+ const found = value ? execOf(value) : null;
235
+ if (found) return found;
236
+ const name = await select<string>({
237
+ message: sysType === "exec" ? `Select the App or Lib or Pkg name` : `Select the App or Lib name`,
238
+ choices: [...appNames, ...libNames, ...pkgChoices],
239
+ });
240
+ const picked = execOf(name);
241
+ if (!picked) throw new Error(`Invalid system name: ${name}`);
242
+ return picked;
292
243
  } else if (sysType === "app") {
293
244
  if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
294
245
  if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
@@ -353,17 +304,10 @@ export const runCommands = async (...commands: CommandCls[]) => {
353
304
  }
354
305
  process.env.AKAN_VERSION = cliPackageJson?.version ?? "0.0.1";
355
306
 
356
- // Custom help handling
357
307
  const hasHelpFlag = process.argv.includes("--help") || process.argv.includes("-h");
358
- const hasCommand = process.argv.length > 2 && !process.argv[2]?.startsWith("-");
359
-
360
- // Show help if: 1) explicit --help flag, or 2) no command provided (just "akan")
361
- if (hasHelpFlag || !hasCommand) {
362
- if (process.argv.length === 2 || (process.argv.length === 3 && hasHelpFlag)) {
363
- // Global help (no specific command)
364
- Logger.rawLog(formatHelp(commands, process.env.AKAN_VERSION));
365
- process.exit(0);
366
- }
308
+ if (process.argv.length === 2 || (process.argv.length === 3 && hasHelpFlag)) {
309
+ Logger.rawLog(formatHelp(commands, process.env.AKAN_VERSION));
310
+ process.exit(0);
367
311
  }
368
312
 
369
313
  program.version(process.env.AKAN_VERSION).description("Akan CLI").configureHelp({
@@ -414,8 +358,6 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
414
358
  }
415
359
  }
416
360
  programCommand = programCommand.option(`-v, --verbose [boolean]`, `verbose output`);
417
-
418
- // Override help completely for each command
419
361
  programCommand.helpInformation = () => {
420
362
  return formatCommandHelp(command, targetMeta.key);
421
363
  };
@@ -444,11 +386,9 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
444
386
  cmdArgs[argMeta.idx] as string,
445
387
  workspace,
446
388
  );
447
- // set app name to env
448
389
  if (commandArgs[argMeta.idx] instanceof AppExecutor)
449
390
  process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
450
- //? Only when exactly one app resolved. Publishing a name while several are in play would make
451
- //? every env-derived answer in this process belong to whichever app happened to be last.
391
+ //? Only for a single app: with several, every env-derived answer would belong to whichever came last.
452
392
  else if (Array.isArray(commandArgs[argMeta.idx])) {
453
393
  const apps = commandArgs[argMeta.idx] as AppExecutor[];
454
394
  if (apps.length === 1 && apps[0]) process.env.AKAN_PUBLIC_APP_NAME = apps[0].name;
@@ -469,5 +409,9 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
469
409
  }
470
410
  }
471
411
  }
472
- await program.parseAsync(process.argv);
412
+ // Handled here, not by the handler above: a rejection reaching the entry's top-level await prints Bun's own trace.
413
+ await program.parseAsync(process.argv).catch((error: unknown) => {
414
+ printCliError(error);
415
+ process.exit(1);
416
+ });
473
417
  };