@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.
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
package/akanContext.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { capitalize } from "akanjs/common";
|
|
3
|
+
import { capitalize, isRecord } from "akanjs/common";
|
|
4
4
|
import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
|
|
5
5
|
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
6
6
|
import { FileSys } from "./fileSys";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type WorkflowApplyReport,
|
|
13
13
|
type WorkflowPlan,
|
|
14
14
|
type WorkflowRunArtifact,
|
|
15
|
+
workflowPathsForPlan,
|
|
15
16
|
workflowRunArtifactPath,
|
|
16
17
|
workflowSyncDir,
|
|
17
18
|
} from "./workflow";
|
|
@@ -122,8 +123,7 @@ export type JsonRpcRequest = {
|
|
|
122
123
|
export type McpFraming = "content-length" | "newline";
|
|
123
124
|
export type AkanMcpMode = "readonly" | "plan" | "apply";
|
|
124
125
|
|
|
125
|
-
//
|
|
126
|
-
// `mcpServers` map; Codex reads a TOML `[mcp_servers.<name>]` table.
|
|
126
|
+
// Cursor and Claude Code read a JSON `mcpServers` map; Codex reads a TOML `[mcp_servers.<name>]` table.
|
|
127
127
|
export type AkanMcpInstallTarget = "cursor" | "claude" | "codex";
|
|
128
128
|
|
|
129
129
|
export type CursorMcpConfig = {
|
|
@@ -132,11 +132,6 @@ export type CursorMcpConfig = {
|
|
|
132
132
|
|
|
133
133
|
export const guidelineResourceUri = (name: string) => `akan://guidelines/${name}`;
|
|
134
134
|
|
|
135
|
-
/**
|
|
136
|
-
* Resources that exist whatever the workspace holds. The guideline entries are **not** here: they are
|
|
137
|
-
* one per directory under `cli/guidelines/`, and a hardcoded pair listed 2 of 31 while `readResource`
|
|
138
|
-
* threw on the other 29. `buildResourceList` is the only place the two halves meet.
|
|
139
|
-
*/
|
|
140
135
|
const staticResourceList = [
|
|
141
136
|
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
142
137
|
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
@@ -167,42 +162,35 @@ export const akanMcpInstallConfigPaths: Record<AkanMcpInstallTarget, string> = {
|
|
|
167
162
|
codex: codexMcpConfigPath,
|
|
168
163
|
};
|
|
169
164
|
|
|
170
|
-
// `akan mcp` resolves the workspace from
|
|
171
|
-
//
|
|
172
|
-
// the server's cwd but sets CLAUDE_PROJECT_DIR in its environment, so we cd into that at runtime.
|
|
173
|
-
// Codex inherits its own launch cwd (it also discovers .codex/config.toml from cwd), so it runs the
|
|
174
|
-
// command directly and must be started from the workspace root.
|
|
165
|
+
// `akan mcp` resolves the workspace from its cwd: Cursor expands ${workspaceFolder}, Claude Code only sets
|
|
166
|
+
// CLAUDE_PROJECT_DIR (its cwd is not guaranteed), and Codex runs from its own launch cwd.
|
|
175
167
|
const cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
176
168
|
const claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
177
169
|
|
|
178
170
|
const akanMcpCommand = (mode: AkanMcpMode, { cd }: { cd?: string } = {}) =>
|
|
179
171
|
cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
180
172
|
|
|
181
|
-
|
|
173
|
+
const bashMcpServer = (mode: AkanMcpMode, cd: string) => ({
|
|
182
174
|
type: "stdio",
|
|
183
175
|
command: "bash",
|
|
184
|
-
args: ["-lc", akanMcpCommand(mode, { cd
|
|
176
|
+
args: ["-lc", akanMcpCommand(mode, { cd })],
|
|
185
177
|
});
|
|
186
178
|
|
|
187
|
-
export const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })],
|
|
191
|
-
});
|
|
179
|
+
export const createAkanCursorMcpServer = (mode: AkanMcpMode = "readonly") => bashMcpServer(mode, cursorWorkspaceFolder);
|
|
180
|
+
|
|
181
|
+
export const createAkanClaudeMcpServer = (mode: AkanMcpMode = "readonly") => bashMcpServer(mode, claudeProjectDir);
|
|
192
182
|
|
|
193
|
-
// JSON-config targets (Cursor, Claude Code) share the same `mcpServers` entry shape.
|
|
194
183
|
export const createAkanMcpServer = (target: "cursor" | "claude", mode: AkanMcpMode = "readonly") =>
|
|
195
184
|
target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
196
185
|
|
|
197
186
|
export const akanCursorMcpServer = createAkanCursorMcpServer();
|
|
198
187
|
|
|
199
|
-
//
|
|
188
|
+
// No TOML serializer is bundled, so the `[mcp_servers.akan]` table is written as text.
|
|
200
189
|
export const codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
201
190
|
export const createAkanCodexMcpServerBlock = (mode: AkanMcpMode = "readonly") =>
|
|
202
191
|
`${codexMcpServerTableHeader}\ncommand = "bash"\nargs = ["-lc", "${akanMcpCommand(mode)}"]\n`;
|
|
203
192
|
|
|
204
|
-
// A TOML table runs from its header until the next top-level `[header]` or EOF.
|
|
205
|
-
// akan table and preserve everything else in the file, mirroring the JSON merge behavior.
|
|
193
|
+
// A TOML table runs from its header until the next top-level `[header]` or EOF.
|
|
206
194
|
const codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
207
195
|
|
|
208
196
|
export const upsertCodexMcpServerBlock = (
|
|
@@ -331,29 +319,12 @@ const moduleShapeFiles = (module: AkanModuleContext) => {
|
|
|
331
319
|
const constantFieldNames = (content: string) =>
|
|
332
320
|
[...content.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*field\(/g)].map((match) => match[1]).filter(Boolean);
|
|
333
321
|
|
|
334
|
-
const safeReadDir = async (dirPath: string) =>
|
|
335
|
-
|
|
336
|
-
return (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
337
|
-
} catch {
|
|
338
|
-
return [];
|
|
339
|
-
}
|
|
340
|
-
};
|
|
322
|
+
const safeReadDir = async (dirPath: string) =>
|
|
323
|
+
(await readdir(dirPath, { withFileTypes: true }).catch(() => [])).sort((a, b) => a.name.localeCompare(b.name));
|
|
341
324
|
|
|
342
|
-
const safeReadText =
|
|
343
|
-
try {
|
|
344
|
-
return await FileSys.readText(filePath);
|
|
345
|
-
} catch {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
};
|
|
325
|
+
const safeReadText = (filePath: string) => FileSys.readText(filePath).catch(() => null);
|
|
349
326
|
|
|
350
|
-
const safeReadJson =
|
|
351
|
-
try {
|
|
352
|
-
return await FileSys.readJson<T>(filePath);
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
};
|
|
327
|
+
const safeReadJson = <T>(filePath: string) => FileSys.readJson<T>(filePath).catch(() => null);
|
|
357
328
|
|
|
358
329
|
const isWorkflowPlan = (value: unknown): value is WorkflowPlan =>
|
|
359
330
|
typeof value === "object" &&
|
|
@@ -361,7 +332,15 @@ const isWorkflowPlan = (value: unknown): value is WorkflowPlan =>
|
|
|
361
332
|
"schemaVersion" in value &&
|
|
362
333
|
value.schemaVersion === 1 &&
|
|
363
334
|
"mode" in value &&
|
|
364
|
-
value.mode === "plan"
|
|
335
|
+
value.mode === "plan" &&
|
|
336
|
+
"inputs" in value &&
|
|
337
|
+
isRecord(value.inputs) &&
|
|
338
|
+
"predictedChanges" in value &&
|
|
339
|
+
Array.isArray(value.predictedChanges) &&
|
|
340
|
+
value.predictedChanges.every((change) => isRecord(change) && typeof change.target === "string");
|
|
341
|
+
|
|
342
|
+
const isPathList = (value: unknown) =>
|
|
343
|
+
Array.isArray(value) && value.every((file) => isRecord(file) && typeof file.path === "string");
|
|
365
344
|
|
|
366
345
|
const isWorkflowApplyReport = (value: unknown): value is WorkflowApplyReport =>
|
|
367
346
|
typeof value === "object" &&
|
|
@@ -369,29 +348,17 @@ const isWorkflowApplyReport = (value: unknown): value is WorkflowApplyReport =>
|
|
|
369
348
|
"schemaVersion" in value &&
|
|
370
349
|
value.schemaVersion === 1 &&
|
|
371
350
|
"mode" in value &&
|
|
372
|
-
(value.mode === "apply" || value.mode === "dry-run")
|
|
351
|
+
(value.mode === "apply" || value.mode === "dry-run") &&
|
|
352
|
+
"changedFiles" in value &&
|
|
353
|
+
isPathList(value.changedFiles) &&
|
|
354
|
+
"generatedFiles" in value &&
|
|
355
|
+
isPathList(value.generatedFiles) &&
|
|
356
|
+
"plan" in value &&
|
|
357
|
+
isWorkflowPlan(value.plan);
|
|
373
358
|
|
|
374
359
|
const isWorkflowRunArtifact = (value: unknown): value is WorkflowRunArtifact =>
|
|
375
360
|
typeof value === "object" && value !== null && "schemaVersion" in value && value.schemaVersion === 1;
|
|
376
361
|
|
|
377
|
-
const planInputString = (plan: WorkflowPlan, key: string) => {
|
|
378
|
-
const value = plan.inputs[key];
|
|
379
|
-
return typeof value === "string" ? value : "";
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
const expandWorkflowTarget = (target: string, plan: WorkflowPlan) => {
|
|
383
|
-
const app = planInputString(plan, "app");
|
|
384
|
-
const module = planInputString(plan, "module");
|
|
385
|
-
const moduleClass = module ? capitalize(module) : "<Module>";
|
|
386
|
-
return target
|
|
387
|
-
.replace(/^\*\//, app ? `apps/${app}/` : "")
|
|
388
|
-
.replaceAll("<module>", module || "<module>")
|
|
389
|
-
.replaceAll("<Module>", moduleClass);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const workflowPathsForPlan = (plan: WorkflowPlan) =>
|
|
393
|
-
plan.predictedChanges.map((change) => expandWorkflowTarget(change.target, plan));
|
|
394
|
-
|
|
395
362
|
const workflowPathsForArtifact = (artifact: WorkflowRunArtifact) => {
|
|
396
363
|
if (isWorkflowPlan(artifact)) return workflowPathsForPlan(artifact);
|
|
397
364
|
if (isWorkflowApplyReport(artifact)) {
|
|
@@ -401,7 +368,8 @@ const workflowPathsForArtifact = (artifact: WorkflowRunArtifact) => {
|
|
|
401
368
|
...workflowPathsForPlan(artifact.plan),
|
|
402
369
|
];
|
|
403
370
|
}
|
|
404
|
-
if ("mode" in artifact && artifact.mode === "validate" &&
|
|
371
|
+
if ("mode" in artifact && artifact.mode === "validate" && isWorkflowPlan(artifact.plan))
|
|
372
|
+
return workflowPathsForPlan(artifact.plan);
|
|
405
373
|
return [];
|
|
406
374
|
};
|
|
407
375
|
|
|
@@ -635,9 +603,7 @@ export class AkanContextAnalyzer {
|
|
|
635
603
|
};
|
|
636
604
|
}
|
|
637
605
|
|
|
638
|
-
//
|
|
639
|
-
// keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
|
|
640
|
-
// running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
|
|
606
|
+
// Nothing re-renders the AGENTS.md block on `bun update`, so the version stamp is the only staleness signal.
|
|
641
607
|
static async #agentGuideDrift(workspace: WorkspaceExecutor) {
|
|
642
608
|
const installed = await readDevkitVersion();
|
|
643
609
|
if (!installed) return null;
|
|
@@ -679,6 +645,10 @@ export class AkanContextAnalyzer {
|
|
|
679
645
|
true,
|
|
680
646
|
),
|
|
681
647
|
];
|
|
648
|
+
const report = (diagnostic: AkanDiagnostic, action: RepairAction) => {
|
|
649
|
+
diagnostics.push({ ...diagnostic, repairActions: [action] });
|
|
650
|
+
repairActions.push(action);
|
|
651
|
+
};
|
|
682
652
|
|
|
683
653
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
684
654
|
const sysPath = path.join(workspace.workspaceRoot, sys.path);
|
|
@@ -687,73 +657,60 @@ export class AkanContextAnalyzer {
|
|
|
687
657
|
const allowed = entry.isDirectory()
|
|
688
658
|
? rootAllowedDirs[sys.type].has(entry.name)
|
|
689
659
|
: rootAllowedFiles[sys.type].has(entry.name);
|
|
690
|
-
if (!allowed)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
660
|
+
if (!allowed)
|
|
661
|
+
report(
|
|
662
|
+
{
|
|
663
|
+
severity: "error",
|
|
664
|
+
code: `${sys.type}-root-unknown-entry`,
|
|
665
|
+
path: `${sys.path}/${entry.name}`,
|
|
666
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
667
|
+
},
|
|
668
|
+
repairAction(
|
|
669
|
+
"module-shape",
|
|
670
|
+
`akan repair module-shape --app ${sys.name}`,
|
|
671
|
+
`Review ${sys.type} root shape and remove or move the unknown entry.`,
|
|
672
|
+
false,
|
|
673
|
+
),
|
|
696
674
|
);
|
|
697
|
-
diagnostics.push({
|
|
698
|
-
severity: "error",
|
|
699
|
-
code: `${sys.type}-root-unknown-entry`,
|
|
700
|
-
path: `${sys.path}/${entry.name}`,
|
|
701
|
-
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
702
|
-
repairActions: [action],
|
|
703
|
-
});
|
|
704
|
-
repairActions.push(action);
|
|
705
|
-
}
|
|
706
675
|
}
|
|
707
676
|
}
|
|
708
677
|
|
|
709
678
|
const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
|
|
710
|
-
if (agentDrift)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
path: "AGENTS.md",
|
|
716
|
-
message: agentDrift.message,
|
|
717
|
-
repairActions: [action],
|
|
718
|
-
});
|
|
719
|
-
repairActions.push(action);
|
|
720
|
-
}
|
|
679
|
+
if (agentDrift)
|
|
680
|
+
report(
|
|
681
|
+
{ severity: "warning", code: agentDrift.code, path: "AGENTS.md", message: agentDrift.message },
|
|
682
|
+
repairAction("generated", "akan agent install agents-md", agentDrift.hint, true),
|
|
683
|
+
);
|
|
721
684
|
|
|
722
685
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
723
686
|
for (const module of sys.modules) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
687
|
+
const moduleShapeCommand = `akan repair module-shape --app ${sys.name} --module ${module.name}`;
|
|
688
|
+
if (!module.abstract.exists)
|
|
689
|
+
report(
|
|
690
|
+
{
|
|
691
|
+
severity: strict ? "error" : "warning",
|
|
692
|
+
code: "module-abstract-missing",
|
|
693
|
+
path: module.abstract.path,
|
|
694
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} should include ${module.abstract.path}`,
|
|
695
|
+
},
|
|
696
|
+
repairAction(
|
|
697
|
+
"module-shape",
|
|
698
|
+
moduleShapeCommand,
|
|
699
|
+
"Create the missing module abstract or inspect required source files.",
|
|
700
|
+
false,
|
|
701
|
+
),
|
|
730
702
|
);
|
|
731
|
-
diagnostics.push({
|
|
732
|
-
severity: strict ? "error" : "warning",
|
|
733
|
-
code: "module-abstract-missing",
|
|
734
|
-
path: module.abstract.path,
|
|
735
|
-
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} should include ${module.abstract.path}`,
|
|
736
|
-
repairActions: [action],
|
|
737
|
-
});
|
|
738
|
-
repairActions.push(action);
|
|
739
|
-
}
|
|
740
703
|
const missingFiles = moduleShapeFiles(module).filter((filename) => !module.files.includes(filename));
|
|
741
|
-
if (missingFiles.length)
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
704
|
+
if (missingFiles.length)
|
|
705
|
+
report(
|
|
706
|
+
{
|
|
707
|
+
severity: "error",
|
|
708
|
+
code: "module-shape-invalid",
|
|
709
|
+
path: module.path,
|
|
710
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} is missing required files: ${missingFiles.join(", ")}`,
|
|
711
|
+
},
|
|
712
|
+
repairAction("module-shape", moduleShapeCommand, "Review missing required module source files.", false),
|
|
747
713
|
);
|
|
748
|
-
diagnostics.push({
|
|
749
|
-
severity: "error",
|
|
750
|
-
code: "module-shape-invalid",
|
|
751
|
-
path: module.path,
|
|
752
|
-
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} is missing required files: ${missingFiles.join(", ")}`,
|
|
753
|
-
repairActions: [action],
|
|
754
|
-
});
|
|
755
|
-
repairActions.push(action);
|
|
756
|
-
}
|
|
757
714
|
if (module.kind !== "service" && module.files.includes(`${module.name}.dictionary.ts`)) {
|
|
758
715
|
const constantPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.constant.ts`);
|
|
759
716
|
const dictionaryPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.dictionary.ts`);
|
|
@@ -764,28 +721,27 @@ export class AkanContextAnalyzer {
|
|
|
764
721
|
if (constantContent && dictionaryContent) {
|
|
765
722
|
for (const fieldName of constantFieldNames(constantContent)) {
|
|
766
723
|
if (new RegExp(`\\b${fieldName}\\s*:`).test(dictionaryContent)) continue;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
724
|
+
report(
|
|
725
|
+
{
|
|
726
|
+
severity: "warning",
|
|
727
|
+
code: "dictionary-label-missing",
|
|
728
|
+
path: `${module.path}/${module.name}.dictionary.ts`,
|
|
729
|
+
message: `Dictionary labels for ${sys.name}:${module.name}.${fieldName} were not found.`,
|
|
730
|
+
},
|
|
731
|
+
repairAction(
|
|
732
|
+
"dictionary",
|
|
733
|
+
`akan repair dictionary --app ${sys.name} --module ${module.name}`,
|
|
734
|
+
"Add missing dictionary labels for source constant fields.",
|
|
735
|
+
false,
|
|
736
|
+
),
|
|
772
737
|
);
|
|
773
|
-
diagnostics.push({
|
|
774
|
-
severity: "warning",
|
|
775
|
-
code: "dictionary-label-missing",
|
|
776
|
-
path: `${module.path}/${module.name}.dictionary.ts`,
|
|
777
|
-
message: `Dictionary labels for ${sys.name}:${module.name}.${fieldName} were not found.`,
|
|
778
|
-
repairActions: [action],
|
|
779
|
-
});
|
|
780
|
-
repairActions.push(action);
|
|
781
738
|
}
|
|
782
739
|
}
|
|
783
740
|
}
|
|
784
741
|
}
|
|
785
742
|
}
|
|
786
743
|
|
|
787
|
-
|
|
788
|
-
// 곳의 추이를 보이게 한다. 유입이 실제로 재발하면 그때 lint 승격을 검토한다 — 증거 기반 에스컬레이션.
|
|
744
|
+
//* Advisory only (warning): promoted to lint only if inline re-authoring actually recurs.
|
|
789
745
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
790
746
|
const sources = await collectRecipeSources(path.join(workspace.workspaceRoot, sys.path, "ui"), "ui");
|
|
791
747
|
if (sources.length === 0) continue;
|
|
@@ -815,11 +771,7 @@ export class AkanContextAnalyzer {
|
|
|
815
771
|
});
|
|
816
772
|
}
|
|
817
773
|
|
|
818
|
-
//
|
|
819
|
-
// from its index gets re-invented inline, and a name lingering in it gets imported and fails. The index is
|
|
820
|
-
// split by ownership: the root AGENTS.md `## Recipes` lists framework recipes only, and every app/lib lists
|
|
821
|
-
// what it may additionally import in its own AGENTS.md `## Recipes In Scope`. Doctor never writes, so this
|
|
822
|
-
// is the check that catches a *committed* stale index (lint/sync self-heal the working tree instead).
|
|
774
|
+
// Doctor never writes, so this is what catches a committed stale recipe index (lint and sync self-heal the tree).
|
|
823
775
|
const scanNames = async (uiDirPath: string, basename?: string) =>
|
|
824
776
|
new Set(scanRecipes(await collectRecipeSources(uiDirPath, "ui", basename)).map((info) => info.name));
|
|
825
777
|
const declaredByImport = new Map<string, Set<string>>();
|
|
@@ -853,24 +805,19 @@ export class AkanContextAnalyzer {
|
|
|
853
805
|
};
|
|
854
806
|
const pushIndexDiagnostic = (indexPath: string, missing: string[], stale: string[], repairCommand: string) => {
|
|
855
807
|
if (missing.length === 0 && stale.length === 0) return;
|
|
856
|
-
const action = repairAction(
|
|
857
|
-
"generated",
|
|
858
|
-
repairCommand,
|
|
859
|
-
"Regenerate the recipe index from the scanned recipes.",
|
|
860
|
-
true,
|
|
861
|
-
);
|
|
862
808
|
const parts = [
|
|
863
809
|
missing.length > 0 ? `${missing.length} declared but unlisted (${missing.slice(0, 5).join(", ")})` : "",
|
|
864
810
|
stale.length > 0 ? `${stale.length} listed but gone (${stale.slice(0, 5).join(", ")})` : "",
|
|
865
811
|
].filter(Boolean);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
812
|
+
report(
|
|
813
|
+
{
|
|
814
|
+
severity: "error",
|
|
815
|
+
code: "recipe-index-stale",
|
|
816
|
+
path: indexPath,
|
|
817
|
+
message: `${indexPath} recipe index is out of date — ${parts.join("; ")}. Agents read this list as authoritative.`,
|
|
818
|
+
},
|
|
819
|
+
repairAction("generated", repairCommand, "Regenerate the recipe index from the scanned recipes.", true),
|
|
820
|
+
);
|
|
874
821
|
};
|
|
875
822
|
const frameworkDeclared = declaredByImport.get("akanjs/ui") ?? new Set<string>();
|
|
876
823
|
if (frameworkDeclared.size > 0) {
|
package/akanMcpContract.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { isRecord } from "akanjs/common";
|
|
3
|
+
import { AkanContextAnalyzer, type AkanDiagnostic, type AkanModuleContext } from "./akanContext";
|
|
3
4
|
import type { Workspace } from "./commandDecorators";
|
|
4
5
|
import type {
|
|
5
6
|
AkanContextEvidence,
|
|
@@ -107,9 +108,6 @@ const inspectAkanContextInputSchema = {
|
|
|
107
108
|
required: ["question", "draft", "review", "request"],
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
111
|
-
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
|
-
|
|
113
111
|
const slugPart = (value: unknown) =>
|
|
114
112
|
typeof value === "string"
|
|
115
113
|
? value
|
|
@@ -124,12 +122,6 @@ const optionalString = (args: Record<string, unknown>, key: string) => {
|
|
|
124
122
|
return typeof value === "string" && value ? value : undefined;
|
|
125
123
|
};
|
|
126
124
|
|
|
127
|
-
const nestedStringArg = (args: Record<string, unknown>, key: string) => {
|
|
128
|
-
const value = args[key];
|
|
129
|
-
if (typeof value !== "string" || !value) throw new Error(`MCP tool argument "${key}" is required.`);
|
|
130
|
-
return value;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
125
|
const isInspectAkanContextRequestType = (value: unknown): value is InspectAkanContextRequest["type"] =>
|
|
134
126
|
typeof value === "string" && inspectAkanContextRequestTypes.includes(value as InspectAkanContextRequest["type"]);
|
|
135
127
|
|
|
@@ -192,49 +184,30 @@ export const inspectAkanContextPropsArg = (args: Record<string, unknown>): Inspe
|
|
|
192
184
|
if (!isInspectAkanContextRequestType(args.request.type)) {
|
|
193
185
|
throw new Error('MCP tool argument "request.type" must be a supported inspect_akan_context request type.');
|
|
194
186
|
}
|
|
195
|
-
const
|
|
187
|
+
const base = { question, draft: { reason: stringArg(args.draft, "reason"), type: args.draft.type }, review };
|
|
196
188
|
const request = args.request;
|
|
197
|
-
if (request.type === "workspaceOverview") return {
|
|
189
|
+
if (request.type === "workspaceOverview") return { ...base, request: { type: request.type } };
|
|
198
190
|
if (request.type === "moduleContext")
|
|
199
191
|
return {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
review,
|
|
203
|
-
request: {
|
|
204
|
-
type: request.type,
|
|
205
|
-
app: nestedStringArg(request, "app"),
|
|
206
|
-
module: nestedStringArg(request, "module"),
|
|
207
|
-
},
|
|
192
|
+
...base,
|
|
193
|
+
request: { type: request.type, app: stringArg(request, "app"), module: stringArg(request, "module") },
|
|
208
194
|
};
|
|
209
195
|
if (request.type === "fieldInsertionContext")
|
|
210
196
|
return {
|
|
211
|
-
|
|
212
|
-
draft,
|
|
213
|
-
review,
|
|
197
|
+
...base,
|
|
214
198
|
request: {
|
|
215
199
|
type: request.type,
|
|
216
|
-
app:
|
|
217
|
-
module:
|
|
218
|
-
field:
|
|
219
|
-
fieldType:
|
|
200
|
+
app: stringArg(request, "app"),
|
|
201
|
+
module: stringArg(request, "module"),
|
|
202
|
+
field: stringArg(request, "field"),
|
|
203
|
+
fieldType: stringArg(request, "fieldType"),
|
|
220
204
|
},
|
|
221
205
|
};
|
|
222
206
|
if (request.type === "workflowDiagnostics")
|
|
223
|
-
return {
|
|
224
|
-
question,
|
|
225
|
-
draft,
|
|
226
|
-
review,
|
|
227
|
-
request: { type: request.type, runIdOrPlan: nestedStringArg(request, "runIdOrPlan") },
|
|
228
|
-
};
|
|
207
|
+
return { ...base, request: { type: request.type, runIdOrPlan: stringArg(request, "runIdOrPlan") } };
|
|
229
208
|
return {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
review,
|
|
233
|
-
request: {
|
|
234
|
-
type: "escape",
|
|
235
|
-
reason: nestedStringArg(request, "reason"),
|
|
236
|
-
nextStep: optionalString(request, "nextStep"),
|
|
237
|
-
},
|
|
209
|
+
...base,
|
|
210
|
+
request: { type: "escape", reason: stringArg(request, "reason"), nextStep: optionalString(request, "nextStep") },
|
|
238
211
|
};
|
|
239
212
|
};
|
|
240
213
|
|
|
@@ -391,11 +364,7 @@ export const applyMcpTools: McpToolDefinition[] = [
|
|
|
391
364
|
},
|
|
392
365
|
];
|
|
393
366
|
|
|
394
|
-
|
|
395
|
-
* `guidelineNames` fills the `get_guideline` enum. Without it the argument is a bare string and a model
|
|
396
|
-
* has to guess one of the ~30 directory names, which it cannot see from here — the names are a
|
|
397
|
-
* filesystem listing, so they can only arrive from a caller that already read it.
|
|
398
|
-
*/
|
|
367
|
+
// `guidelineNames` fills the `get_guideline` enum; they are a filesystem listing only the caller has read.
|
|
399
368
|
export const listAkanMcpTools = (
|
|
400
369
|
mode: "readonly" | "plan" | "apply" = "readonly",
|
|
401
370
|
{ guidelineNames = [] }: { guidelineNames?: readonly string[] } = {},
|
|
@@ -523,29 +492,19 @@ export const inspectAkanContext = async (
|
|
|
523
492
|
strict: true,
|
|
524
493
|
runIdOrPlan: workspacePath(workspace, props.request.runIdOrPlan),
|
|
525
494
|
});
|
|
495
|
+
const toWorkflowDiagnostic = (
|
|
496
|
+
{ severity, code, message, scope }: AkanDiagnostic,
|
|
497
|
+
context: AkanDiagnostic["context"],
|
|
498
|
+
): WorkflowDiagnostic => ({ severity, code, message, scope, context });
|
|
526
499
|
const baselineSummary = createWorkflowBaselineSummary(
|
|
527
|
-
(doctor.baselineDiagnostics ?? []).map(
|
|
528
|
-
(diagnostic): WorkflowDiagnostic => ({
|
|
529
|
-
severity: diagnostic.severity,
|
|
530
|
-
code: diagnostic.code,
|
|
531
|
-
message: diagnostic.message,
|
|
532
|
-
scope: diagnostic.scope,
|
|
533
|
-
context: diagnostic.context,
|
|
534
|
-
}),
|
|
535
|
-
),
|
|
500
|
+
(doctor.baselineDiagnostics ?? []).map((diagnostic) => toWorkflowDiagnostic(diagnostic, diagnostic.context)),
|
|
536
501
|
{ detailsIncluded: false },
|
|
537
502
|
);
|
|
538
503
|
diagnostics.push(
|
|
539
|
-
...(doctor.workflowDiagnostics ?? doctor.diagnostics).map(
|
|
540
|
-
(diagnostic
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
message: diagnostic.message,
|
|
544
|
-
scope: diagnostic.scope,
|
|
545
|
-
context: {
|
|
546
|
-
...diagnostic.context,
|
|
547
|
-
...(diagnostic.path ? { paths: [diagnostic.path] } : {}),
|
|
548
|
-
},
|
|
504
|
+
...(doctor.workflowDiagnostics ?? doctor.diagnostics).map((diagnostic) =>
|
|
505
|
+
toWorkflowDiagnostic(diagnostic, {
|
|
506
|
+
...diagnostic.context,
|
|
507
|
+
...(diagnostic.path ? { paths: [diagnostic.path] } : {}),
|
|
549
508
|
}),
|
|
550
509
|
),
|
|
551
510
|
);
|
|
@@ -574,15 +533,11 @@ export const inspectAkanContext = async (
|
|
|
574
533
|
);
|
|
575
534
|
}
|
|
576
535
|
|
|
536
|
+
const moduleRequest =
|
|
537
|
+
props.request.type === "moduleContext" || props.request.type === "fieldInsertionContext" ? props.request : null;
|
|
577
538
|
const context = await AkanContextAnalyzer.analyze(workspace, {
|
|
578
|
-
app:
|
|
579
|
-
|
|
580
|
-
? props.request.app
|
|
581
|
-
: null,
|
|
582
|
-
module:
|
|
583
|
-
props.request.type === "moduleContext" || props.request.type === "fieldInsertionContext"
|
|
584
|
-
? props.request.module
|
|
585
|
-
: null,
|
|
539
|
+
app: moduleRequest?.app ?? null,
|
|
540
|
+
module: moduleRequest?.module ?? null,
|
|
586
541
|
includeAbstractContent: false,
|
|
587
542
|
});
|
|
588
543
|
|
package/appSelectionMemory.ts
CHANGED
|
@@ -5,10 +5,7 @@ interface StoredSelection {
|
|
|
5
5
|
apps: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
* The apps the last interactive pick chose, so `akan start` + Enter repeats a multi-app session instead
|
|
10
|
-
* of re-ticking it. Advisory only: a missing, unreadable or stale file just means nothing is pre-ticked.
|
|
11
|
-
*/
|
|
8
|
+
// Advisory only: a missing, unreadable or stale file just means nothing is pre-ticked.
|
|
12
9
|
export class AppSelectionMemory {
|
|
13
10
|
static pathIn(workspaceRoot: string) {
|
|
14
11
|
return path.join(workspaceRoot, "local", ".akan", "lastStart.json");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { Logger } from "akanjs/common";
|
|
2
4
|
|
|
3
5
|
export interface ApplicationBuildPhaseResult {
|
|
@@ -35,12 +37,12 @@ export class ApplicationBuildReporter {
|
|
|
35
37
|
Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
static formatError(error: unknown): string {
|
|
40
|
+
static formatError(error: unknown, workspaceRoot = process.cwd()): string {
|
|
39
41
|
if (error instanceof AggregateError) {
|
|
40
42
|
const nestedMessages = error.errors
|
|
41
43
|
.map(
|
|
42
44
|
(nestedError, index) =>
|
|
43
|
-
ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`,
|
|
45
|
+
ApplicationBuildReporter.formatError(nestedError, workspaceRoot).trim() || `Unknown error ${index + 1}`,
|
|
44
46
|
)
|
|
45
47
|
.map((message) => message.replace(/^/gm, " "))
|
|
46
48
|
.join("\n");
|
|
@@ -48,13 +50,43 @@ export class ApplicationBuildReporter {
|
|
|
48
50
|
return nestedMessages ? `${error.message}\n${nestedMessages}` : error.message;
|
|
49
51
|
}
|
|
50
52
|
if (error instanceof Error) {
|
|
51
|
-
const causeMessage = error.cause
|
|
52
|
-
|
|
53
|
+
const causeMessage = error.cause
|
|
54
|
+
? `\nCaused by: ${ApplicationBuildReporter.formatError(error.cause, workspaceRoot)}`
|
|
55
|
+
: "";
|
|
56
|
+
return `${ApplicationBuildReporter.#withLocation(error.message, error, workspaceRoot)}${causeMessage}`;
|
|
53
57
|
}
|
|
54
|
-
if (typeof error === "object" && error !== null && "message" in error)
|
|
58
|
+
if (typeof error === "object" && error !== null && "message" in error)
|
|
59
|
+
return ApplicationBuildReporter.#withLocation(String(error.message), error, workspaceRoot);
|
|
55
60
|
return String(error);
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
static #withLocation(message: string, error: object, workspaceRoot: string): string {
|
|
64
|
+
const { position } = error as { position?: { file: string; line: number; column: number } | null };
|
|
65
|
+
if (!position?.file) return message;
|
|
66
|
+
const file = ApplicationBuildReporter.#relativeToWorkspace(position.file, workspaceRoot);
|
|
67
|
+
const location = position.line > 0 ? `${file}:${position.line}:${position.column}` : file;
|
|
68
|
+
return message.includes(location) ? message : message.replace(/^.*/, (headline) => `${headline} (${location})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Bun names the file by its realpath, so a workspace root reached through a symlink is matched by its own too.
|
|
72
|
+
static #relativeToWorkspace(file: string, workspaceRoot: string): string {
|
|
73
|
+
if (!path.isAbsolute(file)) return file;
|
|
74
|
+
for (const root of [workspaceRoot, ApplicationBuildReporter.#realpathOf(workspaceRoot)]) {
|
|
75
|
+
const relative = path.relative(root, file);
|
|
76
|
+
if (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
77
|
+
return relative.replaceAll(path.sep, "/");
|
|
78
|
+
}
|
|
79
|
+
return file;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static #realpathOf(dir: string): string {
|
|
83
|
+
try {
|
|
84
|
+
return realpathSync(dir);
|
|
85
|
+
} catch {
|
|
86
|
+
return dir;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
static formatDuration(ms: number): string {
|
|
59
91
|
if (ms < 1000) return `${ms}ms`;
|
|
60
92
|
if (ms < 60_000) return `${Math.round(ms / 100) / 10}s`;
|