@akanjs/cli 2.3.11-rc.1 → 2.3.11-rc.10

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/index.js CHANGED
@@ -664,6 +664,7 @@ import {
664
664
  import { readFileSync as readFileSync3 } from "fs";
665
665
  import { copyFile, mkdir as mkdir2, readdir as readDirEntries, stat as stat2 } from "fs/promises";
666
666
  import path7 from "path";
667
+ import { pathToFileURL } from "url";
667
668
  import {
668
669
  capitalize,
669
670
  isRouteSourceFile,
@@ -1105,6 +1106,63 @@ var decreaseBuildNum = async (app) => {
1105
1106
  const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
1106
1107
  fs.writeFileSync(akanConfigPath, akanConfigContent);
1107
1108
  };
1109
+ // pkgs/@akanjs/devkit/firebaseMessagingSw.ts
1110
+ var FIREBASE_WEB_SDK_VERSION = "12.13.0";
1111
+ function normalizeFirebaseClientConfig(config) {
1112
+ if (!config || typeof config !== "object")
1113
+ return null;
1114
+ const value = config;
1115
+ if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
1116
+ return null;
1117
+ }
1118
+ return {
1119
+ apiKey: value.apiKey,
1120
+ ...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
1121
+ projectId: value.projectId,
1122
+ ...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
1123
+ messagingSenderId: value.messagingSenderId,
1124
+ appId: value.appId
1125
+ };
1126
+ }
1127
+ function createFirebaseMessagingServiceWorker(config) {
1128
+ const configJson = JSON.stringify(config);
1129
+ return `/* Generated by Akan.js. Do not edit. */
1130
+ const firebaseConfig = ${configJson};
1131
+
1132
+ if (firebaseConfig) {
1133
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1134
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1135
+
1136
+ firebase.initializeApp(firebaseConfig);
1137
+ const messaging = firebase.messaging();
1138
+
1139
+ const notificationUrl = (payload) =>
1140
+ payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1141
+
1142
+ messaging.onBackgroundMessage((payload) => {
1143
+ const title = payload?.notification?.title || "";
1144
+ const options = {
1145
+ body: payload?.notification?.body,
1146
+ icon: payload?.notification?.icon,
1147
+ image: payload?.notification?.image,
1148
+ data: {
1149
+ url: notificationUrl(payload),
1150
+ FCM_MSG: payload,
1151
+ },
1152
+ };
1153
+ self.registration.showNotification(title, options);
1154
+ });
1155
+ }
1156
+
1157
+ self.addEventListener("notificationclick", (event) => {
1158
+ const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1159
+ event.notification?.close();
1160
+ if (!url) return;
1161
+ event.waitUntil(clients.openWindow(url));
1162
+ });
1163
+ `;
1164
+ }
1165
+
1108
1166
  // pkgs/@akanjs/devkit/getDirname.ts
1109
1167
  import path2 from "path";
1110
1168
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1390,6 +1448,7 @@ ${errorText}, ${warningText} found`];
1390
1448
 
1391
1449
  // pkgs/@akanjs/devkit/scanInfo.ts
1392
1450
  import path5 from "path";
1451
+ import { rm } from "fs/promises";
1393
1452
 
1394
1453
  // pkgs/@akanjs/devkit/dependencyScanner.ts
1395
1454
  import { builtinModules } from "module";
@@ -1708,8 +1767,10 @@ var appRootAllowedFiles = new Set([
1708
1767
  "main.ts",
1709
1768
  "package.json",
1710
1769
  "server.ts",
1711
- "tsconfig.json"
1770
+ "tsconfig.json",
1771
+ "tsconfig.tsbuildinfo"
1712
1772
  ]);
1773
+ var generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"];
1713
1774
  var appRootAllowedDirs = new Set([
1714
1775
  ".akan",
1715
1776
  "android",
@@ -1753,11 +1814,17 @@ var rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
1753
1814
  var isAllowedTestFile = (filename) => testFilePattern.test(filename);
1754
1815
  var isAllowedLibRootFile = (filename) => libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
1755
1816
  var getScanPath = (exec, relativePath) => path5.posix.join(`${exec.type}s`, exec.name, relativePath.split(path5.sep).join("/"));
1817
+ async function clearGeneratedRootCapacitorConfigs(exec) {
1818
+ if (exec.type !== "app")
1819
+ return;
1820
+ await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
1821
+ }
1756
1822
  var getModuleNameFromPath = (kind, modulePath) => {
1757
1823
  const dirname = path5.basename(modulePath);
1758
1824
  return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
1759
1825
  };
1760
1826
  async function assertScanConvention(exec, libRoot) {
1827
+ await clearGeneratedRootCapacitorConfigs(exec);
1761
1828
  const violations = [];
1762
1829
  const addViolation = (relativePath, reason) => {
1763
1830
  violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
@@ -2622,6 +2689,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2622
2689
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2623
2690
  }
2624
2691
  }
2692
+ function validateOverridesSourceExports(source, filePath) {
2693
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2694
+ const fail = (message) => {
2695
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2696
+ };
2697
+ let defaultOverride = null;
2698
+ for (const statement of sourceFile.statements) {
2699
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2700
+ continue;
2701
+ if (ts3.isImportDeclaration(statement))
2702
+ continue;
2703
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2704
+ continue;
2705
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2706
+ defaultOverride = statement;
2707
+ continue;
2708
+ }
2709
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2710
+ }
2711
+ if (!defaultOverride)
2712
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2713
+ const expression = defaultOverride.expression;
2714
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2715
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2716
+ }
2625
2717
 
2626
2718
  class Executor {
2627
2719
  static verbose = false;
@@ -3574,6 +3666,8 @@ class AppExecutor extends SysExecutor {
3574
3666
  ...routeEnv,
3575
3667
  ...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
3576
3668
  });
3669
+ if (type === "build")
3670
+ Object.assign(process.env, env);
3577
3671
  return { env };
3578
3672
  }
3579
3673
  #publicEnv = null;
@@ -3643,7 +3737,11 @@ class AppExecutor extends SysExecutor {
3643
3737
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3644
3738
  }
3645
3739
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3646
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3740
+ const routeSource = await Bun.file(absPath).text();
3741
+ if (parsed.kind === "overrides")
3742
+ validateOverridesSourceExports(routeSource, absPath);
3743
+ else
3744
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3647
3745
  pageKeys.push(key);
3648
3746
  }
3649
3747
  pageKeys.sort();
@@ -3684,8 +3782,30 @@ class AppExecutor extends SysExecutor {
3684
3782
  });
3685
3783
  if (write)
3686
3784
  await this.syncAssets(scanInfo.getScanResult().libDeps);
3785
+ if (write)
3786
+ await this.#syncFirebaseMessagingSw();
3687
3787
  return scanInfo;
3688
3788
  }
3789
+ async#syncFirebaseMessagingSw() {
3790
+ const swRelPath = "public/firebase-messaging-sw.js";
3791
+ if (await FileSys.fileExists(this.getPath(swRelPath)))
3792
+ return;
3793
+ const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3794
+ if (!await FileSys.fileExists(envClientPath))
3795
+ return;
3796
+ let firebaseConfig = null;
3797
+ try {
3798
+ const envUrl = pathToFileURL(envClientPath);
3799
+ envUrl.searchParams.set("t", String(Date.now()));
3800
+ const envModule = await import(envUrl.href);
3801
+ firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
3802
+ } catch {
3803
+ return;
3804
+ }
3805
+ if (!firebaseConfig)
3806
+ return;
3807
+ await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
3808
+ }
3689
3809
  async increaseBuildNum() {
3690
3810
  await increaseBuildNum(this);
3691
3811
  }
@@ -4168,6 +4288,7 @@ var BACKEND_RESTART_DEBOUNCE_MS = 120;
4168
4288
  var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
4169
4289
  var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
4170
4290
  var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
4291
+ var BACKEND_STDERR_TAIL_LIMIT = 40;
4171
4292
  var BUILDER_READY_TIMEOUT_MS = 150000;
4172
4293
  var BUILDER_START_MAX_ATTEMPTS = 3;
4173
4294
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -4370,6 +4491,7 @@ class AkanAppHost {
4370
4491
  #pendingRestartReason = null;
4371
4492
  #backendStartStatus = null;
4372
4493
  #backendBuildStatusGeneration = 0;
4494
+ #backendStderrTail = [];
4373
4495
  #lastGoodFrontend = {};
4374
4496
  #buildStatusByPhase = new Map;
4375
4497
  #pendingBuildStatusReplay = [];
@@ -4421,6 +4543,7 @@ class AkanAppHost {
4421
4543
  this.#backendStartStatus = startStatus;
4422
4544
  this.#setBackendLifecycleState("starting");
4423
4545
  this.#backendReady = false;
4546
+ this.#backendStderrTail = [];
4424
4547
  const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
4425
4548
  cwd: this.app.workspace.workspaceRoot,
4426
4549
  stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
@@ -4454,6 +4577,37 @@ class AkanAppHost {
4454
4577
  });
4455
4578
  this.#backend = backend;
4456
4579
  this.logger.verbose(`backend spawned pid=${backend.pid}`);
4580
+ if (this.withInk) {
4581
+ this.#forwardBackendStream(backend.stderr, "stderr");
4582
+ this.#forwardBackendStream(backend.stdout, "stdout");
4583
+ }
4584
+ }
4585
+ #recordBackendStderr(chunk) {
4586
+ const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
4587
+ if (lines.length === 0)
4588
+ return;
4589
+ this.#backendStderrTail.push(...lines);
4590
+ if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
4591
+ this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
4592
+ }
4593
+ }
4594
+ async#forwardBackendStream(stream, kind) {
4595
+ if (!stream)
4596
+ return;
4597
+ const decoder = new TextDecoder;
4598
+ try {
4599
+ for await (const chunk of stream) {
4600
+ const text = decoder.decode(chunk, { stream: true });
4601
+ if (!text.trim())
4602
+ continue;
4603
+ if (kind === "stderr") {
4604
+ this.#recordBackendStderr(text);
4605
+ this.logger.warn(`[backend] ${text.trimEnd()}`);
4606
+ } else {
4607
+ this.logger.verbose(`[backend] ${text.trimEnd()}`);
4608
+ }
4609
+ }
4610
+ } catch {}
4457
4611
  }
4458
4612
  #nextBackendBuildStatusGeneration(generation) {
4459
4613
  if (typeof generation === "number") {
@@ -4578,6 +4732,11 @@ class AkanAppHost {
4578
4732
  });
4579
4733
  this.#sendOrQueueBuildStatus(failureStatus);
4580
4734
  this.logger.warn(`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`);
4735
+ if (this.#backendStderrTail.length > 0) {
4736
+ this.logger.warn(`[backend-recovery] recent backend stderr:
4737
+ ${this.#backendStderrTail.join(`
4738
+ `)}`);
4739
+ }
4581
4740
  this.#backendRecoveryTimer = setTimeout(() => {
4582
4741
  this.#backendRecoveryTimer = null;
4583
4742
  if (this.#backend)
@@ -4954,7 +5113,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
4954
5113
  var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
4955
5114
  { path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
4956
5115
  { path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
4957
- { path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
4958
5116
  { path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
4959
5117
  ];
4960
5118
  var writeGeneratedSyncState = async (workspace, state) => {
@@ -5943,6 +6101,117 @@ ${values.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${
5943
6101
  return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
5944
6102
  };
5945
6103
  var parseValues = (value) => value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
6104
+ var hasSourceParseErrors = (content, fileName = "source.ts") => hasParseDiagnostics(sourceFileFor(fileName, content));
6105
+ var findClassDeclaration = (source, className) => {
6106
+ let found = null;
6107
+ const visit = (node) => {
6108
+ if (found)
6109
+ return;
6110
+ if (ts4.isClassDeclaration(node) && node.name?.text === className) {
6111
+ found = node;
6112
+ return;
6113
+ }
6114
+ ts4.forEachChild(node, visit);
6115
+ };
6116
+ ts4.forEachChild(source, visit);
6117
+ return found;
6118
+ };
6119
+ var firstHeritageCall = (node) => {
6120
+ const expression = (node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [])[0]?.expression;
6121
+ return expression && ts4.isCallExpression(expression) ? expression : null;
6122
+ };
6123
+ var factoryArrowOf = (call) => {
6124
+ const arg = call.arguments.find((argument) => ts4.isArrowFunction(argument) || ts4.isFunctionExpression(argument));
6125
+ return arg && (ts4.isArrowFunction(arg) || ts4.isFunctionExpression(arg)) ? arg : null;
6126
+ };
6127
+ var hasClassMethod = (content, className, methodName) => {
6128
+ const node = findClassDeclaration(sourceFileFor("service.ts", content), className);
6129
+ return Boolean(node?.members.some((member) => ts4.isMethodDeclaration(member) && nodeName(member.name) === methodName));
6130
+ };
6131
+ var insertClassMethod = (content, className, methodBlock) => {
6132
+ const source = sourceFileFor("service.ts", content);
6133
+ const node = findClassDeclaration(source, className);
6134
+ if (!node)
6135
+ return null;
6136
+ const closeBrace = node.getEnd() - 1;
6137
+ if (content[closeBrace] !== "}")
6138
+ return null;
6139
+ const lead = content.slice(0, closeBrace).endsWith(`
6140
+ `) ? "" : `
6141
+ `;
6142
+ return spliceText(content, closeBrace, closeBrace, `${lead}${methodBlock}
6143
+ `);
6144
+ };
6145
+ var arrowParamInnerRegion = (content, source, arrow) => {
6146
+ if (arrow.parameters.length > 0) {
6147
+ return {
6148
+ start: arrow.parameters[0].getStart(source),
6149
+ end: arrow.parameters[arrow.parameters.length - 1].getEnd()
6150
+ };
6151
+ }
6152
+ const open = content.indexOf("(", arrow.getStart(source));
6153
+ if (open < 0)
6154
+ return null;
6155
+ const close = content.indexOf(")", open);
6156
+ if (close < 0)
6157
+ return null;
6158
+ return { start: open + 1, end: close };
6159
+ };
6160
+ var factoryParamEdit = (content, source, arrow, plan) => {
6161
+ if (arrow.parameters.length > 1)
6162
+ return null;
6163
+ const region = arrowParamInnerRegion(content, source, arrow);
6164
+ if (!region)
6165
+ return null;
6166
+ if (arrow.parameters.length === 0) {
6167
+ return { ...region, text: plan.mode === "destructure" ? `{ ${plan.name} }` : plan.name };
6168
+ }
6169
+ const param = arrow.parameters[0];
6170
+ if (plan.mode === "positional")
6171
+ return nodeName(param.name) === plan.name ? "unchanged" : null;
6172
+ if (!ts4.isObjectBindingPattern(param.name))
6173
+ return null;
6174
+ const names = param.name.elements.map((element) => ts4.isIdentifier(element.name) ? element.name.text : null);
6175
+ if (names.some((name) => name === null))
6176
+ return null;
6177
+ if (names.includes(plan.name))
6178
+ return "unchanged";
6179
+ return {
6180
+ start: param.getStart(source),
6181
+ end: param.getEnd(),
6182
+ text: `{ ${[...names, plan.name].join(", ")} }`
6183
+ };
6184
+ };
6185
+ var factoryObjectOf = (source, className) => {
6186
+ const node = findClassDeclaration(source, className);
6187
+ const call = node ? firstHeritageCall(node) : null;
6188
+ const arrow = call ? factoryArrowOf(call) : null;
6189
+ const object = arrow ? firstObjectReturnedByArrow(arrow) : null;
6190
+ return arrow && object ? { arrow, object } : null;
6191
+ };
6192
+ var hasSignalFactoryEntry = (content, className, entryName) => {
6193
+ const located = factoryObjectOf(sourceFileFor("signal.ts", content), className);
6194
+ return Boolean(located?.object.properties.some((property) => propertyName(property) === entryName));
6195
+ };
6196
+ var insertSignalFactoryEntry = (content, className, entryName, entryLine, param) => {
6197
+ const source = sourceFileFor("signal.ts", content);
6198
+ const located = factoryObjectOf(source, className);
6199
+ if (!located)
6200
+ return null;
6201
+ const locator = locatedObject(source, located.object);
6202
+ if (locator.fields.some((field) => field.name === entryName))
6203
+ return content;
6204
+ const paramEdit = factoryParamEdit(content, source, located.arrow, param);
6205
+ if (paramEdit === null)
6206
+ return null;
6207
+ const withEntry = insertOrderedFieldLine(content, locator, entryName, entryLine, {
6208
+ fieldIndent: " ",
6209
+ closingIndent: ""
6210
+ });
6211
+ if (withEntry === content)
6212
+ return null;
6213
+ return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
6214
+ };
5946
6215
 
5947
6216
  // pkgs/@akanjs/devkit/workflow/moduleIndex.ts
5948
6217
  var indexFileKinds = [
@@ -6660,7 +6929,9 @@ var createWorkflowStepRegistry = ({
6660
6929
  createScalar,
6661
6930
  createUi,
6662
6931
  addField,
6663
- addEnumField
6932
+ addEnumField,
6933
+ addMutation,
6934
+ addSlice
6664
6935
  }) => {
6665
6936
  const inspect = async () => {
6666
6937
  return;
@@ -6736,7 +7007,19 @@ var createWorkflowStepRegistry = ({
6736
7007
  defaultValue: workflowStringInput(plan.inputs.default)
6737
7008
  })),
6738
7009
  [workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
6739
- [workflowStepKey("add-enum-field", "update-option")]: inspect
7010
+ [workflowStepKey("add-enum-field", "update-option")]: inspect,
7011
+ [workflowStepKey("add-mutation", "update-service")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addMutation({
7012
+ app: workflowStringInput(plan.inputs.app),
7013
+ module: workflowStringInput(plan.inputs.module),
7014
+ mutation: workflowStringInput(plan.inputs.mutation)
7015
+ })),
7016
+ [workflowStepKey("add-mutation", "update-signal")]: inspect,
7017
+ [workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addSlice({
7018
+ app: workflowStringInput(plan.inputs.app),
7019
+ module: workflowStringInput(plan.inputs.module),
7020
+ slice: workflowStringInput(plan.inputs.slice)
7021
+ })),
7022
+ [workflowStepKey("add-slice", "update-signal-slice")]: inspect
6740
7023
  };
6741
7024
  };
6742
7025
  class WorkflowExecutor {
@@ -7483,13 +7766,59 @@ var resourceList = [
7483
7766
  { uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
7484
7767
  ];
7485
7768
  var cursorMcpConfigPath = ".cursor/mcp.json";
7769
+ var claudeMcpConfigPath = ".mcp.json";
7770
+ var codexMcpConfigPath = ".codex/config.toml";
7771
+ var akanMcpInstallTargets = ["cursor", "claude", "codex"];
7772
+ var akanMcpInstallConfigPaths = {
7773
+ cursor: cursorMcpConfigPath,
7774
+ claude: claudeMcpConfigPath,
7775
+ codex: codexMcpConfigPath
7776
+ };
7486
7777
  var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
7778
+ var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
7779
+ var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
7487
7780
  var createAkanCursorMcpServer = (mode = "readonly") => ({
7488
7781
  type: "stdio",
7489
7782
  command: "bash",
7490
- args: ["-lc", `cd "${cursorWorkspaceFolder}" && akan mcp --mode ${mode}`]
7783
+ args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
7491
7784
  });
7785
+ var createAkanClaudeMcpServer = (mode = "readonly") => ({
7786
+ type: "stdio",
7787
+ command: "bash",
7788
+ args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
7789
+ });
7790
+ var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
7492
7791
  var akanCursorMcpServer = createAkanCursorMcpServer();
7792
+ var codexMcpServerTableHeader = "[mcp_servers.akan]";
7793
+ var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
7794
+ command = "bash"
7795
+ args = ["-lc", "${akanMcpCommand(mode)}"]
7796
+ `;
7797
+ var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
7798
+ var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
7799
+ const nextBlock = block.endsWith(`
7800
+ `) ? block : `${block}
7801
+ `;
7802
+ const match = existing.match(codexAkanTablePattern);
7803
+ if (!match) {
7804
+ if (!existing.trim())
7805
+ return nextBlock;
7806
+ return `${existing.replace(/\s*$/, "")}
7807
+
7808
+ ${nextBlock}`;
7809
+ }
7810
+ if (match[0].trim() === nextBlock.trim())
7811
+ return existing;
7812
+ if (!force)
7813
+ throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
7814
+ const start = match.index ?? 0;
7815
+ const before = existing.slice(0, start);
7816
+ const after = existing.slice(start + match[0].length);
7817
+ const separator = after && !after.startsWith(`
7818
+ `) ? `
7819
+ ` : "";
7820
+ return `${before}${nextBlock}${separator}${after}`;
7821
+ };
7493
7822
  var renderDoctorText = (result) => {
7494
7823
  const lines = [`Akan doctor status: ${result.status}`];
7495
7824
  if (result.diagnostics.length === 0) {
@@ -7512,7 +7841,6 @@ var generatedFiles = [
7512
7841
  "*/lib/cnst.ts",
7513
7842
  "*/lib/db.ts",
7514
7843
  "*/lib/dict.ts",
7515
- "*/lib/option.ts",
7516
7844
  "*/lib/sig.ts",
7517
7845
  "*/lib/srv.ts",
7518
7846
  "*/lib/st.ts",
@@ -8506,7 +8834,7 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
8506
8834
  }
8507
8835
  }
8508
8836
  // pkgs/@akanjs/devkit/applicationBuildRunner.ts
8509
- import { mkdir as mkdir8, rm as rm3 } from "fs/promises";
8837
+ import { mkdir as mkdir8, rm as rm4 } from "fs/promises";
8510
8838
  import path37 from "path";
8511
8839
 
8512
8840
  // pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
@@ -8521,6 +8849,25 @@ async function appHasStModule(appCwdPath) {
8521
8849
  }
8522
8850
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8523
8851
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8852
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8853
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8854
+ async function writeGeneratedOverridesLayoutFile(opts) {
8855
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8856
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8857
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8858
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8859
+ const source2 = `"use client";
8860
+ import { UiOverrideProvider } from "akanjs/ui";
8861
+ import { createElement, type ReactNode } from "react";
8862
+ import value from ${JSON.stringify(userSpecifier)};
8863
+
8864
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8865
+ return createElement(UiOverrideProvider, { value }, children);
8866
+ }
8867
+ `;
8868
+ await Bun.write(absPath, source2);
8869
+ return absPath;
8870
+ }
8524
8871
  function getRootBoundarySegments(key) {
8525
8872
  const match = LAYOUT_KEY_RE.exec(key);
8526
8873
  if (!match)
@@ -8713,9 +9060,17 @@ async function resolveSsrPageEntries(opts) {
8713
9060
  const segments = getRootBoundarySegments(key);
8714
9061
  return segments !== null && isRootBoundarySegments(segments, basePaths);
8715
9062
  }));
8716
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
8717
- key,
8718
- moduleAbsPath: path13.resolve(absPageDir, key)
9063
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9064
+ const userAbsPath = path13.resolve(absPageDir, key);
9065
+ if (OVERRIDES_KEY_RE.test(key)) {
9066
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9067
+ appCwdPath: opts.appCwdPath,
9068
+ key,
9069
+ userAbsPath
9070
+ });
9071
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9072
+ }
9073
+ return { key, moduleAbsPath: userAbsPath };
8719
9074
  }));
8720
9075
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
8721
9076
  key: implicitRootLayoutKey(boundary.segments),
@@ -8747,7 +9102,7 @@ function computeRouteSeedIndex(pageEntries) {
8747
9102
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
8748
9103
  const parsed = parseRouteModuleKey2(key);
8749
9104
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
8750
- if (parsed.kind === "layout") {
9105
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
8751
9106
  const prefix = parsed.routeSegments.join("/");
8752
9107
  const prev = layoutsByPrefix.get(prefix) ?? [];
8753
9108
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -10264,7 +10619,7 @@ class AllRoutesBuilder {
10264
10619
  }
10265
10620
  }
10266
10621
  // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
10267
- import { mkdir as mkdir5, rm, unlink } from "fs/promises";
10622
+ import { mkdir as mkdir5, rm as rm2, unlink } from "fs/promises";
10268
10623
  import path24 from "path";
10269
10624
 
10270
10625
  // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
@@ -10394,7 +10749,7 @@ class CsrArtifactBuilder {
10394
10749
  const artifact = await this.#loadCsrArtifact();
10395
10750
  const csrBasePaths = [...akanConfig2.basePaths];
10396
10751
  const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
10397
- await rm(this.#outputDir, { recursive: true, force: true });
10752
+ await rm2(this.#outputDir, { recursive: true, force: true });
10398
10753
  await mkdir5(path24.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
10399
10754
  const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath2) => this.#createHtmlFile(basePath2)));
10400
10755
  const result = await Bun.build({
@@ -11128,7 +11483,7 @@ class DevChangePlanner {
11128
11483
  }
11129
11484
  var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(file)))].sort();
11130
11485
  // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
11131
- import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm2, stat as stat3, writeFile } from "fs/promises";
11486
+ import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
11132
11487
  import path28 from "path";
11133
11488
  var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11134
11489
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
@@ -11211,7 +11566,7 @@ class DevGeneratedIndexSync {
11211
11566
  if (content === null) {
11212
11567
  if (!await exists(indexPath))
11213
11568
  return false;
11214
- await rm2(indexPath, { force: true });
11569
+ await rm3(indexPath, { force: true });
11215
11570
  return true;
11216
11571
  }
11217
11572
  const current = await readFile(indexPath, "utf8").catch(() => null);
@@ -12358,7 +12713,7 @@ class ApplicationBuildRunner {
12358
12713
  await this.#app.getPageKeys({ refresh: true });
12359
12714
  const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
12360
12715
  if (clean)
12361
- await rm3(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
12716
+ await rm4(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
12362
12717
  await this.#checkProjectInChildProcess(tsconfigPath);
12363
12718
  }
12364
12719
  async#runPhase(id, label, task, summarize, options = {}) {
@@ -12558,7 +12913,7 @@ void run().catch((error) => {
12558
12913
  }
12559
12914
  }
12560
12915
  // pkgs/@akanjs/devkit/applicationReleasePackager.ts
12561
- import { cp, mkdir as mkdir9, rm as rm4 } from "fs/promises";
12916
+ import { cp, mkdir as mkdir9, rm as rm5 } from "fs/promises";
12562
12917
  import path38 from "path";
12563
12918
 
12564
12919
  // pkgs/@akanjs/devkit/uploadRelease.ts
@@ -12665,7 +13020,7 @@ class ApplicationReleasePackager {
12665
13020
  const platformVersion = akanConfig2.mobile.version;
12666
13021
  const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
12667
13022
  if (await FileSys.dirExists(buildRoot))
12668
- await rm4(buildRoot, { recursive: true, force: true });
13023
+ await rm5(buildRoot, { recursive: true, force: true });
12669
13024
  await mkdir9(buildRoot, { recursive: true });
12670
13025
  if (rebuild || !await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))
12671
13026
  await this.#build();
@@ -12674,7 +13029,7 @@ class ApplicationReleasePackager {
12674
13029
  await mkdir9(buildPath, { recursive: true });
12675
13030
  await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
12676
13031
  await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
12677
- await rm4(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
13032
+ await rm5(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
12678
13033
  const releaseRoot = this.#app.workspace.workspaceRoot;
12679
13034
  const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
12680
13035
  await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
@@ -12690,7 +13045,7 @@ class ApplicationReleasePackager {
12690
13045
  `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
12691
13046
  "./csr"
12692
13047
  ]);
12693
- await rm4("./csr", { recursive: true, force: true });
13048
+ await rm5("./csr", { recursive: true, force: true });
12694
13049
  }
12695
13050
  async#writeSourceArchive({ readme }) {
12696
13051
  const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
@@ -12701,7 +13056,7 @@ class ApplicationReleasePackager {
12701
13056
  await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
12702
13057
  const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
12703
13058
  if (await FileSys.dirExists(targetPath))
12704
- await rm4(targetPath, { recursive: true, force: true });
13059
+ await rm5(targetPath, { recursive: true, force: true });
12705
13060
  }));
12706
13061
  const syncPaths = [".husky", ".gitignore", "package.json"];
12707
13062
  await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
@@ -12716,7 +13071,7 @@ class ApplicationReleasePackager {
12716
13071
  const maxRetry = 3;
12717
13072
  for (let i = 0;i < maxRetry; i++) {
12718
13073
  try {
12719
- await rm4(sourceRoot, { recursive: true, force: true });
13074
+ await rm5(sourceRoot, { recursive: true, force: true });
12720
13075
  } catch {}
12721
13076
  }
12722
13077
  }
@@ -12805,11 +13160,15 @@ async function resolveSignalTestPreloadPath(target) {
12805
13160
  addResolvedPackageCandidate(path39.dirname(Bun.main));
12806
13161
  addResolvedPackageCandidate(import.meta.dir);
12807
13162
  candidates.push(path39.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(path39.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
12808
- for (const candidate of [...new Set(candidates)]) {
13163
+ const uniqueCandidates = [...new Set(candidates)];
13164
+ for (const candidate of uniqueCandidates) {
12809
13165
  if (await Bun.file(candidate).exists())
12810
13166
  return candidate;
12811
13167
  }
12812
- throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}`);
13168
+ throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
13169
+ Probed paths:
13170
+ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
13171
+ `)}`);
12813
13172
  }
12814
13173
  // pkgs/@akanjs/devkit/builder.ts
12815
13174
  import { existsSync as existsSync2 } from "fs";
@@ -12945,7 +13304,7 @@ class Builder {
12945
13304
  }
12946
13305
  }
12947
13306
  // pkgs/@akanjs/devkit/capacitorApp.ts
12948
- import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
13307
+ import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm6, writeFile as writeFile2 } from "fs/promises";
12949
13308
  import os from "os";
12950
13309
  import path41 from "path";
12951
13310
  import { select as select2 } from "@inquirer/prompts";
@@ -13156,7 +13515,7 @@ var rootCapacitorConfigFilenames = [
13156
13515
  ];
13157
13516
  var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
13158
13517
  async function clearRootCapacitorConfigs(appRoot) {
13159
- await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm5(file, { force: true })));
13518
+ await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm6(file, { force: true })));
13160
13519
  }
13161
13520
  async function writeRootCapacitorConfig(appRoot, content) {
13162
13521
  await clearRootCapacitorConfigs(appRoot);
@@ -13177,6 +13536,7 @@ var getLocalIP = () => {
13177
13536
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13178
13537
  var asString = (value) => typeof value === "string" ? value : undefined;
13179
13538
  var firstString = (...values) => values.find((value) => typeof value === "string");
13539
+ var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
13180
13540
  var scoreIosDeviceTarget = (target) => {
13181
13541
  const state = target.state?.toLowerCase() ?? "";
13182
13542
  return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
@@ -13482,6 +13842,8 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13482
13842
  const experimentalConfig = isRecord2(experimental) ? experimental : undefined;
13483
13843
  const pluginsConfig = isRecord2(plugins) ? plugins : {};
13484
13844
  const keyboardPluginConfig = isRecord2(pluginsConfig.Keyboard) ? pluginsConfig.Keyboard : {};
13845
+ const pushNotificationsPluginConfig = isRecord2(pluginsConfig.PushNotifications) ? pluginsConfig.PushNotifications : {};
13846
+ const usesPushNotifications = target.permissions?.includes("push") ?? false;
13485
13847
  const config = {
13486
13848
  ...capacitorConfig,
13487
13849
  appId,
@@ -13490,6 +13852,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13490
13852
  plugins: {
13491
13853
  CapacitorCookies: { enabled: true },
13492
13854
  ...pluginsConfig,
13855
+ ...usesPushNotifications || isRecord2(pluginsConfig.PushNotifications) ? {
13856
+ PushNotifications: {
13857
+ ...usesPushNotifications ? { presentationOptions: ["badge", "sound", "alert"] } : {},
13858
+ ...pushNotificationsPluginConfig
13859
+ }
13860
+ } : {},
13493
13861
  Keyboard: {
13494
13862
  resize: "none",
13495
13863
  ...keyboardPluginConfig
@@ -13561,9 +13929,9 @@ class CapacitorApp {
13561
13929
  await mkdir11(this.targetRoot, { recursive: true });
13562
13930
  if (regenerate) {
13563
13931
  if (!platform || platform === "ios")
13564
- await rm5(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13932
+ await rm6(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13565
13933
  if (!platform || platform === "android")
13566
- await rm5(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13934
+ await rm6(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13567
13935
  }
13568
13936
  const project = this.project;
13569
13937
  await this.project.load();
@@ -13585,7 +13953,7 @@ class CapacitorApp {
13585
13953
  await this.#prepareTargetAssets();
13586
13954
  await this.#prepareExternalFiles("ios");
13587
13955
  await this.#applyIosMetadata();
13588
- await this.#applyPermissions();
13956
+ await this.#applyPermissions({ operation, env });
13589
13957
  await this.#applyDeepLinks("ios", { operation, env });
13590
13958
  await this.project.commit();
13591
13959
  await this.#generateAssets({ operation, env });
@@ -13728,7 +14096,7 @@ ${error.message}`;
13728
14096
  await this.#prepareTargetAssets();
13729
14097
  await this.#prepareExternalFiles("android");
13730
14098
  await this.#applyAndroidMetadata();
13731
- await this.#applyPermissions();
14099
+ await this.#applyPermissions({ operation, env });
13732
14100
  await this.#applyDeepLinks("android", { operation, env });
13733
14101
  await this.project.commit();
13734
14102
  await this.#generateAssets({ operation, env });
@@ -13862,7 +14230,7 @@ ${error.message}`;
13862
14230
  const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13863
14231
  if (!await Bun.file(htmlSource).exists())
13864
14232
  throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
13865
- await rm5(this.targetWebRoot, { recursive: true, force: true });
14233
+ await rm6(this.targetWebRoot, { recursive: true, force: true });
13866
14234
  await mkdir11(this.targetWebRoot, { recursive: true });
13867
14235
  await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13868
14236
  }
@@ -13943,7 +14311,7 @@ ${error.message}`;
13943
14311
  await this.project.android.setVersionCode(this.target.buildNum);
13944
14312
  await this.project.android.setAppName(this.target.appName);
13945
14313
  }
13946
- async#applyPermissions() {
14314
+ async#applyPermissions({ operation, env }) {
13947
14315
  for (const permission of this.target.permissions ?? []) {
13948
14316
  if (permission === "camera")
13949
14317
  await this.addCamera();
@@ -13952,7 +14320,7 @@ ${error.message}`;
13952
14320
  else if (permission === "location")
13953
14321
  await this.addLocation();
13954
14322
  else if (permission === "push")
13955
- await this.addPush();
14323
+ await this.addPush({ operation, env });
13956
14324
  }
13957
14325
  }
13958
14326
  async#applyDeepLinks(platform, { operation, env }) {
@@ -13971,7 +14339,7 @@ ${error.message}`;
13971
14339
  await this.#setUrlSchemesInIos(schemes);
13972
14340
  }
13973
14341
  if (domains.length > 0)
13974
- await this.#setAssociatedDomainsInIos(domains);
14342
+ await this.#setAssociatedDomainsInIos(domains, { operation, env });
13975
14343
  return;
13976
14344
  }
13977
14345
  if (platform === "android") {
@@ -14065,12 +14433,64 @@ ${error.message}`;
14065
14433
  this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
14066
14434
  this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
14067
14435
  }
14068
- async addPush() {
14436
+ async addPush({ operation, env }) {
14069
14437
  await this.#setPermissionInIos({
14070
14438
  userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
14071
14439
  });
14440
+ await Promise.all([
14441
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
14442
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
14443
+ this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
14444
+ this.#ensurePushAppDelegateInIos()
14445
+ ]);
14072
14446
  this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
14073
14447
  }
14448
+ async#ensurePushAppDelegateInIos() {
14449
+ const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
14450
+ if (!await Bun.file(appDelegatePath).exists())
14451
+ return;
14452
+ const editor = await FileEditor.create(appDelegatePath);
14453
+ let content = editor.getContent();
14454
+ if (!content.includes("import FirebaseCore")) {
14455
+ content = content.replace("import Capacitor", `import Capacitor
14456
+ import FirebaseCore`);
14457
+ }
14458
+ if (!content.includes("import FirebaseMessaging")) {
14459
+ content = content.replace("import FirebaseCore", `import FirebaseCore
14460
+ import FirebaseMessaging`);
14461
+ }
14462
+ if (!content.includes("FirebaseApp.configure()")) {
14463
+ content = content.replace(/\n(\s*)return true/, `
14464
+ $1FirebaseApp.configure()
14465
+
14466
+ $1return true`);
14467
+ }
14468
+ if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
14469
+ const delegateMethods = `
14470
+ func application(_ application: UIApplication,
14471
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
14472
+ Messaging.messaging().apnsToken = deviceToken
14473
+ NotificationCenter.default.post(
14474
+ name: .capacitorDidRegisterForRemoteNotifications,
14475
+ object: deviceToken
14476
+ )
14477
+ }
14478
+
14479
+ func application(_ application: UIApplication,
14480
+ didFailToRegisterForRemoteNotificationsWithError error: Error) {
14481
+ NotificationCenter.default.post(
14482
+ name: .capacitorDidFailToRegisterForRemoteNotifications,
14483
+ object: error
14484
+ )
14485
+ }
14486
+ `;
14487
+ content = content.replace(`
14488
+ func applicationWillResignActive`, `${delegateMethods}
14489
+ func applicationWillResignActive`);
14490
+ }
14491
+ if (content !== editor.getContent())
14492
+ await editor.setContent(content).save();
14493
+ }
14074
14494
  async#setPermissionInIos(permissions) {
14075
14495
  const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
14076
14496
  await Promise.all([
@@ -14088,25 +14508,35 @@ ${error.message}`;
14088
14508
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14089
14509
  ]);
14090
14510
  }
14091
- async#setAssociatedDomainsInIos(domains) {
14511
+ async#setAssociatedDomainsInIos(domains, { operation, env }) {
14512
+ await this.#writeEntitlementsInIos(domains, { operation, env });
14513
+ }
14514
+ async#writeEntitlementsInIos(domains, runConfig) {
14092
14515
  const entitlementsRelPath = "App/App.entitlements";
14093
14516
  const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14094
14517
  const values = domains.map((domain) => `applinks:${domain}`);
14518
+ const usesPush = this.target.permissions?.includes("push") ?? false;
14519
+ const apsEnvironment = resolveIosApnsEnvironment(runConfig);
14095
14520
  const body = [
14096
14521
  '<?xml version="1.0" encoding="UTF-8"?>',
14097
14522
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14098
14523
  '<plist version="1.0">',
14099
14524
  "<dict>",
14100
- " <key>com.apple.developer.associated-domains</key>",
14101
- " <array>",
14102
- ...values.map((value) => ` <string>${value}</string>`),
14103
- " </array>",
14525
+ ...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
14526
+ ...values.length > 0 ? [
14527
+ " <key>com.apple.developer.associated-domains</key>",
14528
+ " <array>",
14529
+ ...values.map((value) => ` <string>${value}</string>`),
14530
+ " </array>"
14531
+ ] : [],
14104
14532
  "</dict>",
14105
14533
  "</plist>",
14106
14534
  ""
14107
14535
  ].join(`
14108
14536
  `);
14109
- await writeFile2(entitlementsPath, body);
14537
+ const currentBody = await Bun.file(entitlementsPath).exists() ? await Bun.file(entitlementsPath).text() : undefined;
14538
+ if (currentBody !== body)
14539
+ await writeFile2(entitlementsPath, body);
14110
14540
  await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
14111
14541
  }
14112
14542
  async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
@@ -14210,7 +14640,7 @@ ${filter}$1`);
14210
14640
  for (const permission of permissions) {
14211
14641
  if (this.#hasPermissionInAndroid(permission)) {
14212
14642
  this.app.logger.info(`${permission} already exists in android`);
14213
- return this;
14643
+ continue;
14214
14644
  }
14215
14645
  this.app.logger.info(`Adding ${permission} to android`);
14216
14646
  this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
@@ -14226,7 +14656,7 @@ ${filter}$1`);
14226
14656
  return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
14227
14657
  }
14228
14658
  #hasPermissionInAndroid(permission) {
14229
- return this.#getPermissionsInAndroid().includes(permission);
14659
+ return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
14230
14660
  }
14231
14661
  }
14232
14662
  // pkgs/@akanjs/devkit/commandDecorators/targetMeta.ts
@@ -15322,6 +15752,42 @@ var CONVENTION_SUFFIXES = [
15322
15752
  ".signal.ts",
15323
15753
  ".store.ts"
15324
15754
  ];
15755
+ var PAGE_RESERVED_EXPORTS = new Set([
15756
+ "pageConfig",
15757
+ "head",
15758
+ "metadata",
15759
+ "generateHead",
15760
+ "generateMetadata",
15761
+ "fonts",
15762
+ "manifest",
15763
+ "theme",
15764
+ "reconnect",
15765
+ "wsConnect",
15766
+ "layoutStyle",
15767
+ "gaTrackingId"
15768
+ ]);
15769
+ var RULE_FIXES = {
15770
+ "akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
15771
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15772
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15773
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15774
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15775
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15776
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15777
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15778
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15779
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15780
+ "akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
15781
+ "akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
15782
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15783
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15784
+ "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
15785
+ };
15786
+ function getRuleFix(rule) {
15787
+ if (rule.startsWith("akan.convention"))
15788
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15789
+ return RULE_FIXES[rule];
15790
+ }
15325
15791
 
15326
15792
  class AkanQualityScanner {
15327
15793
  async scan(workspaceRoot) {
@@ -15330,13 +15796,14 @@ class AkanQualityScanner {
15330
15796
  const warnings = [
15331
15797
  ...this.#scanGlobalQuality(sourceFiles),
15332
15798
  ...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
15799
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
15333
15800
  ...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
15334
15801
  ...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
15335
15802
  ];
15336
15803
  return {
15337
15804
  workspaceRoot,
15338
15805
  scannedFiles: sourceFiles.length,
15339
- warnings: warnings.sort(compareWarnings),
15806
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15340
15807
  suggestedRules: SUGGESTED_RULES
15341
15808
  };
15342
15809
  }
@@ -15386,7 +15853,8 @@ class AkanQualityScanner {
15386
15853
  #scanGlobalQuality(sourceFiles) {
15387
15854
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15388
15855
  const warnings = [];
15389
- for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15856
+ const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
15857
+ for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
15390
15858
  if (declarations.length < 2)
15391
15859
  continue;
15392
15860
  warnings.push({
@@ -15457,6 +15925,49 @@ class AkanQualityScanner {
15457
15925
  }
15458
15926
  return warnings;
15459
15927
  }
15928
+ #scanComponentQuality(sourceFile2) {
15929
+ if (!isComponentDeclarationFile(sourceFile2.file))
15930
+ return [];
15931
+ const isPage = isPageRouteFile(sourceFile2.file);
15932
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15933
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15934
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15935
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15936
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15937
+ const warnings = [];
15938
+ for (const declaration of declarations) {
15939
+ if (declaration.isDefaultExport)
15940
+ continue;
15941
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15942
+ continue;
15943
+ if (declaration.exported) {
15944
+ if (isAllowedComponentExport(declaration, isPage))
15945
+ continue;
15946
+ warnings.push({
15947
+ rule: "akan.file.component-export",
15948
+ scope: "file",
15949
+ severity: "warning",
15950
+ file: sourceFile2.file,
15951
+ line: declaration.line,
15952
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15953
+ });
15954
+ continue;
15955
+ }
15956
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15957
+ continue;
15958
+ if (isRestrictedInternalKind(declaration.kind)) {
15959
+ warnings.push({
15960
+ rule: "akan.file.component-internal-declaration",
15961
+ scope: "file",
15962
+ severity: "warning",
15963
+ file: sourceFile2.file,
15964
+ line: declaration.line,
15965
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15966
+ });
15967
+ }
15968
+ }
15969
+ return warnings;
15970
+ }
15460
15971
  #scanConventionQuality(sourceFile2) {
15461
15972
  const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
15462
15973
  if (!suffix)
@@ -15532,6 +16043,8 @@ function formatQualityWarnings(warnings) {
15532
16043
  if (warning.locations?.length) {
15533
16044
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15534
16045
  }
16046
+ if (warning.fix)
16047
+ lines.push(` fix: ${warning.fix}`);
15535
16048
  return lines;
15536
16049
  });
15537
16050
  }
@@ -15540,6 +16053,7 @@ function formatQualityLocation(file, line) {
15540
16053
  }
15541
16054
  function getExportedFunctionLikes(sourceFile2) {
15542
16055
  const declarations = [];
16056
+ const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
15543
16057
  for (const statement of sourceFile2.sourceFile.statements) {
15544
16058
  if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15545
16059
  declarations.push({
@@ -15547,7 +16061,8 @@ function getExportedFunctionLikes(sourceFile2) {
15547
16061
  kind: "function",
15548
16062
  file: sourceFile2.file,
15549
16063
  line: getLine(sourceFile2.sourceFile, statement),
15550
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
16064
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
16065
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15551
16066
  });
15552
16067
  }
15553
16068
  if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
@@ -15556,7 +16071,8 @@ function getExportedFunctionLikes(sourceFile2) {
15556
16071
  kind: "class",
15557
16072
  file: sourceFile2.file,
15558
16073
  line: getLine(sourceFile2.sourceFile, statement),
15559
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
16074
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
16075
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
15560
16076
  });
15561
16077
  }
15562
16078
  if (ts11.isVariableStatement(statement) && isExported(statement)) {
@@ -15568,16 +16084,127 @@ function getExportedFunctionLikes(sourceFile2) {
15568
16084
  kind: "function-variable",
15569
16085
  file: sourceFile2.file,
15570
16086
  line: getLine(sourceFile2.sourceFile, declaration),
15571
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
16087
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
16088
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15572
16089
  });
15573
16090
  }
15574
16091
  }
15575
16092
  }
15576
16093
  return declarations;
15577
16094
  }
16095
+ function isPageRouteFile(file) {
16096
+ const segments = file.split("/");
16097
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
16098
+ }
16099
+ function isUiComponentFile(file) {
16100
+ const segments = file.split("/");
16101
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
16102
+ }
16103
+ function isConventionDuplicateNameExempt(file, isEnumClass) {
16104
+ if (!isInLibModule(file))
16105
+ return false;
16106
+ if (file.endsWith(".tsx"))
16107
+ return true;
16108
+ if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
16109
+ return true;
16110
+ if (file.endsWith(".constant.ts"))
16111
+ return !isEnumClass;
16112
+ return false;
16113
+ }
16114
+ function isInLibModule(file) {
16115
+ const segments = file.split("/");
16116
+ return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
16117
+ }
16118
+ function isEnumClassStatement(sourceFile2, statement) {
16119
+ if (!ts11.isClassDeclaration(statement))
16120
+ return false;
16121
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
16122
+ const expression = heritageClause?.types[0]?.expression;
16123
+ return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
16124
+ }
15578
16125
  function getExportedClassNames(sourceFile2) {
15579
16126
  return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15580
16127
  }
16128
+ function isComponentDeclarationFile(file) {
16129
+ if (!file.endsWith(".tsx"))
16130
+ return false;
16131
+ const segments = file.split("/");
16132
+ const [root, , area] = segments;
16133
+ if (root !== "apps" && root !== "libs")
16134
+ return false;
16135
+ if (area === "lib" || area === "ui")
16136
+ return true;
16137
+ return root === "apps" && area === "page";
16138
+ }
16139
+ function getComponentFileDeclarations(sourceFile2) {
16140
+ const reExportedNames = new Set;
16141
+ for (const statement of sourceFile2.statements) {
16142
+ if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
16143
+ for (const element of statement.exportClause.elements)
16144
+ reExportedNames.add((element.propertyName ?? element.name).text);
16145
+ }
16146
+ }
16147
+ const declarations = [];
16148
+ for (const statement of sourceFile2.statements) {
16149
+ const isDefaultExport = isDefaultExportStatement(statement);
16150
+ const inlineExported = isExported(statement);
16151
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16152
+ if (ts11.isInterfaceDeclaration(statement))
16153
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16154
+ else if (ts11.isTypeAliasDeclaration(statement))
16155
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16156
+ else if (ts11.isEnumDeclaration(statement))
16157
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16158
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16159
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16160
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16161
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16162
+ else if (ts11.isVariableStatement(statement)) {
16163
+ for (const declaration of statement.declarationList.declarations) {
16164
+ if (!ts11.isIdentifier(declaration.name))
16165
+ continue;
16166
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16167
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16168
+ }
16169
+ }
16170
+ }
16171
+ return declarations;
16172
+ }
16173
+ function getCompoundComponentNames(sourceFile2) {
16174
+ const names = new Set;
16175
+ for (const statement of sourceFile2.statements) {
16176
+ if (!ts11.isExpressionStatement(statement))
16177
+ continue;
16178
+ const { expression } = statement;
16179
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16180
+ continue;
16181
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16182
+ continue;
16183
+ names.add(expression.left.name.text);
16184
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16185
+ names.add(expression.right.text);
16186
+ }
16187
+ return names;
16188
+ }
16189
+ function isComponentValueKind(kind) {
16190
+ return kind === "variable" || kind === "function" || kind === "class";
16191
+ }
16192
+ function isRestrictedInternalKind(kind) {
16193
+ return kind === "interface" || kind === "type" || kind === "function";
16194
+ }
16195
+ function isAllowedComponentExport(declaration, isPage) {
16196
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16197
+ return true;
16198
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16199
+ }
16200
+ function isPascalCaseName(name) {
16201
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16202
+ }
16203
+ function isDefaultExportStatement(statement) {
16204
+ if (ts11.isExportAssignment(statement))
16205
+ return !statement.isExportEquals;
16206
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16207
+ }
15581
16208
  function getPlaceholderExportWarnings(sourceFile2) {
15582
16209
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
15583
16210
  return [];
@@ -15593,11 +16220,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
15593
16220
  function getDictionaryTextWarnings(sourceFile2) {
15594
16221
  if (!sourceFile2.file.endsWith(".dictionary.ts"))
15595
16222
  return [];
15596
- const stalePatterns = [
15597
- { pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
15598
- { pattern: /settting/, label: "misspelling: settting" },
15599
- { pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
15600
- ];
16223
+ const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
15601
16224
  return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15602
16225
  rule: "akan.file.dictionary-stale-text",
15603
16226
  scope: "file",
@@ -15718,10 +16341,8 @@ function getConventionDescription(suffix, modelName) {
15718
16341
  }
15719
16342
  function getLibRootFile(file) {
15720
16343
  const segments = file.split("/");
15721
- if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
16344
+ if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
15722
16345
  return segments[3];
15723
- if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
15724
- return segments[4];
15725
16346
  return null;
15726
16347
  }
15727
16348
  function getModuleUiWarning(file) {
@@ -15862,26 +16483,61 @@ var targetPaths = {
15862
16483
  "agents-md": "AGENTS.md",
15863
16484
  claude: "CLAUDE.md"
15864
16485
  };
15865
- var targetTitle = {
15866
- cursor: "Akan Cursor Rules",
15867
- "agents-md": "Akan Agent Rules",
15868
- claude: "Akan Claude Rules"
16486
+ var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
16487
+ var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
16488
+ var SAMPLE_ARTIFACTS = [
16489
+ { probe: "lib/task/task.constant.ts", target: "lib/task", label: "sample database module" },
16490
+ { probe: "lib/_noti/noti.service.ts", target: "lib/_noti", label: "sample service module" },
16491
+ {
16492
+ probe: "lib/__scalar/workHistory/workHistory.dictionary.ts",
16493
+ target: "lib/__scalar/workHistory",
16494
+ label: "sample scalar module"
16495
+ },
16496
+ { probe: "page/task/_index.tsx", target: "page/task", label: "sample task pages" }
16497
+ ];
16498
+ var DEFAULT_INDEX_MARKER = "Akan.js template";
16499
+ var renderSampleCleanup = async (workspace, appNames) => {
16500
+ const items = [];
16501
+ for (const appName of appNames) {
16502
+ for (const artifact2 of SAMPLE_ARTIFACTS) {
16503
+ if (await workspace.exists(`apps/${appName}/${artifact2.probe}`)) {
16504
+ items.push(`- \`apps/${appName}/${artifact2.target}\` \u2014 ${artifact2.label}; delete it once you no longer need the reference.`);
16505
+ }
16506
+ }
16507
+ const indexPath = `apps/${appName}/page/_index.tsx`;
16508
+ if (await workspace.exists(indexPath)) {
16509
+ const content = await workspace.readFile(indexPath).catch(() => "");
16510
+ if (content.includes(DEFAULT_INDEX_MARKER)) {
16511
+ items.push(`- \`${indexPath}\` \u2014 default Akan landing page; replace it with your own home page.`);
16512
+ }
16513
+ }
16514
+ }
16515
+ if (items.length === 0)
16516
+ return "";
16517
+ return `## Start Clean (Remove Scaffolded Samples)
16518
+
16519
+ This workspace was scaffolded with reference samples so the Akan conventions are visible in real code. They are
16520
+ not part of your product. Before building real features, remove the samples below and run \`akan sync <app>\`:
16521
+
16522
+ ${items.join(`
16523
+ `)}
16524
+
16525
+ Keep a sample only while you are still learning its pattern; delete it once your own modules cover the same ground.
16526
+
16527
+ `;
15869
16528
  };
15870
- var renderAgentRules = async (workspace, target) => {
16529
+ var renderManagedBlock = async (workspace) => {
15871
16530
  const context = await AkanContextAnalyzer.analyze(workspace);
15872
16531
  const frameworkGuide = await Prompter.getInstruction("framework");
15873
- return `# ${targetTitle[target]}
15874
-
15875
- This file is generated by \`akan agent install ${target}\`.
15876
-
15877
- ## Workspace
16532
+ const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
16533
+ return `## Workspace
15878
16534
 
15879
16535
  - Repo: ${context.repoName}
15880
16536
  - Apps: ${context.apps.map((app) => app.name).join(", ") || "none"}
15881
16537
  - Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
15882
16538
  - Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
15883
16539
 
15884
- ## Akan Module Abstracts
16540
+ ${sampleCleanup}## Akan Module Abstracts
15885
16541
 
15886
16542
  - Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
15887
16543
  - Update the abstract when business invariants, workflows, or public behavior change.
@@ -15903,6 +16559,7 @@ If generated output is stale or broken, update the owning source file and run \`
15903
16559
  - After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
15904
16560
  - 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.
15905
16561
  - For compound requests, split the request into workflows and apply each \`planPath\` in order, such as \`create-module\` followed by \`add-field\`.
16562
+ - **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.
15906
16563
 
15907
16564
  ## Validation
15908
16565
 
@@ -15911,20 +16568,75 @@ ${context.validationCommands.map((command3) => `- \`${command3}\``).join(`
15911
16568
 
15912
16569
  ## Framework Guide
15913
16570
 
15914
- ${frameworkGuide.trim()}
16571
+ ${frameworkGuide.trim()}`;
16572
+ };
16573
+ var upsertManagedBlock = (existing, block) => {
16574
+ const managed = `${AGENT_BLOCK_START}
16575
+ ${block}
16576
+ ${AGENT_BLOCK_END}`;
16577
+ const startIndex = existing.indexOf(AGENT_BLOCK_START);
16578
+ const endIndex = existing.indexOf(AGENT_BLOCK_END);
16579
+ if (startIndex >= 0 && endIndex > startIndex) {
16580
+ return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
16581
+ }
16582
+ return `${existing.replace(/\s*$/, "")}
16583
+
16584
+ ${managed}
16585
+ `;
16586
+ };
16587
+ var renderAgentsMd = async (workspace, existing) => {
16588
+ const block = await renderManagedBlock(workspace);
16589
+ if (existing?.trim())
16590
+ return upsertManagedBlock(existing, block);
16591
+ const context = await AkanContextAnalyzer.analyze(workspace);
16592
+ return `# ${context.repoName} Agent Guide
16593
+
16594
+ This file is the single source of truth for coding agents in this workspace. Claude Code reads it through
16595
+ \`CLAUDE.md\` (\`@AGENTS.md\`) and Cursor through \`.cursor/rules/akan.mdc\`. The section between the
16596
+ \`akan:agent\` markers is regenerated by \`akan agent install\`; edit anything outside the markers freely.
16597
+
16598
+ ${AGENT_BLOCK_START}
16599
+ ${block}
16600
+ ${AGENT_BLOCK_END}
16601
+ `;
16602
+ };
16603
+ var renderClaudeMd = async (workspace) => {
16604
+ const context = await AkanContextAnalyzer.analyze(workspace);
16605
+ return `# ${context.repoName} \u2014 Claude Code Guide
16606
+
16607
+ @AGENTS.md
15915
16608
  `;
15916
16609
  };
16610
+ var renderCursorRule = () => `---
16611
+ description: Akan workspace agent guide
16612
+ alwaysApply: true
16613
+ ---
16614
+
16615
+ Follow the workspace agent guide, which is the single source of truth for Akan conventions,
16616
+ generated-file rules, and the MCP workflow policy.
16617
+
16618
+ @AGENTS.md
16619
+ `;
16620
+ var renderTarget = async (workspace, target, existing) => {
16621
+ if (target === "agents-md")
16622
+ return await renderAgentsMd(workspace, existing);
16623
+ if (target === "claude")
16624
+ return await renderClaudeMd(workspace);
16625
+ return renderCursorRule();
16626
+ };
15917
16627
 
15918
16628
  class AgentRunner extends runner("agent") {
15919
16629
  async install(workspace, targets, { force = false } = {}) {
15920
16630
  const written = [];
15921
16631
  for (const target of targets) {
15922
16632
  const filePath = targetPaths[target];
15923
- if (!force && await workspace.exists(filePath)) {
16633
+ const exists2 = await workspace.exists(filePath);
16634
+ if (exists2 && !force && target !== "agents-md") {
15924
16635
  throw new Error(`${filePath} already exists. Re-run with --force to overwrite it.`);
15925
16636
  }
15926
- const content = await renderAgentRules(workspace, target);
15927
- await workspace.writeFile(filePath, content, { overwrite: force });
16637
+ const existing = exists2 ? await workspace.readFile(filePath) : null;
16638
+ const content = await renderTarget(workspace, target, existing);
16639
+ await workspace.writeFile(filePath, content, { overwrite: true });
15928
16640
  written.push(filePath);
15929
16641
  }
15930
16642
  return written;
@@ -17807,8 +18519,7 @@ var addEnumFieldWorkflowSpec = {
17807
18519
  predictedChanges: [
17808
18520
  { target: "*/lib/<module>/<module>.constant.ts", action: "modify", reason: "Enum field shape is added." },
17809
18521
  { target: "*/lib/<module>/<module>.dictionary.ts", action: "modify", reason: "Enum labels are added." },
17810
- { target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." },
17811
- { target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may change after sync." }
18522
+ { target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
17812
18523
  ],
17813
18524
  validation: [
17814
18525
  ...baseValidation,
@@ -17999,8 +18710,18 @@ var addMutationWorkflowSpec = {
17999
18710
  }
18000
18711
  ],
18001
18712
  predictedChanges: [
18002
- { target: "*/lib/<module>/<module>.service.ts", action: "modify", reason: "Service mutation may be added." },
18003
- { target: "*/lib/<module>/<module>.signal.ts", action: "modify", reason: "Signal mutation may be added." },
18713
+ {
18714
+ target: "*/lib/<module>/<module>.service.ts",
18715
+ action: "modify",
18716
+ applyScope: "auto",
18717
+ reason: "Service mutation method stub is added."
18718
+ },
18719
+ {
18720
+ target: "*/lib/<module>/<module>.signal.ts",
18721
+ action: "modify",
18722
+ applyScope: "auto",
18723
+ reason: "Signal endpoint mutation is added."
18724
+ },
18004
18725
  { target: "*/lib/srv.ts", action: "sync", reason: "Generated service barrel may change after sync." },
18005
18726
  { target: "*/lib/sig.ts", action: "sync", reason: "Generated signal barrel may change after sync." }
18006
18727
  ],
@@ -18072,9 +18793,24 @@ var addSliceWorkflowSpec = {
18072
18793
  }
18073
18794
  ],
18074
18795
  predictedChanges: [
18075
- { target: "*/lib/<module>/<module>.service.ts", action: "modify", reason: "Query helper may be added." },
18076
- { target: "*/lib/<module>/<module>.signal.ts", action: "modify", reason: "Signal slice may be added." },
18077
- { target: "*/lib/<module>/<Module>.Zone.tsx", action: "modify", reason: "Zone may connect slice state." },
18796
+ {
18797
+ target: "*/lib/<module>/<module>.service.ts",
18798
+ action: "modify",
18799
+ applyScope: "auto",
18800
+ reason: "Query helper method stub is added."
18801
+ },
18802
+ {
18803
+ target: "*/lib/<module>/<module>.signal.ts",
18804
+ action: "modify",
18805
+ applyScope: "auto",
18806
+ reason: "Signal slice entry is added."
18807
+ },
18808
+ {
18809
+ target: "*/lib/<module>/<Module>.Zone.tsx",
18810
+ action: "modify",
18811
+ applyScope: "manual-review",
18812
+ reason: "Zone may connect slice state."
18813
+ },
18078
18814
  { target: "*/lib/sig.ts", action: "sync", reason: "Generated signal barrel may change after sync." }
18079
18815
  ],
18080
18816
  validation: [
@@ -18188,8 +18924,7 @@ var createScalarWorkflowSpec = {
18188
18924
  }
18189
18925
  ],
18190
18926
  predictedChanges: [
18191
- { target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." },
18192
- { target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may include scalar options." }
18927
+ { target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
18193
18928
  ],
18194
18929
  validation: baseValidation,
18195
18930
  completionCriteria: ["Scalar files exist under __scalar.", "Generated files are refreshed.", "Lint passes."]
@@ -19386,6 +20121,12 @@ class PrimitiveScript extends script("primitive", [ModuleScript]) {
19386
20121
  code: "primitive-field-type-unsupported",
19387
20122
  message: `Field type "${input7.type}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
19388
20123
  input: "type"
20124
+ } : null,
20125
+ input7.type && input7.type.toLowerCase() === "upload" ? {
20126
+ severity: "error",
20127
+ code: "primitive-field-type-upload-misuse",
20128
+ message: "Upload is not a model field type. Declare an image/file field as a relation to the File model (e.g. field(File)); Upload is only valid in a { fileUpload: true } signal body. Note the File model is provided by the shared file library.",
20129
+ input: "type"
19389
20130
  } : null
19390
20131
  ]);
19391
20132
  if (!sys3 || !input7.module || !input7.field || !input7.type || diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
@@ -19584,6 +20325,206 @@ class PrimitiveScript extends script("primitive", [ModuleScript]) {
19584
20325
  nextActions: nextActionsForTarget(sys3.name)
19585
20326
  });
19586
20327
  }
20328
+ async addMutation(workspace, input7) {
20329
+ const moduleClassName = input7.module ? moduleComponentName(input7.module) : "";
20330
+ const serviceRef = lowerlize(moduleClassName);
20331
+ const name = input7.mutation;
20332
+ return await this.#writeServiceSignalEntry(workspace, {
20333
+ command: "add-mutation",
20334
+ app: input7.app,
20335
+ module: input7.module,
20336
+ entryKind: "mutation",
20337
+ entryName: name,
20338
+ requiredInput: "mutation",
20339
+ serviceMethod: name ? {
20340
+ name,
20341
+ block: [
20342
+ ` async ${name}() {`,
20343
+ ` // TODO(service): implement ${moduleClassName}Service.${name}`,
20344
+ ` return true;`,
20345
+ ` }`
20346
+ ].join(`
20347
+ `)
20348
+ } : null,
20349
+ signal: name ? {
20350
+ className: `${moduleClassName}Endpoint`,
20351
+ entryLine: [
20352
+ `${name}: mutation(Boolean)`,
20353
+ ` .exec(async function () {`,
20354
+ ` return await this.${serviceRef}Service.${name}();`,
20355
+ ` }),`
20356
+ ].join(`
20357
+ `),
20358
+ param: { mode: "destructure", name: "mutation" }
20359
+ } : null
20360
+ });
20361
+ }
20362
+ async addSlice(workspace, input7) {
20363
+ const moduleClassName = input7.module ? moduleComponentName(input7.module) : "";
20364
+ const serviceRef = lowerlize(moduleClassName);
20365
+ const name = input7.slice;
20366
+ const queryName = name ? `query${capitalize12(name)}` : null;
20367
+ return await this.#writeServiceSignalEntry(workspace, {
20368
+ command: "add-slice",
20369
+ app: input7.app,
20370
+ module: input7.module,
20371
+ entryKind: "slice",
20372
+ entryName: name,
20373
+ requiredInput: "slice",
20374
+ serviceMethod: name && queryName ? {
20375
+ name: queryName,
20376
+ block: [
20377
+ ` ${queryName}() {`,
20378
+ ` // TODO(service): return a QueryOf for the ${name} slice`,
20379
+ ` return {};`,
20380
+ ` }`
20381
+ ].join(`
20382
+ `)
20383
+ } : null,
20384
+ signal: name && queryName ? {
20385
+ className: `${moduleClassName}Slice`,
20386
+ entryLine: [
20387
+ `${name}: init()`,
20388
+ ` .exec(function () {`,
20389
+ ` return this.${serviceRef}Service.${queryName}();`,
20390
+ ` }),`
20391
+ ].join(`
20392
+ `),
20393
+ param: { mode: "positional", name: "init" }
20394
+ } : null
20395
+ });
20396
+ }
20397
+ async#writeServiceSignalEntry(workspace, spec) {
20398
+ const sys3 = await this.resolveSys(workspace, spec.app);
20399
+ const diagnostics = compactDiagnostics([
20400
+ !sys3 && { severity: "error", code: "primitive-target-missing", message: "Target app or library was not found." },
20401
+ !spec.module && {
20402
+ severity: "error",
20403
+ code: "primitive-input-missing",
20404
+ message: "Module is required.",
20405
+ input: "module"
20406
+ },
20407
+ !spec.entryName && {
20408
+ severity: "error",
20409
+ code: "primitive-input-missing",
20410
+ message: `${capitalize12(spec.requiredInput)} name is required.`,
20411
+ input: spec.requiredInput
20412
+ }
20413
+ ]);
20414
+ if (!sys3 || !spec.module || !spec.entryName || !spec.serviceMethod || !spec.signal) {
20415
+ return createPrimitiveWriteReport({
20416
+ command: spec.command,
20417
+ changedFiles: [],
20418
+ generatedFiles: [],
20419
+ validationCommands: [],
20420
+ diagnostics,
20421
+ nextActions: []
20422
+ });
20423
+ }
20424
+ const paths = moduleSourcePaths(spec.module);
20425
+ const servicePath = paths.service;
20426
+ const signalPath = paths.signal;
20427
+ const changedFiles = [];
20428
+ const generatedFiles2 = generatedFilesForSync(sys3);
20429
+ const [hasServiceFile, hasSignalFile] = await Promise.all([sys3.exists(servicePath), sys3.exists(signalPath)]);
20430
+ if (!hasServiceFile) {
20431
+ diagnostics.push({
20432
+ severity: "error",
20433
+ code: "primitive-source-missing",
20434
+ message: `Service source file was not found: ${servicePath}.`
20435
+ });
20436
+ }
20437
+ if (!hasSignalFile) {
20438
+ diagnostics.push({
20439
+ severity: "error",
20440
+ code: "primitive-source-missing",
20441
+ message: `Signal source file was not found: ${signalPath}.`
20442
+ });
20443
+ }
20444
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
20445
+ return createPrimitiveWriteReport({
20446
+ command: spec.command,
20447
+ changedFiles,
20448
+ generatedFiles: generatedFiles2,
20449
+ validationCommands: validationCommandsForTarget(sys3.name),
20450
+ diagnostics,
20451
+ nextActions: nextActionsForTarget(sys3.name)
20452
+ });
20453
+ }
20454
+ const serviceClassName = `${moduleComponentName(spec.module)}Service`;
20455
+ const serviceContent = await sys3.readFile(servicePath);
20456
+ const signalContent = await sys3.readFile(signalPath);
20457
+ const serviceMethodExists = hasClassMethod(serviceContent, serviceClassName, spec.serviceMethod.name);
20458
+ if (serviceMethodExists) {
20459
+ diagnostics.push({
20460
+ severity: "error",
20461
+ code: "primitive-service-method-exists",
20462
+ input: spec.requiredInput,
20463
+ message: `Method "${spec.serviceMethod.name}" already exists in ${serviceClassName}.`
20464
+ });
20465
+ }
20466
+ if (hasSignalFactoryEntry(signalContent, spec.signal.className, spec.entryName)) {
20467
+ diagnostics.push({
20468
+ severity: "error",
20469
+ code: "primitive-signal-entry-exists",
20470
+ input: spec.requiredInput,
20471
+ message: `Entry "${spec.entryName}" already exists in ${spec.signal.className}.`
20472
+ });
20473
+ }
20474
+ const nextServiceContent = serviceMethodExists ? serviceContent : insertClassMethod(serviceContent, serviceClassName, spec.serviceMethod.block);
20475
+ const nextSignalContent = insertSignalFactoryEntry(signalContent, spec.signal.className, spec.entryName, spec.signal.entryLine, spec.signal.param);
20476
+ if (!nextServiceContent) {
20477
+ diagnostics.push({
20478
+ severity: "error",
20479
+ code: "primitive-service-shape-unsupported",
20480
+ message: `Could not find ${serviceClassName} class body in ${servicePath}.`
20481
+ });
20482
+ }
20483
+ if (!nextSignalContent) {
20484
+ diagnostics.push({
20485
+ severity: "error",
20486
+ code: "primitive-signal-shape-unsupported",
20487
+ message: `Could not find a safe insertion point in ${spec.signal.className} within ${signalPath}. Ensure the class exists and its factory returns an object literal.`
20488
+ });
20489
+ }
20490
+ if (nextServiceContent && hasSourceParseErrors(nextServiceContent, "service.ts")) {
20491
+ diagnostics.push({
20492
+ severity: "error",
20493
+ code: "primitive-post-edit-service-parse-failed",
20494
+ failureScope: "source-change",
20495
+ message: `Edited ${servicePath} did not parse cleanly; refusing to write source.`
20496
+ });
20497
+ }
20498
+ if (nextSignalContent && hasSourceParseErrors(nextSignalContent, "signal.ts")) {
20499
+ diagnostics.push({
20500
+ severity: "error",
20501
+ code: "primitive-post-edit-signal-parse-failed",
20502
+ failureScope: "source-change",
20503
+ message: `Edited ${signalPath} did not parse cleanly; refusing to write source.`
20504
+ });
20505
+ }
20506
+ if (nextSignalContent && !hasSignalFactoryEntry(nextSignalContent, spec.signal.className, spec.entryName)) {
20507
+ diagnostics.push({
20508
+ severity: "error",
20509
+ code: "primitive-post-edit-signal-verify-failed",
20510
+ failureScope: "source-change",
20511
+ message: `Edited ${signalPath} did not contain entry "${spec.entryName}" in ${spec.signal.className}.`
20512
+ });
20513
+ }
20514
+ if (!diagnostics.some((diagnostic) => diagnostic.severity === "error") && nextServiceContent && nextSignalContent) {
20515
+ await sys3.writeFile(servicePath, nextServiceContent);
20516
+ await sys3.writeFile(signalPath, nextSignalContent);
20517
+ changedFiles.push(sourceFile(sys3, servicePath, "modify", `Service ${spec.entryKind} was added.`), sourceFile(sys3, signalPath, "modify", `Signal ${spec.entryKind} was added.`));
20518
+ }
20519
+ return createPrimitiveWriteReport({
20520
+ command: spec.command,
20521
+ changedFiles,
20522
+ generatedFiles: generatedFiles2,
20523
+ validationCommands: validationCommandsForTarget(sys3.name),
20524
+ diagnostics,
20525
+ nextActions: nextActionsForTarget(sys3.name)
20526
+ });
20527
+ }
19587
20528
  }
19588
20529
 
19589
20530
  // pkgs/@akanjs/cli/scalar/scalar.prompt.ts
@@ -19801,7 +20742,9 @@ var createCliWorkflowStepRegistry = (workspace) => createWorkflowStepRegistry({
19801
20742
  createScalar: (sys3, scalar) => CommandContainer.get(ScalarScript).createScalar(sys3, scalar),
19802
20743
  createUi: (input8) => CommandContainer.get(PrimitiveScript).createUi(workspace, input8),
19803
20744
  addField: (input8) => CommandContainer.get(PrimitiveScript).addField(workspace, input8),
19804
- addEnumField: (input8) => CommandContainer.get(PrimitiveScript).addEnumField(workspace, input8)
20745
+ addEnumField: (input8) => CommandContainer.get(PrimitiveScript).addEnumField(workspace, input8),
20746
+ addMutation: (input8) => CommandContainer.get(PrimitiveScript).addMutation(workspace, input8),
20747
+ addSlice: (input8) => CommandContainer.get(PrimitiveScript).addSlice(workspace, input8)
19805
20748
  });
19806
20749
 
19807
20750
  // pkgs/@akanjs/cli/context/context.runner.ts
@@ -19852,14 +20795,18 @@ class ContextRunner extends runner("context") {
19852
20795
  return await Prompter.getInstruction(name);
19853
20796
  }
19854
20797
  async installMcp(workspace, target, { force = false, mode = "readonly" } = {}) {
19855
- if (target !== "cursor")
19856
- throw new Error(`Unknown MCP install target: ${target}. Use cursor.`);
19857
- const existing = await workspace.exists(cursorMcpConfigPath) ? await workspace.readJson(cursorMcpConfigPath) : {};
20798
+ if (target === "codex")
20799
+ return await this.#installCodexMcp(workspace, { force, mode });
20800
+ return await this.#installJsonMcp(workspace, target, { force, mode });
20801
+ }
20802
+ async#installJsonMcp(workspace, target, { force, mode }) {
20803
+ const configPath2 = akanMcpInstallConfigPaths[target];
20804
+ const existing = await workspace.exists(configPath2) ? await workspace.readJson(configPath2) : {};
19858
20805
  const mcpServers = existing.mcpServers ?? {};
19859
20806
  const currentAkanServer = mcpServers.akan;
19860
- const nextAkanServer = createAkanCursorMcpServer(mode);
20807
+ const nextAkanServer = createAkanMcpServer(target, mode);
19861
20808
  if (currentAkanServer && !force && JSON.stringify(currentAkanServer) !== JSON.stringify(nextAkanServer)) {
19862
- throw new Error(`${cursorMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
20809
+ throw new Error(`${configPath2} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
19863
20810
  }
19864
20811
  const nextConfig = {
19865
20812
  ...existing,
@@ -19868,9 +20815,15 @@ class ContextRunner extends runner("context") {
19868
20815
  akan: nextAkanServer
19869
20816
  }
19870
20817
  };
19871
- await workspace.writeFile(cursorMcpConfigPath, `${JSON.stringify(nextConfig, null, 2)}
20818
+ await workspace.writeFile(configPath2, `${JSON.stringify(nextConfig, null, 2)}
19872
20819
  `);
19873
- return cursorMcpConfigPath;
20820
+ return configPath2;
20821
+ }
20822
+ async#installCodexMcp(workspace, { force, mode }) {
20823
+ const existing = await workspace.exists(codexMcpConfigPath) ? await workspace.readFile(codexMcpConfigPath) : "";
20824
+ const merged = upsertCodexMcpServerBlock(existing, createAkanCodexMcpServerBlock(mode), { force });
20825
+ await workspace.writeFile(codexMcpConfigPath, merged);
20826
+ return codexMcpConfigPath;
19874
20827
  }
19875
20828
  listMcpTools(mode = "readonly") {
19876
20829
  return listAkanMcpTools(mode);
@@ -20162,13 +21115,26 @@ ${payload}`);
20162
21115
  agent: "`akan agent install <target>` writes editor-specific agent rules with overwrite protection.",
20163
21116
  mcp: "`akan mcp --mode readonly|plan|apply` starts the Akan MCP server over stdio with an explicit permission mode.",
20164
21117
  "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.',
20165
- "mcp-install": "`akan mcp-install cursor --mode readonly|plan|apply` installs the Akan MCP server config for Cursor."
21118
+ "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."
20166
21119
  };
20167
21120
  return explanations[command3] ?? `No detailed explanation is available for ${command3}. Run \`akan --help\` for command help.`;
20168
21121
  }
20169
21122
  }
20170
21123
 
20171
21124
  // pkgs/@akanjs/cli/context/context.script.ts
21125
+ var resolveMcpInstallTargets = (target) => {
21126
+ if (!target || target === "all")
21127
+ return [...akanMcpInstallTargets];
21128
+ if (akanMcpInstallTargets.includes(target))
21129
+ return [target];
21130
+ throw new Error(`Unknown MCP install target: ${target}. Use cursor, claude, codex, or all.`);
21131
+ };
21132
+ var mcpTargetLabels = {
21133
+ cursor: "Cursor",
21134
+ claude: "Claude Code",
21135
+ codex: "Codex"
21136
+ };
21137
+
20172
21138
  class ContextScript extends script("context", [ContextRunner]) {
20173
21139
  async context(workspace, options = {}) {
20174
21140
  Logger17.rawLog(await this.contextRunner.getContext(workspace, options));
@@ -20177,11 +21143,15 @@ class ContextScript extends script("context", [ContextRunner]) {
20177
21143
  Logger17.rawLog(await this.contextRunner.doctor(workspace, options));
20178
21144
  }
20179
21145
  async mcpInstall(workspace, target, { force = false, mode = "readonly" } = {}) {
20180
- if (target && target !== "cursor")
20181
- throw new Error(`Unknown MCP install target: ${target}. Use cursor.`);
20182
- const written = await this.contextRunner.installMcp(workspace, "cursor", { force, mode });
20183
- Logger17.rawLog(`Akan MCP server installed for Cursor:
20184
- - ${written}`);
21146
+ const targets = resolveMcpInstallTargets(target);
21147
+ const written = [];
21148
+ for (const t of targets) {
21149
+ const configPath2 = await this.contextRunner.installMcp(workspace, t, { force, mode });
21150
+ written.push(`${mcpTargetLabels[t]}: ${configPath2}`);
21151
+ }
21152
+ Logger17.rawLog(`Akan MCP server installed (${mode} mode):
21153
+ ${written.map((line) => `- ${line}`).join(`
21154
+ `)}`);
20185
21155
  }
20186
21156
  async mcp(workspace, { mode = "readonly" } = {}) {
20187
21157
  await this.contextRunner.runMcp(workspace, { mode });
@@ -20220,7 +21190,7 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
20220
21190
  }).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
20221
21191
  await this.contextScript.doctor(workspace, { format, strict });
20222
21192
  }),
20223
- 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, {
21193
+ 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, {
20224
21194
  desc: "MCP permission mode",
20225
21195
  default: "readonly",
20226
21196
  enum: ["readonly", "plan", "apply"]
@@ -20605,7 +21575,7 @@ class LibraryCommand extends command("library", [LibraryScript], ({ public: targ
20605
21575
  }
20606
21576
 
20607
21577
  // pkgs/@akanjs/cli/localRegistry/localRegistry.runner.ts
20608
- import { mkdir as mkdir13, rm as rm6 } from "fs/promises";
21578
+ import { mkdir as mkdir13, rm as rm7 } from "fs/promises";
20609
21579
  import path47 from "path";
20610
21580
  import { Logger as Logger19 } from "akanjs/common";
20611
21581
  var defaultLocalRegistryUrl = "http://127.0.0.1:4873";
@@ -20648,13 +21618,13 @@ class LocalRegistryRunner extends runner("localRegistry") {
20648
21618
  try {
20649
21619
  await workspace.spawn("docker", ["rm", "-f", containerName], { stdio: "inherit" });
20650
21620
  } catch {}
20651
- await rm6(path47.join(workspace.workspaceRoot, ".akan/verdaccio"), { recursive: true, force: true });
21621
+ await rm7(path47.join(workspace.workspaceRoot, ".akan/verdaccio"), { recursive: true, force: true });
20652
21622
  Logger19.info("Local registry storage has been reset");
20653
21623
  }
20654
21624
  async smoke(workspace, { registryUrl } = {}) {
20655
21625
  const registry = this.getRegistryUrl(registryUrl);
20656
21626
  const smokeRoot = path47.join(workspace.workspaceRoot, ".akan/e2e");
20657
- await rm6(path47.join(smokeRoot, smokeRepoName), { recursive: true, force: true });
21627
+ await rm7(path47.join(smokeRoot, smokeRepoName), { recursive: true, force: true });
20658
21628
  await workspace.spawn(process.execPath, [
20659
21629
  "dist/pkgs/create-akan-workspace/index.js",
20660
21630
  smokeRepoName,
@@ -20992,7 +21962,9 @@ class WorkflowScript extends script("workflow", [WorkflowRunner, ModuleScript, S
20992
21962
  createScalar: (sys3, scalar) => this.scalarScript.createScalar(sys3, scalar),
20993
21963
  createUi: (input8) => this.primitiveScript.createUi(workspace, input8),
20994
21964
  addField: (input8) => this.primitiveScript.addField(workspace, input8),
20995
- addEnumField: (input8) => this.primitiveScript.addEnumField(workspace, input8)
21965
+ addEnumField: (input8) => this.primitiveScript.addEnumField(workspace, input8),
21966
+ addMutation: (input8) => this.primitiveScript.addMutation(workspace, input8),
21967
+ addSlice: (input8) => this.primitiveScript.addSlice(workspace, input8)
20996
21968
  })
20997
21969
  }));
20998
21970
  return;
@@ -21038,6 +22010,7 @@ import path48 from "path";
21038
22010
  var defaultWorkspacePeerDependencies = new Set([
21039
22011
  "@react-spring/web",
21040
22012
  "@use-gesture/react",
22013
+ "chance",
21041
22014
  "croner",
21042
22015
  "daisyui",
21043
22016
  "react",