@akanjs/cli 2.3.11-rc.6 → 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 +49 -3
- package/index.js +92 -21
- package/package.json +2 -2
- package/templates/appSample/lib/task/task.service.ts +1 -1
- 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 +55 -3
- package/templates/workspaceRoot/biome.json.template +4 -0
- 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 })]
|
|
7658
7669
|
});
|
|
7670
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
7671
|
+
type: "stdio",
|
|
7672
|
+
command: "bash",
|
|
7673
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
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",
|
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",
|
|
@@ -18092,8 +18138,7 @@ var addEnumFieldWorkflowSpec = {
|
|
|
18092
18138
|
predictedChanges: [
|
|
18093
18139
|
{ target: "*/lib/<module>/<module>.constant.ts", action: "modify", reason: "Enum field shape is added." },
|
|
18094
18140
|
{ target: "*/lib/<module>/<module>.dictionary.ts", action: "modify", reason: "Enum labels are added." },
|
|
18095
|
-
{ target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
|
|
18096
|
-
{ 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." }
|
|
18097
18142
|
],
|
|
18098
18143
|
validation: [
|
|
18099
18144
|
...baseValidation,
|
|
@@ -18498,8 +18543,7 @@ var createScalarWorkflowSpec = {
|
|
|
18498
18543
|
}
|
|
18499
18544
|
],
|
|
18500
18545
|
predictedChanges: [
|
|
18501
|
-
{ target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
|
|
18502
|
-
{ 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." }
|
|
18503
18547
|
],
|
|
18504
18548
|
validation: baseValidation,
|
|
18505
18549
|
completionCriteria: ["Scalar files exist under __scalar.", "Generated files are refreshed.", "Lint passes."]
|
|
@@ -20370,14 +20414,18 @@ class ContextRunner extends runner("context") {
|
|
|
20370
20414
|
return await Prompter.getInstruction(name);
|
|
20371
20415
|
}
|
|
20372
20416
|
async installMcp(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
20373
|
-
if (target
|
|
20374
|
-
|
|
20375
|
-
|
|
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) : {};
|
|
20376
20424
|
const mcpServers = existing.mcpServers ?? {};
|
|
20377
20425
|
const currentAkanServer = mcpServers.akan;
|
|
20378
|
-
const nextAkanServer =
|
|
20426
|
+
const nextAkanServer = createAkanMcpServer(target, mode);
|
|
20379
20427
|
if (currentAkanServer && !force && JSON.stringify(currentAkanServer) !== JSON.stringify(nextAkanServer)) {
|
|
20380
|
-
throw new Error(`${
|
|
20428
|
+
throw new Error(`${configPath2} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
20381
20429
|
}
|
|
20382
20430
|
const nextConfig = {
|
|
20383
20431
|
...existing,
|
|
@@ -20386,9 +20434,15 @@ class ContextRunner extends runner("context") {
|
|
|
20386
20434
|
akan: nextAkanServer
|
|
20387
20435
|
}
|
|
20388
20436
|
};
|
|
20389
|
-
await workspace.writeFile(
|
|
20437
|
+
await workspace.writeFile(configPath2, `${JSON.stringify(nextConfig, null, 2)}
|
|
20390
20438
|
`);
|
|
20391
|
-
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;
|
|
20392
20446
|
}
|
|
20393
20447
|
listMcpTools(mode = "readonly") {
|
|
20394
20448
|
return listAkanMcpTools(mode);
|
|
@@ -20680,13 +20734,26 @@ ${payload}`);
|
|
|
20680
20734
|
agent: "`akan agent install <target>` writes editor-specific agent rules with overwrite protection.",
|
|
20681
20735
|
mcp: "`akan mcp --mode readonly|plan|apply` starts the Akan MCP server over stdio with an explicit permission mode.",
|
|
20682
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.',
|
|
20683
|
-
"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."
|
|
20684
20738
|
};
|
|
20685
20739
|
return explanations[command3] ?? `No detailed explanation is available for ${command3}. Run \`akan --help\` for command help.`;
|
|
20686
20740
|
}
|
|
20687
20741
|
}
|
|
20688
20742
|
|
|
20689
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
|
+
|
|
20690
20757
|
class ContextScript extends script("context", [ContextRunner]) {
|
|
20691
20758
|
async context(workspace, options = {}) {
|
|
20692
20759
|
Logger17.rawLog(await this.contextRunner.getContext(workspace, options));
|
|
@@ -20695,11 +20762,15 @@ class ContextScript extends script("context", [ContextRunner]) {
|
|
|
20695
20762
|
Logger17.rawLog(await this.contextRunner.doctor(workspace, options));
|
|
20696
20763
|
}
|
|
20697
20764
|
async mcpInstall(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
20698
|
-
|
|
20699
|
-
|
|
20700
|
-
const
|
|
20701
|
-
|
|
20702
|
-
|
|
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
|
+
`)}`);
|
|
20703
20774
|
}
|
|
20704
20775
|
async mcp(workspace, { mode = "readonly" } = {}) {
|
|
20705
20776
|
await this.contextRunner.runMcp(workspace, { mode });
|
|
@@ -20738,7 +20809,7 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
|
|
|
20738
20809
|
}).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
|
|
20739
20810
|
await this.contextScript.doctor(workspace, { format, strict });
|
|
20740
20811
|
}),
|
|
20741
|
-
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, {
|
|
20742
20813
|
desc: "MCP permission mode",
|
|
20743
20814
|
default: "readonly",
|
|
20744
20815
|
enum: ["readonly", "plan", "apply"]
|
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",
|
|
@@ -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.
|
|
@@ -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`
|
|
@@ -112,6 +111,12 @@ akan test <%= appName %>
|
|
|
112
111
|
akan build <%= appName %>
|
|
113
112
|
```
|
|
114
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
|
+
|
|
115
120
|
### Other Frequently Used Commands
|
|
116
121
|
|
|
117
122
|
```bash
|
|
@@ -230,6 +235,7 @@ final fallback when no CLI command covers the change.
|
|
|
230
235
|
| Add a pure workflow / integration (e.g., Payment, Email) | `lib/_<service>/` → service, signal, store, dictionary, abstract | `akan sync <name>` |
|
|
231
236
|
| Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
|
|
232
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 |
|
|
233
239
|
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
|
|
234
240
|
| Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
|
|
235
241
|
| Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
|
|
@@ -247,6 +253,7 @@ final fallback when no CLI command covers the change.
|
|
|
247
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 |
|
|
248
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 |
|
|
249
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 |
|
|
250
257
|
|
|
251
258
|
## Generated File Tracker (Quick Reference)
|
|
252
259
|
|
|
@@ -257,7 +264,6 @@ These files are regenerated by `akan sync` and overwritten on every sync. **Do n
|
|
|
257
264
|
| `*/lib/cnst.ts` | All `*/lib/*/**.constant.ts` | Barrel for all constants |
|
|
258
265
|
| `*/lib/db.ts` | All `*/lib/<model>/*.document.ts` | Barrel for all document models |
|
|
259
266
|
| `*/lib/dict.ts` | All `*/lib/*/**.dictionary.ts` | Barrel for all dictionaries |
|
|
260
|
-
| `*/lib/option.ts` | Generated option helpers | Option helper entry |
|
|
261
267
|
| `*/lib/sig.ts` | All `*/lib/**/**.signal.ts` | Barrel for all signals |
|
|
262
268
|
| `*/lib/srv.ts` | All `*/lib/**/**.service.ts` | Barrel for all services |
|
|
263
269
|
| `*/lib/st.ts` | All `*/lib/**/**.store.ts` | Barrel for all stores |
|
|
@@ -338,6 +344,10 @@ Three patterns: injecting an **external adapter** (`use<>()`), another **module'
|
|
|
338
344
|
or a **predefined framework adapter** (`plug()`). A field named `<refName>Service` resolves to the service
|
|
339
345
|
registered under `<refName>` — the `Service`/`Signal` suffix is required and stripped to derive the lookup key.
|
|
340
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.
|
|
350
|
+
|
|
341
351
|
**A. Adapter injection via `use<>()` (for external clients / global singletons)**
|
|
342
352
|
|
|
343
353
|
```typescript
|
|
@@ -411,12 +421,19 @@ store's generated `upload<Field>On<Model>(fileList)` action handle the upload.
|
|
|
411
421
|
|
|
412
422
|
A Slice is a named, filtered data view. Add file entries and connect from a page.
|
|
413
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
|
+
|
|
414
430
|
```typescript
|
|
415
431
|
// 1. apps/<app>/lib/<model>/<model>.signal.ts — Define the slice
|
|
416
432
|
export class TaskSlice extends slice(srv.task, (init) => ({
|
|
417
433
|
inTodo: init()
|
|
418
434
|
.search("statuses", [cnst.TaskStatus])
|
|
419
435
|
.exec(function (statuses?) {
|
|
436
|
+
// ✅ query<Filter> — a query descriptor. ❌ listByStatuses(...) returns an array and fails at runtime.
|
|
420
437
|
return this.taskService.queryByStatuses(statuses ?? ["todo", "inProgress"]);
|
|
421
438
|
}),
|
|
422
439
|
})) {}
|
|
@@ -595,6 +612,41 @@ For each business question, follow this chain:
|
|
|
595
612
|
| What client state is shared? | `store.ts` — `store()` with auto-generated form/insight state + custom actions |
|
|
596
613
|
| What should users see? | `View.tsx` + `Zone.tsx` (detail/container), `Template.tsx` (forms), `Unit.tsx` (cards), `Util.tsx` (buttons) |
|
|
597
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
|
+
|
|
598
650
|
## Auto-Generated API Reference
|
|
599
651
|
|
|
600
652
|
akan sync automatically generates APIs across all layers. Only write custom logic — never hand-write what the framework generates.
|
|
@@ -618,7 +670,7 @@ akan sync automatically generates APIs across all layers. Only write custom logi
|
|
|
618
670
|
|---------------|-------------|
|
|
619
671
|
| `this.<model>Model` | Auto-injected model adaptor |
|
|
620
672
|
| `get<Model>(id)`, `load<Model>(id)` | Single document lookup |
|
|
621
|
-
| `
|
|
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`) |
|
|
622
674
|
| `list<Query>(args)`, `find<Query>(args)`, `pick<Query>(args)` | Filter-based queries |
|
|
623
675
|
| `exists<Query>(args)`, `count<Query>(args)`, `insight<Query>(args)` | Filter-based helpers |
|
|
624
676
|
| `_preCreate`, `_postCreate`, `_preUpdate`, `_postUpdate`, `_preRemove`, `_postRemove` | Lifecycle hooks (override to add logic) |
|
|
@@ -147,6 +147,10 @@
|
|
|
147
147
|
"includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
|
|
148
148
|
"plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
|
|
149
149
|
},
|
|
150
|
+
{
|
|
151
|
+
"includes": ["**/*.signal.ts"],
|
|
152
|
+
"plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
|
|
153
|
+
},
|
|
150
154
|
{
|
|
151
155
|
"includes": [
|
|
152
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",
|