@akanjs/cli 2.4.0 → 2.4.1-rc.1
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/agent.command-h4afc69n.js +26 -0
- package/application.command-47mj9qsy.js +165 -0
- package/applicationBuildRunner-yz144508.js +284 -0
- package/applicationReleasePackager-brvth6rs.js +245 -0
- package/capacitorApp-y0h6cgft.js +58 -0
- package/cloud.command-qgjxja3n.js +94 -0
- package/commandManifest.json +1 -0
- package/context.command-nqbtak4f.js +82 -0
- package/dependencyScanner-m4x5maek.js +9 -0
- package/guideline.command-ya0dh44f.js +356 -0
- package/incrementalBuilder.proc.js +89 -17394
- package/index-1xdrsbry.js +1447 -0
- package/index-45aj5ry0.js +990 -0
- package/index-5vvwc0cz.js +559 -0
- package/index-61keag0s.js +40 -0
- package/index-6pz1j0zj.js +62 -0
- package/index-73pr2cmy.js +534 -0
- package/index-76rn3g2c.js +76 -0
- package/index-77crfweb.js +1884 -0
- package/index-85msc0wg.js +161 -0
- package/index-8pkbzj26.js +840 -0
- package/index-8rc0bm04.js +514 -0
- package/index-a5rmdgy4.js +4359 -0
- package/index-a6sbyy0b.js +2769 -0
- package/index-fgc8r6dj.js +33 -0
- package/index-h6ca6qg0.js +2777 -0
- package/index-hdqztm58.js +758 -0
- package/index-hwzpw9c1.js +202 -0
- package/index-pmm9e2jf.js +120 -0
- package/index-qaq13qk3.js +80 -0
- package/index-qhtr07v8.js +1072 -0
- package/index-r24hmh0q.js +4 -0
- package/index-sgmas1fc.js +462 -0
- package/index-ss469dec.js +11 -0
- package/index-swf4bmbg.js +25 -0
- package/index-wnp7hwq7.js +193 -0
- package/index-wq8jwx8z.js +224 -0
- package/index-x53a5nya.js +301 -0
- package/index-y3hdhy4p.js +229 -0
- package/index-z9gvz7b0.js +83 -0
- package/index.js +54 -23340
- package/library.command-r15zdqvp.js +33 -0
- package/localRegistry.command-p1pgxw78.js +178 -0
- package/module.command-qrj3kmyz.js +54 -0
- package/package.command-r8sq5kzp.js +43 -0
- package/package.json +2 -2
- package/page.command-c6xdx0xm.js +24 -0
- package/primitive.command-pv9ssmtf.js +62 -0
- package/quality.command-es67wvdp.js +809 -0
- package/repair.command-677675vw.js +67 -0
- package/routeSourceValidator-wbhmbwpj.js +132 -0
- package/scalar.command-kabkd6wd.js +35 -0
- package/templates/facetIndex/index.ts +1 -1
- package/typeChecker-kravn7ns.js +8 -0
- package/typecheck.proc.js +4 -194
- package/workflow.command-64r6cw0w.js +108 -0
- package/workspace.command-xk68sd6c.js +435 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
workflowRunArtifactPath,
|
|
4
|
+
workflowSyncDir
|
|
5
|
+
} from "./index-h6ca6qg0.js";
|
|
6
|
+
import {
|
|
7
|
+
AppExecutor,
|
|
8
|
+
LibExecutor
|
|
9
|
+
} from "./index-a6sbyy0b.js";
|
|
10
|
+
import {
|
|
11
|
+
FileSys
|
|
12
|
+
} from "./index-61keag0s.js";
|
|
13
|
+
|
|
14
|
+
// pkgs/@akanjs/devkit/akanContext.ts
|
|
15
|
+
import { readdir } from "fs/promises";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { capitalize } from "akanjs/common";
|
|
18
|
+
var resourceList = [
|
|
19
|
+
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
20
|
+
{ uri: "akan://guidelines/framework", name: "Framework guideline", mimeType: "text/markdown" },
|
|
21
|
+
{ uri: "akan://guidelines/modelSignal", name: "Model signal guideline", mimeType: "text/markdown" },
|
|
22
|
+
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
23
|
+
{ uri: "akan://workspace/apps", name: "Workspace apps", mimeType: "application/json" },
|
|
24
|
+
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
25
|
+
];
|
|
26
|
+
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
27
|
+
var claudeMcpConfigPath = ".mcp.json";
|
|
28
|
+
var codexMcpConfigPath = ".codex/config.toml";
|
|
29
|
+
var akanMcpInstallTargets = ["cursor", "claude", "codex"];
|
|
30
|
+
var akanMcpInstallConfigPaths = {
|
|
31
|
+
cursor: cursorMcpConfigPath,
|
|
32
|
+
claude: claudeMcpConfigPath,
|
|
33
|
+
codex: codexMcpConfigPath
|
|
34
|
+
};
|
|
35
|
+
var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
36
|
+
var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
37
|
+
var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
38
|
+
var createAkanCursorMcpServer = (mode = "readonly") => ({
|
|
39
|
+
type: "stdio",
|
|
40
|
+
command: "bash",
|
|
41
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
|
|
42
|
+
});
|
|
43
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
44
|
+
type: "stdio",
|
|
45
|
+
command: "bash",
|
|
46
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
47
|
+
});
|
|
48
|
+
var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
49
|
+
var akanCursorMcpServer = createAkanCursorMcpServer();
|
|
50
|
+
var codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
51
|
+
var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
|
|
52
|
+
command = "bash"
|
|
53
|
+
args = ["-lc", "${akanMcpCommand(mode)}"]
|
|
54
|
+
`;
|
|
55
|
+
var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
56
|
+
var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
|
|
57
|
+
const nextBlock = block.endsWith(`
|
|
58
|
+
`) ? block : `${block}
|
|
59
|
+
`;
|
|
60
|
+
const match = existing.match(codexAkanTablePattern);
|
|
61
|
+
if (!match) {
|
|
62
|
+
if (!existing.trim())
|
|
63
|
+
return nextBlock;
|
|
64
|
+
return `${existing.replace(/\s*$/, "")}
|
|
65
|
+
|
|
66
|
+
${nextBlock}`;
|
|
67
|
+
}
|
|
68
|
+
if (match[0].trim() === nextBlock.trim())
|
|
69
|
+
return existing;
|
|
70
|
+
if (!force)
|
|
71
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
72
|
+
const start = match.index ?? 0;
|
|
73
|
+
const before = existing.slice(0, start);
|
|
74
|
+
const after = existing.slice(start + match[0].length);
|
|
75
|
+
const separator = after && !after.startsWith(`
|
|
76
|
+
`) ? `
|
|
77
|
+
` : "";
|
|
78
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
79
|
+
};
|
|
80
|
+
var renderDoctorText = (result) => {
|
|
81
|
+
const lines = [`Akan doctor status: ${result.status}`];
|
|
82
|
+
if (result.diagnostics.length === 0) {
|
|
83
|
+
lines.push("", "No Akan workspace diagnostics found.");
|
|
84
|
+
} else {
|
|
85
|
+
lines.push("", ...result.diagnostics.map((diagnostic) => [
|
|
86
|
+
`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
|
|
87
|
+
diagnostic.path ? ` ${diagnostic.path}` : ""
|
|
88
|
+
].filter(Boolean).join(`
|
|
89
|
+
`)));
|
|
90
|
+
}
|
|
91
|
+
lines.push("", "Generated file freshness:", `Status: ${result.generatedFilesFreshness.status}`, result.generatedFilesFreshness.message, `Refresh: ${result.generatedFilesFreshness.refreshCommand}`, "", "Repair actions:", ...result.repairActions.length ? result.repairActions.map((action) => `- ${action.command}`) : ["- none"], "", "Validation commands:", ...result.validationCommands.map((command) => `- ${command}`));
|
|
92
|
+
return `${lines.join(`
|
|
93
|
+
`)}
|
|
94
|
+
`;
|
|
95
|
+
};
|
|
96
|
+
var generatedFiles = [
|
|
97
|
+
"apps/*/client.ts",
|
|
98
|
+
"apps/*/server.ts",
|
|
99
|
+
"*/lib/cnst.ts",
|
|
100
|
+
"*/lib/db.ts",
|
|
101
|
+
"*/lib/dict.ts",
|
|
102
|
+
"*/lib/sig.ts",
|
|
103
|
+
"*/lib/srv.ts",
|
|
104
|
+
"*/lib/st.ts",
|
|
105
|
+
"*/lib/useClient.ts",
|
|
106
|
+
"*/lib/useServer.ts",
|
|
107
|
+
"*/lib/**/index.ts",
|
|
108
|
+
"*/ui/index.ts",
|
|
109
|
+
"*/webkit/index.ts",
|
|
110
|
+
"*/srvkit/index.ts",
|
|
111
|
+
"*/common/index.ts"
|
|
112
|
+
];
|
|
113
|
+
var validationCommands = [
|
|
114
|
+
"akan sync <app-or-lib>",
|
|
115
|
+
"akan lint <app-or-lib-or-pkg>",
|
|
116
|
+
"akan typecheck <app-name>",
|
|
117
|
+
"akan test <app-or-lib-or-pkg>",
|
|
118
|
+
"akan build <app-name>",
|
|
119
|
+
"akan doctor --strict --format json"
|
|
120
|
+
];
|
|
121
|
+
var unknownGeneratedFilesFreshness = {
|
|
122
|
+
status: "unknown",
|
|
123
|
+
message: "Run sync before validation so generated Akan files match the current source conventions.",
|
|
124
|
+
refreshCommand: "akan sync <app-or-lib>",
|
|
125
|
+
verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"]
|
|
126
|
+
};
|
|
127
|
+
var repairAction = (kind, command, reason, safeToRun) => ({
|
|
128
|
+
kind,
|
|
129
|
+
command,
|
|
130
|
+
reason,
|
|
131
|
+
safeToRun
|
|
132
|
+
});
|
|
133
|
+
var moduleShapeFiles = (module) => {
|
|
134
|
+
if (module.kind === "service") {
|
|
135
|
+
return [`${module.name}.dictionary.ts`, `${module.name}.service.ts`, `${module.name}.signal.ts`];
|
|
136
|
+
}
|
|
137
|
+
if (module.kind === "scalar")
|
|
138
|
+
return [`${module.name}.constant.ts`, `${module.name}.dictionary.ts`];
|
|
139
|
+
return [
|
|
140
|
+
`${module.name}.constant.ts`,
|
|
141
|
+
`${module.name}.dictionary.ts`,
|
|
142
|
+
`${module.name}.service.ts`,
|
|
143
|
+
`${module.name}.store.ts`,
|
|
144
|
+
`${module.name}.signal.ts`
|
|
145
|
+
];
|
|
146
|
+
};
|
|
147
|
+
var constantFieldNames = (content) => [...content.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*field\(/g)].map((match) => match[1]).filter(Boolean);
|
|
148
|
+
var appRootAllowFiles = new Set([
|
|
149
|
+
"akan.app.json",
|
|
150
|
+
"akan.config.ts",
|
|
151
|
+
"capacitor.config.ts",
|
|
152
|
+
"client.ts",
|
|
153
|
+
"main.ts",
|
|
154
|
+
"package.json",
|
|
155
|
+
"server.ts",
|
|
156
|
+
"tsconfig.json"
|
|
157
|
+
]);
|
|
158
|
+
var appRootAllowDirs = new Set([
|
|
159
|
+
".akan",
|
|
160
|
+
"android",
|
|
161
|
+
"common",
|
|
162
|
+
"env",
|
|
163
|
+
"ios",
|
|
164
|
+
"lib",
|
|
165
|
+
"page",
|
|
166
|
+
"private",
|
|
167
|
+
"public",
|
|
168
|
+
"script",
|
|
169
|
+
"srvkit",
|
|
170
|
+
"ui",
|
|
171
|
+
"webkit"
|
|
172
|
+
]);
|
|
173
|
+
var safeReadDir = async (dirPath) => {
|
|
174
|
+
try {
|
|
175
|
+
return (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var safeReadText = async (filePath) => {
|
|
181
|
+
try {
|
|
182
|
+
return await FileSys.readText(filePath);
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var safeReadJson = async (filePath) => {
|
|
188
|
+
try {
|
|
189
|
+
return await FileSys.readJson(filePath);
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var isWorkflowPlan = (value) => typeof value === "object" && value !== null && ("schemaVersion" in value) && value.schemaVersion === 1 && ("mode" in value) && value.mode === "plan";
|
|
195
|
+
var isWorkflowApplyReport = (value) => typeof value === "object" && value !== null && ("schemaVersion" in value) && value.schemaVersion === 1 && ("mode" in value) && (value.mode === "apply" || value.mode === "dry-run");
|
|
196
|
+
var isWorkflowRunArtifact = (value) => typeof value === "object" && value !== null && ("schemaVersion" in value) && value.schemaVersion === 1;
|
|
197
|
+
var planInputString = (plan, key) => {
|
|
198
|
+
const value = plan.inputs[key];
|
|
199
|
+
return typeof value === "string" ? value : "";
|
|
200
|
+
};
|
|
201
|
+
var expandWorkflowTarget = (target, plan) => {
|
|
202
|
+
const app = planInputString(plan, "app");
|
|
203
|
+
const module = planInputString(plan, "module");
|
|
204
|
+
const moduleClass = module ? capitalize(module) : "<Module>";
|
|
205
|
+
return target.replace(/^\*\//, app ? `apps/${app}/` : "").replaceAll("<module>", module || "<module>").replaceAll("<Module>", moduleClass);
|
|
206
|
+
};
|
|
207
|
+
var workflowPathsForPlan = (plan) => plan.predictedChanges.map((change) => expandWorkflowTarget(change.target, plan));
|
|
208
|
+
var workflowPathsForArtifact = (artifact) => {
|
|
209
|
+
if (isWorkflowPlan(artifact))
|
|
210
|
+
return workflowPathsForPlan(artifact);
|
|
211
|
+
if (isWorkflowApplyReport(artifact)) {
|
|
212
|
+
return [
|
|
213
|
+
...artifact.changedFiles.map((file) => file.path),
|
|
214
|
+
...artifact.generatedFiles.map((file) => file.path),
|
|
215
|
+
...workflowPathsForPlan(artifact.plan)
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
if ("mode" in artifact && artifact.mode === "validate" && artifact.plan)
|
|
219
|
+
return workflowPathsForPlan(artifact.plan);
|
|
220
|
+
return [];
|
|
221
|
+
};
|
|
222
|
+
var loadWorkflowContextPaths = async (workspace, runIdOrPlan, changedFiles) => {
|
|
223
|
+
const paths = [...changedFiles];
|
|
224
|
+
if (!runIdOrPlan)
|
|
225
|
+
return paths;
|
|
226
|
+
const inputPath = path.isAbsolute(runIdOrPlan) ? runIdOrPlan : path.join(workspace.workspaceRoot, runIdOrPlan);
|
|
227
|
+
const artifact = await safeReadJson(inputPath) ?? await safeReadJson(path.join(workspace.workspaceRoot, workflowRunArtifactPath(runIdOrPlan)));
|
|
228
|
+
if (artifact && isWorkflowRunArtifact(artifact))
|
|
229
|
+
paths.push(...workflowPathsForArtifact(artifact));
|
|
230
|
+
return [...new Set(paths.filter(Boolean))];
|
|
231
|
+
};
|
|
232
|
+
var pathKey = (value) => value.replaceAll("\\", "/").replaceAll("*", "").replace(/<[^>]+>/g, "").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
233
|
+
var isWorkflowRelatedDiagnostic = (diagnostic, workflowPaths) => {
|
|
234
|
+
if (!diagnostic.path)
|
|
235
|
+
return false;
|
|
236
|
+
const diagnosticPath = pathKey(diagnostic.path);
|
|
237
|
+
return workflowPaths.some((workflowPath) => {
|
|
238
|
+
const candidate = pathKey(workflowPath);
|
|
239
|
+
if (!candidate)
|
|
240
|
+
return false;
|
|
241
|
+
return diagnosticPath.startsWith(candidate) || candidate.startsWith(diagnosticPath) || diagnosticPath.includes(candidate);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
var readGeneratedSyncStates = async (workspace) => {
|
|
245
|
+
const syncDir = path.join(workspace.workspaceRoot, workflowSyncDir);
|
|
246
|
+
const entries = await safeReadDir(syncDir);
|
|
247
|
+
const states = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => safeReadJson(path.join(syncDir, entry.name))));
|
|
248
|
+
return states.filter((state) => !!state && state.schemaVersion === 1 && typeof state.target === "string" && typeof state.syncedAt === "string");
|
|
249
|
+
};
|
|
250
|
+
var generatedFreshnessFromStates = async (workspace) => {
|
|
251
|
+
const states = await readGeneratedSyncStates(workspace);
|
|
252
|
+
if (states.length === 0)
|
|
253
|
+
return unknownGeneratedFilesFreshness;
|
|
254
|
+
const targets = states.sort((a, b) => a.target.localeCompare(b.target)).map((state) => ({
|
|
255
|
+
target: state.target,
|
|
256
|
+
status: state.status === "passed" ? "fresh" : "stale",
|
|
257
|
+
lastSyncedAt: state.syncedAt,
|
|
258
|
+
runId: state.runId,
|
|
259
|
+
generatedFiles: state.generatedFiles.map((file) => file.path),
|
|
260
|
+
reason: state.status === "passed" ? `Generated files were refreshed by ${state.command}.` : `Last generated repair command failed: ${state.command}.`
|
|
261
|
+
}));
|
|
262
|
+
const lastFresh = targets.filter((target) => target.status === "fresh" && target.lastSyncedAt).sort((a, b) => (b.lastSyncedAt ?? "").localeCompare(a.lastSyncedAt ?? ""))[0];
|
|
263
|
+
return {
|
|
264
|
+
status: targets.some((target) => target.status === "fresh") ? "fresh" : "stale",
|
|
265
|
+
message: lastFresh ? `Generated files were refreshed for ${lastFresh.target} at ${lastFresh.lastSyncedAt}.` : "Generated sync state exists, but the last recorded repair did not pass.",
|
|
266
|
+
refreshCommand: "akan sync <app-or-lib>",
|
|
267
|
+
verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"],
|
|
268
|
+
targets
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
var parseAbstractSummary = (relativePath, content, includeContent) => {
|
|
272
|
+
if (content === null)
|
|
273
|
+
return { path: relativePath, exists: false, headings: [] };
|
|
274
|
+
const headings = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("#")).map((line) => line.replace(/^#+\s*/, "").trim()).filter(Boolean);
|
|
275
|
+
return {
|
|
276
|
+
path: relativePath,
|
|
277
|
+
exists: true,
|
|
278
|
+
title: headings[0],
|
|
279
|
+
headings: headings.slice(0, 8),
|
|
280
|
+
...includeContent ? { content } : {}
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
var readFiles = async (dirPath) => (await safeReadDir(dirPath)).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
284
|
+
var getRelative = (workspace, absolutePath) => path.relative(workspace.workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
285
|
+
var createModuleContext = async (workspace, sys, kind, folderName, moduleName, includeAbstractContent) => {
|
|
286
|
+
const modulePath = kind === "scalar" ? path.join(sys.cwdPath, "lib", "__scalar", moduleName) : path.join(sys.cwdPath, "lib", folderName);
|
|
287
|
+
const relativePath = getRelative(workspace, modulePath);
|
|
288
|
+
const abstractPath = `${relativePath}/${moduleName}.abstract.md`;
|
|
289
|
+
const abstractContent = await safeReadText(path.join(workspace.workspaceRoot, abstractPath));
|
|
290
|
+
return {
|
|
291
|
+
kind,
|
|
292
|
+
name: moduleName,
|
|
293
|
+
folderName,
|
|
294
|
+
sysName: sys.name,
|
|
295
|
+
sysType: sys.type,
|
|
296
|
+
path: relativePath,
|
|
297
|
+
abstract: parseAbstractSummary(abstractPath, abstractContent, includeAbstractContent),
|
|
298
|
+
files: await readFiles(modulePath)
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
var getSysModules = async (workspace, sys, {
|
|
302
|
+
includeAbstractContent = false,
|
|
303
|
+
module: moduleFilter
|
|
304
|
+
} = {}) => {
|
|
305
|
+
const libPath = path.join(sys.cwdPath, "lib");
|
|
306
|
+
const entries = await safeReadDir(libPath);
|
|
307
|
+
const modules = [];
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (!entry.isDirectory())
|
|
310
|
+
continue;
|
|
311
|
+
if (entry.name === "__scalar")
|
|
312
|
+
continue;
|
|
313
|
+
if (entry.name.startsWith("__"))
|
|
314
|
+
continue;
|
|
315
|
+
if (entry.name.startsWith("_")) {
|
|
316
|
+
const serviceName = entry.name.replace(/^_+/, "");
|
|
317
|
+
if (moduleFilter && moduleFilter !== serviceName && moduleFilter !== entry.name)
|
|
318
|
+
continue;
|
|
319
|
+
if (!await FileSys.fileExists(path.join(libPath, entry.name, `${serviceName}.service.ts`)))
|
|
320
|
+
continue;
|
|
321
|
+
modules.push(await createModuleContext(workspace, sys, "service", entry.name, serviceName, includeAbstractContent));
|
|
322
|
+
} else {
|
|
323
|
+
if (moduleFilter && moduleFilter !== entry.name)
|
|
324
|
+
continue;
|
|
325
|
+
if (!await FileSys.fileExists(path.join(libPath, entry.name, `${entry.name}.constant.ts`)))
|
|
326
|
+
continue;
|
|
327
|
+
modules.push(await createModuleContext(workspace, sys, "domain", entry.name, entry.name, includeAbstractContent));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const scalarRoot = path.join(libPath, "__scalar");
|
|
331
|
+
for (const entry of await safeReadDir(scalarRoot)) {
|
|
332
|
+
if (!entry.isDirectory() || entry.name.startsWith("_"))
|
|
333
|
+
continue;
|
|
334
|
+
if (moduleFilter && moduleFilter !== entry.name)
|
|
335
|
+
continue;
|
|
336
|
+
if (!await FileSys.fileExists(path.join(scalarRoot, entry.name, `${entry.name}.constant.ts`)))
|
|
337
|
+
continue;
|
|
338
|
+
modules.push(await createModuleContext(workspace, sys, "scalar", entry.name, entry.name, includeAbstractContent));
|
|
339
|
+
}
|
|
340
|
+
return modules.sort((a, b) => `${a.sysName}:${a.path}`.localeCompare(`${b.sysName}:${b.path}`));
|
|
341
|
+
};
|
|
342
|
+
var getSysContext = async (workspace, type, name, options) => {
|
|
343
|
+
const sys = type === "app" ? AppExecutor.from(workspace, name) : LibExecutor.from(workspace, name);
|
|
344
|
+
return {
|
|
345
|
+
type,
|
|
346
|
+
name,
|
|
347
|
+
path: `${type}s/${name}`,
|
|
348
|
+
hasConfig: await FileSys.fileExists(path.join(sys.cwdPath, "akan.config.ts")),
|
|
349
|
+
modules: await getSysModules(workspace, sys, {
|
|
350
|
+
includeAbstractContent: options.includeAbstractContent,
|
|
351
|
+
module: options.module
|
|
352
|
+
})
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
class AkanContextAnalyzer {
|
|
357
|
+
static async analyze(workspace, options = {}) {
|
|
358
|
+
const [appNames, libNames, pkgNames] = await workspace.getExecs();
|
|
359
|
+
const rootPackageJson = await safeReadJson(path.join(workspace.workspaceRoot, "package.json"));
|
|
360
|
+
const filteredApps = options.app ? appNames.filter((name) => name === options.app) : appNames;
|
|
361
|
+
const [apps, libs, pkgs] = await Promise.all([
|
|
362
|
+
Promise.all(filteredApps.map((name) => getSysContext(workspace, "app", name, options))),
|
|
363
|
+
Promise.all(libNames.map((name) => getSysContext(workspace, "lib", name, options))),
|
|
364
|
+
Promise.all(pkgNames.map(async (name) => {
|
|
365
|
+
const packageJson = await safeReadJson(path.join(workspace.workspaceRoot, "pkgs", name, "package.json"));
|
|
366
|
+
return {
|
|
367
|
+
name,
|
|
368
|
+
path: `pkgs/${name}`,
|
|
369
|
+
...packageJson?.version ? { version: packageJson.version } : {}
|
|
370
|
+
};
|
|
371
|
+
}))
|
|
372
|
+
]);
|
|
373
|
+
return {
|
|
374
|
+
schemaVersion: 1,
|
|
375
|
+
repoName: workspace.repoName,
|
|
376
|
+
root: workspace.workspaceRoot,
|
|
377
|
+
packageVersion: rootPackageJson?.dependencies?.akanjs ?? rootPackageJson?.devDependencies?.["@akanjs/devkit"],
|
|
378
|
+
apps,
|
|
379
|
+
libs,
|
|
380
|
+
pkgs,
|
|
381
|
+
generatedFiles,
|
|
382
|
+
validationCommands
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
static async doctor(workspace, {
|
|
386
|
+
strict = false,
|
|
387
|
+
runIdOrPlan = null,
|
|
388
|
+
changedFiles = []
|
|
389
|
+
} = {}) {
|
|
390
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
391
|
+
const workflowPaths = await loadWorkflowContextPaths(workspace, runIdOrPlan, changedFiles);
|
|
392
|
+
const diagnostics = [];
|
|
393
|
+
const repairActions = [
|
|
394
|
+
repairAction("generated", "akan repair generated --app <app-or-lib>", "Refresh generated Akan files.", true),
|
|
395
|
+
repairAction("format", "akan repair format --target <app-or-lib-or-pkg>", "Run the formatter/linter repair path.", true)
|
|
396
|
+
];
|
|
397
|
+
for (const app of context.apps) {
|
|
398
|
+
const appPath = path.join(workspace.workspaceRoot, app.path);
|
|
399
|
+
for (const entry of await safeReadDir(appPath)) {
|
|
400
|
+
const allowed = entry.isDirectory() ? appRootAllowDirs.has(entry.name) : appRootAllowFiles.has(entry.name);
|
|
401
|
+
if (!allowed) {
|
|
402
|
+
const action = repairAction("module-shape", `akan repair module-shape --app ${app.name}`, "Review app root shape and remove or move the unknown entry.", false);
|
|
403
|
+
diagnostics.push({
|
|
404
|
+
severity: "error",
|
|
405
|
+
code: "app-root-unknown-entry",
|
|
406
|
+
path: `${app.path}/${entry.name}`,
|
|
407
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in app root: ${app.path}/${entry.name}`,
|
|
408
|
+
repairActions: [action]
|
|
409
|
+
});
|
|
410
|
+
repairActions.push(action);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
415
|
+
for (const module of sys.modules) {
|
|
416
|
+
if (!module.abstract.exists) {
|
|
417
|
+
const action = repairAction("module-shape", `akan repair module-shape --app ${sys.name} --module ${module.name}`, "Create the missing module abstract or inspect required source files.", false);
|
|
418
|
+
diagnostics.push({
|
|
419
|
+
severity: strict ? "error" : "warning",
|
|
420
|
+
code: "module-abstract-missing",
|
|
421
|
+
path: module.abstract.path,
|
|
422
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} should include ${module.abstract.path}`,
|
|
423
|
+
repairActions: [action]
|
|
424
|
+
});
|
|
425
|
+
repairActions.push(action);
|
|
426
|
+
}
|
|
427
|
+
const missingFiles = moduleShapeFiles(module).filter((filename) => !module.files.includes(filename));
|
|
428
|
+
if (missingFiles.length) {
|
|
429
|
+
const action = repairAction("module-shape", `akan repair module-shape --app ${sys.name} --module ${module.name}`, "Review missing required module source files.", false);
|
|
430
|
+
diagnostics.push({
|
|
431
|
+
severity: "error",
|
|
432
|
+
code: "module-shape-invalid",
|
|
433
|
+
path: module.path,
|
|
434
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} is missing required files: ${missingFiles.join(", ")}`,
|
|
435
|
+
repairActions: [action]
|
|
436
|
+
});
|
|
437
|
+
repairActions.push(action);
|
|
438
|
+
}
|
|
439
|
+
if (module.kind !== "service" && module.files.includes(`${module.name}.dictionary.ts`)) {
|
|
440
|
+
const constantPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.constant.ts`);
|
|
441
|
+
const dictionaryPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.dictionary.ts`);
|
|
442
|
+
const [constantContent, dictionaryContent] = await Promise.all([
|
|
443
|
+
safeReadText(constantPath),
|
|
444
|
+
safeReadText(dictionaryPath)
|
|
445
|
+
]);
|
|
446
|
+
if (constantContent && dictionaryContent) {
|
|
447
|
+
for (const fieldName of constantFieldNames(constantContent)) {
|
|
448
|
+
if (new RegExp(`\\b${fieldName}\\s*:`).test(dictionaryContent))
|
|
449
|
+
continue;
|
|
450
|
+
const action = repairAction("dictionary", `akan repair dictionary --app ${sys.name} --module ${module.name}`, "Add missing dictionary labels for source constant fields.", false);
|
|
451
|
+
diagnostics.push({
|
|
452
|
+
severity: "warning",
|
|
453
|
+
code: "dictionary-label-missing",
|
|
454
|
+
path: `${module.path}/${module.name}.dictionary.ts`,
|
|
455
|
+
message: `Dictionary labels for ${sys.name}:${module.name}.${fieldName} were not found.`,
|
|
456
|
+
repairActions: [action]
|
|
457
|
+
});
|
|
458
|
+
repairActions.push(action);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const scopedDiagnostics = diagnostics.map((diagnostic) => ({
|
|
465
|
+
...diagnostic,
|
|
466
|
+
scope: workflowPaths.length ? isWorkflowRelatedDiagnostic(diagnostic, workflowPaths) ? "workflow" : "baseline" : diagnostic.scope,
|
|
467
|
+
context: workflowPaths.length ? { ...diagnostic.context, paths: workflowPaths } : diagnostic.context
|
|
468
|
+
}));
|
|
469
|
+
const workflowDiagnostics = scopedDiagnostics.filter((diagnostic) => diagnostic.scope === "workflow");
|
|
470
|
+
const baselineDiagnostics = scopedDiagnostics.filter((diagnostic) => diagnostic.scope === "baseline");
|
|
471
|
+
return {
|
|
472
|
+
schemaVersion: 1,
|
|
473
|
+
repoName: context.repoName,
|
|
474
|
+
root: context.root,
|
|
475
|
+
strict,
|
|
476
|
+
status: scopedDiagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed",
|
|
477
|
+
diagnostics: scopedDiagnostics,
|
|
478
|
+
generatedFiles: context.generatedFiles,
|
|
479
|
+
generatedFilesFreshness: await generatedFreshnessFromStates(workspace),
|
|
480
|
+
validationCommands: context.validationCommands,
|
|
481
|
+
repairActions,
|
|
482
|
+
...workflowPaths.length ? { baselineDiagnostics, workflowDiagnostics } : {}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
static findModules(context, moduleName, { app = null } = {}) {
|
|
486
|
+
const modules = [...context.apps, ...context.libs].filter((sys) => !app || sys.name === app).flatMap((sys) => sys.modules);
|
|
487
|
+
return moduleName ? modules.filter((module) => module.name === moduleName || module.folderName === moduleName) : modules;
|
|
488
|
+
}
|
|
489
|
+
static renderMarkdown(context, { module: moduleName } = {}) {
|
|
490
|
+
const lines = [`# Akan Workspace Context`, "", `- Repo: ${context.repoName}`, `- Root: ${context.root}`];
|
|
491
|
+
if (context.packageVersion)
|
|
492
|
+
lines.push(`- Akan version: ${context.packageVersion}`);
|
|
493
|
+
lines.push("", "## Apps", ...context.apps.map((app) => `- ${app.name}: ${app.modules.length} module(s)`));
|
|
494
|
+
lines.push("", "## Libraries", ...context.libs.map((lib) => `- ${lib.name}: ${lib.modules.length} module(s)`));
|
|
495
|
+
lines.push("", "## Packages", ...context.pkgs.map((pkg) => `- ${pkg.name}${pkg.version ? ` (${pkg.version})` : ""}`));
|
|
496
|
+
const modules = AkanContextAnalyzer.findModules(context, moduleName);
|
|
497
|
+
lines.push("", "## Modules");
|
|
498
|
+
for (const module of modules) {
|
|
499
|
+
lines.push("", `### ${module.sysName}:${module.name} (${module.kind})`, `- Path: ${module.path}`);
|
|
500
|
+
lines.push(`- Abstract: ${module.abstract.exists ? module.abstract.path : "missing"}`);
|
|
501
|
+
if (module.abstract.exists && module.abstract.content)
|
|
502
|
+
lines.push("", module.abstract.content.trim(), "");
|
|
503
|
+
else if (module.abstract.headings.length)
|
|
504
|
+
lines.push(`- Abstract headings: ${module.abstract.headings.join(", ")}`);
|
|
505
|
+
lines.push(`- Files: ${module.files.join(", ") || "none"}`);
|
|
506
|
+
}
|
|
507
|
+
lines.push("", "## Validation", ...context.validationCommands.map((command) => `- \`${command}\``));
|
|
508
|
+
return `${lines.join(`
|
|
509
|
+
`)}
|
|
510
|
+
`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export { resourceList, codexMcpConfigPath, akanMcpInstallTargets, akanMcpInstallConfigPaths, createAkanMcpServer, createAkanCodexMcpServerBlock, upsertCodexMcpServerBlock, renderDoctorText, AkanContextAnalyzer };
|