@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/executors.test.ts CHANGED
@@ -1,25 +1,14 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { lstat, mkdir, mkdtemp, readFile, readlink, rm, stat, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { lstat, mkdir, readFile, readlink, rm, stat, writeFile } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { AkanAppConfig } from "./akanConfig";
6
5
  import { AppExecutor, CommandExecutionError, Executor, PkgExecutor, WorkspaceExecutor } from "./executors";
7
6
  import { AppInfo } from "./scanInfo";
7
+ import { isolateEnv, tempDirs, writeJson, writeText } from "./testHelpers";
8
8
  import type { PackageJson } from "./types";
9
9
 
10
- const originalEnv = { ...process.env };
11
- const tempRoots: string[] = [];
12
-
13
- const makeTempRoot = async () => {
14
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-devkit-"));
15
- tempRoots.push(root);
16
- return root;
17
- };
18
-
19
- const writeJson = async (filePath: string, value: object) => {
20
- await mkdir(path.dirname(filePath), { recursive: true });
21
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
22
- };
10
+ isolateEnv();
11
+ const makeTempRoot = tempDirs("akan-devkit-");
23
12
 
24
13
  const PAGE_SOURCE = "export default function Page() {\n return null;\n}\n";
25
14
 
@@ -39,49 +28,34 @@ const rootPackageJson = (extra: Partial<PackageJson> = {}): PackageJson => ({
39
28
  ...extra,
40
29
  });
41
30
 
42
- beforeEach(() => {
43
- process.env = { ...originalEnv };
44
- });
45
-
46
- afterEach(async () => {
47
- process.env = { ...originalEnv };
48
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
49
- });
50
-
51
31
  describe("Executor filesystem helpers", () => {
52
32
  test("reports command failures with command context and captured output", async () => {
53
33
  const root = await makeTempRoot();
54
34
  const exec = new Executor("fixture", root);
55
35
 
56
- let error: unknown;
57
- try {
58
- await exec.spawn(process.execPath, ["--eval", "console.error('spawn failed'); process.exit(7)"]);
59
- } catch (caught) {
60
- error = caught;
61
- }
62
-
36
+ const error = await exec
37
+ .spawn(process.execPath, ["--eval", "console.error('spawn failed'); process.exit(7)"])
38
+ .catch((caught: unknown) => caught);
63
39
  expect(error).toBeInstanceOf(CommandExecutionError);
64
- expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
65
- expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
66
- expect((error as CommandExecutionError).message).toContain("exit code: 7");
67
- expect((error as CommandExecutionError).message).toContain("spawn failed");
40
+ const { message } = error as CommandExecutionError;
41
+ expect(message).toContain(`Command failed: ${process.execPath}`);
42
+ expect(message).toContain(`cwd: ${root}`);
43
+ expect(message).toContain("exit code: 7");
44
+ expect(message).toContain("spawn failed");
68
45
  });
69
46
 
70
47
  test("reports inherited stdio command failures with a fallback message", async () => {
71
48
  const root = await makeTempRoot();
72
49
  const exec = new Executor("fixture", root);
73
50
 
74
- let error: unknown;
75
- try {
76
- await exec.spawn(process.execPath, ["--eval", "process.exit(3)"], { stdio: "inherit" });
77
- } catch (caught) {
78
- error = caught;
79
- }
80
-
51
+ const error = await exec
52
+ .spawn(process.execPath, ["--eval", "process.exit(3)"], { stdio: "inherit" })
53
+ .catch((caught: unknown) => caught);
81
54
  expect(error).toBeInstanceOf(CommandExecutionError);
82
- expect((error as CommandExecutionError).message).toContain(`Command failed: ${process.execPath}`);
83
- expect((error as CommandExecutionError).message).toContain(`cwd: ${root}`);
84
- expect((error as CommandExecutionError).message).toContain("exit code: 3");
55
+ const { message } = error as CommandExecutionError;
56
+ expect(message).toContain(`Command failed: ${process.execPath}`);
57
+ expect(message).toContain(`cwd: ${root}`);
58
+ expect(message).toContain("exit code: 3");
85
59
  });
86
60
 
87
61
  test("resolves paths and reads/writes files relative to cwd", async () => {
@@ -105,6 +79,31 @@ describe("Executor filesystem helpers", () => {
105
79
  expect(entries.dirs).toContain("nested");
106
80
  });
107
81
 
82
+ test("a refreshed tsconfig re-reads the config it extends", async () => {
83
+ const root = await makeTempRoot();
84
+ const appDir = path.join(root, "apps/demo");
85
+ await writeJson(path.join(root, "tsconfig.json"), {
86
+ compilerOptions: { target: "es2022", paths: { "@libs/*": ["libs/*"] } },
87
+ });
88
+ await writeJson(path.join(appDir, "tsconfig.json"), {
89
+ extends: "../../tsconfig.json",
90
+ compilerOptions: { target: "esnext" },
91
+ references: [{ path: "../shared" }],
92
+ });
93
+ const exec = new Executor("fixture", appDir);
94
+ expect(await exec.getTsConfig()).toEqual({
95
+ extends: "../../tsconfig.json",
96
+ compilerOptions: { target: "esnext", paths: { "@libs/*": ["libs/*"] } },
97
+ references: [{ path: "../shared" }],
98
+ });
99
+
100
+ await writeJson(path.join(root, "tsconfig.json"), { compilerOptions: { target: "es2020" } });
101
+ await writeJson(path.join(appDir, "tsconfig.json"), { extends: "../../tsconfig.json", compilerOptions: {} });
102
+ const refreshed = { extends: "../../tsconfig.json", compilerOptions: { target: "es2020" } };
103
+ expect(await exec.getTsConfig("tsconfig.json", { refresh: true })).toEqual(refreshed);
104
+ expect(await new Executor("fixture", appDir).getTsConfig()).toEqual(refreshed);
105
+ });
106
+
108
107
  test("applies CLI template files with dictionary replacement and overwrite control", async () => {
109
108
  const root = await makeTempRoot();
110
109
  const exec = new Executor("fixture", root);
@@ -128,9 +127,7 @@ describe("Executor filesystem helpers", () => {
128
127
  });
129
128
 
130
129
  test("hands every scaffolded TypeScript file to the formatter, and nothing else", async () => {
131
- // A template emits identifiers it cannot sort — `import { fetch, Task, usePage }` is right for a model
132
- // named Task and wrong for Zoo — and unsorted imports fail `biome check`. So the scaffold is formatted
133
- // on the way out, or it is red for most model names whatever the template says.
130
+ // A template cannot sort imports that depend on the model name, and unsorted imports fail `biome check`.
134
131
  const root = await makeTempRoot();
135
132
  const exec = new Executor("fixture", root);
136
133
  const formatted: string[] = [];
@@ -152,8 +149,7 @@ describe("Executor filesystem helpers", () => {
152
149
  });
153
150
 
154
151
  test("a formatter that cannot run does not fail the scaffold", async () => {
155
- // `create-akan-workspace` scaffolds before `bun install`, so there is no local Biome binary and often
156
- // no config above the target. An unformatted file is a lint fix; a failed scaffold is not.
152
+ // create-akan-workspace scaffolds before `bun install`, so Biome may be absent; a lint fix beats a failed scaffold.
157
153
  const root = await makeTempRoot();
158
154
  const exec = new Executor("fixture", root);
159
155
  exec.getLinter = () => {
@@ -191,8 +187,7 @@ describe("Executor filesystem helpers", () => {
191
187
  "AI Development Guide",
192
188
  );
193
189
  expect(await readFile(path.join(root, "workspace/docs/GENERATED.md"), "utf8")).toContain("Generated Akan Files");
194
- // Rules and their plugin registrations live in the package's base config, so a framework release reaches an
195
- // existing workspace on `bun update` — the workspace file only extends it and scopes its own files.
190
+ // Rules live in the package's base config, so a framework release reaches an existing workspace on `bun update`.
196
191
  expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
197
192
  '"extends": ["@akanjs/devkit/biome.base.json"]',
198
193
  );
@@ -312,11 +307,18 @@ describe("Workspace and app executor environment contracts", () => {
312
307
  expect(env.AKAN_PUBLIC_SERVER_PORT).toBe("8285");
313
308
  expect(env.EXTRA).toBe("ok");
314
309
 
310
+ const shellOperationMode = process.env.AKAN_PUBLIC_OPERATION_MODE;
311
+ process.env.AKAN_PUBLIC_OPERATION_MODE = "local";
315
312
  const prepared = await app.prepareCommand("build");
313
+ const bakedOperationMode = process.env.AKAN_PUBLIC_OPERATION_MODE;
314
+ if (shellOperationMode === undefined) delete process.env.AKAN_PUBLIC_OPERATION_MODE;
315
+ else process.env.AKAN_PUBLIC_OPERATION_MODE = shellOperationMode;
316
+ expect(bakedOperationMode).toBeUndefined();
316
317
  expect(prepared.env.AKAN_COMMAND_TYPE).toBe("build");
317
318
  expect(prepared.env.AKAN_PUBLIC_BASE_PATHS).toBe("admin");
318
- // Bundling reads `process.env` through getPublicEnv and `define`s every AKAN_PUBLIC_* into a literal, so a
319
- // dev port published here would be baked into the artifact and outrank the PORT the container is run with.
319
+ expect(prepared.env.AKAN_DATABASE_MODE).toBe("single");
320
+ expect(prepared.env.AKAN_DATABASE_MODES).toBe("single");
321
+ // Bundling bakes each AKAN_PUBLIC_* into a literal, so a dev port published here would outrank the container PORT.
320
322
  expect(process.env.AKAN_PUBLIC_APP_NAME).toBe("demo");
321
323
  expect(process.env.AKAN_PUBLIC_CLIENT_PORT).toBeUndefined();
322
324
  expect(process.env.AKAN_PUBLIC_SERVER_PORT).toBeUndefined();
@@ -452,7 +454,6 @@ describe("Workspace and app executor environment contracts", () => {
452
454
  });
453
455
 
454
456
  describe("syncAssets", () => {
455
- // `AppExecutor.from` memoises by name, so each test needs a name no other test has used.
456
457
  const makeAppWithLibAssets = async (appName: string) => {
457
458
  const root = await makeTempRoot();
458
459
  process.env.AKAN_PUBLIC_REPO_NAME = "repo";
@@ -527,7 +528,6 @@ describe("Workspace and app executor environment contracts", () => {
527
528
  });
528
529
 
529
530
  describe("devOnly routes", () => {
530
- // `AppExecutor.from` memoises by name, so each test needs a name no other test has used.
531
531
  const makeAppWithRoutes = async (appName: string, routes: Record<string, string>) => {
532
532
  const root = await makeTempRoot();
533
533
  process.env.AKAN_PUBLIC_REPO_NAME = "repo";
@@ -621,7 +621,6 @@ describe("Workspace and app executor environment contracts", () => {
621
621
  await mkdir(path.join(root, "apps", name), { recursive: true });
622
622
  await writeFile(path.join(root, "apps", name, "akan.config.ts"), "export default {};\n");
623
623
  }
624
- // `AppExecutor.from` memoises by name, so each test needs names no other test has used.
625
624
  return new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
626
625
  };
627
626
 
@@ -637,8 +636,7 @@ describe("Workspace and app executor environment contracts", () => {
637
636
  const app = AppExecutor.from(workspace, "drift-b");
638
637
  expect(await app.getDevPort()).toBe(8282);
639
638
 
640
- // Sorts ahead of `drift-b`, so the same app now answers with a different port — and a dev host
641
- // recomputes this on every restart.
639
+ // Sorts ahead of `drift-b`, so the same app gets another port — and a dev host recomputes it on each restart.
642
640
  await mkdir(path.join(workspace.workspaceRoot, "apps/drift-a"), { recursive: true });
643
641
  await writeFile(path.join(workspace.workspaceRoot, "apps/drift-a/akan.config.ts"), "export default {};\n");
644
642
 
@@ -727,7 +725,7 @@ describe("Workspace and app executor environment contracts", () => {
727
725
  });
728
726
  });
729
727
 
730
- test("accepts metadata route exports during page key discovery", async () => {
728
+ test("accepts head route exports during page key discovery", async () => {
731
729
  const root = await makeTempRoot();
732
730
  process.env.AKAN_PUBLIC_REPO_NAME = "repo";
733
731
  process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
@@ -737,24 +735,19 @@ describe("Workspace and app executor environment contracts", () => {
737
735
  await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
738
736
  await writeFile(
739
737
  path.join(root, "apps/demo/page/_layout.tsx"),
740
- [
741
- "export const head = null;",
742
- "export const metadata = { title: 'Root' };",
743
- "export default function Layout({ children }) { return children; }",
744
- "",
745
- ].join("\n"),
738
+ ["export const head = null;", "export default function Layout({ children }) { return children; }", ""].join("\n"),
746
739
  );
747
740
  await writeFile(
748
741
  path.join(root, "apps/demo/page/docs/_layout.tsx"),
749
742
  [
750
- "export async function generateMetadata() { return { title: 'Docs' }; }",
743
+ "export async function generateHead() { return null; }",
751
744
  "export default function Layout({ children }) { return children; }",
752
745
  "",
753
746
  ].join("\n"),
754
747
  );
755
748
  await writeFile(
756
749
  path.join(root, "apps/demo/page/docs/intro.tsx"),
757
- ["export const metadata = { title: 'Intro' };", "export default function Page() { return null; }", ""].join("\n"),
750
+ ["export const head = null;", "export default function Page() { return null; }", ""].join("\n"),
758
751
  );
759
752
 
760
753
  const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
@@ -767,7 +760,7 @@ describe("Workspace and app executor environment contracts", () => {
767
760
  ]);
768
761
  });
769
762
 
770
- test("rejects conflicting metadata route exports during page key discovery", async () => {
763
+ test("rejects a metadata route export during page key discovery", async () => {
771
764
  const root = await makeTempRoot();
772
765
  process.env.AKAN_PUBLIC_REPO_NAME = "repo";
773
766
  process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
@@ -776,47 +769,16 @@ describe("Workspace and app executor environment contracts", () => {
776
769
  await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
777
770
  await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
778
771
  await writeFile(
779
- path.join(root, "apps/demo/page/conflict.tsx"),
780
- [
781
- "export const metadata = { title: 'Conflict' };",
782
- "export function generateMetadata() { return { title: 'Conflict' }; }",
783
- "export default function Page() { return null; }",
784
- "",
785
- ].join("\n"),
772
+ path.join(root, "apps/demo/page/legacy.tsx"),
773
+ ["export const metadata = { title: 'Legacy' };", "export default function Page() { return null; }", ""].join(
774
+ "\n",
775
+ ),
786
776
  );
787
777
 
788
778
  const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
789
779
  const app = AppExecutor.from(workspace, "demo");
790
780
 
791
- await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
792
- "metadata and generateMetadata cannot both be exported",
793
- );
794
- });
795
-
796
- test("rejects mixed head and metadata route export channels during page key discovery", async () => {
797
- const root = await makeTempRoot();
798
- process.env.AKAN_PUBLIC_REPO_NAME = "repo";
799
- process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
800
- process.env.AKAN_PUBLIC_ENV = "local";
801
- await writeJson(path.join(root, "package.json"), rootPackageJson());
802
- await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
803
- await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
804
- await writeFile(
805
- path.join(root, "apps/demo/page/mixed.tsx"),
806
- [
807
- "export const head = null;",
808
- "export const metadata = { title: 'Mixed' };",
809
- "export default function Page() { return null; }",
810
- "",
811
- ].join("\n"),
812
- );
813
-
814
- const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
815
- const app = AppExecutor.from(workspace, "demo");
816
-
817
- await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
818
- "head/generateHead and metadata/generateMetadata cannot both be exported",
819
- );
781
+ await expect(app.getPageKeys({ refresh: true })).rejects.toThrow('unsupported export "metadata"');
820
782
  });
821
783
 
822
784
  test("assigns start command ports from sorted app order", async () => {
@@ -985,3 +947,67 @@ describe("scan info construction", () => {
985
947
  expect(info.file.document.scalars.has("money")).toBe(true);
986
948
  });
987
949
  });
950
+
951
+ describe("WorkspaceExecutor listing", () => {
952
+ test("lists apps, libs and pkgs in name order", async () => {
953
+ const root = await makeTempRoot();
954
+ for (const app of ["zeta", "alpha", "mid"]) await writeText(path.join(root, "apps", app, "akan.config.ts"), "");
955
+ for (const lib of ["util", "shared"]) await writeText(path.join(root, "libs", lib, "akan.config.ts"), "");
956
+ for (const pkg of ["zeta-tool", "@sample/tool", "akanjs"])
957
+ await writeJson(path.join(root, "pkgs", pkg, "package.json"), { name: pkg });
958
+
959
+ const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
960
+ expect(await workspace.getExecs()).toEqual([
961
+ ["alpha", "mid", "zeta"],
962
+ ["shared", "util"],
963
+ ["@sample/tool", "akanjs", "zeta-tool"],
964
+ ]);
965
+ });
966
+ });
967
+
968
+ describe("SysExecutor module listing", () => {
969
+ test("lists only the module folders that hold the module's own file", async () => {
970
+ const root = await makeTempRoot();
971
+ const lib = path.join(root, "apps/modlist/lib");
972
+ for (const file of [
973
+ "cnst.ts",
974
+ "post/post.constant.ts",
975
+ "post/Post.View.tsx",
976
+ "post/Post.Unit.tsx",
977
+ "draft/draft.document.ts",
978
+ "_auth/auth.service.ts",
979
+ "_notes/notes.md",
980
+ "__scalar/money/money.constant.ts",
981
+ "__scalar/stale/stale.dictionary.ts",
982
+ ])
983
+ await writeText(path.join(lib, file), "export {};\n");
984
+
985
+ const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
986
+ const app = AppExecutor.from(workspace, "modlist");
987
+ expect(await app.getDatabaseModules()).toEqual(["post"]);
988
+ expect(await app.getServiceModules()).toEqual(["_auth"]);
989
+ expect(await app.getScalarModules()).toEqual(["money"]);
990
+ expect(await app.getViewComponents()).toEqual(["post"]);
991
+ expect(await app.getUnitComponents()).toEqual(["post"]);
992
+ expect(await app.getTemplateComponents()).toEqual([]);
993
+ expect((await app.getViewsSourceCode()).map(({ filePath }) => filePath)).toEqual([
994
+ "apps/modlist/lib/post/Post.View.tsx",
995
+ ]);
996
+ expect((await app.getScalarConstantFiles()).map(({ filePath }) => filePath)).toEqual([
997
+ "apps/modlist/lib/__scalar/money/money.constant.ts",
998
+ ]);
999
+ });
1000
+
1001
+ test("reads scalar dictionaries from the scalar folder", async () => {
1002
+ const root = await makeTempRoot();
1003
+ const scalarDir = path.join(root, "apps/scalardict/lib/__scalar/money");
1004
+ await writeText(path.join(scalarDir, "money.constant.ts"), "export {};\n");
1005
+ await writeText(path.join(scalarDir, "money.dictionary.ts"), "export const money = {};\n");
1006
+
1007
+ const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
1008
+ const app = AppExecutor.from(workspace, "scalardict");
1009
+ expect(await app.getScalarDictionaryFiles()).toEqual([
1010
+ { filePath: "apps/scalardict/lib/__scalar/money/money.dictionary.ts", content: "export const money = {};\n" },
1011
+ ]);
1012
+ });
1013
+ });