@akanjs/cli 2.3.11-rc.5 → 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 +36 -9
- package/index.js +81 -10
- 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/AGENTS.md.template +78 -5
- package/templates/workspaceRoot/biome.json.template +2 -1
|
@@ -15557,7 +15557,8 @@ class AkanQualityScanner {
|
|
|
15557
15557
|
#scanGlobalQuality(sourceFiles) {
|
|
15558
15558
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15559
15559
|
const warnings = [];
|
|
15560
|
-
|
|
15560
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15561
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15561
15562
|
if (declarations.length < 2)
|
|
15562
15563
|
continue;
|
|
15563
15564
|
warnings.push({
|
|
@@ -15711,6 +15712,7 @@ function formatQualityLocation(file, line) {
|
|
|
15711
15712
|
}
|
|
15712
15713
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15713
15714
|
const declarations = [];
|
|
15715
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15714
15716
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15715
15717
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15716
15718
|
declarations.push({
|
|
@@ -15718,7 +15720,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15718
15720
|
kind: "function",
|
|
15719
15721
|
file: sourceFile2.file,
|
|
15720
15722
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15721
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15723
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15724
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15722
15725
|
});
|
|
15723
15726
|
}
|
|
15724
15727
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15727,7 +15730,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15727
15730
|
kind: "class",
|
|
15728
15731
|
file: sourceFile2.file,
|
|
15729
15732
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15730
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15733
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15734
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15731
15735
|
});
|
|
15732
15736
|
}
|
|
15733
15737
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15739,13 +15743,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15739
15743
|
kind: "function-variable",
|
|
15740
15744
|
file: sourceFile2.file,
|
|
15741
15745
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15742
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15746
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15747
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15743
15748
|
});
|
|
15744
15749
|
}
|
|
15745
15750
|
}
|
|
15746
15751
|
}
|
|
15747
15752
|
return declarations;
|
|
15748
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
|
+
}
|
|
15749
15780
|
function getExportedClassNames(sourceFile2) {
|
|
15750
15781
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15751
15782
|
}
|
|
@@ -15764,11 +15795,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15764
15795
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15765
15796
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15766
15797
|
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
|
-
];
|
|
15798
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15772
15799
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15773
15800
|
rule: "akan.file.dictionary-stale-text",
|
|
15774
15801
|
scope: "file",
|
package/index.js
CHANGED
|
@@ -15555,7 +15555,8 @@ class AkanQualityScanner {
|
|
|
15555
15555
|
#scanGlobalQuality(sourceFiles) {
|
|
15556
15556
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15557
15557
|
const warnings = [];
|
|
15558
|
-
|
|
15558
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15559
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15559
15560
|
if (declarations.length < 2)
|
|
15560
15561
|
continue;
|
|
15561
15562
|
warnings.push({
|
|
@@ -15709,6 +15710,7 @@ function formatQualityLocation(file, line) {
|
|
|
15709
15710
|
}
|
|
15710
15711
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15711
15712
|
const declarations = [];
|
|
15713
|
+
const pageExempt = isPageRouteFile(sourceFile2.file);
|
|
15712
15714
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15713
15715
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15714
15716
|
declarations.push({
|
|
@@ -15716,7 +15718,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15716
15718
|
kind: "function",
|
|
15717
15719
|
file: sourceFile2.file,
|
|
15718
15720
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15719
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
15721
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
15722
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15720
15723
|
});
|
|
15721
15724
|
}
|
|
15722
15725
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15725,7 +15728,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15725
15728
|
kind: "class",
|
|
15726
15729
|
file: sourceFile2.file,
|
|
15727
15730
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15728
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
15731
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
15732
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15729
15733
|
});
|
|
15730
15734
|
}
|
|
15731
15735
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15737,13 +15741,40 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15737
15741
|
kind: "function-variable",
|
|
15738
15742
|
file: sourceFile2.file,
|
|
15739
15743
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15740
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
15744
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
15745
|
+
duplicateNameExempt: pageExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15741
15746
|
});
|
|
15742
15747
|
}
|
|
15743
15748
|
}
|
|
15744
15749
|
}
|
|
15745
15750
|
return declarations;
|
|
15746
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
|
+
}
|
|
15747
15778
|
function getExportedClassNames(sourceFile2) {
|
|
15748
15779
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15749
15780
|
}
|
|
@@ -15762,11 +15793,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15762
15793
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15763
15794
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15764
15795
|
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
|
-
];
|
|
15796
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15770
15797
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15771
15798
|
rule: "akan.file.dictionary-stale-text",
|
|
15772
15799
|
scope: "file",
|
|
@@ -16031,9 +16058,51 @@ var targetPaths = {
|
|
|
16031
16058
|
};
|
|
16032
16059
|
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
16033
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
|
+
`;
|
|
16101
|
+
};
|
|
16034
16102
|
var renderManagedBlock = async (workspace) => {
|
|
16035
16103
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16036
16104
|
const frameworkGuide = await Prompter.getInstruction("framework");
|
|
16105
|
+
const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
|
|
16037
16106
|
return `## Workspace
|
|
16038
16107
|
|
|
16039
16108
|
- Repo: ${context.repoName}
|
|
@@ -16041,7 +16110,7 @@ var renderManagedBlock = async (workspace) => {
|
|
|
16041
16110
|
- Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
|
|
16042
16111
|
- Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
|
|
16043
16112
|
|
|
16044
|
-
## Akan Module Abstracts
|
|
16113
|
+
${sampleCleanup}## Akan Module Abstracts
|
|
16045
16114
|
|
|
16046
16115
|
- Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
|
|
16047
16116
|
- Update the abstract when business invariants, workflows, or public behavior change.
|
|
@@ -16063,6 +16132,7 @@ If generated output is stale or broken, update the owning source file and run \`
|
|
|
16063
16132
|
- After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
|
|
16064
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.
|
|
16065
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.
|
|
16066
16136
|
|
|
16067
16137
|
## Validation
|
|
16068
16138
|
|
|
@@ -21488,6 +21558,7 @@ import path48 from "path";
|
|
|
21488
21558
|
var defaultWorkspacePeerDependencies = new Set([
|
|
21489
21559
|
"@react-spring/web",
|
|
21490
21560
|
"@use-gesture/react",
|
|
21561
|
+
"chance",
|
|
21491
21562
|
"croner",
|
|
21492
21563
|
"daisyui",
|
|
21493
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 {
|
|
@@ -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
|