@akanjs/devkit 2.3.11-rc.5 → 2.3.11-rc.7
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/akanContext.ts +67 -2
- package/executors.ts +6 -0
- package/lint/no-redeclare-predefined-endpoint.grit +6 -4
- package/package.json +2 -2
- package/qualityScanner.ts +42 -6
- package/workflow/artifacts.ts +0 -1
package/akanContext.ts
CHANGED
|
@@ -119,6 +119,10 @@ export type JsonRpcRequest = {
|
|
|
119
119
|
export type McpFraming = "content-length" | "newline";
|
|
120
120
|
export type AkanMcpMode = "readonly" | "plan" | "apply";
|
|
121
121
|
|
|
122
|
+
// Coding-agent tools that can host the Akan MCP server. Cursor and Claude Code both read a JSON
|
|
123
|
+
// `mcpServers` map; Codex reads a TOML `[mcp_servers.<name>]` table.
|
|
124
|
+
export type AkanMcpInstallTarget = "cursor" | "claude" | "codex";
|
|
125
|
+
|
|
122
126
|
export type CursorMcpConfig = {
|
|
123
127
|
mcpServers?: Record<string, unknown>;
|
|
124
128
|
};
|
|
@@ -133,17 +137,79 @@ export const resourceList = [
|
|
|
133
137
|
];
|
|
134
138
|
|
|
135
139
|
export const cursorMcpConfigPath = ".cursor/mcp.json";
|
|
140
|
+
// Claude Code reads project-scoped MCP servers from `.mcp.json` at the workspace root.
|
|
141
|
+
export const claudeMcpConfigPath = ".mcp.json";
|
|
142
|
+
// Codex reads project-scoped config (trusted projects) from `.codex/config.toml`.
|
|
143
|
+
export const codexMcpConfigPath = ".codex/config.toml";
|
|
144
|
+
|
|
145
|
+
export const akanMcpInstallTargets: AkanMcpInstallTarget[] = ["cursor", "claude", "codex"];
|
|
146
|
+
|
|
147
|
+
export const akanMcpInstallConfigPaths: Record<AkanMcpInstallTarget, string> = {
|
|
148
|
+
cursor: cursorMcpConfigPath,
|
|
149
|
+
claude: claudeMcpConfigPath,
|
|
150
|
+
codex: codexMcpConfigPath,
|
|
151
|
+
};
|
|
136
152
|
|
|
153
|
+
// `akan mcp` resolves the workspace from process.cwd(), so every launcher must run it from the
|
|
154
|
+
// workspace root. Cursor expands its own ${workspaceFolder} variable. Claude Code does not guarantee
|
|
155
|
+
// the server's cwd but sets CLAUDE_PROJECT_DIR in its environment, so we cd into that at runtime.
|
|
156
|
+
// Codex inherits its own launch cwd (it also discovers .codex/config.toml from cwd), so it runs the
|
|
157
|
+
// command directly and must be started from the workspace root.
|
|
137
158
|
const cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
159
|
+
const claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
160
|
+
|
|
161
|
+
const akanMcpCommand = (mode: AkanMcpMode, { cd }: { cd?: string } = {}) =>
|
|
162
|
+
cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
138
163
|
|
|
139
164
|
export const createAkanCursorMcpServer = (mode: AkanMcpMode = "readonly") => ({
|
|
140
165
|
type: "stdio",
|
|
141
166
|
command: "bash",
|
|
142
|
-
args: ["-lc",
|
|
167
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const createAkanClaudeMcpServer = (mode: AkanMcpMode = "readonly") => ({
|
|
171
|
+
type: "stdio",
|
|
172
|
+
command: "bash",
|
|
173
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })],
|
|
143
174
|
});
|
|
144
175
|
|
|
176
|
+
// JSON-config targets (Cursor, Claude Code) share the same `mcpServers` entry shape.
|
|
177
|
+
export const createAkanMcpServer = (target: "cursor" | "claude", mode: AkanMcpMode = "readonly") =>
|
|
178
|
+
target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
179
|
+
|
|
145
180
|
export const akanCursorMcpServer = createAkanCursorMcpServer();
|
|
146
181
|
|
|
182
|
+
// Codex config is TOML and we have no TOML serializer, so we build the `[mcp_servers.akan]` table as text.
|
|
183
|
+
export const codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
184
|
+
export const createAkanCodexMcpServerBlock = (mode: AkanMcpMode = "readonly") =>
|
|
185
|
+
`${codexMcpServerTableHeader}\ncommand = "bash"\nargs = ["-lc", "${akanMcpCommand(mode)}"]\n`;
|
|
186
|
+
|
|
187
|
+
// A TOML table runs from its header until the next top-level `[header]` or EOF. We upsert only the
|
|
188
|
+
// akan table and preserve everything else in the file, mirroring the JSON merge behavior.
|
|
189
|
+
const codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
190
|
+
|
|
191
|
+
export const upsertCodexMcpServerBlock = (
|
|
192
|
+
existing: string,
|
|
193
|
+
block: string,
|
|
194
|
+
{ force = false }: { force?: boolean } = {},
|
|
195
|
+
) => {
|
|
196
|
+
const nextBlock = block.endsWith("\n") ? block : `${block}\n`;
|
|
197
|
+
const match = existing.match(codexAkanTablePattern);
|
|
198
|
+
if (!match) {
|
|
199
|
+
if (!existing.trim()) return nextBlock;
|
|
200
|
+
return `${existing.replace(/\s*$/, "")}\n\n${nextBlock}`;
|
|
201
|
+
}
|
|
202
|
+
if (match[0].trim() === nextBlock.trim()) return existing;
|
|
203
|
+
if (!force)
|
|
204
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
205
|
+
const start = match.index ?? 0;
|
|
206
|
+
const before = existing.slice(0, start);
|
|
207
|
+
const after = existing.slice(start + match[0].length);
|
|
208
|
+
// The matched table absorbed its trailing blank line, so re-insert one before any following table.
|
|
209
|
+
const separator = after && !after.startsWith("\n") ? "\n" : "";
|
|
210
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
211
|
+
};
|
|
212
|
+
|
|
147
213
|
export const renderDoctorText = (result: AkanDoctorResult) => {
|
|
148
214
|
const lines = [`Akan doctor status: ${result.status}`];
|
|
149
215
|
if (result.diagnostics.length === 0) {
|
|
@@ -189,7 +255,6 @@ const generatedFiles = [
|
|
|
189
255
|
"*/lib/cnst.ts",
|
|
190
256
|
"*/lib/db.ts",
|
|
191
257
|
"*/lib/dict.ts",
|
|
192
|
-
"*/lib/option.ts",
|
|
193
258
|
"*/lib/sig.ts",
|
|
194
259
|
"*/lib/srv.ts",
|
|
195
260
|
"*/lib/st.ts",
|
package/executors.ts
CHANGED
|
@@ -1392,6 +1392,12 @@ export class AppExecutor extends SysExecutor {
|
|
|
1392
1392
|
...routeEnv,
|
|
1393
1393
|
...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
|
|
1394
1394
|
});
|
|
1395
|
+
// `start` spawns subprocesses that carry `env`, but `build` runs its phases in this same process and
|
|
1396
|
+
// reads `process.env` directly. Publish the resolved env here so SSR/CSR bundling (fed by getPublicEnv,
|
|
1397
|
+
// which filters to AKAN_PUBLIC_*) sees AKAN_PUBLIC_APP_NAME — otherwise SSR throws
|
|
1398
|
+
// "environment variable AKAN_PUBLIC_APP_NAME is required". Only AKAN_PUBLIC_* is baked into bundles, so
|
|
1399
|
+
// this does not leak non-public env.
|
|
1400
|
+
if (type === "build") Object.assign(process.env, env);
|
|
1395
1401
|
return { env };
|
|
1396
1402
|
}
|
|
1397
1403
|
#publicEnv: Record<string, string> | null = null;
|
|
@@ -2,8 +2,10 @@ engine biome(1.0)
|
|
|
2
2
|
language js(typescript, jsx)
|
|
3
3
|
|
|
4
4
|
// Every model already gets auto-generated CRUD endpoints in its slice fetch
|
|
5
|
-
// contract (see pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts
|
|
6
|
-
//
|
|
5
|
+
// contract (see pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts and
|
|
6
|
+
// pkgs/akanjs/fetch/client/fetchClient.ts):
|
|
7
|
+
// <refName>, light<Model>, create<Model>, update<Model>, remove<Model>,
|
|
8
|
+
// view<Model>, edit<Model>, merge<Model>.
|
|
7
9
|
// Re-declaring one of them inside the `endpoint(...)` block of a
|
|
8
10
|
// `<model>.signal.ts` file shadows the framework contract and can pass
|
|
9
11
|
// sync/typecheck/build while failing at runtime, so flag it here.
|
|
@@ -15,12 +17,12 @@ language js(typescript, jsx)
|
|
|
15
17
|
$filename <: r".*/([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
|
|
16
18
|
or {
|
|
17
19
|
$key <: $model,
|
|
18
|
-
$key <: r"(?:light|create|update|remove)([A-Z][a-zA-Z0-9]*)"($keyCap) where {
|
|
20
|
+
$key <: r"(?:light|create|update|remove|view|edit|merge)([A-Z][a-zA-Z0-9]*)"($keyCap) where {
|
|
19
21
|
$keyCap <: $Cap
|
|
20
22
|
}
|
|
21
23
|
},
|
|
22
24
|
register_diagnostic(
|
|
23
25
|
span = $key,
|
|
24
|
-
message = "This endpoint name collides with an auto-generated CRUD endpoint (<model>, light/create/update/remove<Model>). Rename it or move the logic into the service; do not re-declare predefined endpoints."
|
|
26
|
+
message = "This endpoint name collides with an auto-generated CRUD endpoint (<model>, light/create/update/remove/view/edit/merge<Model>). Rename it or move the logic into the service; do not re-declare predefined endpoints."
|
|
25
27
|
)
|
|
26
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.11-rc.
|
|
3
|
+
"version": "2.3.11-rc.7",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.3.11-rc.
|
|
35
|
+
"akanjs": "2.3.11-rc.7",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "5.5.23",
|
package/qualityScanner.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface ExportedFunctionLike {
|
|
|
37
37
|
file: string;
|
|
38
38
|
line: number;
|
|
39
39
|
bodyFingerprint?: string;
|
|
40
|
+
duplicateNameExempt: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
interface TopLevelDeclaration {
|
|
@@ -177,7 +178,8 @@ export class AkanQualityScanner {
|
|
|
177
178
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
178
179
|
const warnings: QualityWarning[] = [];
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
182
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
181
183
|
if (declarations.length < 2) continue;
|
|
182
184
|
warnings.push({
|
|
183
185
|
rule: "akan.global.duplicate-exported-function-name",
|
|
@@ -345,6 +347,7 @@ function formatQualityLocation(file: string | undefined, line: number | undefine
|
|
|
345
347
|
|
|
346
348
|
function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionLike[] {
|
|
347
349
|
const declarations: ExportedFunctionLike[] = [];
|
|
350
|
+
const pageExempt = isPageRouteFile(sourceFile.file);
|
|
348
351
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
349
352
|
if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
350
353
|
declarations.push({
|
|
@@ -353,6 +356,7 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
|
|
|
353
356
|
file: sourceFile.file,
|
|
354
357
|
line: getLine(sourceFile.sourceFile, statement),
|
|
355
358
|
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement.body),
|
|
359
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
|
|
356
360
|
});
|
|
357
361
|
}
|
|
358
362
|
if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -362,6 +366,9 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
|
|
|
362
366
|
file: sourceFile.file,
|
|
363
367
|
line: getLine(sourceFile.sourceFile, statement),
|
|
364
368
|
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, statement),
|
|
369
|
+
duplicateNameExempt:
|
|
370
|
+
pageExempt ||
|
|
371
|
+
isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement)),
|
|
365
372
|
});
|
|
366
373
|
}
|
|
367
374
|
if (ts.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -373,6 +380,7 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
|
|
|
373
380
|
file: sourceFile.file,
|
|
374
381
|
line: getLine(sourceFile.sourceFile, declaration),
|
|
375
382
|
bodyFingerprint: getBodyFingerprint(sourceFile.sourceFile, declaration.initializer),
|
|
383
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile.file, false),
|
|
376
384
|
});
|
|
377
385
|
}
|
|
378
386
|
}
|
|
@@ -380,6 +388,38 @@ function getExportedFunctionLikes(sourceFile: SourceFileInfo): ExportedFunctionL
|
|
|
380
388
|
return declarations;
|
|
381
389
|
}
|
|
382
390
|
|
|
391
|
+
function isPageRouteFile(file: string) {
|
|
392
|
+
const segments = file.split("/");
|
|
393
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isConventionDuplicateNameExempt(file: string, isEnumClass: boolean) {
|
|
397
|
+
if (!isInLibModule(file)) return false;
|
|
398
|
+
if (file.endsWith(".tsx")) return true;
|
|
399
|
+
if (
|
|
400
|
+
file.endsWith(".document.ts") ||
|
|
401
|
+
file.endsWith(".service.ts") ||
|
|
402
|
+
file.endsWith(".signal.ts") ||
|
|
403
|
+
file.endsWith(".store.ts")
|
|
404
|
+
)
|
|
405
|
+
return true;
|
|
406
|
+
// Model view classes may repeat across modules; enum classes must stay uniquely named.
|
|
407
|
+
if (file.endsWith(".constant.ts")) return !isEnumClass;
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isInLibModule(file: string) {
|
|
412
|
+
const segments = file.split("/");
|
|
413
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement) {
|
|
417
|
+
if (!ts.isClassDeclaration(statement)) return false;
|
|
418
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
419
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
420
|
+
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
421
|
+
}
|
|
422
|
+
|
|
383
423
|
function getExportedClassNames(sourceFile: ts.SourceFile) {
|
|
384
424
|
return sourceFile.statements
|
|
385
425
|
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement) && !!statement.name)
|
|
@@ -403,11 +443,7 @@ function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarnin
|
|
|
403
443
|
|
|
404
444
|
function getDictionaryTextWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
405
445
|
if (!sourceFile.file.endsWith(".dictionary.ts")) return [];
|
|
406
|
-
const stalePatterns = [
|
|
407
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
408
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
409
|
-
{ pattern: /배너 수/, label: "stale copied Korean domain noun: 배너 수" },
|
|
410
|
-
];
|
|
446
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
411
447
|
return stalePatterns.flatMap(({ pattern, label }) =>
|
|
412
448
|
findPatternLines(sourceFile.content, pattern).map((line) => ({
|
|
413
449
|
rule: "akan.file.dictionary-stale-text",
|
package/workflow/artifacts.ts
CHANGED
|
@@ -196,7 +196,6 @@ export const generatedFilePathsForTarget = (targetRoot: string, reason = "Genera
|
|
|
196
196
|
[
|
|
197
197
|
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
198
198
|
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
199
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
200
199
|
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason },
|
|
201
200
|
] satisfies PrimitiveGeneratedFile[];
|
|
202
201
|
|