@akanjs/cli 2.3.11-rc.4 → 2.3.11-rc.6
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 +38 -10
- package/index.js +145 -25
- package/package.json +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -3
- 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__.Zone.tsx +1 -1
- package/templates/workspaceRoot/.cursor/rules/akan.mdc.template +4 -33
- package/templates/workspaceRoot/AGENTS.md.template +88 -5
- package/templates/workspaceRoot/biome.json.template +2 -1
|
@@ -1710,7 +1710,8 @@ var appRootAllowedFiles = new Set([
|
|
|
1710
1710
|
"main.ts",
|
|
1711
1711
|
"package.json",
|
|
1712
1712
|
"server.ts",
|
|
1713
|
-
"tsconfig.json"
|
|
1713
|
+
"tsconfig.json",
|
|
1714
|
+
"tsconfig.tsbuildinfo"
|
|
1714
1715
|
]);
|
|
1715
1716
|
var appRootAllowedDirs = new Set([
|
|
1716
1717
|
".akan",
|
|
@@ -15556,7 +15557,8 @@ class AkanQualityScanner {
|
|
|
15556
15557
|
#scanGlobalQuality(sourceFiles) {
|
|
15557
15558
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15558
15559
|
const warnings = [];
|
|
15559
|
-
|
|
15560
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15561
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15560
15562
|
if (declarations.length < 2)
|
|
15561
15563
|
continue;
|
|
15562
15564
|
warnings.push({
|
|
@@ -15710,6 +15712,7 @@ function formatQualityLocation(file, line) {
|
|
|
15710
15712
|
}
|
|
15711
15713
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15712
15714
|
const declarations = [];
|
|
15715
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15713
15716
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15714
15717
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15715
15718
|
declarations.push({
|
|
@@ -15717,7 +15720,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15717
15720
|
kind: "function",
|
|
15718
15721
|
file: sourceFile2.file,
|
|
15719
15722
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15720
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15723
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15724
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15721
15725
|
});
|
|
15722
15726
|
}
|
|
15723
15727
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15726,7 +15730,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15726
15730
|
kind: "class",
|
|
15727
15731
|
file: sourceFile2.file,
|
|
15728
15732
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15729
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15733
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15734
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15730
15735
|
});
|
|
15731
15736
|
}
|
|
15732
15737
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15738,13 +15743,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15738
15743
|
kind: "function-variable",
|
|
15739
15744
|
file: sourceFile2.file,
|
|
15740
15745
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15741
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15746
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15747
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15742
15748
|
});
|
|
15743
15749
|
}
|
|
15744
15750
|
}
|
|
15745
15751
|
}
|
|
15746
15752
|
return declarations;
|
|
15747
15753
|
}
|
|
15754
|
+
function isPageRouteFile(file) {
|
|
15755
|
+
const segments = file.split("/");
|
|
15756
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15757
|
+
}
|
|
15758
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15759
|
+
if (!isInLibModule(file))
|
|
15760
|
+
return false;
|
|
15761
|
+
if (file.endsWith(".tsx"))
|
|
15762
|
+
return true;
|
|
15763
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
15764
|
+
return true;
|
|
15765
|
+
if (file.endsWith(".constant.ts"))
|
|
15766
|
+
return !isEnumClass;
|
|
15767
|
+
return false;
|
|
15768
|
+
}
|
|
15769
|
+
function isInLibModule(file) {
|
|
15770
|
+
const segments = file.split("/");
|
|
15771
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
15772
|
+
}
|
|
15773
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
15774
|
+
if (!ts11.isClassDeclaration(statement))
|
|
15775
|
+
return false;
|
|
15776
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
15777
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
15778
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
15779
|
+
}
|
|
15748
15780
|
function getExportedClassNames(sourceFile2) {
|
|
15749
15781
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15750
15782
|
}
|
|
@@ -15763,11 +15795,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15763
15795
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15764
15796
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15765
15797
|
return [];
|
|
15766
|
-
const stalePatterns = [
|
|
15767
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15768
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15769
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15770
|
-
];
|
|
15798
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15771
15799
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15772
15800
|
rule: "akan.file.dictionary-stale-text",
|
|
15773
15801
|
scope: "file",
|
package/index.js
CHANGED
|
@@ -1708,7 +1708,8 @@ var appRootAllowedFiles = new Set([
|
|
|
1708
1708
|
"main.ts",
|
|
1709
1709
|
"package.json",
|
|
1710
1710
|
"server.ts",
|
|
1711
|
-
"tsconfig.json"
|
|
1711
|
+
"tsconfig.json",
|
|
1712
|
+
"tsconfig.tsbuildinfo"
|
|
1712
1713
|
]);
|
|
1713
1714
|
var appRootAllowedDirs = new Set([
|
|
1714
1715
|
".akan",
|
|
@@ -15554,7 +15555,8 @@ class AkanQualityScanner {
|
|
|
15554
15555
|
#scanGlobalQuality(sourceFiles) {
|
|
15555
15556
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15556
15557
|
const warnings = [];
|
|
15557
|
-
|
|
15558
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15559
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15558
15560
|
if (declarations.length < 2)
|
|
15559
15561
|
continue;
|
|
15560
15562
|
warnings.push({
|
|
@@ -15708,6 +15710,7 @@ function formatQualityLocation(file, line) {
|
|
|
15708
15710
|
}
|
|
15709
15711
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15710
15712
|
const declarations = [];
|
|
15713
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15711
15714
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15712
15715
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15713
15716
|
declarations.push({
|
|
@@ -15715,7 +15718,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15715
15718
|
kind: "function",
|
|
15716
15719
|
file: sourceFile2.file,
|
|
15717
15720
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15718
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15721
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15722
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15719
15723
|
});
|
|
15720
15724
|
}
|
|
15721
15725
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15724,7 +15728,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15724
15728
|
kind: "class",
|
|
15725
15729
|
file: sourceFile2.file,
|
|
15726
15730
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15727
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15731
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15732
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15728
15733
|
});
|
|
15729
15734
|
}
|
|
15730
15735
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15736,13 +15741,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15736
15741
|
kind: "function-variable",
|
|
15737
15742
|
file: sourceFile2.file,
|
|
15738
15743
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15739
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15744
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15745
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15740
15746
|
});
|
|
15741
15747
|
}
|
|
15742
15748
|
}
|
|
15743
15749
|
}
|
|
15744
15750
|
return declarations;
|
|
15745
15751
|
}
|
|
15752
|
+
function isPageRouteFile(file) {
|
|
15753
|
+
const segments = file.split("/");
|
|
15754
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
15755
|
+
}
|
|
15756
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
15757
|
+
if (!isInLibModule(file))
|
|
15758
|
+
return false;
|
|
15759
|
+
if (file.endsWith(".tsx"))
|
|
15760
|
+
return true;
|
|
15761
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
15762
|
+
return true;
|
|
15763
|
+
if (file.endsWith(".constant.ts"))
|
|
15764
|
+
return !isEnumClass;
|
|
15765
|
+
return false;
|
|
15766
|
+
}
|
|
15767
|
+
function isInLibModule(file) {
|
|
15768
|
+
const segments = file.split("/");
|
|
15769
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
15770
|
+
}
|
|
15771
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
15772
|
+
if (!ts11.isClassDeclaration(statement))
|
|
15773
|
+
return false;
|
|
15774
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
15775
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
15776
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
15777
|
+
}
|
|
15746
15778
|
function getExportedClassNames(sourceFile2) {
|
|
15747
15779
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15748
15780
|
}
|
|
@@ -15761,11 +15793,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15761
15793
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15762
15794
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15763
15795
|
return [];
|
|
15764
|
-
const stalePatterns = [
|
|
15765
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15766
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15767
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15768
|
-
];
|
|
15796
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15769
15797
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15770
15798
|
rule: "akan.file.dictionary-stale-text",
|
|
15771
15799
|
scope: "file",
|
|
@@ -16028,26 +16056,61 @@ var targetPaths = {
|
|
|
16028
16056
|
"agents-md": "AGENTS.md",
|
|
16029
16057
|
claude: "CLAUDE.md"
|
|
16030
16058
|
};
|
|
16031
|
-
var
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
16059
|
+
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
16060
|
+
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
16061
|
+
var SAMPLE_ARTIFACTS = [
|
|
16062
|
+
{ probe: "lib/task/task.constant.ts", target: "lib/task", label: "sample database module" },
|
|
16063
|
+
{ probe: "lib/_noti/noti.service.ts", target: "lib/_noti", label: "sample service module" },
|
|
16064
|
+
{
|
|
16065
|
+
probe: "lib/__scalar/workHistory/workHistory.dictionary.ts",
|
|
16066
|
+
target: "lib/__scalar/workHistory",
|
|
16067
|
+
label: "sample scalar module"
|
|
16068
|
+
},
|
|
16069
|
+
{ probe: "page/task/_index.tsx", target: "page/task", label: "sample task pages" }
|
|
16070
|
+
];
|
|
16071
|
+
var DEFAULT_INDEX_MARKER = "Akan.js template";
|
|
16072
|
+
var renderSampleCleanup = async (workspace, appNames) => {
|
|
16073
|
+
const items = [];
|
|
16074
|
+
for (const appName of appNames) {
|
|
16075
|
+
for (const artifact2 of SAMPLE_ARTIFACTS) {
|
|
16076
|
+
if (await workspace.exists(`apps/${appName}/${artifact2.probe}`)) {
|
|
16077
|
+
items.push(`- \`apps/${appName}/${artifact2.target}\` \u2014 ${artifact2.label}; delete it once you no longer need the reference.`);
|
|
16078
|
+
}
|
|
16079
|
+
}
|
|
16080
|
+
const indexPath = `apps/${appName}/page/_index.tsx`;
|
|
16081
|
+
if (await workspace.exists(indexPath)) {
|
|
16082
|
+
const content = await workspace.readFile(indexPath).catch(() => "");
|
|
16083
|
+
if (content.includes(DEFAULT_INDEX_MARKER)) {
|
|
16084
|
+
items.push(`- \`${indexPath}\` \u2014 default Akan landing page; replace it with your own home page.`);
|
|
16085
|
+
}
|
|
16086
|
+
}
|
|
16087
|
+
}
|
|
16088
|
+
if (items.length === 0)
|
|
16089
|
+
return "";
|
|
16090
|
+
return `## Start Clean (Remove Scaffolded Samples)
|
|
16091
|
+
|
|
16092
|
+
This workspace was scaffolded with reference samples so the Akan conventions are visible in real code. They are
|
|
16093
|
+
not part of your product. Before building real features, remove the samples below and run \`akan sync <app>\`:
|
|
16094
|
+
|
|
16095
|
+
${items.join(`
|
|
16096
|
+
`)}
|
|
16097
|
+
|
|
16098
|
+
Keep a sample only while you are still learning its pattern; delete it once your own modules cover the same ground.
|
|
16099
|
+
|
|
16100
|
+
`;
|
|
16035
16101
|
};
|
|
16036
|
-
var
|
|
16102
|
+
var renderManagedBlock = async (workspace) => {
|
|
16037
16103
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16038
16104
|
const frameworkGuide = await Prompter.getInstruction("framework");
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
This file is generated by \`akan agent install ${target}\`.
|
|
16042
|
-
|
|
16043
|
-
## Workspace
|
|
16105
|
+
const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
|
|
16106
|
+
return `## Workspace
|
|
16044
16107
|
|
|
16045
16108
|
- Repo: ${context.repoName}
|
|
16046
16109
|
- Apps: ${context.apps.map((app) => app.name).join(", ") || "none"}
|
|
16047
16110
|
- Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
|
|
16048
16111
|
- Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
|
|
16049
16112
|
|
|
16050
|
-
## Akan Module Abstracts
|
|
16113
|
+
${sampleCleanup}## Akan Module Abstracts
|
|
16051
16114
|
|
|
16052
16115
|
- Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
|
|
16053
16116
|
- Update the abstract when business invariants, workflows, or public behavior change.
|
|
@@ -16069,6 +16132,7 @@ If generated output is stale or broken, update the owning source file and run \`
|
|
|
16069
16132
|
- After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
|
|
16070
16133
|
- 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.
|
|
16071
16134
|
- For compound requests, split the request into workflows and apply each \`planPath\` in order, such as \`create-module\` followed by \`add-field\`.
|
|
16135
|
+
- **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.
|
|
16072
16136
|
|
|
16073
16137
|
## Validation
|
|
16074
16138
|
|
|
@@ -16077,8 +16141,61 @@ ${context.validationCommands.map((command3) => `- \`${command3}\``).join(`
|
|
|
16077
16141
|
|
|
16078
16142
|
## Framework Guide
|
|
16079
16143
|
|
|
16080
|
-
${frameworkGuide.trim()}
|
|
16144
|
+
${frameworkGuide.trim()}`;
|
|
16145
|
+
};
|
|
16146
|
+
var upsertManagedBlock = (existing, block) => {
|
|
16147
|
+
const managed = `${AGENT_BLOCK_START}
|
|
16148
|
+
${block}
|
|
16149
|
+
${AGENT_BLOCK_END}`;
|
|
16150
|
+
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
16151
|
+
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
16152
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
16153
|
+
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
16154
|
+
}
|
|
16155
|
+
return `${existing.replace(/\s*$/, "")}
|
|
16156
|
+
|
|
16157
|
+
${managed}
|
|
16158
|
+
`;
|
|
16159
|
+
};
|
|
16160
|
+
var renderAgentsMd = async (workspace, existing) => {
|
|
16161
|
+
const block = await renderManagedBlock(workspace);
|
|
16162
|
+
if (existing?.trim())
|
|
16163
|
+
return upsertManagedBlock(existing, block);
|
|
16164
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16165
|
+
return `# ${context.repoName} Agent Guide
|
|
16166
|
+
|
|
16167
|
+
This file is the single source of truth for coding agents in this workspace. Claude Code reads it through
|
|
16168
|
+
\`CLAUDE.md\` (\`@AGENTS.md\`) and Cursor through \`.cursor/rules/akan.mdc\`. The section between the
|
|
16169
|
+
\`akan:agent\` markers is regenerated by \`akan agent install\`; edit anything outside the markers freely.
|
|
16170
|
+
|
|
16171
|
+
${AGENT_BLOCK_START}
|
|
16172
|
+
${block}
|
|
16173
|
+
${AGENT_BLOCK_END}
|
|
16174
|
+
`;
|
|
16175
|
+
};
|
|
16176
|
+
var renderClaudeMd = async (workspace) => {
|
|
16177
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16178
|
+
return `# ${context.repoName} \u2014 Claude Code Guide
|
|
16179
|
+
|
|
16180
|
+
@AGENTS.md
|
|
16181
|
+
`;
|
|
16182
|
+
};
|
|
16183
|
+
var renderCursorRule = () => `---
|
|
16184
|
+
description: Akan workspace agent guide
|
|
16185
|
+
alwaysApply: true
|
|
16186
|
+
---
|
|
16187
|
+
|
|
16188
|
+
Follow the workspace agent guide, which is the single source of truth for Akan conventions,
|
|
16189
|
+
generated-file rules, and the MCP workflow policy.
|
|
16190
|
+
|
|
16191
|
+
@AGENTS.md
|
|
16081
16192
|
`;
|
|
16193
|
+
var renderTarget = async (workspace, target, existing) => {
|
|
16194
|
+
if (target === "agents-md")
|
|
16195
|
+
return await renderAgentsMd(workspace, existing);
|
|
16196
|
+
if (target === "claude")
|
|
16197
|
+
return await renderClaudeMd(workspace);
|
|
16198
|
+
return renderCursorRule();
|
|
16082
16199
|
};
|
|
16083
16200
|
|
|
16084
16201
|
class AgentRunner extends runner("agent") {
|
|
@@ -16086,11 +16203,13 @@ class AgentRunner extends runner("agent") {
|
|
|
16086
16203
|
const written = [];
|
|
16087
16204
|
for (const target of targets) {
|
|
16088
16205
|
const filePath = targetPaths[target];
|
|
16089
|
-
|
|
16206
|
+
const exists2 = await workspace.exists(filePath);
|
|
16207
|
+
if (exists2 && !force && target !== "agents-md") {
|
|
16090
16208
|
throw new Error(`${filePath} already exists. Re-run with --force to overwrite it.`);
|
|
16091
16209
|
}
|
|
16092
|
-
const
|
|
16093
|
-
await workspace
|
|
16210
|
+
const existing = exists2 ? await workspace.readFile(filePath) : null;
|
|
16211
|
+
const content = await renderTarget(workspace, target, existing);
|
|
16212
|
+
await workspace.writeFile(filePath, content, { overwrite: true });
|
|
16094
16213
|
written.push(filePath);
|
|
16095
16214
|
}
|
|
16096
16215
|
return written;
|
|
@@ -21439,6 +21558,7 @@ import path48 from "path";
|
|
|
21439
21558
|
var defaultWorkspacePeerDependencies = new Set([
|
|
21440
21559
|
"@react-spring/web",
|
|
21441
21560
|
"@use-gesture/react",
|
|
21561
|
+
"chance",
|
|
21442
21562
|
"croner",
|
|
21443
21563
|
"daisyui",
|
|
21444
21564
|
"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.6",
|
|
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.6",
|
|
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,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 { Load } from "akanjs/ui";
|
|
14
|
-
import { cnst, ${dict.Model} } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
14
|
+
import { type cnst, ${dict.Model} } from "@${scanInfo?.type ?? "apps"}/${dict.sysName}/client";
|
|
15
15
|
import type { ClientInit, ClientView, SliceMeta } from "akanjs/fetch";
|
|
16
16
|
|
|
17
17
|
interface CardProps {
|
|
@@ -1,38 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Akan
|
|
2
|
+
description: Akan workspace agent guide
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Follow the workspace agent guide, which is the single source of truth for Akan conventions,
|
|
7
|
+
generated-file rules, and the MCP workflow policy.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
- Pages live under `apps/<app>/page`; index routes use `_index.tsx`, and nested layouts use `_layout.tsx`.
|
|
10
|
-
- Database domain modules live under `lib/<model>`.
|
|
11
|
-
- Service modules live under `lib/_<service>`.
|
|
12
|
-
- Scalar modules live under `lib/__scalar/<scalar>`.
|
|
13
|
-
- Module abstracts live beside module code as `<model>.abstract.md`, `<service>.abstract.md`, or
|
|
14
|
-
`<scalar>.abstract.md`. Read them before changing module behavior.
|
|
15
|
-
- Do not hand-edit generated Akan files such as `akan.app.json`, `client.ts`, `server.ts`, generated facet indexes,
|
|
16
|
-
`lib/cnst.ts`, `lib/dict.ts`, `lib/db.ts`, `lib/srv.ts`, `lib/st.ts`, `lib/sig.ts`, `lib/useClient.ts`, or
|
|
17
|
-
`lib/useServer.ts`.
|
|
18
|
-
- Prefer Akan MCP workflows before direct source edits: use `akan mcp --mode plan` for workflow discovery and
|
|
19
|
-
planning, then `akan mcp --mode apply` only for allowlisted apply, validation, and repair tools.
|
|
20
|
-
- If `plan_workflow` returns `planPath` or `next.tool=apply_workflow`, call `apply_workflow({ planPath })` before
|
|
21
|
-
editing source files directly.
|
|
22
|
-
- After `apply_workflow`, run `run_validation` with `validationTarget` when present; otherwise use `applyReportPath`.
|
|
23
|
-
- Direct source edits are denied when an allowlisted Akan workflow or repair tool can perform the change.
|
|
24
|
-
- Direct edits are fallback only after `list_workflows`/`explain_workflow` show no matching workflow, or after
|
|
25
|
-
`apply_workflow` reports unsupported/no-op/failed diagnostics that require manual action.
|
|
26
|
-
- For compound requests, split the request into workflows and apply each `planPath` in order, such as `create-module`
|
|
27
|
-
followed by `add-field`.
|
|
28
|
-
- If generated output is stale or broken, update the owning source file and run `akan repair generated` or
|
|
29
|
-
`akan sync <app-or-lib>` instead of patching generated files.
|
|
30
|
-
- For new domain behavior, inspect sibling `constant`, `dictionary`, `signal`, `document`, `service`, `store`, and UI
|
|
31
|
-
module files before changing shape.
|
|
32
|
-
- Update `*.abstract.md` when business invariants, workflows, or public behavior change. Do not update it for
|
|
33
|
-
formatting-only, import-only, or style-only changes.
|
|
34
|
-
- Respect server/client import boundaries. Use `akanjs/server` only in server-side code and `akanjs/client` only in
|
|
35
|
-
client/page runtime code.
|
|
36
|
-
- Treat `AKAN_PUBLIC_*` env vars as public values. Do not store secrets in them.
|
|
37
|
-
- Verify changes with the smallest relevant command: `akan lint <target>`, `akan test <target>`, or
|
|
38
|
-
`akan build <app-name>`.
|
|
9
|
+
@AGENTS.md
|
|
@@ -87,7 +87,8 @@ When adding a new database-backed domain module (e.g., product, user):
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
# 1. Scaffold the module with Akan CLI (creates constant, service, signal, store, document files)
|
|
90
|
-
|
|
90
|
+
# The target app/lib is a POSITIONAL argument, not a --app flag.
|
|
91
|
+
akan create-module <module-name> <%= appName %>
|
|
91
92
|
|
|
92
93
|
# 2. Start dev server with HMR and type checking at http://localhost:8282
|
|
93
94
|
akan start <%= appName %>
|
|
@@ -114,12 +115,32 @@ akan build <%= appName %>
|
|
|
114
115
|
### Other Frequently Used Commands
|
|
115
116
|
|
|
116
117
|
```bash
|
|
117
|
-
akan create-scalar <scalar-name>
|
|
118
|
-
akan create-service <service-name>
|
|
118
|
+
akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (lib/__scalar/<scalar-name>/)
|
|
119
|
+
akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
|
|
119
120
|
akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
|
|
120
121
|
akan lint <%= appName %> # Lint only (no typecheck)
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
**CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
|
|
125
|
+
|
|
126
|
+
- Scaffolding and whole-app commands take the target app/lib as a **positional** argument, not a flag:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
akan create-module photo <%= appName %>
|
|
130
|
+
akan create-scalar money <%= appName %>
|
|
131
|
+
akan create-service billing <%= appName %>
|
|
132
|
+
akan sync <%= appName %>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- Only the source-limited field commands use `--app`/`--module` flags:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
akan add-field --app <%= appName %> --module photo --field width --type Int
|
|
139
|
+
akan add-enum-field --app <%= appName %> --module photo --field status --values draft,active
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Passing `--app` to `create-module` is not recognized, and the target app will not resolve.
|
|
143
|
+
|
|
123
144
|
For the default generated app, start with:
|
|
124
145
|
|
|
125
146
|
```bash
|
|
@@ -130,6 +151,11 @@ akan start <%= appName %>
|
|
|
130
151
|
|
|
131
152
|
Almost every Akan.js change follows this pattern. **Missing sync or repair is the #1 cause of agent confusion.**
|
|
132
153
|
|
|
154
|
+
> **If the Akan MCP tools are not connected in your agent, skip straight to the CLI-only fallback below.**
|
|
155
|
+
> `akan mcp --mode plan/apply` starts a stdio MCP server that only works when your agent is wired to it as an
|
|
156
|
+
> MCP client. When those `list_workflows` / `plan_workflow` / `apply_workflow` tools are not available, the CLI
|
|
157
|
+
> commands are a fully supported, first-class path — you are not losing any capability by using them.
|
|
158
|
+
|
|
133
159
|
1. **Plan** — Ask the Akan MCP server for the workflow first.
|
|
134
160
|
```
|
|
135
161
|
akan mcp --mode plan
|
|
@@ -171,12 +197,31 @@ Almost every Akan.js change follows this pattern. **Missing sync or repair is th
|
|
|
171
197
|
|
|
172
198
|
If `akan sync` gives errors, try:
|
|
173
199
|
- `akan build <%= appName %>` — full rebuild catches type errors sync may miss
|
|
174
|
-
- Re-run `akan create-module <name>
|
|
200
|
+
- Re-run `akan create-module <name> <%= appName %>` if the scaffold is corrupted
|
|
175
201
|
|
|
176
202
|
For compound natural-language requests, split the request into workflows and apply each artifact in order. For example,
|
|
177
203
|
"create a project module and add a budget field" should run `create-module` plan/apply first, then `add-field`
|
|
178
204
|
plan/apply, then validation/doctor on the returned `validationTarget`.
|
|
179
205
|
|
|
206
|
+
### CLI-Only Fallback (MCP Not Connected)
|
|
207
|
+
|
|
208
|
+
When the Akan MCP tools are not loaded, run the CLI commands directly. Each MCP tool maps 1:1 to a CLI command,
|
|
209
|
+
and the CLI emits the same structured report via `--format json`:
|
|
210
|
+
|
|
211
|
+
| MCP tool | CLI-only equivalent |
|
|
212
|
+
|----------|---------------------|
|
|
213
|
+
| `list_workflows` | `akan workflow list` |
|
|
214
|
+
| `explain_workflow <name>` | `akan workflow explain <name>` |
|
|
215
|
+
| `plan_workflow <name> ...` | `akan workflow plan <name> ... --format json --out <planPath>` |
|
|
216
|
+
| `apply_workflow { planPath }` | `akan workflow apply <planPath> --format json` (add `--dry-run` to preview) |
|
|
217
|
+
| `run_validation { validationTarget }` | `akan doctor --strict --format json` (or `akan typecheck <%= appName %>`) |
|
|
218
|
+
| `repair_generated` / `repair_imports` / `repair_module_shape` | `akan repair generated\|imports\|module-shape --app <%= appName %> --format json` |
|
|
219
|
+
|
|
220
|
+
The scaffolding primitives (`akan create-module`, `akan create-scalar`, `akan create-service`, `akan add-field`,
|
|
221
|
+
`akan add-enum-field`) are the same primitives the workflows call, so `create-module <name> <%= appName %>` followed
|
|
222
|
+
by `akan sync <%= appName %>` is equivalent to running the `create-module` workflow. Direct source edits remain the
|
|
223
|
+
final fallback when no CLI command covers the change.
|
|
224
|
+
|
|
180
225
|
## Quick Decision Matrix — "Where do I put this code?"
|
|
181
226
|
|
|
182
227
|
| You want to... | Create in... | Run after... |
|
|
@@ -289,7 +334,9 @@ const form = st.use.taskForm();
|
|
|
289
334
|
|
|
290
335
|
### Recipe 2: Injecting a Dependency into a Service
|
|
291
336
|
|
|
292
|
-
|
|
337
|
+
Three patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
|
|
338
|
+
or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
|
|
339
|
+
registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
|
|
293
340
|
|
|
294
341
|
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
295
342
|
|
|
@@ -332,6 +379,32 @@ export class TaskService extends serve(db.task, ({ service }) => ({
|
|
|
332
379
|
}
|
|
333
380
|
```
|
|
334
381
|
|
|
382
|
+
**C. Predefined framework adapter injection via `plug()` (storage, cache, queue, schedule, …)**
|
|
383
|
+
|
|
384
|
+
Akan ships predefined adapter roles from `akanjs/service`: `StorageAdaptorRole`, `CacheAdaptorRole`,
|
|
385
|
+
`QueueAdaptorRole`, `ScheduleAdaptorRole`, `DatabaseAdaptorRole`, `WebsocketAdaptorRole`,
|
|
386
|
+
`LoggingAdaptorRole`, `CompressAdaptorRole`. `plug()` injects the concrete adapter bound to that role (the
|
|
387
|
+
default `StorageAdaptor` binding is `BlobStorage`). `plug()` also accepts a concrete adapter class directly.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// In <model>.service.ts — inject the framework storage adapter by role
|
|
391
|
+
import { plug, serve, StorageAdaptorRole } from "akanjs/service";
|
|
392
|
+
|
|
393
|
+
export class TaskService extends serve(db.task, ({ plug }) => ({
|
|
394
|
+
storage: plug(StorageAdaptorRole),
|
|
395
|
+
})) {
|
|
396
|
+
async attach(taskId: string, path: string, localPath: string) {
|
|
397
|
+
// BlobStorage returns a URL under blobStorage.urlPrefix (default "/api/localFile/getBlob").
|
|
398
|
+
return await this.storage.uploadDataFromLocal({ path, localPath });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`
|
|
404
|
+
(see `libs/shared/lib/file/file.service.ts`). Injecting a file/image field is usually simpler than calling
|
|
405
|
+
storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
|
|
406
|
+
store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
|
|
407
|
+
|
|
335
408
|
---
|
|
336
409
|
|
|
337
410
|
### Recipe 3: Creating and Using a Slice
|
|
@@ -576,3 +649,13 @@ akan sync automatically generates APIs across all layers. Only write custom logi
|
|
|
576
649
|
| `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
|
|
577
650
|
|
|
578
651
|
**Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
|
|
652
|
+
|
|
653
|
+
## Generated Context
|
|
654
|
+
|
|
655
|
+
The section below is regenerated by `akan agent install` (run `bun run setup:agent`). It lists this
|
|
656
|
+
workspace's apps, generated files, validation commands, and framework guide. Edit anything outside the
|
|
657
|
+
markers freely; content between them is overwritten on the next install.
|
|
658
|
+
|
|
659
|
+
<!-- akan:agent:start -->
|
|
660
|
+
<!-- Populated by `akan agent install`. Run `bun run setup:agent` to refresh. -->
|
|
661
|
+
<!-- akan:agent:end -->
|