@akanjs/devkit 3.0.0-beta.19 → 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 (233) hide show
  1. package/abstractDoc.test.ts +6 -15
  2. package/abstractDoc.ts +1 -4
  3. package/agentsIndex.test.ts +4 -7
  4. package/agentsIndex.ts +6 -22
  5. package/akanApp/BackendImportGraph.test.ts +8 -22
  6. package/akanApp/BackendImportGraph.ts +1 -20
  7. package/akanApp/akanApp.host.test.ts +1 -3
  8. package/akanApp/akanApp.host.ts +64 -250
  9. package/akanApp/devHostPolicy.test.ts +10 -34
  10. package/akanApp/devHostPolicy.ts +12 -98
  11. package/akanConfig/akanConfig.test.ts +30 -39
  12. package/akanConfig/akanConfig.ts +40 -136
  13. package/akanContext.ts +106 -159
  14. package/akanMcpContract.ts +28 -73
  15. package/appSelectionMemory.ts +1 -4
  16. package/applicationBuildReporter.ts +37 -5
  17. package/applicationBuildRunner.ts +25 -59
  18. package/applicationReleasePackager.ts +24 -44
  19. package/applicationTestPreload.ts +3 -12
  20. package/artifact/implicitRootLayout.test.ts +3 -19
  21. package/artifact/implicitRootLayout.ts +34 -91
  22. package/artifact/routeSeedIndex.ts +11 -44
  23. package/biomeBase.ts +2 -5
  24. package/capacitorApp.test.ts +4 -14
  25. package/capacitorApp.ts +86 -235
  26. package/cloud/cloudApi.test.ts +202 -0
  27. package/cloud/cloudApi.ts +33 -57
  28. package/cloud/configLock.test.ts +64 -0
  29. package/cloud/configLock.ts +96 -0
  30. package/cloud/constants.ts +3 -2
  31. package/cloud/globalConfig.ts +64 -31
  32. package/codeAgent/agent/AkanCodeServices.ts +15 -57
  33. package/codeAgent/agent/AkanContextFiles.ts +5 -28
  34. package/codeAgent/agent/CodeAgent.ts +33 -108
  35. package/codeAgent/agent/CodeAgentAsks.ts +7 -30
  36. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
  37. package/codeAgent/agent/CodeAgentGate.ts +8 -34
  38. package/codeAgent/agent/CodeAgentProxy.ts +4 -13
  39. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
  40. package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
  41. package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
  42. package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
  43. package/codeAgent/agent/CodeAgentUi.ts +2 -7
  44. package/codeAgent/agent/CodeMailbox.ts +7 -34
  45. package/codeAgent/agent/CodeSessionFork.ts +4 -19
  46. package/codeAgent/agent/CodeSessionIndex.ts +5 -31
  47. package/codeAgent/agent/SubagentPool.ts +48 -58
  48. package/codeAgent/agent/akanCodeModel.ts +5 -39
  49. package/codeAgent/agent/akanCodePaths.ts +4 -24
  50. package/codeAgent/agent/akanContextFiles.test.ts +0 -2
  51. package/codeAgent/agent/akanSystemPrompt.ts +1 -9
  52. package/codeAgent/agent/codeAgent.test.ts +178 -0
  53. package/codeAgent/agent/codeAgentContract.test.ts +5 -28
  54. package/codeAgent/agent/codeMailbox.test.ts +0 -4
  55. package/codeAgent/agent/codeSessionFork.test.ts +0 -2
  56. package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
  57. package/codeAgent/agent/consoleToStderr.ts +2 -12
  58. package/codeAgent/agent/jsonLines.ts +29 -0
  59. package/codeAgent/agent/stringArg.ts +5 -0
  60. package/codeAgent/feedback/DevLogFeedback.ts +3 -18
  61. package/codeAgent/feedback/PreviewChrome.ts +5 -29
  62. package/codeAgent/feedback/PreviewFeedback.ts +2 -9
  63. package/codeAgent/feedback/PreviewView.ts +3 -15
  64. package/codeAgent/feedback/TurnFeedback.ts +2 -17
  65. package/codeAgent/feedback/VerifyFeedback.ts +1 -7
  66. package/codeAgent/feedback/previewChrome.test.ts +1 -1
  67. package/codeAgent/index.ts +2 -10
  68. package/codeAgent/skills/akan-code/SKILL.md +12 -4
  69. package/codeAgent/tools/AkanCli.ts +4 -16
  70. package/codeAgent/tools/AkanCodePlugins.ts +21 -43
  71. package/codeAgent/tools/AkanEditScope.ts +2 -12
  72. package/codeAgent/tools/AkanEnvKeys.ts +3 -11
  73. package/codeAgent/tools/AkanToolPack.ts +6 -28
  74. package/codeAgent/tools/AkanVerifier.ts +1 -8
  75. package/codeAgent/tools/AskToolPack.ts +3 -15
  76. package/codeAgent/tools/MailToolPack.ts +0 -9
  77. package/codeAgent/tools/McpClient.ts +3 -24
  78. package/codeAgent/tools/McpOAuth.ts +6 -41
  79. package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
  80. package/codeAgent/tools/McpServerConfig.ts +7 -26
  81. package/codeAgent/tools/McpSignIn.ts +46 -49
  82. package/codeAgent/tools/McpTokenStore.ts +6 -17
  83. package/codeAgent/tools/McpToolPack.ts +107 -35
  84. package/codeAgent/tools/SessionToolPack.ts +1 -11
  85. package/codeAgent/tools/WebToolPack.ts +1 -7
  86. package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
  87. package/codeAgent/tools/askToolPack.test.ts +0 -3
  88. package/codeAgent/tools/mcpOAuth.test.ts +315 -27
  89. package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
  90. package/codegenLock.test.ts +4 -13
  91. package/codegenLock.ts +3 -17
  92. package/commandDecorators/argMeta.ts +1 -5
  93. package/commandDecorators/camelToKebabCase.ts +1 -0
  94. package/commandDecorators/command.ts +44 -102
  95. package/commandDecorators/commandBuilder.ts +30 -61
  96. package/commandDecorators/commandDecorators.test.ts +1 -9
  97. package/commandDecorators/dependencyBuilder.ts +10 -1
  98. package/commandDecorators/helpFormatter.ts +66 -144
  99. package/commandDecorators/index.ts +0 -2
  100. package/commandDecorators/targetMeta.ts +9 -7
  101. package/createTunnel.ts +7 -14
  102. package/dependencyScanner.test.ts +4 -16
  103. package/dependencyScanner.ts +27 -78
  104. package/devkitUtils.test.ts +47 -19
  105. package/executors.test.ts +114 -60
  106. package/executors.ts +165 -484
  107. package/fileEditor.ts +9 -37
  108. package/{getDirname.test.ts → fileSys.test.ts} +1 -1
  109. package/fileSys.ts +6 -8
  110. package/formSetterScanner.ts +3 -16
  111. package/frontendBuild/allRoutesBuilder.ts +7 -21
  112. package/frontendBuild/autoImportSync.test.ts +14 -38
  113. package/frontendBuild/autoImportSync.ts +15 -67
  114. package/frontendBuild/buildRouteClient.test.ts +10 -36
  115. package/frontendBuild/bundleDefine.ts +12 -0
  116. package/frontendBuild/clientBuildTypes.ts +5 -36
  117. package/frontendBuild/clientEntriesBundler.ts +9 -40
  118. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  119. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  120. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  121. package/frontendBuild/cssCandidateCache.ts +6 -41
  122. package/frontendBuild/cssCompiler.ts +29 -78
  123. package/frontendBuild/cssImportResolver.ts +5 -18
  124. package/frontendBuild/devChangePlanner.ts +7 -10
  125. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  126. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  127. package/frontendBuild/fontOptimizer.test.ts +5 -13
  128. package/frontendBuild/fontOptimizer.ts +15 -72
  129. package/frontendBuild/fontPruner.test.ts +5 -10
  130. package/frontendBuild/fontPruner.ts +5 -20
  131. package/frontendBuild/frontendBuild.test.ts +27 -52
  132. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  133. package/frontendBuild/hmrWatcher.test.ts +7 -31
  134. package/frontendBuild/hmrWatcher.ts +14 -71
  135. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  136. package/frontendBuild/precompressArtifacts.ts +5 -18
  137. package/frontendBuild/routeClientBuilder.ts +7 -47
  138. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  139. package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
  140. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  141. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  142. package/frontendBuild/styleContract.ts +1 -8
  143. package/frontendBuild/themeValidator.ts +4 -26
  144. package/frontendBuild/watchRootResolver.test.ts +23 -42
  145. package/frontendBuild/watchRootResolver.ts +5 -27
  146. package/getRelatedCnsts.ts +12 -92
  147. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  148. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  149. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  150. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  151. package/incrementalBuilder/builderChannel.test.ts +24 -49
  152. package/incrementalBuilder/builderChannel.ts +6 -37
  153. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  154. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  155. package/incrementalBuilder/devWatchBatch.test.ts +14 -18
  156. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  157. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  158. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  159. package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
  160. package/index.ts +3 -15
  161. package/integration/devResourceProbe.ts +50 -56
  162. package/integration/devStability.integration.test.ts +47 -194
  163. package/integration/devStabilityHarness.test.ts +9 -29
  164. package/integration/devStabilityHarness.ts +65 -330
  165. package/integration/ssrMemoryProbe.ts +41 -167
  166. package/libSource.test.ts +4 -15
  167. package/libSource.ts +19 -31
  168. package/lint/gritRules.test.ts +6 -23
  169. package/linter.test.ts +1 -2
  170. package/linter.ts +22 -115
  171. package/mobile/mobileTarget.ts +14 -13
  172. package/package.json +2 -2
  173. package/packageExports.test.ts +3 -18
  174. package/packageExportsMap.ts +3 -19
  175. package/piDependencyPins.test.ts +3 -14
  176. package/platformTest/IdleWatchedProcess.test.ts +5 -13
  177. package/platformTest/IdleWatchedProcess.ts +1 -5
  178. package/platformTest/LinuxDockerTarget.ts +20 -35
  179. package/platformTest/PlatformTestRun.test.ts +5 -9
  180. package/platformTest/PlatformTestTarget.ts +24 -2
  181. package/platformTest/TestSnapshot.test.ts +6 -14
  182. package/platformTest/TestSnapshot.ts +1 -5
  183. package/platformTest/WindowsSshTarget.ts +6 -22
  184. package/prompter.ts +3 -6
  185. package/qualityScanner.test.ts +6 -24
  186. package/qualityScanner.ts +196 -284
  187. package/recipeScanner.test.ts +7 -12
  188. package/recipeScanner.ts +9 -38
  189. package/repoIdentity.ts +2 -9
  190. package/routeSourceValidator.ts +7 -30
  191. package/scanInfo.ts +82 -214
  192. package/selectModel.ts +1 -2
  193. package/semver.ts +3 -5
  194. package/slicePlanner.test.ts +6 -24
  195. package/slicePlanner.ts +11 -32
  196. package/spinner.test.ts +3 -8
  197. package/spinner.ts +4 -16
  198. package/sshTunnel.ts +1 -6
  199. package/ssrScanner.test.ts +1 -6
  200. package/ssrScanner.ts +103 -113
  201. package/subspace.test.ts +6 -25
  202. package/subspace.ts +31 -109
  203. package/subspaceConfig.ts +3 -11
  204. package/testHelpers.ts +49 -34
  205. package/transforms/asyncDefaultExportDetector.ts +3 -11
  206. package/transforms/barrelAnalyzer.ts +43 -135
  207. package/transforms/barrelImportsPlugin.ts +38 -175
  208. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  209. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  210. package/transforms/moduleSyntax.ts +32 -0
  211. package/transforms/rscUseClientTransform.ts +35 -20
  212. package/transforms/transforms.test.ts +153 -44
  213. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  214. package/transforms/useClientBundlePlugin.ts +2 -22
  215. package/typeChecker.ts +7 -63
  216. package/uploadRelease.ts +10 -25
  217. package/workflow/artifacts.ts +18 -0
  218. package/workflow/executor.test.ts +1 -12
  219. package/workflow/executor.ts +85 -146
  220. package/workflow/index.ts +0 -1
  221. package/workflow/moduleIndex.test.ts +4 -15
  222. package/workflow/moduleIndex.ts +9 -49
  223. package/workflow/plan.ts +18 -25
  224. package/workflow/render.ts +80 -96
  225. package/workflow/source.ts +33 -56
  226. package/workflow/sourceAst.ts +50 -0
  227. package/workspaceLayout.ts +4 -19
  228. package/commandDecorators/commandMeta.ts +0 -7
  229. package/commandDecorators/types.ts +0 -10
  230. package/getDirname.ts +0 -4
  231. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  232. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  233. package/workflow/primitive.ts +0 -59
package/typeChecker.ts CHANGED
@@ -38,31 +38,15 @@ export class TypeChecker {
38
38
  }
39
39
  this.config = parsedConfig;
40
40
  }
41
- /**
42
- * Find tsconfig.json by walking up the directory tree
43
- */
44
41
  #findConfigFile(searchPath: string): string | undefined {
45
42
  return ts.findConfigFile(searchPath, (fileName) => ts.sys.fileExists(fileName), "tsconfig.json");
46
43
  }
47
44
 
48
- /**
49
- * Type-check a single TypeScript file
50
- * @param filePath - Path to the TypeScript file to check
51
- * @returns Array of diagnostic messages
52
- */
53
- check(filePath: string): {
54
- diagnostics: ts.Diagnostic[];
55
- errors: ts.Diagnostic[];
56
- warnings: ts.Diagnostic[];
57
- fileDiagnostics: ts.Diagnostic[];
58
- fileErrors: ts.Diagnostic[];
59
- fileWarnings: ts.Diagnostic[];
60
- } {
45
+ check(filePath: string) {
61
46
  const program = ts.createProgram([filePath], this.config.options);
62
47
  const diagnostics = [
63
48
  ...program.getSemanticDiagnostics(),
64
49
  ...program.getSyntacticDiagnostics(),
65
- // Only check declaration diagnostics when declaration emit is enabled
66
50
  ...(this.config.options.declaration ? program.getDeclarationDiagnostics() : []),
67
51
  ];
68
52
  const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error);
@@ -73,11 +57,6 @@ export class TypeChecker {
73
57
  return { diagnostics, errors, warnings, fileDiagnostics, fileErrors, fileWarnings };
74
58
  }
75
59
 
76
- /**
77
- * Format diagnostics for console output
78
- * @param diagnostics - Array of TypeScript diagnostics
79
- * @returns Formatted string
80
- */
81
60
  formatDiagnostics(diagnostics: ts.Diagnostic[]): string {
82
61
  if (diagnostics.length === 0) return chalk.bold("✅ No type errors found");
83
62
 
@@ -86,48 +65,24 @@ export class TypeChecker {
86
65
  let warningCount = 0;
87
66
  let suggestionCount = 0;
88
67
 
89
- // Group diagnostics by file
90
68
  const diagnosticsByFile = new Map<string, ts.Diagnostic[]>();
91
69
  diagnostics.forEach((diagnostic) => {
92
70
  if (diagnostic.category === ts.DiagnosticCategory.Error) errorCount++;
93
71
  else if (diagnostic.category === ts.DiagnosticCategory.Warning) warningCount++;
94
72
  else if (diagnostic.category === ts.DiagnosticCategory.Suggestion) suggestionCount++;
95
-
96
- if (diagnostic.file) {
97
- const fileName = diagnostic.file.fileName;
98
- if (!diagnosticsByFile.has(fileName)) diagnosticsByFile.set(fileName, []);
99
- const fileDiagnostics = diagnosticsByFile.get(fileName);
100
- if (fileDiagnostics) fileDiagnostics.push(diagnostic);
101
- } else {
102
- if (!diagnosticsByFile.has("")) diagnosticsByFile.set("", []);
103
- const fileDiagnostics = diagnosticsByFile.get("");
104
- if (fileDiagnostics) fileDiagnostics.push(diagnostic);
105
- }
73
+ diagnosticsByFile.getOrInsert(diagnostic.file?.fileName ?? "", []).push(diagnostic);
106
74
  });
107
75
 
108
- // Format diagnostics by file
109
76
  diagnosticsByFile.forEach((fileDiagnostics, fileName) => {
110
77
  if (fileName) output.push(`\n${chalk.cyan(fileName)}`);
111
78
 
112
79
  fileDiagnostics.forEach((diagnostic) => {
113
- const categoryText =
114
- diagnostic.category === ts.DiagnosticCategory.Error
115
- ? "error"
116
- : diagnostic.category === ts.DiagnosticCategory.Warning
117
- ? "warning"
118
- : "suggestion";
119
- const categoryColor =
120
- diagnostic.category === ts.DiagnosticCategory.Error
121
- ? chalk.red
122
- : diagnostic.category === ts.DiagnosticCategory.Warning
123
- ? chalk.yellow
124
- : chalk.blue;
125
- const icon =
80
+ const [categoryText, categoryColor, icon] =
126
81
  diagnostic.category === ts.DiagnosticCategory.Error
127
- ? "❌"
82
+ ? (["error", chalk.red, "❌"] as const)
128
83
  : diagnostic.category === ts.DiagnosticCategory.Warning
129
- ? "⚠️"
130
- : "💡";
84
+ ? (["warning", chalk.yellow, "⚠️"] as const)
85
+ : (["suggestion", chalk.blue, "💡"] as const);
131
86
  const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
132
87
  const tsCode = chalk.dim(`(TS${diagnostic.code})`);
133
88
 
@@ -137,7 +92,6 @@ export class TypeChecker {
137
92
  output.push(`\n ${icon} ${categoryColor(categoryText)}: ${message} ${tsCode}`);
138
93
  output.push(` ${chalk.gray("at")} ${fileName}:${chalk.bold(`${line + 1}:${character + 1}`)}`);
139
94
 
140
- // Show source line with underline
141
95
  const sourceLines = diagnostic.file.text.split("\n");
142
96
  if (line < sourceLines.length) {
143
97
  const sourceLine = sourceLines[line];
@@ -145,7 +99,6 @@ export class TypeChecker {
145
99
 
146
100
  output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
147
101
 
148
- // Create underline with squiggly for TypeScript
149
102
  const underlinePrefix = " ".repeat(character);
150
103
  const length = diagnostic.length ?? 1;
151
104
  const underline = "~".repeat(Math.max(1, length));
@@ -166,11 +119,6 @@ export class TypeChecker {
166
119
  return `\n${summary.join(", ")} found${output.join("\n")}`;
167
120
  }
168
121
 
169
- /**
170
- * Get detailed diagnostic information with code snippet
171
- * @param filePath - Path to the TypeScript file to check
172
- * @returns Object containing diagnostics and detailed information
173
- */
174
122
  getDetailedDiagnostics(filePath: string): {
175
123
  diagnostics: ts.Diagnostic[];
176
124
  details: { line: number; column: number; message: string; code: number; codeSnippet?: string }[];
@@ -199,11 +147,7 @@ export class TypeChecker {
199
147
  return { diagnostics, details };
200
148
  }
201
149
 
202
- /**
203
- * Check if a file has type errors
204
- * @param filePath - Path to the TypeScript file to check
205
- * @returns true if there are no type errors, false otherwise
206
- */
150
+ /** Also false when the check itself throws. */
207
151
  hasNoTypeErrors(filePath: string): boolean {
208
152
  try {
209
153
  const { diagnostics } = this.check(filePath);
package/uploadRelease.ts CHANGED
@@ -2,10 +2,8 @@ import { Logger, RestClient } from "akanjs/common";
2
2
 
3
3
  import { Spinner } from "./spinner";
4
4
 
5
- const spinning = (message: string) => {
6
- const spinner = new Spinner(message, { prefix: message, enableSpin: true }).start();
7
- return spinner;
8
- };
5
+ const spinning = (message: string) => new Spinner(message, { prefix: message, enableSpin: true }).start();
6
+
9
7
  export const uploadRelease = async (
10
8
  appName: string,
11
9
  {
@@ -27,33 +25,20 @@ export const uploadRelease = async (
27
25
  const logger = new Logger("uploadRelease");
28
26
  const basePath = local ? "http://localhost:8282/backend" : "https://cloud.akanjs.com/backend";
29
27
  const httpClient = new RestClient(basePath);
30
- const buildPath = `${workspaceRoot}/releases/builds/${appName}-release.tar.gz`;
31
- const appBuildPath = `${workspaceRoot}/releases/builds/${appName}-appBuild.zip`;
32
- const sourcePath = `${workspaceRoot}/releases/sources/${appName}-source.tar.gz`;
33
-
34
28
  const readingFilesSpinner = spinning("Reading files...");
35
29
  try {
36
- const buildFile = Bun.file(buildPath);
37
- const sourceFile = Bun.file(sourcePath);
38
- const appBuildFile = Bun.file(appBuildPath);
39
- const buildStat = { mtime: new Date(buildFile.lastModified), size: buildFile.size };
40
- const sourceStat = { mtime: new Date(sourceFile.lastModified), size: sourceFile.size };
41
- const appBuildStat = { mtime: new Date(appBuildFile.lastModified), size: appBuildFile.size };
30
+ const uploads = [
31
+ [`builds/${appName}-release.tar.gz`, `${appName}-release.tar.gz`],
32
+ [`sources/${appName}-source.tar.gz`, `${appName}-source.tar.gz`],
33
+ [`builds/${appName}-appBuild.zip`, `${appName}-appBuild.zip`],
34
+ ].map(([relativePath, name]) => ({ file: Bun.file(`${workspaceRoot}/releases/${relativePath}`), name }));
35
+ const metas = uploads.map(({ file }) => ({ lastModifiedAt: new Date(file.lastModified), size: file.size }));
42
36
  readingFilesSpinner.succeed("Reading files... done");
43
37
 
44
38
  const preparingFormSpinner = spinning("Preparing form data...");
45
39
  const formData = new FormData();
46
- formData.append("files", buildFile, `${appName}-release.tar.gz`);
47
- formData.append("files", sourceFile, `${appName}-source.tar.gz`);
48
- formData.append("files", appBuildFile, `${appName}-appBuild.zip`);
49
- formData.append(
50
- "metas",
51
- JSON.stringify([
52
- { lastModifiedAt: buildStat.mtime, size: buildStat.size },
53
- { lastModifiedAt: sourceStat.mtime, size: sourceStat.size },
54
- { lastModifiedAt: appBuildStat.mtime, size: appBuildStat.size },
55
- ]),
56
- );
40
+ for (const { file, name } of uploads) formData.append("files", file, name);
41
+ formData.append("metas", JSON.stringify(metas));
57
42
  formData.append("type", "release");
58
43
  preparingFormSpinner.succeed("Preparing form data... done");
59
44
 
@@ -1,3 +1,4 @@
1
+ import { capitalize } from "akanjs/common";
1
2
  import type { Workspace } from "../commandDecorators";
2
3
  import type {
3
4
  GeneratedSyncState,
@@ -146,6 +147,23 @@ export const workflowCommandsForPlan = (plan: WorkflowPlan) =>
146
147
  kind: validation.kind,
147
148
  })) satisfies WorkflowApplyCommand[];
148
149
 
150
+ const planInputString = (plan: WorkflowPlan, key: string) => {
151
+ const value = plan.inputs[key];
152
+ return typeof value === "string" ? value : "";
153
+ };
154
+
155
+ export const workflowPathsForPlan = (plan: WorkflowPlan) => {
156
+ const app = planInputString(plan, "app");
157
+ const module = planInputString(plan, "module");
158
+ const moduleClass = module ? capitalize(module) : "<Module>";
159
+ return plan.predictedChanges.map((change) =>
160
+ change.target
161
+ .replace(/^\*\//, app ? `apps/${app}/` : "")
162
+ .replaceAll("<module>", module || "<module>")
163
+ .replaceAll("<Module>", moduleClass),
164
+ );
165
+ };
166
+
149
167
  export const workflowRunsDir = ".akan/workflows/runs";
150
168
 
151
169
  export const createWorkflowRunId = (prefix = "run") =>
@@ -9,11 +9,6 @@ import type {
9
9
  WorkflowStepResult,
10
10
  } from "./types";
11
11
 
12
- /**
13
- * `WorkflowExecutor` takes its registry by constructor injection and its plan as data, so `apply` runs with
14
- * no filesystem and no workspace — the `workspace` argument is what gates every post-apply check. These
15
- * cover the ordering rules that `cli/workflow/workflow.test.ts` can only reach through a real scaffold.
16
- */
17
12
  const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
18
13
  id,
19
14
  tool,
@@ -21,8 +16,7 @@ const step = (id: string, tool = `tool:${id}`): WorkflowStep => ({
21
16
  description: id,
22
17
  });
23
18
 
24
- // Every field spelled out rather than cast through `unknown`: `apply` reads `validation` before the first
25
- // step runs, so a partial literal fails at runtime instead of at the type level.
19
+ // Not a cast partial: `apply` reads `validation` before the first step, so a missing field fails only at runtime.
26
20
  const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []): WorkflowPlan => ({
27
21
  schemaVersion: 1,
28
22
  workflow: "add-field",
@@ -37,10 +31,6 @@ const planOf = (steps: WorkflowStep[], diagnostics: WorkflowDiagnostic[] = []):
37
31
  requiresApproval: true,
38
32
  });
39
33
 
40
- /**
41
- * A registry that records **which key** each call resolved through, not just that a step ran — the
42
- * resolution order is the thing under test, and every candidate key would otherwise record the same step id.
43
- */
44
34
  const changed = (path: string): PrimitiveChangedFile => ({ path, action: "modify", reason: "test" });
45
35
 
46
36
  const recordingRegistry = (results: Record<string, WorkflowStepResult | undefined> = {}) => {
@@ -79,7 +69,6 @@ describe("WorkflowExecutor.apply", () => {
79
69
 
80
70
  test("resolves a runner by workflow-scoped key first, then tool, then bare step id", async () => {
81
71
  const { calls, registry, on } = recordingRegistry();
82
- // All three keys are registered for the same step, so only the winner records a call.
83
72
  on(workflowStepKey("add-field", "scoped"));
84
73
  on("tool:scoped");
85
74
  on("scoped");
@@ -48,16 +48,13 @@ const workflowStringListInput = (value: WorkflowInputValue | undefined) =>
48
48
  const workflowStringArrayInput = (value: WorkflowInputValue | undefined) => (Array.isArray(value) ? value : null);
49
49
  const workflowBooleanInput = (value: WorkflowInputValue | undefined) => (typeof value === "boolean" ? value : null);
50
50
 
51
- const postApplyDiagnostic = (code: string, message: string, target: string): WorkflowDiagnostic => ({
52
- severity: "error",
53
- code,
54
- message,
55
- failureScope: "source-change",
56
- context: { target },
57
- });
58
-
59
- const postApplyWarning = (code: string, message: string, target: string): WorkflowDiagnostic => ({
60
- severity: "warning",
51
+ const postApplyDiagnostic = (
52
+ code: string,
53
+ message: string,
54
+ target: string,
55
+ severity: WorkflowDiagnostic["severity"] = "error",
56
+ ): WorkflowDiagnostic => ({
57
+ severity,
61
58
  code,
62
59
  message,
63
60
  failureScope: "source-change",
@@ -114,27 +111,23 @@ const checkTypeScriptSyntax = async (workspace: Workspace, filePath: string) =>
114
111
 
115
112
  const checkChangedFile = async (workspace: Workspace, file: PrimitiveChangedFile): Promise<WorkflowStepResult> => {
116
113
  if (file.action === "remove") return { postApplyChecks: [] };
117
- const diagnostics: WorkflowDiagnostic[] = [];
118
- const checks: WorkflowPostApplyCheck[] = [];
119
- const pathIssue = await checkPathCasing(workspace, file.path);
120
- if (pathIssue) {
121
- diagnostics.push(postApplyDiagnostic(pathIssue.code, pathIssue.message, file.path));
122
- checks.push({ ...pathIssue, target: file.path, status: "failed" });
123
- return { diagnostics, postApplyChecks: checks };
124
- }
125
- const syntaxIssue = await checkTypeScriptSyntax(workspace, file.path);
126
- if (syntaxIssue) {
127
- diagnostics.push(postApplyDiagnostic(syntaxIssue.code, syntaxIssue.message, file.path));
128
- checks.push({ ...syntaxIssue, target: file.path, status: "failed" });
129
- return { diagnostics, postApplyChecks: checks };
114
+ const issue = (await checkPathCasing(workspace, file.path)) ?? (await checkTypeScriptSyntax(workspace, file.path));
115
+ if (issue) {
116
+ return {
117
+ diagnostics: [postApplyDiagnostic(issue.code, issue.message, file.path)],
118
+ postApplyChecks: [{ ...issue, target: file.path, status: "failed" }],
119
+ };
130
120
  }
131
- checks.push({
132
- code: "workflow-post-apply-file-valid",
133
- target: file.path,
134
- status: "passed",
135
- message: "Changed file exists with exact casing and parses as source when applicable.",
136
- });
137
- return { postApplyChecks: checks };
121
+ return {
122
+ postApplyChecks: [
123
+ {
124
+ code: "workflow-post-apply-file-valid",
125
+ target: file.path,
126
+ status: "passed",
127
+ message: "Changed file exists with exact casing and parses as source when applicable.",
128
+ },
129
+ ],
130
+ };
138
131
  };
139
132
 
140
133
  const checkRecommendationPath = async (
@@ -199,18 +192,6 @@ const workflowModuleContext = async (workspace: Workspace, plan: WorkflowPlan):
199
192
  };
200
193
  };
201
194
 
202
- const structureCheck = (
203
- code: string,
204
- target: string,
205
- status: WorkflowPostApplyCheck["status"],
206
- message: string,
207
- ): WorkflowPostApplyCheck => ({
208
- code,
209
- target,
210
- status,
211
- message,
212
- });
213
-
214
195
  const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan): Promise<WorkflowStepResult> => {
215
196
  if (plan.workflow !== "add-field" && plan.workflow !== "add-enum-field") return {};
216
197
  const app = workflowStringInput(plan.inputs.app);
@@ -260,16 +241,14 @@ const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan):
260
241
  .map((diagnostic) => diagnostic.message),
261
242
  ].filter((failure): failure is string => failure !== null);
262
243
  const constantValid = constantFailures.length === 0;
263
- postApplyChecks.push(
264
- structureCheck(
265
- constantValid ? "workflow-post-apply-constant-shape-valid" : "workflow-post-apply-structure-invalid",
266
- constantPath,
267
- constantValid ? "passed" : "failed",
268
- constantValid
269
- ? `${inputClassName} keeps via structure, builder usage, imports, and requested field presence.`
270
- : constantFailures.join(" "),
271
- ),
272
- );
244
+ postApplyChecks.push({
245
+ code: constantValid ? "workflow-post-apply-constant-shape-valid" : "workflow-post-apply-structure-invalid",
246
+ target: constantPath,
247
+ status: constantValid ? "passed" : "failed",
248
+ message: constantValid
249
+ ? `${inputClassName} keeps via structure, builder usage, imports, and requested field presence.`
250
+ : constantFailures.join(" "),
251
+ });
273
252
  if (!constantValid) {
274
253
  diagnostics.push(
275
254
  postApplyDiagnostic("workflow-post-apply-structure-invalid", constantFailures.join(" "), constantPath),
@@ -294,16 +273,14 @@ const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan):
294
273
  .map((diagnostic) => diagnostic.message),
295
274
  ].filter((failure): failure is string => failure !== null);
296
275
  const dictionaryValid = dictionaryFailures.length === 0;
297
- postApplyChecks.push(
298
- structureCheck(
299
- dictionaryValid ? "workflow-post-apply-dictionary-shape-valid" : "workflow-post-apply-structure-invalid",
300
- dictionaryPath,
301
- dictionaryValid ? "passed" : "failed",
302
- dictionaryValid
303
- ? `.model<${moduleClassName}> keeps the requested field inside the model object and preserves dictionary chain order.`
304
- : dictionaryFailures.join(" "),
305
- ),
306
- );
276
+ postApplyChecks.push({
277
+ code: dictionaryValid ? "workflow-post-apply-dictionary-shape-valid" : "workflow-post-apply-structure-invalid",
278
+ target: dictionaryPath,
279
+ status: dictionaryValid ? "passed" : "failed",
280
+ message: dictionaryValid
281
+ ? `.model<${moduleClassName}> keeps the requested field inside the model object and preserves dictionary chain order.`
282
+ : dictionaryFailures.join(" "),
283
+ });
307
284
  if (!dictionaryValid) {
308
285
  diagnostics.push(
309
286
  postApplyDiagnostic("workflow-post-apply-structure-invalid", dictionaryFailures.join(" "), dictionaryPath),
@@ -313,22 +290,21 @@ const checkAddFieldStructure = async (workspace: Workspace, plan: WorkflowPlan):
313
290
  const constantOrderValid = fieldOrderValid(constantNames, fieldName);
314
291
  const dictionaryOrderValid = fieldOrderValid(dictionaryStructure.fields, fieldName);
315
292
  const orderValid = constantOrderValid && dictionaryOrderValid;
316
- postApplyChecks.push(
317
- structureCheck(
318
- "workflow-post-apply-field-order-valid",
319
- `${constantPath}, ${dictionaryPath}`,
320
- orderValid ? "passed" : "failed",
321
- orderValid
322
- ? `Field "${fieldName}" follows the shared priority ordering policy.`
323
- : `Field "${fieldName}" is present but does not match the shared priority ordering policy.`,
324
- ),
325
- );
293
+ postApplyChecks.push({
294
+ code: "workflow-post-apply-field-order-valid",
295
+ target: `${constantPath}, ${dictionaryPath}`,
296
+ status: orderValid ? "passed" : "failed",
297
+ message: orderValid
298
+ ? `Field "${fieldName}" follows the shared priority ordering policy.`
299
+ : `Field "${fieldName}" is present but does not match the shared priority ordering policy.`,
300
+ });
326
301
  if (!orderValid) {
327
302
  diagnostics.push(
328
- postApplyWarning(
303
+ postApplyDiagnostic(
329
304
  "workflow-post-apply-field-order-mismatch",
330
305
  `Field "${fieldName}" is present but does not match the shared priority ordering policy.`,
331
306
  `${constantPath}, ${dictionaryPath}`,
307
+ "warning",
332
308
  ),
333
309
  );
334
310
  }
@@ -365,6 +341,14 @@ const unsupportedInput = (input: string, message: string): WorkflowDiagnostic =>
365
341
  message,
366
342
  });
367
343
 
344
+ const surfaceReview = (
345
+ plan: WorkflowPlan,
346
+ { code, target, action, message, reason }: Record<"code" | "target" | "action" | "message" | "reason", string>,
347
+ ): WorkflowStepResult => ({
348
+ recommendations: [{ code, kind: "manual-action", target, action, confidence: "medium", message }],
349
+ nextActions: [{ command: `akan workflow explain ${plan.workflow}`, reason }],
350
+ });
351
+
368
352
  const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
369
353
  const app = workflowStringInput(plan.inputs.app);
370
354
  const module = workflowStringInput(plan.inputs.module) ?? "<module>";
@@ -374,75 +358,41 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
374
358
  const surfaces = workflowStringArrayInput(plan.inputs.surfaces);
375
359
  const templateRequested = surfaces?.includes("template") ?? false;
376
360
  const moduleClassName = moduleComponentName(module);
377
- const target = `${app ? `apps/${app}` : "*"}/${moduleSourcePaths(module).template}`;
378
- return {
379
- recommendations: [
380
- {
381
- code: "add-field-ui-surface-review",
382
- kind: "manual-action",
383
- target,
384
- action: templateRequested
385
- ? `Template was requested for ${field}. If no Template file changed, users will not see the field in the form yet because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Add it inside Layout.Template near existing Field components.`
386
- : `Template was not selected, so users will not see ${field} in the form from this apply. If list/card display is needed, include ${field} in Light${moduleClassName} projection data and place it in the local Unit/View card layout.`,
387
- confidence: "medium",
388
- message: `Review user-visible UI for ${module}.${field}; recommended component is ${policy.component}.`,
389
- },
390
- ],
391
- nextActions: [
392
- {
393
- command: `akan workflow explain ${plan.workflow}`,
394
- reason: "Review UI surface guidance before manually editing ambiguous UI files.",
395
- },
396
- ],
397
- };
361
+ return surfaceReview(plan, {
362
+ code: "add-field-ui-surface-review",
363
+ target: `${app ? `apps/${app}` : "*"}/${moduleSourcePaths(module).template}`,
364
+ action: templateRequested
365
+ ? `Template was requested for ${field}. If no Template file changed, users will not see the field in the form yet because the file was missing, the generated ${module}Form/Layout.Template pattern was not found, or ${policy.component} needs option binding. Add it inside Layout.Template near existing Field components.`
366
+ : `Template was not selected, so users will not see ${field} in the form from this apply. If list/card display is needed, include ${field} in Light${moduleClassName} projection data and place it in the local Unit/View card layout.`,
367
+ message: `Review user-visible UI for ${module}.${field}; recommended component is ${policy.component}.`,
368
+ reason: "Review UI surface guidance before manually editing ambiguous UI files.",
369
+ });
398
370
  };
399
371
 
400
372
  const addMutationActionSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
401
373
  const module = workflowStringInput(plan.inputs.module) ?? "<module>";
402
374
  const mutation = workflowStringInput(plan.inputs.mutation) ?? "<mutation>";
403
375
  const moduleClassName = moduleComponentName(module);
404
- return {
405
- recommendations: [
406
- {
407
- code: "add-mutation-action-surface-review",
408
- kind: "manual-action",
409
- target: `*/${moduleSourcePaths(module).store}`,
410
- action: `Name the endpoint's guards and add its dictionary .endpoint() entry with a .desc(). After sync, fetch.${mutation} exists; write a store action only for a toast, an optimistic update, or a multi-field write, and put the control that calls it in ${moduleClassName}.Util.tsx.`,
411
- confidence: "medium",
412
- message: `Workflow apply does not write store or UI code for ${module}.${mutation}; review whether a screen should call it.`,
413
- },
414
- ],
415
- nextActions: [
416
- {
417
- command: `akan workflow explain ${plan.workflow}`,
418
- reason: "Review action surface guidance before manually editing store or UI files.",
419
- },
420
- ],
421
- };
376
+ return surfaceReview(plan, {
377
+ code: "add-mutation-action-surface-review",
378
+ target: `*/${moduleSourcePaths(module).store}`,
379
+ action: `Name the endpoint's guards and add its dictionary .endpoint() entry with a .desc(). After sync, fetch.${mutation} exists; write a store action only for a toast, an optimistic update, or a multi-field write, and put the control that calls it in ${moduleClassName}.Util.tsx.`,
380
+ message: `Workflow apply does not write store or UI code for ${module}.${mutation}; review whether a screen should call it.`,
381
+ reason: "Review action surface guidance before manually editing store or UI files.",
382
+ });
422
383
  };
423
384
 
424
385
  const addSliceViewSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
425
386
  const module = workflowStringInput(plan.inputs.module) ?? "<module>";
426
387
  const slice = workflowStringInput(plan.inputs.slice) ?? "<slice>";
427
388
  const moduleClassName = moduleComponentName(module);
428
- return {
429
- recommendations: [
430
- {
431
- code: "add-slice-view-surface-review",
432
- kind: "manual-action",
433
- target: `*/${moduleSourcePaths(module).zone}`,
434
- action: `Fill in the service query stub and add the dictionary .slice() entry. After sync, load fetch.init${moduleClassName}${capitalize(slice)}() in the page and pass the result to a ${moduleClassName}.Zone as an init prop.`,
435
- confidence: "medium",
436
- message: `Workflow apply does not write page or Zone code for ${module}.${slice}; review where the list should render.`,
437
- },
438
- ],
439
- nextActions: [
440
- {
441
- command: `akan workflow explain ${plan.workflow}`,
442
- reason: "Review view surface guidance before manually editing Zone or page files.",
443
- },
444
- ],
445
- };
389
+ return surfaceReview(plan, {
390
+ code: "add-slice-view-surface-review",
391
+ target: `*/${moduleSourcePaths(module).zone}`,
392
+ action: `Fill in the service query stub and add the dictionary .slice() entry. After sync, load fetch.init${moduleClassName}${capitalize(slice)}() in the page and pass the result to a ${moduleClassName}.Zone as an init prop.`,
393
+ message: `Workflow apply does not write page or Zone code for ${module}.${slice}; review where the list should render.`,
394
+ reason: "Review view surface guidance before manually editing Zone or page files.",
395
+ });
446
396
  };
447
397
 
448
398
  export const createWorkflowStepRegistry = ({
@@ -581,9 +531,8 @@ export class WorkflowExecutor {
581
531
  const postApplyChecks: WorkflowPostApplyCheck[] = [];
582
532
  const recommendations = [...plan.recommendations];
583
533
  const nextActions: PrimitiveNextAction[] = [];
584
-
585
- if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
586
- return createWorkflowApplyReport({
534
+ const report = () =>
535
+ createWorkflowApplyReport({
587
536
  workflow: plan.workflow,
588
537
  mode: "apply",
589
538
  changedFiles,
@@ -595,7 +544,8 @@ export class WorkflowExecutor {
595
544
  nextActions,
596
545
  plan,
597
546
  });
598
- }
547
+
548
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return report();
599
549
 
600
550
  recommendedValidationCommands.push(...workflowCommandsForPlan(plan));
601
551
  nextActions.push(...workflowCommandsForPlan(plan));
@@ -646,17 +596,6 @@ export class WorkflowExecutor {
646
596
  );
647
597
  }
648
598
 
649
- return createWorkflowApplyReport({
650
- workflow: plan.workflow,
651
- mode: "apply",
652
- changedFiles,
653
- generatedFiles,
654
- recommendedValidationCommands,
655
- diagnostics,
656
- postApplyChecks,
657
- recommendations,
658
- nextActions,
659
- plan,
660
- });
599
+ return report();
661
600
  }
662
601
  }
package/workflow/index.ts CHANGED
@@ -2,7 +2,6 @@ export * from "./artifacts";
2
2
  export * from "./executor";
3
3
  export * from "./moduleIndex";
4
4
  export * from "./plan";
5
- export * from "./primitive";
6
5
  export * from "./render";
7
6
  export * from "./rolloutGate";
8
7
  export * from "./source";
@@ -1,31 +1,20 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
5
3
  import type { AkanModuleContext } from "../akanContext";
6
4
  import type { Workspace } from "../commandDecorators";
5
+ import { tempDirs, writeText } from "../testHelpers";
7
6
  import { buildAkanModuleContextIndex } from "./moduleIndex";
8
7
 
9
- const tempRoots: string[] = [];
10
-
11
- afterEach(async () => {
12
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
13
- });
8
+ const tempRoot = tempDirs("akan-module-index-");
14
9
 
15
10
  const makeWorkspace = async () => {
16
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-module-index-"));
17
- tempRoots.push(root);
11
+ const root = await tempRoot();
18
12
  return {
19
13
  root,
20
14
  workspace: { workspaceRoot: root } as Workspace,
21
15
  };
22
16
  };
23
17
 
24
- const writeText = async (filePath: string, content: string) => {
25
- await mkdir(path.dirname(filePath), { recursive: true });
26
- await writeFile(filePath, content);
27
- };
28
-
29
18
  const moduleContext = (files: string[], name = "post"): AkanModuleContext => ({
30
19
  kind: "domain",
31
20
  name,