@akanjs/cli 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/incrementalBuilder.proc.js +85 -12
- package/index.js +173 -31
- package/package.json +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -3
- package/templates/appSample/lib/task/task.service.ts +1 -1
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
- package/templates/crudPages/[__model__Id]/page.tsx +3 -3
- package/templates/crudPages/page.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +3 -3
- package/templates/module/__Model__.Template.tsx +4 -5
- package/templates/module/__Model__.Unit.tsx +1 -1
- package/templates/module/__Model__.View.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +5 -3
- package/templates/module/__model__.constant.ts +2 -1
- package/templates/module/__model__.dictionary.ts +3 -1
- package/templates/workspaceRoot/AGENTS.md.template +133 -8
- package/templates/workspaceRoot/biome.json.template +6 -1
- package/templates/workspaceRoot/docs/GENERATED.md.template +0 -1
- package/templates/workspaceRoot/package.json.template +3 -3
|
@@ -3577,6 +3577,8 @@ class AppExecutor extends SysExecutor {
|
|
|
3577
3577
|
...routeEnv,
|
|
3578
3578
|
...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
|
|
3579
3579
|
});
|
|
3580
|
+
if (type === "build")
|
|
3581
|
+
Object.assign(process.env, env);
|
|
3580
3582
|
return { env };
|
|
3581
3583
|
}
|
|
3582
3584
|
#publicEnv = null;
|
|
@@ -4996,7 +4998,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
|
|
|
4996
4998
|
var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
|
|
4997
4999
|
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
4998
5000
|
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
4999
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
5000
5001
|
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
|
|
5001
5002
|
];
|
|
5002
5003
|
var writeGeneratedSyncState = async (workspace, state) => {
|
|
@@ -7650,13 +7651,59 @@ var resourceList = [
|
|
|
7650
7651
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
7651
7652
|
];
|
|
7652
7653
|
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
7654
|
+
var claudeMcpConfigPath = ".mcp.json";
|
|
7655
|
+
var codexMcpConfigPath = ".codex/config.toml";
|
|
7656
|
+
var akanMcpInstallTargets = ["cursor", "claude", "codex"];
|
|
7657
|
+
var akanMcpInstallConfigPaths = {
|
|
7658
|
+
cursor: cursorMcpConfigPath,
|
|
7659
|
+
claude: claudeMcpConfigPath,
|
|
7660
|
+
codex: codexMcpConfigPath
|
|
7661
|
+
};
|
|
7653
7662
|
var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
7663
|
+
var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
7664
|
+
var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
7654
7665
|
var createAkanCursorMcpServer = (mode = "readonly") => ({
|
|
7655
7666
|
type: "stdio",
|
|
7656
7667
|
command: "bash",
|
|
7657
|
-
args: ["-lc",
|
|
7668
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
|
|
7669
|
+
});
|
|
7670
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
7671
|
+
type: "stdio",
|
|
7672
|
+
command: "bash",
|
|
7673
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
7658
7674
|
});
|
|
7675
|
+
var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
7659
7676
|
var akanCursorMcpServer = createAkanCursorMcpServer();
|
|
7677
|
+
var codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
7678
|
+
var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
|
|
7679
|
+
command = "bash"
|
|
7680
|
+
args = ["-lc", "${akanMcpCommand(mode)}"]
|
|
7681
|
+
`;
|
|
7682
|
+
var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
7683
|
+
var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
|
|
7684
|
+
const nextBlock = block.endsWith(`
|
|
7685
|
+
`) ? block : `${block}
|
|
7686
|
+
`;
|
|
7687
|
+
const match = existing.match(codexAkanTablePattern);
|
|
7688
|
+
if (!match) {
|
|
7689
|
+
if (!existing.trim())
|
|
7690
|
+
return nextBlock;
|
|
7691
|
+
return `${existing.replace(/\s*$/, "")}
|
|
7692
|
+
|
|
7693
|
+
${nextBlock}`;
|
|
7694
|
+
}
|
|
7695
|
+
if (match[0].trim() === nextBlock.trim())
|
|
7696
|
+
return existing;
|
|
7697
|
+
if (!force)
|
|
7698
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
7699
|
+
const start = match.index ?? 0;
|
|
7700
|
+
const before = existing.slice(0, start);
|
|
7701
|
+
const after = existing.slice(start + match[0].length);
|
|
7702
|
+
const separator = after && !after.startsWith(`
|
|
7703
|
+
`) ? `
|
|
7704
|
+
` : "";
|
|
7705
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
7706
|
+
};
|
|
7660
7707
|
var renderDoctorText = (result) => {
|
|
7661
7708
|
const lines = [`Akan doctor status: ${result.status}`];
|
|
7662
7709
|
if (result.diagnostics.length === 0) {
|
|
@@ -7679,7 +7726,6 @@ var generatedFiles = [
|
|
|
7679
7726
|
"*/lib/cnst.ts",
|
|
7680
7727
|
"*/lib/db.ts",
|
|
7681
7728
|
"*/lib/dict.ts",
|
|
7682
|
-
"*/lib/option.ts",
|
|
7683
7729
|
"*/lib/sig.ts",
|
|
7684
7730
|
"*/lib/srv.ts",
|
|
7685
7731
|
"*/lib/st.ts",
|
|
@@ -15557,7 +15603,8 @@ class AkanQualityScanner {
|
|
|
15557
15603
|
#scanGlobalQuality(sourceFiles) {
|
|
15558
15604
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15559
15605
|
const warnings = [];
|
|
15560
|
-
|
|
15606
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15607
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15561
15608
|
if (declarations.length < 2)
|
|
15562
15609
|
continue;
|
|
15563
15610
|
warnings.push({
|
|
@@ -15711,6 +15758,7 @@ function formatQualityLocation(file, line) {
|
|
|
15711
15758
|
}
|
|
15712
15759
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15713
15760
|
const declarations = [];
|
|
15761
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15714
15762
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15715
15763
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15716
15764
|
declarations.push({
|
|
@@ -15718,7 +15766,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15718
15766
|
kind: "function",
|
|
15719
15767
|
file: sourceFile2.file,
|
|
15720
15768
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15721
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15769
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15770
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15722
15771
|
});
|
|
15723
15772
|
}
|
|
15724
15773
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15727,7 +15776,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15727
15776
|
kind: "class",
|
|
15728
15777
|
file: sourceFile2.file,
|
|
15729
15778
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15730
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15779
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15780
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15731
15781
|
});
|
|
15732
15782
|
}
|
|
15733
15783
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15739,13 +15789,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15739
15789
|
kind: "function-variable",
|
|
15740
15790
|
file: sourceFile2.file,
|
|
15741
15791
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15742
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15792
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15793
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15743
15794
|
});
|
|
15744
15795
|
}
|
|
15745
15796
|
}
|
|
15746
15797
|
}
|
|
15747
15798
|
return declarations;
|
|
15748
15799
|
}
|
|
15800
|
+
function isPageRouteFile(file) {
|
|
15801
|
+
const segments = file.split("/");
|
|
15802
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15803
|
+
}
|
|
15804
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15805
|
+
if (!isInLibModule(file))
|
|
15806
|
+
return false;
|
|
15807
|
+
if (file.endsWith(".tsx"))
|
|
15808
|
+
return true;
|
|
15809
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
15810
|
+
return true;
|
|
15811
|
+
if (file.endsWith(".constant.ts"))
|
|
15812
|
+
return !isEnumClass;
|
|
15813
|
+
return false;
|
|
15814
|
+
}
|
|
15815
|
+
function isInLibModule(file) {
|
|
15816
|
+
const segments = file.split("/");
|
|
15817
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
15818
|
+
}
|
|
15819
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
15820
|
+
if (!ts11.isClassDeclaration(statement))
|
|
15821
|
+
return false;
|
|
15822
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
15823
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
15824
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
15825
|
+
}
|
|
15749
15826
|
function getExportedClassNames(sourceFile2) {
|
|
15750
15827
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15751
15828
|
}
|
|
@@ -15764,11 +15841,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15764
15841
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15765
15842
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15766
15843
|
return [];
|
|
15767
|
-
const stalePatterns = [
|
|
15768
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15769
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15770
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15771
|
-
];
|
|
15844
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15772
15845
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15773
15846
|
rule: "akan.file.dictionary-stale-text",
|
|
15774
15847
|
scope: "file",
|
package/index.js
CHANGED
|
@@ -3575,6 +3575,8 @@ class AppExecutor extends SysExecutor {
|
|
|
3575
3575
|
...routeEnv,
|
|
3576
3576
|
...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
|
|
3577
3577
|
});
|
|
3578
|
+
if (type === "build")
|
|
3579
|
+
Object.assign(process.env, env);
|
|
3578
3580
|
return { env };
|
|
3579
3581
|
}
|
|
3580
3582
|
#publicEnv = null;
|
|
@@ -4994,7 +4996,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
|
|
|
4994
4996
|
var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
|
|
4995
4997
|
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
4996
4998
|
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
4997
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
4998
4999
|
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
|
|
4999
5000
|
];
|
|
5000
5001
|
var writeGeneratedSyncState = async (workspace, state) => {
|
|
@@ -7648,13 +7649,59 @@ var resourceList = [
|
|
|
7648
7649
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
7649
7650
|
];
|
|
7650
7651
|
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
7652
|
+
var claudeMcpConfigPath = ".mcp.json";
|
|
7653
|
+
var codexMcpConfigPath = ".codex/config.toml";
|
|
7654
|
+
var akanMcpInstallTargets = ["cursor", "claude", "codex"];
|
|
7655
|
+
var akanMcpInstallConfigPaths = {
|
|
7656
|
+
cursor: cursorMcpConfigPath,
|
|
7657
|
+
claude: claudeMcpConfigPath,
|
|
7658
|
+
codex: codexMcpConfigPath
|
|
7659
|
+
};
|
|
7651
7660
|
var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
7661
|
+
var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
7662
|
+
var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
7652
7663
|
var createAkanCursorMcpServer = (mode = "readonly") => ({
|
|
7653
7664
|
type: "stdio",
|
|
7654
7665
|
command: "bash",
|
|
7655
|
-
args: ["-lc",
|
|
7666
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
|
|
7667
|
+
});
|
|
7668
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
7669
|
+
type: "stdio",
|
|
7670
|
+
command: "bash",
|
|
7671
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
7656
7672
|
});
|
|
7673
|
+
var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
7657
7674
|
var akanCursorMcpServer = createAkanCursorMcpServer();
|
|
7675
|
+
var codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
7676
|
+
var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
|
|
7677
|
+
command = "bash"
|
|
7678
|
+
args = ["-lc", "${akanMcpCommand(mode)}"]
|
|
7679
|
+
`;
|
|
7680
|
+
var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
7681
|
+
var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
|
|
7682
|
+
const nextBlock = block.endsWith(`
|
|
7683
|
+
`) ? block : `${block}
|
|
7684
|
+
`;
|
|
7685
|
+
const match = existing.match(codexAkanTablePattern);
|
|
7686
|
+
if (!match) {
|
|
7687
|
+
if (!existing.trim())
|
|
7688
|
+
return nextBlock;
|
|
7689
|
+
return `${existing.replace(/\s*$/, "")}
|
|
7690
|
+
|
|
7691
|
+
${nextBlock}`;
|
|
7692
|
+
}
|
|
7693
|
+
if (match[0].trim() === nextBlock.trim())
|
|
7694
|
+
return existing;
|
|
7695
|
+
if (!force)
|
|
7696
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
7697
|
+
const start = match.index ?? 0;
|
|
7698
|
+
const before = existing.slice(0, start);
|
|
7699
|
+
const after = existing.slice(start + match[0].length);
|
|
7700
|
+
const separator = after && !after.startsWith(`
|
|
7701
|
+
`) ? `
|
|
7702
|
+
` : "";
|
|
7703
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
7704
|
+
};
|
|
7658
7705
|
var renderDoctorText = (result) => {
|
|
7659
7706
|
const lines = [`Akan doctor status: ${result.status}`];
|
|
7660
7707
|
if (result.diagnostics.length === 0) {
|
|
@@ -7677,7 +7724,6 @@ var generatedFiles = [
|
|
|
7677
7724
|
"*/lib/cnst.ts",
|
|
7678
7725
|
"*/lib/db.ts",
|
|
7679
7726
|
"*/lib/dict.ts",
|
|
7680
|
-
"*/lib/option.ts",
|
|
7681
7727
|
"*/lib/sig.ts",
|
|
7682
7728
|
"*/lib/srv.ts",
|
|
7683
7729
|
"*/lib/st.ts",
|
|
@@ -15555,7 +15601,8 @@ class AkanQualityScanner {
|
|
|
15555
15601
|
#scanGlobalQuality(sourceFiles) {
|
|
15556
15602
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15557
15603
|
const warnings = [];
|
|
15558
|
-
|
|
15604
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15605
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15559
15606
|
if (declarations.length < 2)
|
|
15560
15607
|
continue;
|
|
15561
15608
|
warnings.push({
|
|
@@ -15709,6 +15756,7 @@ function formatQualityLocation(file, line) {
|
|
|
15709
15756
|
}
|
|
15710
15757
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15711
15758
|
const declarations = [];
|
|
15759
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15712
15760
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15713
15761
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15714
15762
|
declarations.push({
|
|
@@ -15716,7 +15764,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15716
15764
|
kind: "function",
|
|
15717
15765
|
file: sourceFile2.file,
|
|
15718
15766
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15719
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15767
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15768
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15720
15769
|
});
|
|
15721
15770
|
}
|
|
15722
15771
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15725,7 +15774,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15725
15774
|
kind: "class",
|
|
15726
15775
|
file: sourceFile2.file,
|
|
15727
15776
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15728
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15777
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15778
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15729
15779
|
});
|
|
15730
15780
|
}
|
|
15731
15781
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15737,13 +15787,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15737
15787
|
kind: "function-variable",
|
|
15738
15788
|
file: sourceFile2.file,
|
|
15739
15789
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15740
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15790
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15791
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15741
15792
|
});
|
|
15742
15793
|
}
|
|
15743
15794
|
}
|
|
15744
15795
|
}
|
|
15745
15796
|
return declarations;
|
|
15746
15797
|
}
|
|
15798
|
+
function isPageRouteFile(file) {
|
|
15799
|
+
const segments = file.split("/");
|
|
15800
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15801
|
+
}
|
|
15802
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15803
|
+
if (!isInLibModule(file))
|
|
15804
|
+
return false;
|
|
15805
|
+
if (file.endsWith(".tsx"))
|
|
15806
|
+
return true;
|
|
15807
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
15808
|
+
return true;
|
|
15809
|
+
if (file.endsWith(".constant.ts"))
|
|
15810
|
+
return !isEnumClass;
|
|
15811
|
+
return false;
|
|
15812
|
+
}
|
|
15813
|
+
function isInLibModule(file) {
|
|
15814
|
+
const segments = file.split("/");
|
|
15815
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
15816
|
+
}
|
|
15817
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
15818
|
+
if (!ts11.isClassDeclaration(statement))
|
|
15819
|
+
return false;
|
|
15820
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
15821
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
15822
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
15823
|
+
}
|
|
15747
15824
|
function getExportedClassNames(sourceFile2) {
|
|
15748
15825
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15749
15826
|
}
|
|
@@ -15762,11 +15839,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15762
15839
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15763
15840
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15764
15841
|
return [];
|
|
15765
|
-
const stalePatterns = [
|
|
15766
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15767
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15768
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15769
|
-
];
|
|
15842
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15770
15843
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15771
15844
|
rule: "akan.file.dictionary-stale-text",
|
|
15772
15845
|
scope: "file",
|
|
@@ -16031,9 +16104,51 @@ var targetPaths = {
|
|
|
16031
16104
|
};
|
|
16032
16105
|
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
16033
16106
|
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
16107
|
+
var SAMPLE_ARTIFACTS = [
|
|
16108
|
+
{ probe: "lib/task/task.constant.ts", target: "lib/task", label: "sample database module" },
|
|
16109
|
+
{ probe: "lib/_noti/noti.service.ts", target: "lib/_noti", label: "sample service module" },
|
|
16110
|
+
{
|
|
16111
|
+
probe: "lib/__scalar/workHistory/workHistory.dictionary.ts",
|
|
16112
|
+
target: "lib/__scalar/workHistory",
|
|
16113
|
+
label: "sample scalar module"
|
|
16114
|
+
},
|
|
16115
|
+
{ probe: "page/task/_index.tsx", target: "page/task", label: "sample task pages" }
|
|
16116
|
+
];
|
|
16117
|
+
var DEFAULT_INDEX_MARKER = "Akan.js template";
|
|
16118
|
+
var renderSampleCleanup = async (workspace, appNames) => {
|
|
16119
|
+
const items = [];
|
|
16120
|
+
for (const appName of appNames) {
|
|
16121
|
+
for (const artifact2 of SAMPLE_ARTIFACTS) {
|
|
16122
|
+
if (await workspace.exists(`apps/${appName}/${artifact2.probe}`)) {
|
|
16123
|
+
items.push(`- \`apps/${appName}/${artifact2.target}\` \u2014 ${artifact2.label}; delete it once you no longer need the reference.`);
|
|
16124
|
+
}
|
|
16125
|
+
}
|
|
16126
|
+
const indexPath = `apps/${appName}/page/_index.tsx`;
|
|
16127
|
+
if (await workspace.exists(indexPath)) {
|
|
16128
|
+
const content = await workspace.readFile(indexPath).catch(() => "");
|
|
16129
|
+
if (content.includes(DEFAULT_INDEX_MARKER)) {
|
|
16130
|
+
items.push(`- \`${indexPath}\` \u2014 default Akan landing page; replace it with your own home page.`);
|
|
16131
|
+
}
|
|
16132
|
+
}
|
|
16133
|
+
}
|
|
16134
|
+
if (items.length === 0)
|
|
16135
|
+
return "";
|
|
16136
|
+
return `## Start Clean (Remove Scaffolded Samples)
|
|
16137
|
+
|
|
16138
|
+
This workspace was scaffolded with reference samples so the Akan conventions are visible in real code. They are
|
|
16139
|
+
not part of your product. Before building real features, remove the samples below and run \`akan sync <app>\`:
|
|
16140
|
+
|
|
16141
|
+
${items.join(`
|
|
16142
|
+
`)}
|
|
16143
|
+
|
|
16144
|
+
Keep a sample only while you are still learning its pattern; delete it once your own modules cover the same ground.
|
|
16145
|
+
|
|
16146
|
+
`;
|
|
16147
|
+
};
|
|
16034
16148
|
var renderManagedBlock = async (workspace) => {
|
|
16035
16149
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16036
16150
|
const frameworkGuide = await Prompter.getInstruction("framework");
|
|
16151
|
+
const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
|
|
16037
16152
|
return `## Workspace
|
|
16038
16153
|
|
|
16039
16154
|
- Repo: ${context.repoName}
|
|
@@ -16041,7 +16156,7 @@ var renderManagedBlock = async (workspace) => {
|
|
|
16041
16156
|
- Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
|
|
16042
16157
|
- Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
|
|
16043
16158
|
|
|
16044
|
-
## Akan Module Abstracts
|
|
16159
|
+
${sampleCleanup}## Akan Module Abstracts
|
|
16045
16160
|
|
|
16046
16161
|
- Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
|
|
16047
16162
|
- Update the abstract when business invariants, workflows, or public behavior change.
|
|
@@ -16063,6 +16178,7 @@ If generated output is stale or broken, update the owning source file and run \`
|
|
|
16063
16178
|
- After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
|
|
16064
16179
|
- If no workflow exists, or apply reports unsupported/no-op/failed diagnostics that require manual action, keep edits scoped to owning source files and never patch generated files directly.
|
|
16065
16180
|
- For compound requests, split the request into workflows and apply each \`planPath\` in order, such as \`create-module\` followed by \`add-field\`.
|
|
16181
|
+
- **CLI-only fallback (MCP not connected):** \`akan mcp\` starts a stdio MCP server, so the \`list_workflows\`/\`plan_workflow\`/\`apply_workflow\` tools exist only when your agent is wired to it as an MCP client. When they are unavailable, the CLI is a first-class equivalent: \`akan workflow list\` / \`explain <name>\` / \`plan <name> ... --format json --out <planPath>\` / \`apply <planPath> --format json\`, \`akan doctor --strict --format json\` for validation, and \`akan repair generated|imports|module-shape --app <app> --format json\` for repairs. Scaffolding primitives (\`create-module\`/\`create-scalar\`/\`create-service\` take the target app/lib as a POSITIONAL arg; \`add-field\`/\`add-enum-field\` use \`--app\`/\`--module\` flags) call the same code the workflows do.
|
|
16066
16182
|
|
|
16067
16183
|
## Validation
|
|
16068
16184
|
|
|
@@ -18022,8 +18138,7 @@ var addEnumFieldWorkflowSpec = {
|
|
|
18022
18138
|
predictedChanges: [
|
|
18023
18139
|
{ target: "*/lib/<module>/<module>.constant.ts", action: "modify", reason: "Enum field shape is added." },
|
|
18024
18140
|
{ target: "*/lib/<module>/<module>.dictionary.ts", action: "modify", reason: "Enum labels are added." },
|
|
18025
|
-
{ target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
|
|
18026
|
-
{ target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may change after sync." }
|
|
18141
|
+
{ target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
|
|
18027
18142
|
],
|
|
18028
18143
|
validation: [
|
|
18029
18144
|
...baseValidation,
|
|
@@ -18428,8 +18543,7 @@ var createScalarWorkflowSpec = {
|
|
|
18428
18543
|
}
|
|
18429
18544
|
],
|
|
18430
18545
|
predictedChanges: [
|
|
18431
|
-
{ target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
|
|
18432
|
-
{ target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may include scalar options." }
|
|
18546
|
+
{ target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
|
|
18433
18547
|
],
|
|
18434
18548
|
validation: baseValidation,
|
|
18435
18549
|
completionCriteria: ["Scalar files exist under __scalar.", "Generated files are refreshed.", "Lint passes."]
|
|
@@ -20300,14 +20414,18 @@ class ContextRunner extends runner("context") {
|
|
|
20300
20414
|
return await Prompter.getInstruction(name);
|
|
20301
20415
|
}
|
|
20302
20416
|
async installMcp(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
20303
|
-
if (target
|
|
20304
|
-
|
|
20305
|
-
|
|
20417
|
+
if (target === "codex")
|
|
20418
|
+
return await this.#installCodexMcp(workspace, { force, mode });
|
|
20419
|
+
return await this.#installJsonMcp(workspace, target, { force, mode });
|
|
20420
|
+
}
|
|
20421
|
+
async#installJsonMcp(workspace, target, { force, mode }) {
|
|
20422
|
+
const configPath2 = akanMcpInstallConfigPaths[target];
|
|
20423
|
+
const existing = await workspace.exists(configPath2) ? await workspace.readJson(configPath2) : {};
|
|
20306
20424
|
const mcpServers = existing.mcpServers ?? {};
|
|
20307
20425
|
const currentAkanServer = mcpServers.akan;
|
|
20308
|
-
const nextAkanServer =
|
|
20426
|
+
const nextAkanServer = createAkanMcpServer(target, mode);
|
|
20309
20427
|
if (currentAkanServer && !force && JSON.stringify(currentAkanServer) !== JSON.stringify(nextAkanServer)) {
|
|
20310
|
-
throw new Error(`${
|
|
20428
|
+
throw new Error(`${configPath2} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
20311
20429
|
}
|
|
20312
20430
|
const nextConfig = {
|
|
20313
20431
|
...existing,
|
|
@@ -20316,9 +20434,15 @@ class ContextRunner extends runner("context") {
|
|
|
20316
20434
|
akan: nextAkanServer
|
|
20317
20435
|
}
|
|
20318
20436
|
};
|
|
20319
|
-
await workspace.writeFile(
|
|
20437
|
+
await workspace.writeFile(configPath2, `${JSON.stringify(nextConfig, null, 2)}
|
|
20320
20438
|
`);
|
|
20321
|
-
return
|
|
20439
|
+
return configPath2;
|
|
20440
|
+
}
|
|
20441
|
+
async#installCodexMcp(workspace, { force, mode }) {
|
|
20442
|
+
const existing = await workspace.exists(codexMcpConfigPath) ? await workspace.readFile(codexMcpConfigPath) : "";
|
|
20443
|
+
const merged = upsertCodexMcpServerBlock(existing, createAkanCodexMcpServerBlock(mode), { force });
|
|
20444
|
+
await workspace.writeFile(codexMcpConfigPath, merged);
|
|
20445
|
+
return codexMcpConfigPath;
|
|
20322
20446
|
}
|
|
20323
20447
|
listMcpTools(mode = "readonly") {
|
|
20324
20448
|
return listAkanMcpTools(mode);
|
|
@@ -20610,13 +20734,26 @@ ${payload}`);
|
|
|
20610
20734
|
agent: "`akan agent install <target>` writes editor-specific agent rules with overwrite protection.",
|
|
20611
20735
|
mcp: "`akan mcp --mode readonly|plan|apply` starts the Akan MCP server over stdio with an explicit permission mode.",
|
|
20612
20736
|
"mcp-call": '`akan mcp-call <tool> --mode plan --args \'{"workflow":"add-field","inputs":{"app":"demo"}}\'` calls one MCP tool through the same runner path for debugging.',
|
|
20613
|
-
"mcp-install": "`akan mcp-install cursor --mode readonly|plan|apply` installs the Akan MCP server config for Cursor."
|
|
20737
|
+
"mcp-install": "`akan mcp-install [cursor|claude|codex|all] --mode readonly|plan|apply` installs the Akan MCP server config for Cursor (.cursor/mcp.json), Claude Code (.mcp.json), and Codex (.codex/config.toml). Defaults to all targets."
|
|
20614
20738
|
};
|
|
20615
20739
|
return explanations[command3] ?? `No detailed explanation is available for ${command3}. Run \`akan --help\` for command help.`;
|
|
20616
20740
|
}
|
|
20617
20741
|
}
|
|
20618
20742
|
|
|
20619
20743
|
// pkgs/@akanjs/cli/context/context.script.ts
|
|
20744
|
+
var resolveMcpInstallTargets = (target) => {
|
|
20745
|
+
if (!target || target === "all")
|
|
20746
|
+
return [...akanMcpInstallTargets];
|
|
20747
|
+
if (akanMcpInstallTargets.includes(target))
|
|
20748
|
+
return [target];
|
|
20749
|
+
throw new Error(`Unknown MCP install target: ${target}. Use cursor, claude, codex, or all.`);
|
|
20750
|
+
};
|
|
20751
|
+
var mcpTargetLabels = {
|
|
20752
|
+
cursor: "Cursor",
|
|
20753
|
+
claude: "Claude Code",
|
|
20754
|
+
codex: "Codex"
|
|
20755
|
+
};
|
|
20756
|
+
|
|
20620
20757
|
class ContextScript extends script("context", [ContextRunner]) {
|
|
20621
20758
|
async context(workspace, options = {}) {
|
|
20622
20759
|
Logger17.rawLog(await this.contextRunner.getContext(workspace, options));
|
|
@@ -20625,11 +20762,15 @@ class ContextScript extends script("context", [ContextRunner]) {
|
|
|
20625
20762
|
Logger17.rawLog(await this.contextRunner.doctor(workspace, options));
|
|
20626
20763
|
}
|
|
20627
20764
|
async mcpInstall(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
20628
|
-
|
|
20629
|
-
|
|
20630
|
-
const
|
|
20631
|
-
|
|
20632
|
-
|
|
20765
|
+
const targets = resolveMcpInstallTargets(target);
|
|
20766
|
+
const written = [];
|
|
20767
|
+
for (const t of targets) {
|
|
20768
|
+
const configPath2 = await this.contextRunner.installMcp(workspace, t, { force, mode });
|
|
20769
|
+
written.push(`${mcpTargetLabels[t]}: ${configPath2}`);
|
|
20770
|
+
}
|
|
20771
|
+
Logger17.rawLog(`Akan MCP server installed (${mode} mode):
|
|
20772
|
+
${written.map((line) => `- ${line}`).join(`
|
|
20773
|
+
`)}`);
|
|
20633
20774
|
}
|
|
20634
20775
|
async mcp(workspace, { mode = "readonly" } = {}) {
|
|
20635
20776
|
await this.contextRunner.runMcp(workspace, { mode });
|
|
@@ -20668,7 +20809,7 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
|
|
|
20668
20809
|
}).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
|
|
20669
20810
|
await this.contextScript.doctor(workspace, { format, strict });
|
|
20670
20811
|
}),
|
|
20671
|
-
mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor" }).arg("target", String, { desc: "cursor", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
|
|
20812
|
+
mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor, Claude Code, and Codex" }).arg("target", String, { desc: "cursor, claude, codex, or all", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
|
|
20672
20813
|
desc: "MCP permission mode",
|
|
20673
20814
|
default: "readonly",
|
|
20674
20815
|
enum: ["readonly", "plan", "apply"]
|
|
@@ -21488,6 +21629,7 @@ import path48 from "path";
|
|
|
21488
21629
|
var defaultWorkspacePeerDependencies = new Set([
|
|
21489
21630
|
"@react-spring/web",
|
|
21490
21631
|
"@use-gesture/react",
|
|
21632
|
+
"chance",
|
|
21491
21633
|
"croner",
|
|
21492
21634
|
"daisyui",
|
|
21493
21635
|
"react",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/cli",
|
|
3
|
-
"version": "2.3.11-rc.
|
|
3
|
+
"version": "2.3.11-rc.7",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@langchain/openai": "^1.4.6",
|
|
35
35
|
"@tailwindcss/node": "^4.3.0",
|
|
36
36
|
"@trapezedev/project": "^7.1.4",
|
|
37
|
-
"akanjs": "2.3.11-rc.
|
|
37
|
+
"akanjs": "2.3.11-rc.7",
|
|
38
38
|
"chalk": "^5.6.2",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"daisyui": "5.5.23",
|
|
@@ -4,12 +4,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
|
|
|
4
4
|
return {
|
|
5
5
|
filename: "Task.Zone.tsx",
|
|
6
6
|
content: `"use client";
|
|
7
|
-
import { Task, usePage } from "@apps/${dict.appName}/client";
|
|
7
|
+
import { type cnst, Task, usePage } from "@apps/${dict.appName}/client";
|
|
8
8
|
import type { ClientInit, ClientView } from "akanjs/fetch";
|
|
9
9
|
import { Link, Load } from "akanjs/ui";
|
|
10
10
|
|
|
11
|
-
import * as cnst from "../cnst";
|
|
12
|
-
|
|
13
11
|
// ===== Task.Zone.tsx =====
|
|
14
12
|
// Convention: lib/<module>/ — PascalCase .tsx, Zone suffix = composition layer between pages and UI.
|
|
15
13
|
// Zone components use Load.Units / Load.View from akanjs/ui — the framework convention for data-bound zones.
|
|
@@ -10,7 +10,7 @@ import * as db from "../db";
|
|
|
10
10
|
// Convention: <module>.service.ts — business logic orchestration for a database module.
|
|
11
11
|
// Extends serve(db.<module>, depsCallback) from akanjs/service — binds to the DB model, receives DI deps.
|
|
12
12
|
// Auto-generated by akan sync (do not write manually):
|
|
13
|
-
// getTask(id),
|
|
13
|
+
// getTask(id), createTask(data), updateTask(id, data), removeTask(id),
|
|
14
14
|
// listByStatus(status), searchDocs(text), and all filter+query methods from document.ts.
|
|
15
15
|
// Manual below: lifecycle hooks, custom business logic methods.
|
|
16
16
|
// Registered by akan sync into srv.ts barrel.
|
|
@@ -10,14 +10,14 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
10
10
|
filename: "_index.tsx",
|
|
11
11
|
content: `
|
|
12
12
|
import { Load } from "akanjs/ui";
|
|
13
|
-
import { fetch, usePage, ${dict.Model} } from "
|
|
13
|
+
import { fetch, usePage, ${dict.Model} } from "@apps/${dict.appName}/client";
|
|
14
14
|
import type { PageConfig } from "akanjs/client";
|
|
15
15
|
|
|
16
16
|
interface PageProps {
|
|
17
17
|
params: { ${dict.model}Id: string };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export default function Page({ params }: PageProps) {
|
|
20
|
+
export default async function Page({ params }: PageProps) {
|
|
21
21
|
const { l } = usePage();
|
|
22
22
|
const { ${dict.model}Id } = params;
|
|
23
23
|
const { ${dict.model}, ${dict.model}Edit } = await fetch.edit${dict.Model}(${dict.model}Id);
|
|
@@ -9,8 +9,8 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
9
9
|
return {
|
|
10
10
|
filename: "_index.tsx",
|
|
11
11
|
content: `
|
|
12
|
-
import { ${dict.Model}, fetch, usePage } from "
|
|
13
|
-
import { Link
|
|
12
|
+
import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
|
|
13
|
+
import { Link } from "akanjs/ui";
|
|
14
14
|
import type { PageConfig } from "akanjs/client";
|
|
15
15
|
|
|
16
16
|
interface PageProps {
|
|
@@ -29,7 +29,7 @@ export async function generateHead({ params }: PageProps) {
|
|
|
29
29
|
</>
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
|
-
export default function Page({ params }: PageProps) {
|
|
32
|
+
export default async function Page({ params }: PageProps) {
|
|
33
33
|
const { l } = usePage();
|
|
34
34
|
const { ${dict.model}Id } = params;
|
|
35
35
|
const { ${dict.model}, ${dict.model}View } = await fetch.view${dict.Model}(${dict.model}Id);
|
|
@@ -10,10 +10,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
10
10
|
filename: "_index.tsx",
|
|
11
11
|
content: `
|
|
12
12
|
import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
|
|
13
|
-
import { Link
|
|
13
|
+
import { Link } from "akanjs/ui";
|
|
14
14
|
import type { PageConfig } from "akanjs/client";
|
|
15
15
|
|
|
16
|
-
export default function Page() {
|
|
16
|
+
export default async function Page() {
|
|
17
17
|
const { l } = usePage();
|
|
18
18
|
const { ${dict.model}InitInPublic } = await fetch.init${dict.Model}InPublic();
|
|
19
19
|
return (
|
|
@@ -10,10 +10,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
10
10
|
filename: "_index.tsx",
|
|
11
11
|
content: `
|
|
12
12
|
import type { PageConfig } from "akanjs/client";
|
|
13
|
-
import {
|
|
14
|
-
import { ${dict.Model},
|
|
13
|
+
import { Model } from "akanjs/ui";
|
|
14
|
+
import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
|
|
15
15
|
|
|
16
|
-
export default function Page() {
|
|
16
|
+
export default async function Page() {
|
|
17
17
|
const { l } = usePage();
|
|
18
18
|
const { ${dict.model}InitInPublic } = await fetch.init${dict.Model}InPublic();
|
|
19
19
|
return (
|
|
@@ -11,7 +11,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
11
11
|
content: `
|
|
12
12
|
"use client";
|
|
13
13
|
import { Field, Layout } from "akanjs/ui";
|
|
14
|
-
import {
|
|
14
|
+
import { st, usePage } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
15
15
|
|
|
16
16
|
interface GeneralProps {
|
|
17
17
|
className?: string;
|
|
@@ -23,10 +23,9 @@ export const General = ({ className }: GeneralProps) => {
|
|
|
23
23
|
return (
|
|
24
24
|
<Layout.Template className={className}>
|
|
25
25
|
<Field.Text
|
|
26
|
-
label={l("${dict.model}.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
onChange={st.do.setIdOn${dict.Model}}
|
|
26
|
+
label={l("${dict.model}.name")}
|
|
27
|
+
value={${dict.model}Form.name}
|
|
28
|
+
onChange={st.do.setNameOn${dict.Model}}
|
|
30
29
|
/>
|
|
31
30
|
</Layout.Template>
|
|
32
31
|
);
|
|
@@ -17,7 +17,7 @@ export const Card = ({ ${dict.model}, href }: ModelProps<"${dict.model}", cnst.L
|
|
|
17
17
|
const { l } = usePage();
|
|
18
18
|
return (
|
|
19
19
|
<Link href={href} className="w-full">
|
|
20
|
-
<div>{l("${dict.model}.
|
|
20
|
+
<div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
|
|
21
21
|
</Link>
|
|
22
22
|
);
|
|
23
23
|
};
|
|
@@ -20,7 +20,7 @@ export const General = ({ className, ${dict.model} }: GeneralProps) => {
|
|
|
20
20
|
const { l } = usePage();
|
|
21
21
|
return (
|
|
22
22
|
<div className={clsx("w-full", className)}>
|
|
23
|
-
<div>{l("${dict.model}.
|
|
23
|
+
<div>{l("${dict.model}.name")}: {${dict.model}.name}</div>
|
|
24
24
|
</div>
|
|
25
25
|
);
|
|
26
26
|
};
|
|
@@ -11,7 +11,9 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
11
11
|
content: `
|
|
12
12
|
"use client";
|
|
13
13
|
import { Load } from "akanjs/ui";
|
|
14
|
-
|
|
14
|
+
// Alias the domain namespace so the Card/View exports below never collide with the model name
|
|
15
|
+
// (a model literally named "card" or "view" would otherwise shadow this import).
|
|
16
|
+
import { type cnst, ${dict.Model} as ${dict.Model}Domain } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
15
17
|
import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
|
|
16
18
|
|
|
17
19
|
interface CardProps {
|
|
@@ -25,7 +27,7 @@ export const Card = ({ className, init, slice }: CardProps) => {
|
|
|
25
27
|
className={className}
|
|
26
28
|
init={init}
|
|
27
29
|
renderItem={(${dict.model}) => (
|
|
28
|
-
<${dict.Model}.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
|
|
30
|
+
<${dict.Model}Domain.Unit.Card key={${dict.model}.id} href={\`/${dict.model}/\${${dict.model}.id}\`} ${dict.model}={${dict.model}} />
|
|
29
31
|
)}
|
|
30
32
|
/>
|
|
31
33
|
);
|
|
@@ -36,7 +38,7 @@ interface ViewProps {
|
|
|
36
38
|
view: ClientView<"${dict.model}", cnst.${dict.Model}>;
|
|
37
39
|
}
|
|
38
40
|
export const View = ({ view }: ViewProps) => {
|
|
39
|
-
return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}.View.General ${dict.model}={${dict.model}} />} />;
|
|
41
|
+
return <Load.View view={view} renderView={(${dict.model}) => <${dict.Model}Domain.View.General ${dict.model}={${dict.model}} />} />;
|
|
40
42
|
};
|
|
41
43
|
`,
|
|
42
44
|
};
|
|
@@ -10,11 +10,12 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
|
|
|
10
10
|
import { via } from "akanjs/constant";
|
|
11
11
|
|
|
12
12
|
export class ${dict.Model}Input extends via((field) => ({
|
|
13
|
+
name: field(String),
|
|
13
14
|
})) {}
|
|
14
15
|
|
|
15
16
|
export class ${dict.Model}Object extends via(${dict.Model}Input, (field) => ({})) {}
|
|
16
17
|
|
|
17
|
-
export class Light${dict.Model} extends via(${dict.Model}Object, [] as const, (resolve) => ({})) {}
|
|
18
|
+
export class Light${dict.Model} extends via(${dict.Model}Object, ["name"] as const, (resolve) => ({})) {}
|
|
18
19
|
|
|
19
20
|
export class ${dict.Model} extends via(${dict.Model}Object, Light${dict.Model}, (resolve) => ({})) {}
|
|
20
21
|
|
|
@@ -24,7 +24,9 @@ export const dictionary = modelDictionary(["en", "ko"])
|
|
|
24
24
|
.of((t) =>
|
|
25
25
|
t(["${modelLabelEn}", "${modelLabelKo}"]).desc(["${modelDescEn}", "${modelDescKo}"])
|
|
26
26
|
)
|
|
27
|
-
.model<${dict.Model}>((t) => ({
|
|
27
|
+
.model<${dict.Model}>((t) => ({
|
|
28
|
+
name: t(["Name", "이름"]),
|
|
29
|
+
}))
|
|
28
30
|
.insight<${dict.Model}Insight>((t) => ({}))
|
|
29
31
|
.slice<${dict.Model}Slice>((fn) => ({
|
|
30
32
|
inPublic: fn(["${dict.Model} In Public", "${dict.Model} 공개"]).arg((t) => ({})),
|
|
@@ -28,7 +28,6 @@ Common generated files include:
|
|
|
28
28
|
- `*/lib/cnst.ts`
|
|
29
29
|
- `*/lib/db.ts`
|
|
30
30
|
- `*/lib/dict.ts`
|
|
31
|
-
- `*/lib/option.ts`
|
|
32
31
|
- `*/lib/sig.ts`
|
|
33
32
|
- `*/lib/srv.ts`
|
|
34
33
|
- `*/lib/st.ts`
|
|
@@ -87,7 +86,8 @@ When adding a new database-backed domain module (e.g., product, user):
|
|
|
87
86
|
|
|
88
87
|
```bash
|
|
89
88
|
# 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
|
|
90
|
-
|
|
89
|
+
# The target app/lib is a POSITIONAL argument, not a --app flag.
|
|
90
|
+
akan create-module <module-name> <%= appName %>
|
|
91
91
|
|
|
92
92
|
# 2. Start dev server with HMR and type checking at http://localhost:8282
|
|
93
93
|
akan start <%= appName %>
|
|
@@ -111,15 +111,41 @@ akan test <%= appName %>
|
|
|
111
111
|
akan build <%= appName %>
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
**Verify endpoints with signal tests, not raw HTTP.** The canonical way to check a query/mutation/slice
|
|
115
|
+
contract is an in-memory signal test (`<model>.signal.test.ts`), using the test fetch harness
|
|
116
|
+
(`getOrSetupSignalTestFetch`) — it is fast, needs no running server, and exercises `fetch.*`, `view/edit/merge<Model>`,
|
|
117
|
+
and slice `init`/`list`/`insight` directly. Prefer it over `curl`: the dev gateway locale-prefixes routes (`/en/...`),
|
|
118
|
+
so hand-rolled HTTP calls against a raw path can redirect unexpectedly. See `akan test <%= appName %>`.
|
|
119
|
+
|
|
114
120
|
### Other Frequently Used Commands
|
|
115
121
|
|
|
116
122
|
```bash
|
|
117
|
-
akan create-scalar <scalar-name>
|
|
118
|
-
akan create-service <service-name>
|
|
123
|
+
akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
|
|
124
|
+
akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
|
|
119
125
|
akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
|
|
120
126
|
akan lint <%= appName %> # Lint only (no typecheck)
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
**CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
|
|
130
|
+
|
|
131
|
+
- Scaffolding and whole-app commands take the target app/lib as a **positional** argument, not a flag:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
akan create-module photo <%= appName %>
|
|
135
|
+
akan create-scalar money <%= appName %>
|
|
136
|
+
akan create-service billing <%= appName %>
|
|
137
|
+
akan sync <%= appName %>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- Only the source-limited field commands use `--app`/`--module` flags:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
akan add-field --app <%= appName %> --module photo --field width --type Int
|
|
144
|
+
akan add-enum-field --app <%= appName %> --module photo --field status --values draft,active
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Passing `--app` to `create-module` is not recognized, and the target app will not resolve.
|
|
148
|
+
|
|
123
149
|
For the default generated app, start with:
|
|
124
150
|
|
|
125
151
|
```bash
|
|
@@ -130,6 +156,11 @@ akan start <%= appName %>
|
|
|
130
156
|
|
|
131
157
|
Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
|
|
132
158
|
|
|
159
|
+
> **If the Akan MCP tools are not connected in your agent, skip straight to the CLI-only fallback below.**
|
|
160
|
+
> `akan mcp --mode plan/apply` starts a stdio MCP server that only works when your agent is wired to it as an
|
|
161
|
+
> MCP client. When those `list_workflows` / `plan_workflow` / `apply_workflow` tools are not available, the CLI
|
|
162
|
+
> commands are a fully supported, first-class path — you are not losing any capability by using them.
|
|
163
|
+
|
|
133
164
|
1. **Plan** — Ask the Akan MCP server for the workflow first.
|
|
134
165
|
```
|
|
135
166
|
akan mcp --mode plan
|
|
@@ -171,12 +202,31 @@ Almost every Akan.js change follows this pattern. **Missing sync or repair is th
|
|
|
171
202
|
|
|
172
203
|
If `akan sync` gives errors, try:
|
|
173
204
|
- `akan build <%= appName %>` — full rebuild catches type errors sync may miss
|
|
174
|
-
- Re-run `akan create-module <name>
|
|
205
|
+
- Re-run `akan create-module <name> <%= appName %>` if the scaffold is corrupted
|
|
175
206
|
|
|
176
207
|
For compound natural-language requests, split the request into workflows and apply each artifact in order. For example,
|
|
177
208
|
"create a project module and add a budget field" should run `create-module` plan/apply first, then `add-field`
|
|
178
209
|
plan/apply, then validation/doctor on the returned `validationTarget`.
|
|
179
210
|
|
|
211
|
+
### CLI-Only Fallback (MCP Not Connected)
|
|
212
|
+
|
|
213
|
+
When the Akan MCP tools are not loaded, run the CLI commands directly. Each MCP tool maps 1:1 to a CLI command,
|
|
214
|
+
and the CLI emits the same structured report via `--format json`:
|
|
215
|
+
|
|
216
|
+
| MCP tool | CLI-only equivalent |
|
|
217
|
+
|----------|---------------------|
|
|
218
|
+
| `list_workflows` | `akan workflow list` |
|
|
219
|
+
| `explain_workflow <name>` | `akan workflow explain <name>` |
|
|
220
|
+
| `plan_workflow <name> ...` | `akan workflow plan <name> ... --format json --out <planPath>` |
|
|
221
|
+
| `apply_workflow { planPath }` | `akan workflow apply <planPath> --format json` (add `--dry-run` to preview) |
|
|
222
|
+
| `run_validation { validationTarget }` | `akan doctor --strict --format json` (or `akan typecheck <%= appName %>`) |
|
|
223
|
+
| `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <%= appName %> --format json` |
|
|
224
|
+
|
|
225
|
+
The scaffolding primitives (`akan create-module`, `akan create-scalar`, `akan create-service`, `akan add-field`,
|
|
226
|
+
`akan add-enum-field`) are the same primitives the workflows call, so `create-module <name> <%= appName %>` followed
|
|
227
|
+
by `akan sync <%= appName %>` is equivalent to running the `create-module` workflow. Direct source edits remain the
|
|
228
|
+
final fallback when no CLI command covers the change.
|
|
229
|
+
|
|
180
230
|
## Quick Decision Matrix — "Where do I put this code?"
|
|
181
231
|
|
|
182
232
|
| You want to... | Create in... | Run after... |
|
|
@@ -185,6 +235,7 @@ plan/apply, then validation/doctor on the returned `validationTarget`.
|
|
|
185
235
|
| Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
|
|
186
236
|
| Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
|
|
187
237
|
| Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
|
|
238
|
+
| Change the app color theme / design tokens | `apps/<app>/page/styles.css` → edit the daisyUI `@plugin "daisyui/theme"` blocks (`light`/`dark`, the `--color-*` variables) | akan start hot-reloads |
|
|
188
239
|
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
|
|
189
240
|
| Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
|
|
190
241
|
| Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
|
|
@@ -202,6 +253,7 @@ plan/apply, then validation/doctor on the returned `validationTarget`.
|
|
|
202
253
|
| Skip running `akan sync` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan sync <name>` after every file add, remove, or rename |
|
|
203
254
|
| Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
|
|
204
255
|
| Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
|
|
256
|
+
| Name a custom `Endpoint`/`Slice` like a generated CRUD op — `create<Model>`, `update<Model>`, `remove<Model>`, `view<Model>`, `edit<Model>`, `merge<Model>` | These names are already auto-generated. A collision can pass sync/typecheck/build and only fail at runtime. | Pick a distinct verb, e.g. `startTask`/`archiveTask`, never `createTask` for a custom endpoint |
|
|
205
257
|
|
|
206
258
|
## Generated File Tracker (Quick Reference)
|
|
207
259
|
|
|
@@ -212,7 +264,6 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
|
|
|
212
264
|
| `*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
|
|
213
265
|
| `*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
|
|
214
266
|
| `*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
|
|
215
|
-
| `*/lib/option.ts` | Generated option helpers | Option helper entry |
|
|
216
267
|
| `*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
|
|
217
268
|
| `*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
|
|
218
269
|
| `*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
|
|
@@ -289,7 +340,13 @@ const form = st.use.taskForm();
|
|
|
289
340
|
|
|
290
341
|
### Recipe 2: Injecting a Dependency into a Service
|
|
291
342
|
|
|
292
|
-
|
|
343
|
+
Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
|
|
344
|
+
or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
|
|
345
|
+
registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
|
|
346
|
+
|
|
347
|
+
> `apps/<app>/lib/option.ts` is a **user-owned** file scaffolded once — edit it to register adapters/DI. Unlike the
|
|
348
|
+
> barrels (`cnst.ts`, `db.ts`, `srv.ts`, …) it is **not** overwritten by `akan sync`, so your `.use(...)` registrations
|
|
349
|
+
> are safe.
|
|
293
350
|
|
|
294
351
|
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
295
352
|
|
|
@@ -332,18 +389,51 @@ export class TaskService extends serve(db.task, ({ service }) => ({
|
|
|
332
389
|
}
|
|
333
390
|
```
|
|
334
391
|
|
|
392
|
+
**C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
|
|
393
|
+
|
|
394
|
+
Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
|
|
395
|
+
`QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
|
|
396
|
+
`LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
|
|
397
|
+
default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// In <model>.service.ts — inject the framework storage adapter by role
|
|
401
|
+
import { plug, serve, StorageAdaptorRole } from "akanjs/service";
|
|
402
|
+
|
|
403
|
+
export class TaskService extends serve(db.task, ({ plug }) => ({
|
|
404
|
+
storage: plug(StorageAdaptorRole),
|
|
405
|
+
})) {
|
|
406
|
+
async attach(taskId: string, path: string, localPath: string) {
|
|
407
|
+
// BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
|
|
408
|
+
return await this.storage.uploadDataFromLocal({ path, localPath });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`
|
|
414
|
+
(see `libs/shared/lib/file/file.service.ts`). Injecting a file/image field is usually simpler than calling
|
|
415
|
+
storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
|
|
416
|
+
store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
|
|
417
|
+
|
|
335
418
|
---
|
|
336
419
|
|
|
337
420
|
### Recipe 3: Creating and Using a Slice
|
|
338
421
|
|
|
339
422
|
A Slice is a named, filtered data view. Add file entries and connect from a page.
|
|
340
423
|
|
|
424
|
+
> **Silent failure — a slice `exec` must return a query descriptor, never an executed list.**
|
|
425
|
+
> Return `this.taskService.queryByStatuses(...)` (the `query<Filter>` builder), **not**
|
|
426
|
+
> `this.taskService.listByStatuses(...)` / `listBy...(...)` (which returns a `Promise<Doc[]>`).
|
|
427
|
+
> Returning an array type-checks but throws at runtime during insight aggregation with the opaque
|
|
428
|
+
> `Error: Unknown document field path: 0`. If you see that error, your slice is returning a list, not a query.
|
|
429
|
+
|
|
341
430
|
```typescript
|
|
342
431
|
// 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
|
|
343
432
|
export class TaskSlice extends slice(srv.task, (init) => ({
|
|
344
433
|
inTodo: init()
|
|
345
434
|
.search("statuses", [cnst.TaskStatus])
|
|
346
435
|
.exec(function (statuses?) {
|
|
436
|
+
// ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
|
|
347
437
|
return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
|
|
348
438
|
}),
|
|
349
439
|
})) {}
|
|
@@ -522,6 +612,41 @@ For each business question, follow this chain:
|
|
|
522
612
|
| What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
|
|
523
613
|
| What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
|
|
524
614
|
|
|
615
|
+
## Modeling & Query Gotchas
|
|
616
|
+
|
|
617
|
+
A short list of things the type system does not always catch:
|
|
618
|
+
|
|
619
|
+
- **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
|
|
620
|
+
a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
|
|
621
|
+
`Unknown document field path: 0`. (See Recipe 3.)
|
|
622
|
+
- **Custom endpoint names must not collide with generated CRUD.** `create/update/remove/view/edit/merge<Model>`
|
|
623
|
+
already exist. A collision can build green and fail only at runtime — pick a distinct verb.
|
|
624
|
+
- **Numbers are `Int` or `Float`, never `Number`.** `field(Number)` / `.body("x", Number)` fail to typecheck. Use
|
|
625
|
+
`Int` for counts, `Float` for decimals.
|
|
626
|
+
- **Array fields use `field([T])`.** e.g. `tags: field([String])`, `images: field([File])` — not `field(String)` with
|
|
627
|
+
a suffix.
|
|
628
|
+
- **Reading a secret field needs an explicit select.** `field(...).secret()` values (e.g. `passwordHash`) are stripped
|
|
629
|
+
from query results by default. Fetch them with `{ select: { <field>: true } }`, e.g.
|
|
630
|
+
`this.userModel.pickById(id, { select: { passwordHash: true } })`.
|
|
631
|
+
|
|
632
|
+
## Current User, Guards & Auth-Gated Pages
|
|
633
|
+
|
|
634
|
+
Built-in user authentication (session / JWT / password hashing) ships as a separate Akan auth library, not in the
|
|
635
|
+
core framework. The core framework gives you the composition points below; wire the auth library through them.
|
|
636
|
+
|
|
637
|
+
- **Guards** attach at the signal declaration, not per-method:
|
|
638
|
+
`endpoint(srv.task, { guards: { root: SignedIn } }, ({ mutation }) => ({...}))` or
|
|
639
|
+
`slice(srv.task, { guards: { root: SignedIn, get: Public, cru: Public } }, ...)`. `Public` always allows; other
|
|
640
|
+
guards implement the `Guard` interface in `srvkit/` (server-only) and read the request context.
|
|
641
|
+
- **Read the current user inside a custom endpoint** by injecting an `InternalArg` with `.with(...)`:
|
|
642
|
+
`mutation(cnst.Task).with(CurrentUserId).exec(async function (currentUserId) { ... })`. The `Guard` /
|
|
643
|
+
`InternalArg` helpers live in `srvkit/` and read `context.getHttpContext().req.user`.
|
|
644
|
+
- **Auto-generated CRUD and `serve()` service methods / lifecycle hooks do not receive session context.** If an
|
|
645
|
+
operation needs the acting user, expose a custom endpoint that takes it via `.with(CurrentUserId)` — never trust a
|
|
646
|
+
client-supplied user id.
|
|
647
|
+
- **SSR auth-gated pages: guard at the layout.** Check the session in the `_layout.tsx` loader and redirect when it is
|
|
648
|
+
absent, so nested pages never render for signed-out users.
|
|
649
|
+
|
|
525
650
|
## Auto-Generated API Reference
|
|
526
651
|
|
|
527
652
|
akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
|
|
@@ -545,7 +670,7 @@ akan sync automatically generates APIs across all layers. Only write custom logi
|
|
|
545
670
|
|---------------|-------------|
|
|
546
671
|
| `this.<model>Model` | Auto-injected model adaptor |
|
|
547
672
|
| `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
|
|
548
|
-
| `
|
|
673
|
+
| `create<Model>(data)`, `update<Model>(id, data)`, `remove<Model>(id)` | CRUD operations — named after the model, e.g. `createTask`/`updateTask`/`removeTask` (there is no literal `createModel`) |
|
|
549
674
|
| `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
|
|
550
675
|
| `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
|
|
551
676
|
| `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"!!**/dist",
|
|
51
51
|
"!!**/.akan",
|
|
52
52
|
"!!**/dump",
|
|
53
|
-
"!!**/local"
|
|
53
|
+
"!!**/local",
|
|
54
|
+
"!!**/ios/DerivedData"
|
|
54
55
|
]
|
|
55
56
|
},
|
|
56
57
|
"formatter": {
|
|
@@ -146,6 +147,10 @@
|
|
|
146
147
|
"includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
|
|
147
148
|
"plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
|
|
148
149
|
},
|
|
150
|
+
{
|
|
151
|
+
"includes": ["**/*.signal.ts"],
|
|
152
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
|
|
153
|
+
},
|
|
149
154
|
{
|
|
150
155
|
"includes": [
|
|
151
156
|
"**/*.constant.ts",
|
|
@@ -17,7 +17,6 @@ sync or build can overwrite local changes.
|
|
|
17
17
|
| `apps/*/lib/cnst.ts` | Re-exports constants and model shapes from module `*.constant.ts` files. |
|
|
18
18
|
| `apps/*/lib/db.ts` | Re-exports database models from module `*.document.ts` files. |
|
|
19
19
|
| `apps/*/lib/dict.ts` | Re-exports dictionaries from module `*.dictionary.ts` files. |
|
|
20
|
-
| `apps/*/lib/option.ts` | Re-exports generated option helpers. |
|
|
21
20
|
| `apps/*/lib/srv.ts` | Re-exports services from module `*.service.ts` files. |
|
|
22
21
|
| `apps/*/lib/sig.ts` | Re-exports endpoints, slices, and internals from module `*.signal.ts` files. |
|
|
23
22
|
| `apps/*/lib/st.ts` | Re-exports stores from module `*.store.ts` files. |
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
"agent:setup": "akan agent install all --force",
|
|
14
14
|
"agent:doctor": "akan doctor --strict --format json",
|
|
15
15
|
"agent:context": "akan context --format json",
|
|
16
|
-
"agent:mcp:readonly": "akan mcp-install
|
|
17
|
-
"agent:mcp:plan": "akan mcp-install
|
|
18
|
-
"agent:mcp:apply": "akan mcp-install
|
|
16
|
+
"agent:mcp:readonly": "akan mcp-install all --mode readonly --force",
|
|
17
|
+
"agent:mcp:plan": "akan mcp-install all --mode plan --force",
|
|
18
|
+
"agent:mcp:apply": "akan mcp-install all --mode apply --force",
|
|
19
19
|
"agent:workflows": "akan workflow list",
|
|
20
20
|
"agent:sample:context": "akan context --format markdown --app <%= appName %> --module project",
|
|
21
21
|
"agent:sample:service": "akan create-service billing <%= appName %> --format json",
|