@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.
@@ -666,6 +666,7 @@ import {
666
666
  import { readFileSync as readFileSync3 } from "fs";
667
667
  import { copyFile, mkdir as mkdir2, readdir as readDirEntries, stat as stat2 } from "fs/promises";
668
668
  import path7 from "path";
669
+ import { pathToFileURL } from "url";
669
670
  import {
670
671
  capitalize,
671
672
  isRouteSourceFile,
@@ -1107,6 +1108,63 @@ var decreaseBuildNum = async (app) => {
1107
1108
  const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
1108
1109
  fs.writeFileSync(akanConfigPath, akanConfigContent);
1109
1110
  };
1111
+ // pkgs/@akanjs/devkit/firebaseMessagingSw.ts
1112
+ var FIREBASE_WEB_SDK_VERSION = "12.13.0";
1113
+ function normalizeFirebaseClientConfig(config) {
1114
+ if (!config || typeof config !== "object")
1115
+ return null;
1116
+ const value = config;
1117
+ if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
1118
+ return null;
1119
+ }
1120
+ return {
1121
+ apiKey: value.apiKey,
1122
+ ...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
1123
+ projectId: value.projectId,
1124
+ ...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
1125
+ messagingSenderId: value.messagingSenderId,
1126
+ appId: value.appId
1127
+ };
1128
+ }
1129
+ function createFirebaseMessagingServiceWorker(config) {
1130
+ const configJson = JSON.stringify(config);
1131
+ return `/* Generated by Akan.js. Do not edit. */
1132
+ const firebaseConfig = ${configJson};
1133
+
1134
+ if (firebaseConfig) {
1135
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1136
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1137
+
1138
+ firebase.initializeApp(firebaseConfig);
1139
+ const messaging = firebase.messaging();
1140
+
1141
+ const notificationUrl = (payload) =>
1142
+ payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1143
+
1144
+ messaging.onBackgroundMessage((payload) => {
1145
+ const title = payload?.notification?.title || "";
1146
+ const options = {
1147
+ body: payload?.notification?.body,
1148
+ icon: payload?.notification?.icon,
1149
+ image: payload?.notification?.image,
1150
+ data: {
1151
+ url: notificationUrl(payload),
1152
+ FCM_MSG: payload,
1153
+ },
1154
+ };
1155
+ self.registration.showNotification(title, options);
1156
+ });
1157
+ }
1158
+
1159
+ self.addEventListener("notificationclick", (event) => {
1160
+ const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1161
+ event.notification?.close();
1162
+ if (!url) return;
1163
+ event.waitUntil(clients.openWindow(url));
1164
+ });
1165
+ `;
1166
+ }
1167
+
1110
1168
  // pkgs/@akanjs/devkit/getDirname.ts
1111
1169
  import path2 from "path";
1112
1170
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1392,6 +1450,7 @@ ${errorText}, ${warningText} found`];
1392
1450
 
1393
1451
  // pkgs/@akanjs/devkit/scanInfo.ts
1394
1452
  import path5 from "path";
1453
+ import { rm } from "fs/promises";
1395
1454
 
1396
1455
  // pkgs/@akanjs/devkit/dependencyScanner.ts
1397
1456
  import { builtinModules } from "module";
@@ -1710,8 +1769,10 @@ var appRootAllowedFiles = new Set([
1710
1769
  "main.ts",
1711
1770
  "package.json",
1712
1771
  "server.ts",
1713
- "tsconfig.json"
1772
+ "tsconfig.json",
1773
+ "tsconfig.tsbuildinfo"
1714
1774
  ]);
1775
+ var generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"];
1715
1776
  var appRootAllowedDirs = new Set([
1716
1777
  ".akan",
1717
1778
  "android",
@@ -1755,11 +1816,17 @@ var rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
1755
1816
  var isAllowedTestFile = (filename) => testFilePattern.test(filename);
1756
1817
  var isAllowedLibRootFile = (filename) => libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
1757
1818
  var getScanPath = (exec, relativePath) => path5.posix.join(`${exec.type}s`, exec.name, relativePath.split(path5.sep).join("/"));
1819
+ async function clearGeneratedRootCapacitorConfigs(exec) {
1820
+ if (exec.type !== "app")
1821
+ return;
1822
+ await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
1823
+ }
1758
1824
  var getModuleNameFromPath = (kind, modulePath) => {
1759
1825
  const dirname = path5.basename(modulePath);
1760
1826
  return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
1761
1827
  };
1762
1828
  async function assertScanConvention(exec, libRoot) {
1829
+ await clearGeneratedRootCapacitorConfigs(exec);
1763
1830
  const violations = [];
1764
1831
  const addViolation = (relativePath, reason) => {
1765
1832
  violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
@@ -2624,6 +2691,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
2624
2691
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
2625
2692
  }
2626
2693
  }
2694
+ function validateOverridesSourceExports(source, filePath) {
2695
+ const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
2696
+ const fail = (message) => {
2697
+ throw new Error(`[route-convention] ${message}: ${filePath}`);
2698
+ };
2699
+ let defaultOverride = null;
2700
+ for (const statement of sourceFile.statements) {
2701
+ if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
2702
+ continue;
2703
+ if (ts3.isImportDeclaration(statement))
2704
+ continue;
2705
+ if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
2706
+ continue;
2707
+ if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
2708
+ defaultOverride = statement;
2709
+ continue;
2710
+ }
2711
+ fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
2712
+ }
2713
+ if (!defaultOverride)
2714
+ fail(`_overrides.tsx must "export default override({ ... })"`);
2715
+ const expression = defaultOverride.expression;
2716
+ if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
2717
+ fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
2718
+ }
2627
2719
 
2628
2720
  class Executor {
2629
2721
  static verbose = false;
@@ -3576,6 +3668,8 @@ class AppExecutor extends SysExecutor {
3576
3668
  ...routeEnv,
3577
3669
  ...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
3578
3670
  });
3671
+ if (type === "build")
3672
+ Object.assign(process.env, env);
3579
3673
  return { env };
3580
3674
  }
3581
3675
  #publicEnv = null;
@@ -3645,7 +3739,11 @@ class AppExecutor extends SysExecutor {
3645
3739
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
3646
3740
  }
3647
3741
  const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
3648
- validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
3742
+ const routeSource = await Bun.file(absPath).text();
3743
+ if (parsed.kind === "overrides")
3744
+ validateOverridesSourceExports(routeSource, absPath);
3745
+ else
3746
+ validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
3649
3747
  pageKeys.push(key);
3650
3748
  }
3651
3749
  pageKeys.sort();
@@ -3686,8 +3784,30 @@ class AppExecutor extends SysExecutor {
3686
3784
  });
3687
3785
  if (write)
3688
3786
  await this.syncAssets(scanInfo.getScanResult().libDeps);
3787
+ if (write)
3788
+ await this.#syncFirebaseMessagingSw();
3689
3789
  return scanInfo;
3690
3790
  }
3791
+ async#syncFirebaseMessagingSw() {
3792
+ const swRelPath = "public/firebase-messaging-sw.js";
3793
+ if (await FileSys.fileExists(this.getPath(swRelPath)))
3794
+ return;
3795
+ const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
3796
+ if (!await FileSys.fileExists(envClientPath))
3797
+ return;
3798
+ let firebaseConfig = null;
3799
+ try {
3800
+ const envUrl = pathToFileURL(envClientPath);
3801
+ envUrl.searchParams.set("t", String(Date.now()));
3802
+ const envModule = await import(envUrl.href);
3803
+ firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
3804
+ } catch {
3805
+ return;
3806
+ }
3807
+ if (!firebaseConfig)
3808
+ return;
3809
+ await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
3810
+ }
3691
3811
  async increaseBuildNum() {
3692
3812
  await increaseBuildNum(this);
3693
3813
  }
@@ -4170,6 +4290,7 @@ var BACKEND_RESTART_DEBOUNCE_MS = 120;
4170
4290
  var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
4171
4291
  var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
4172
4292
  var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
4293
+ var BACKEND_STDERR_TAIL_LIMIT = 40;
4173
4294
  var BUILDER_READY_TIMEOUT_MS = 150000;
4174
4295
  var BUILDER_START_MAX_ATTEMPTS = 3;
4175
4296
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -4372,6 +4493,7 @@ class AkanAppHost {
4372
4493
  #pendingRestartReason = null;
4373
4494
  #backendStartStatus = null;
4374
4495
  #backendBuildStatusGeneration = 0;
4496
+ #backendStderrTail = [];
4375
4497
  #lastGoodFrontend = {};
4376
4498
  #buildStatusByPhase = new Map;
4377
4499
  #pendingBuildStatusReplay = [];
@@ -4423,6 +4545,7 @@ class AkanAppHost {
4423
4545
  this.#backendStartStatus = startStatus;
4424
4546
  this.#setBackendLifecycleState("starting");
4425
4547
  this.#backendReady = false;
4548
+ this.#backendStderrTail = [];
4426
4549
  const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
4427
4550
  cwd: this.app.workspace.workspaceRoot,
4428
4551
  stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
@@ -4456,6 +4579,37 @@ class AkanAppHost {
4456
4579
  });
4457
4580
  this.#backend = backend;
4458
4581
  this.logger.verbose(`backend spawned pid=${backend.pid}`);
4582
+ if (this.withInk) {
4583
+ this.#forwardBackendStream(backend.stderr, "stderr");
4584
+ this.#forwardBackendStream(backend.stdout, "stdout");
4585
+ }
4586
+ }
4587
+ #recordBackendStderr(chunk) {
4588
+ const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
4589
+ if (lines.length === 0)
4590
+ return;
4591
+ this.#backendStderrTail.push(...lines);
4592
+ if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
4593
+ this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
4594
+ }
4595
+ }
4596
+ async#forwardBackendStream(stream, kind) {
4597
+ if (!stream)
4598
+ return;
4599
+ const decoder = new TextDecoder;
4600
+ try {
4601
+ for await (const chunk of stream) {
4602
+ const text = decoder.decode(chunk, { stream: true });
4603
+ if (!text.trim())
4604
+ continue;
4605
+ if (kind === "stderr") {
4606
+ this.#recordBackendStderr(text);
4607
+ this.logger.warn(`[backend] ${text.trimEnd()}`);
4608
+ } else {
4609
+ this.logger.verbose(`[backend] ${text.trimEnd()}`);
4610
+ }
4611
+ }
4612
+ } catch {}
4459
4613
  }
4460
4614
  #nextBackendBuildStatusGeneration(generation) {
4461
4615
  if (typeof generation === "number") {
@@ -4580,6 +4734,11 @@ class AkanAppHost {
4580
4734
  });
4581
4735
  this.#sendOrQueueBuildStatus(failureStatus);
4582
4736
  this.logger.warn(`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`);
4737
+ if (this.#backendStderrTail.length > 0) {
4738
+ this.logger.warn(`[backend-recovery] recent backend stderr:
4739
+ ${this.#backendStderrTail.join(`
4740
+ `)}`);
4741
+ }
4583
4742
  this.#backendRecoveryTimer = setTimeout(() => {
4584
4743
  this.#backendRecoveryTimer = null;
4585
4744
  if (this.#backend)
@@ -4956,7 +5115,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
4956
5115
  var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
4957
5116
  { path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
4958
5117
  { path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
4959
- { path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
4960
5118
  { path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
4961
5119
  ];
4962
5120
  var writeGeneratedSyncState = async (workspace, state) => {
@@ -5945,6 +6103,117 @@ ${values.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${
5945
6103
  return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
5946
6104
  };
5947
6105
  var parseValues = (value) => value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
6106
+ var hasSourceParseErrors = (content, fileName = "source.ts") => hasParseDiagnostics(sourceFileFor(fileName, content));
6107
+ var findClassDeclaration = (source, className) => {
6108
+ let found = null;
6109
+ const visit = (node) => {
6110
+ if (found)
6111
+ return;
6112
+ if (ts4.isClassDeclaration(node) && node.name?.text === className) {
6113
+ found = node;
6114
+ return;
6115
+ }
6116
+ ts4.forEachChild(node, visit);
6117
+ };
6118
+ ts4.forEachChild(source, visit);
6119
+ return found;
6120
+ };
6121
+ var firstHeritageCall = (node) => {
6122
+ const expression = (node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [])[0]?.expression;
6123
+ return expression && ts4.isCallExpression(expression) ? expression : null;
6124
+ };
6125
+ var factoryArrowOf = (call) => {
6126
+ const arg = call.arguments.find((argument) => ts4.isArrowFunction(argument) || ts4.isFunctionExpression(argument));
6127
+ return arg && (ts4.isArrowFunction(arg) || ts4.isFunctionExpression(arg)) ? arg : null;
6128
+ };
6129
+ var hasClassMethod = (content, className, methodName) => {
6130
+ const node = findClassDeclaration(sourceFileFor("service.ts", content), className);
6131
+ return Boolean(node?.members.some((member) => ts4.isMethodDeclaration(member) && nodeName(member.name) === methodName));
6132
+ };
6133
+ var insertClassMethod = (content, className, methodBlock) => {
6134
+ const source = sourceFileFor("service.ts", content);
6135
+ const node = findClassDeclaration(source, className);
6136
+ if (!node)
6137
+ return null;
6138
+ const closeBrace = node.getEnd() - 1;
6139
+ if (content[closeBrace] !== "}")
6140
+ return null;
6141
+ const lead = content.slice(0, closeBrace).endsWith(`
6142
+ `) ? "" : `
6143
+ `;
6144
+ return spliceText(content, closeBrace, closeBrace, `${lead}${methodBlock}
6145
+ `);
6146
+ };
6147
+ var arrowParamInnerRegion = (content, source, arrow) => {
6148
+ if (arrow.parameters.length > 0) {
6149
+ return {
6150
+ start: arrow.parameters[0].getStart(source),
6151
+ end: arrow.parameters[arrow.parameters.length - 1].getEnd()
6152
+ };
6153
+ }
6154
+ const open = content.indexOf("(", arrow.getStart(source));
6155
+ if (open < 0)
6156
+ return null;
6157
+ const close = content.indexOf(")", open);
6158
+ if (close < 0)
6159
+ return null;
6160
+ return { start: open + 1, end: close };
6161
+ };
6162
+ var factoryParamEdit = (content, source, arrow, plan) => {
6163
+ if (arrow.parameters.length > 1)
6164
+ return null;
6165
+ const region = arrowParamInnerRegion(content, source, arrow);
6166
+ if (!region)
6167
+ return null;
6168
+ if (arrow.parameters.length === 0) {
6169
+ return { ...region, text: plan.mode === "destructure" ? `{ ${plan.name} }` : plan.name };
6170
+ }
6171
+ const param = arrow.parameters[0];
6172
+ if (plan.mode === "positional")
6173
+ return nodeName(param.name) === plan.name ? "unchanged" : null;
6174
+ if (!ts4.isObjectBindingPattern(param.name))
6175
+ return null;
6176
+ const names = param.name.elements.map((element) => ts4.isIdentifier(element.name) ? element.name.text : null);
6177
+ if (names.some((name) => name === null))
6178
+ return null;
6179
+ if (names.includes(plan.name))
6180
+ return "unchanged";
6181
+ return {
6182
+ start: param.getStart(source),
6183
+ end: param.getEnd(),
6184
+ text: `{ ${[...names, plan.name].join(", ")} }`
6185
+ };
6186
+ };
6187
+ var factoryObjectOf = (source, className) => {
6188
+ const node = findClassDeclaration(source, className);
6189
+ const call = node ? firstHeritageCall(node) : null;
6190
+ const arrow = call ? factoryArrowOf(call) : null;
6191
+ const object = arrow ? firstObjectReturnedByArrow(arrow) : null;
6192
+ return arrow && object ? { arrow, object } : null;
6193
+ };
6194
+ var hasSignalFactoryEntry = (content, className, entryName) => {
6195
+ const located = factoryObjectOf(sourceFileFor("signal.ts", content), className);
6196
+ return Boolean(located?.object.properties.some((property) => propertyName(property) === entryName));
6197
+ };
6198
+ var insertSignalFactoryEntry = (content, className, entryName, entryLine, param) => {
6199
+ const source = sourceFileFor("signal.ts", content);
6200
+ const located = factoryObjectOf(source, className);
6201
+ if (!located)
6202
+ return null;
6203
+ const locator = locatedObject(source, located.object);
6204
+ if (locator.fields.some((field) => field.name === entryName))
6205
+ return content;
6206
+ const paramEdit = factoryParamEdit(content, source, located.arrow, param);
6207
+ if (paramEdit === null)
6208
+ return null;
6209
+ const withEntry = insertOrderedFieldLine(content, locator, entryName, entryLine, {
6210
+ fieldIndent: " ",
6211
+ closingIndent: ""
6212
+ });
6213
+ if (withEntry === content)
6214
+ return null;
6215
+ return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
6216
+ };
5948
6217
 
5949
6218
  // pkgs/@akanjs/devkit/workflow/moduleIndex.ts
5950
6219
  var indexFileKinds = [
@@ -6662,7 +6931,9 @@ var createWorkflowStepRegistry = ({
6662
6931
  createScalar,
6663
6932
  createUi,
6664
6933
  addField,
6665
- addEnumField
6934
+ addEnumField,
6935
+ addMutation,
6936
+ addSlice
6666
6937
  }) => {
6667
6938
  const inspect = async () => {
6668
6939
  return;
@@ -6738,7 +7009,19 @@ var createWorkflowStepRegistry = ({
6738
7009
  defaultValue: workflowStringInput(plan.inputs.default)
6739
7010
  })),
6740
7011
  [workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
6741
- [workflowStepKey("add-enum-field", "update-option")]: inspect
7012
+ [workflowStepKey("add-enum-field", "update-option")]: inspect,
7013
+ [workflowStepKey("add-mutation", "update-service")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addMutation({
7014
+ app: workflowStringInput(plan.inputs.app),
7015
+ module: workflowStringInput(plan.inputs.module),
7016
+ mutation: workflowStringInput(plan.inputs.mutation)
7017
+ })),
7018
+ [workflowStepKey("add-mutation", "update-signal")]: inspect,
7019
+ [workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addSlice({
7020
+ app: workflowStringInput(plan.inputs.app),
7021
+ module: workflowStringInput(plan.inputs.module),
7022
+ slice: workflowStringInput(plan.inputs.slice)
7023
+ })),
7024
+ [workflowStepKey("add-slice", "update-signal-slice")]: inspect
6742
7025
  };
6743
7026
  };
6744
7027
  class WorkflowExecutor {
@@ -7485,13 +7768,59 @@ var resourceList = [
7485
7768
  { uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
7486
7769
  ];
7487
7770
  var cursorMcpConfigPath = ".cursor/mcp.json";
7771
+ var claudeMcpConfigPath = ".mcp.json";
7772
+ var codexMcpConfigPath = ".codex/config.toml";
7773
+ var akanMcpInstallTargets = ["cursor", "claude", "codex"];
7774
+ var akanMcpInstallConfigPaths = {
7775
+ cursor: cursorMcpConfigPath,
7776
+ claude: claudeMcpConfigPath,
7777
+ codex: codexMcpConfigPath
7778
+ };
7488
7779
  var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
7780
+ var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
7781
+ var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
7489
7782
  var createAkanCursorMcpServer = (mode = "readonly") => ({
7490
7783
  type: "stdio",
7491
7784
  command: "bash",
7492
- args: ["-lc", `cd "${cursorWorkspaceFolder}" && akan mcp --mode ${mode}`]
7785
+ args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
7786
+ });
7787
+ var createAkanClaudeMcpServer = (mode = "readonly") => ({
7788
+ type: "stdio",
7789
+ command: "bash",
7790
+ args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
7493
7791
  });
7792
+ var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
7494
7793
  var akanCursorMcpServer = createAkanCursorMcpServer();
7794
+ var codexMcpServerTableHeader = "[mcp_servers.akan]";
7795
+ var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
7796
+ command = "bash"
7797
+ args = ["-lc", "${akanMcpCommand(mode)}"]
7798
+ `;
7799
+ var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
7800
+ var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
7801
+ const nextBlock = block.endsWith(`
7802
+ `) ? block : `${block}
7803
+ `;
7804
+ const match = existing.match(codexAkanTablePattern);
7805
+ if (!match) {
7806
+ if (!existing.trim())
7807
+ return nextBlock;
7808
+ return `${existing.replace(/\s*$/, "")}
7809
+
7810
+ ${nextBlock}`;
7811
+ }
7812
+ if (match[0].trim() === nextBlock.trim())
7813
+ return existing;
7814
+ if (!force)
7815
+ throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
7816
+ const start = match.index ?? 0;
7817
+ const before = existing.slice(0, start);
7818
+ const after = existing.slice(start + match[0].length);
7819
+ const separator = after && !after.startsWith(`
7820
+ `) ? `
7821
+ ` : "";
7822
+ return `${before}${nextBlock}${separator}${after}`;
7823
+ };
7495
7824
  var renderDoctorText = (result) => {
7496
7825
  const lines = [`Akan doctor status: ${result.status}`];
7497
7826
  if (result.diagnostics.length === 0) {
@@ -7514,7 +7843,6 @@ var generatedFiles = [
7514
7843
  "*/lib/cnst.ts",
7515
7844
  "*/lib/db.ts",
7516
7845
  "*/lib/dict.ts",
7517
- "*/lib/option.ts",
7518
7846
  "*/lib/sig.ts",
7519
7847
  "*/lib/srv.ts",
7520
7848
  "*/lib/st.ts",
@@ -8508,7 +8836,7 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
8508
8836
  }
8509
8837
  }
8510
8838
  // pkgs/@akanjs/devkit/applicationBuildRunner.ts
8511
- import { mkdir as mkdir8, rm as rm3 } from "fs/promises";
8839
+ import { mkdir as mkdir8, rm as rm4 } from "fs/promises";
8512
8840
  import path37 from "path";
8513
8841
 
8514
8842
  // pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
@@ -8523,6 +8851,25 @@ async function appHasStModule(appCwdPath) {
8523
8851
  }
8524
8852
  var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
8525
8853
  var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
8854
+ var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
8855
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
8856
+ async function writeGeneratedOverridesLayoutFile(opts) {
8857
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
8858
+ const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
8859
+ const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
8860
+ const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
8861
+ const source2 = `"use client";
8862
+ import { UiOverrideProvider } from "akanjs/ui";
8863
+ import { createElement, type ReactNode } from "react";
8864
+ import value from ${JSON.stringify(userSpecifier)};
8865
+
8866
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
8867
+ return createElement(UiOverrideProvider, { value }, children);
8868
+ }
8869
+ `;
8870
+ await Bun.write(absPath, source2);
8871
+ return absPath;
8872
+ }
8526
8873
  function getRootBoundarySegments(key) {
8527
8874
  const match = LAYOUT_KEY_RE.exec(key);
8528
8875
  if (!match)
@@ -8715,9 +9062,17 @@ async function resolveSsrPageEntries(opts) {
8715
9062
  const segments = getRootBoundarySegments(key);
8716
9063
  return segments !== null && isRootBoundarySegments(segments, basePaths);
8717
9064
  }));
8718
- const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) => ({
8719
- key,
8720
- moduleAbsPath: path13.resolve(absPageDir, key)
9065
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
9066
+ const userAbsPath = path13.resolve(absPageDir, key);
9067
+ if (OVERRIDES_KEY_RE.test(key)) {
9068
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
9069
+ appCwdPath: opts.appCwdPath,
9070
+ key,
9071
+ userAbsPath
9072
+ });
9073
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
9074
+ }
9075
+ return { key, moduleAbsPath: userAbsPath };
8721
9076
  }));
8722
9077
  const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
8723
9078
  key: implicitRootLayoutKey(boundary.segments),
@@ -8749,7 +9104,7 @@ function computeRouteSeedIndex(pageEntries) {
8749
9104
  for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
8750
9105
  const parsed = parseRouteModuleKey2(key);
8751
9106
  const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
8752
- if (parsed.kind === "layout") {
9107
+ if (parsed.kind === "layout" || parsed.kind === "overrides") {
8753
9108
  const prefix = parsed.routeSegments.join("/");
8754
9109
  const prev = layoutsByPrefix.get(prefix) ?? [];
8755
9110
  layoutsByPrefix.set(prefix, [...prev, ...files]);
@@ -10266,7 +10621,7 @@ class AllRoutesBuilder {
10266
10621
  }
10267
10622
  }
10268
10623
  // pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
10269
- import { mkdir as mkdir5, rm, unlink } from "fs/promises";
10624
+ import { mkdir as mkdir5, rm as rm2, unlink } from "fs/promises";
10270
10625
  import path24 from "path";
10271
10626
 
10272
10627
  // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
@@ -10396,7 +10751,7 @@ class CsrArtifactBuilder {
10396
10751
  const artifact = await this.#loadCsrArtifact();
10397
10752
  const csrBasePaths = [...akanConfig2.basePaths];
10398
10753
  const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
10399
- await rm(this.#outputDir, { recursive: true, force: true });
10754
+ await rm2(this.#outputDir, { recursive: true, force: true });
10400
10755
  await mkdir5(path24.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
10401
10756
  const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath2) => this.#createHtmlFile(basePath2)));
10402
10757
  const result = await Bun.build({
@@ -11130,7 +11485,7 @@ class DevChangePlanner {
11130
11485
  }
11131
11486
  var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(file)))].sort();
11132
11487
  // pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
11133
- import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm2, stat as stat3, writeFile } from "fs/promises";
11488
+ import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
11134
11489
  import path28 from "path";
11135
11490
  var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
11136
11491
  var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
@@ -11213,7 +11568,7 @@ class DevGeneratedIndexSync {
11213
11568
  if (content === null) {
11214
11569
  if (!await exists(indexPath))
11215
11570
  return false;
11216
- await rm2(indexPath, { force: true });
11571
+ await rm3(indexPath, { force: true });
11217
11572
  return true;
11218
11573
  }
11219
11574
  const current = await readFile(indexPath, "utf8").catch(() => null);
@@ -12360,7 +12715,7 @@ class ApplicationBuildRunner {
12360
12715
  await this.#app.getPageKeys({ refresh: true });
12361
12716
  const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
12362
12717
  if (clean)
12363
- await rm3(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
12718
+ await rm4(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
12364
12719
  await this.#checkProjectInChildProcess(tsconfigPath);
12365
12720
  }
12366
12721
  async#runPhase(id, label, task, summarize, options = {}) {
@@ -12560,7 +12915,7 @@ void run().catch((error) => {
12560
12915
  }
12561
12916
  }
12562
12917
  // pkgs/@akanjs/devkit/applicationReleasePackager.ts
12563
- import { cp, mkdir as mkdir9, rm as rm4 } from "fs/promises";
12918
+ import { cp, mkdir as mkdir9, rm as rm5 } from "fs/promises";
12564
12919
  import path38 from "path";
12565
12920
 
12566
12921
  // pkgs/@akanjs/devkit/uploadRelease.ts
@@ -12667,7 +13022,7 @@ class ApplicationReleasePackager {
12667
13022
  const platformVersion = akanConfig2.mobile.version;
12668
13023
  const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
12669
13024
  if (await FileSys.dirExists(buildRoot))
12670
- await rm4(buildRoot, { recursive: true, force: true });
13025
+ await rm5(buildRoot, { recursive: true, force: true });
12671
13026
  await mkdir9(buildRoot, { recursive: true });
12672
13027
  if (rebuild || !await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))
12673
13028
  await this.#build();
@@ -12676,7 +13031,7 @@ class ApplicationReleasePackager {
12676
13031
  await mkdir9(buildPath, { recursive: true });
12677
13032
  await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
12678
13033
  await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
12679
- await rm4(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
13034
+ await rm5(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
12680
13035
  const releaseRoot = this.#app.workspace.workspaceRoot;
12681
13036
  const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
12682
13037
  await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
@@ -12692,7 +13047,7 @@ class ApplicationReleasePackager {
12692
13047
  `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
12693
13048
  "./csr"
12694
13049
  ]);
12695
- await rm4("./csr", { recursive: true, force: true });
13050
+ await rm5("./csr", { recursive: true, force: true });
12696
13051
  }
12697
13052
  async#writeSourceArchive({ readme }) {
12698
13053
  const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
@@ -12703,7 +13058,7 @@ class ApplicationReleasePackager {
12703
13058
  await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
12704
13059
  const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
12705
13060
  if (await FileSys.dirExists(targetPath))
12706
- await rm4(targetPath, { recursive: true, force: true });
13061
+ await rm5(targetPath, { recursive: true, force: true });
12707
13062
  }));
12708
13063
  const syncPaths = [".husky", ".gitignore", "package.json"];
12709
13064
  await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
@@ -12718,7 +13073,7 @@ class ApplicationReleasePackager {
12718
13073
  const maxRetry = 3;
12719
13074
  for (let i = 0;i < maxRetry; i++) {
12720
13075
  try {
12721
- await rm4(sourceRoot, { recursive: true, force: true });
13076
+ await rm5(sourceRoot, { recursive: true, force: true });
12722
13077
  } catch {}
12723
13078
  }
12724
13079
  }
@@ -12807,11 +13162,15 @@ async function resolveSignalTestPreloadPath(target) {
12807
13162
  addResolvedPackageCandidate(path39.dirname(Bun.main));
12808
13163
  addResolvedPackageCandidate(import.meta.dir);
12809
13164
  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));
12810
- for (const candidate of [...new Set(candidates)]) {
13165
+ const uniqueCandidates = [...new Set(candidates)];
13166
+ for (const candidate of uniqueCandidates) {
12811
13167
  if (await Bun.file(candidate).exists())
12812
13168
  return candidate;
12813
13169
  }
12814
- throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}`);
13170
+ throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
13171
+ Probed paths:
13172
+ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
13173
+ `)}`);
12815
13174
  }
12816
13175
  // pkgs/@akanjs/devkit/builder.ts
12817
13176
  import { existsSync as existsSync2 } from "fs";
@@ -12947,7 +13306,7 @@ class Builder {
12947
13306
  }
12948
13307
  }
12949
13308
  // pkgs/@akanjs/devkit/capacitorApp.ts
12950
- import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
13309
+ import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm6, writeFile as writeFile2 } from "fs/promises";
12951
13310
  import os from "os";
12952
13311
  import path41 from "path";
12953
13312
  import { select as select2 } from "@inquirer/prompts";
@@ -13158,7 +13517,7 @@ var rootCapacitorConfigFilenames = [
13158
13517
  ];
13159
13518
  var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
13160
13519
  async function clearRootCapacitorConfigs(appRoot) {
13161
- await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm5(file, { force: true })));
13520
+ await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm6(file, { force: true })));
13162
13521
  }
13163
13522
  async function writeRootCapacitorConfig(appRoot, content) {
13164
13523
  await clearRootCapacitorConfigs(appRoot);
@@ -13179,6 +13538,7 @@ var getLocalIP = () => {
13179
13538
  var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13180
13539
  var asString = (value) => typeof value === "string" ? value : undefined;
13181
13540
  var firstString = (...values) => values.find((value) => typeof value === "string");
13541
+ var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
13182
13542
  var scoreIosDeviceTarget = (target) => {
13183
13543
  const state = target.state?.toLowerCase() ?? "";
13184
13544
  return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
@@ -13484,6 +13844,8 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13484
13844
  const experimentalConfig = isRecord2(experimental) ? experimental : undefined;
13485
13845
  const pluginsConfig = isRecord2(plugins) ? plugins : {};
13486
13846
  const keyboardPluginConfig = isRecord2(pluginsConfig.Keyboard) ? pluginsConfig.Keyboard : {};
13847
+ const pushNotificationsPluginConfig = isRecord2(pluginsConfig.PushNotifications) ? pluginsConfig.PushNotifications : {};
13848
+ const usesPushNotifications = target.permissions?.includes("push") ?? false;
13487
13849
  const config = {
13488
13850
  ...capacitorConfig,
13489
13851
  appId,
@@ -13492,6 +13854,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13492
13854
  plugins: {
13493
13855
  CapacitorCookies: { enabled: true },
13494
13856
  ...pluginsConfig,
13857
+ ...usesPushNotifications || isRecord2(pluginsConfig.PushNotifications) ? {
13858
+ PushNotifications: {
13859
+ ...usesPushNotifications ? { presentationOptions: ["badge", "sound", "alert"] } : {},
13860
+ ...pushNotificationsPluginConfig
13861
+ }
13862
+ } : {},
13495
13863
  Keyboard: {
13496
13864
  resize: "none",
13497
13865
  ...keyboardPluginConfig
@@ -13563,9 +13931,9 @@ class CapacitorApp {
13563
13931
  await mkdir11(this.targetRoot, { recursive: true });
13564
13932
  if (regenerate) {
13565
13933
  if (!platform || platform === "ios")
13566
- await rm5(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13934
+ await rm6(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13567
13935
  if (!platform || platform === "android")
13568
- await rm5(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13936
+ await rm6(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13569
13937
  }
13570
13938
  const project = this.project;
13571
13939
  await this.project.load();
@@ -13587,7 +13955,7 @@ class CapacitorApp {
13587
13955
  await this.#prepareTargetAssets();
13588
13956
  await this.#prepareExternalFiles("ios");
13589
13957
  await this.#applyIosMetadata();
13590
- await this.#applyPermissions();
13958
+ await this.#applyPermissions({ operation, env });
13591
13959
  await this.#applyDeepLinks("ios", { operation, env });
13592
13960
  await this.project.commit();
13593
13961
  await this.#generateAssets({ operation, env });
@@ -13730,7 +14098,7 @@ ${error.message}`;
13730
14098
  await this.#prepareTargetAssets();
13731
14099
  await this.#prepareExternalFiles("android");
13732
14100
  await this.#applyAndroidMetadata();
13733
- await this.#applyPermissions();
14101
+ await this.#applyPermissions({ operation, env });
13734
14102
  await this.#applyDeepLinks("android", { operation, env });
13735
14103
  await this.project.commit();
13736
14104
  await this.#generateAssets({ operation, env });
@@ -13864,7 +14232,7 @@ ${error.message}`;
13864
14232
  const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13865
14233
  if (!await Bun.file(htmlSource).exists())
13866
14234
  throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
13867
- await rm5(this.targetWebRoot, { recursive: true, force: true });
14235
+ await rm6(this.targetWebRoot, { recursive: true, force: true });
13868
14236
  await mkdir11(this.targetWebRoot, { recursive: true });
13869
14237
  await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13870
14238
  }
@@ -13945,7 +14313,7 @@ ${error.message}`;
13945
14313
  await this.project.android.setVersionCode(this.target.buildNum);
13946
14314
  await this.project.android.setAppName(this.target.appName);
13947
14315
  }
13948
- async#applyPermissions() {
14316
+ async#applyPermissions({ operation, env }) {
13949
14317
  for (const permission of this.target.permissions ?? []) {
13950
14318
  if (permission === "camera")
13951
14319
  await this.addCamera();
@@ -13954,7 +14322,7 @@ ${error.message}`;
13954
14322
  else if (permission === "location")
13955
14323
  await this.addLocation();
13956
14324
  else if (permission === "push")
13957
- await this.addPush();
14325
+ await this.addPush({ operation, env });
13958
14326
  }
13959
14327
  }
13960
14328
  async#applyDeepLinks(platform, { operation, env }) {
@@ -13973,7 +14341,7 @@ ${error.message}`;
13973
14341
  await this.#setUrlSchemesInIos(schemes);
13974
14342
  }
13975
14343
  if (domains.length > 0)
13976
- await this.#setAssociatedDomainsInIos(domains);
14344
+ await this.#setAssociatedDomainsInIos(domains, { operation, env });
13977
14345
  return;
13978
14346
  }
13979
14347
  if (platform === "android") {
@@ -14067,12 +14435,64 @@ ${error.message}`;
14067
14435
  this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
14068
14436
  this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
14069
14437
  }
14070
- async addPush() {
14438
+ async addPush({ operation, env }) {
14071
14439
  await this.#setPermissionInIos({
14072
14440
  userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
14073
14441
  });
14442
+ await Promise.all([
14443
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
14444
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
14445
+ this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
14446
+ this.#ensurePushAppDelegateInIos()
14447
+ ]);
14074
14448
  this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
14075
14449
  }
14450
+ async#ensurePushAppDelegateInIos() {
14451
+ const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
14452
+ if (!await Bun.file(appDelegatePath).exists())
14453
+ return;
14454
+ const editor = await FileEditor.create(appDelegatePath);
14455
+ let content = editor.getContent();
14456
+ if (!content.includes("import FirebaseCore")) {
14457
+ content = content.replace("import Capacitor", `import Capacitor
14458
+ import FirebaseCore`);
14459
+ }
14460
+ if (!content.includes("import FirebaseMessaging")) {
14461
+ content = content.replace("import FirebaseCore", `import FirebaseCore
14462
+ import FirebaseMessaging`);
14463
+ }
14464
+ if (!content.includes("FirebaseApp.configure()")) {
14465
+ content = content.replace(/\n(\s*)return true/, `
14466
+ $1FirebaseApp.configure()
14467
+
14468
+ $1return true`);
14469
+ }
14470
+ if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
14471
+ const delegateMethods = `
14472
+ func application(_ application: UIApplication,
14473
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
14474
+ Messaging.messaging().apnsToken = deviceToken
14475
+ NotificationCenter.default.post(
14476
+ name: .capacitorDidRegisterForRemoteNotifications,
14477
+ object: deviceToken
14478
+ )
14479
+ }
14480
+
14481
+ func application(_ application: UIApplication,
14482
+ didFailToRegisterForRemoteNotificationsWithError error: Error) {
14483
+ NotificationCenter.default.post(
14484
+ name: .capacitorDidFailToRegisterForRemoteNotifications,
14485
+ object: error
14486
+ )
14487
+ }
14488
+ `;
14489
+ content = content.replace(`
14490
+ func applicationWillResignActive`, `${delegateMethods}
14491
+ func applicationWillResignActive`);
14492
+ }
14493
+ if (content !== editor.getContent())
14494
+ await editor.setContent(content).save();
14495
+ }
14076
14496
  async#setPermissionInIos(permissions) {
14077
14497
  const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
14078
14498
  await Promise.all([
@@ -14090,25 +14510,35 @@ ${error.message}`;
14090
14510
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14091
14511
  ]);
14092
14512
  }
14093
- async#setAssociatedDomainsInIos(domains) {
14513
+ async#setAssociatedDomainsInIos(domains, { operation, env }) {
14514
+ await this.#writeEntitlementsInIos(domains, { operation, env });
14515
+ }
14516
+ async#writeEntitlementsInIos(domains, runConfig) {
14094
14517
  const entitlementsRelPath = "App/App.entitlements";
14095
14518
  const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14096
14519
  const values = domains.map((domain) => `applinks:${domain}`);
14520
+ const usesPush = this.target.permissions?.includes("push") ?? false;
14521
+ const apsEnvironment = resolveIosApnsEnvironment(runConfig);
14097
14522
  const body = [
14098
14523
  '<?xml version="1.0" encoding="UTF-8"?>',
14099
14524
  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14100
14525
  '<plist version="1.0">',
14101
14526
  "<dict>",
14102
- " <key>com.apple.developer.associated-domains</key>",
14103
- " <array>",
14104
- ...values.map((value) => ` <string>${value}</string>`),
14105
- " </array>",
14527
+ ...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
14528
+ ...values.length > 0 ? [
14529
+ " <key>com.apple.developer.associated-domains</key>",
14530
+ " <array>",
14531
+ ...values.map((value) => ` <string>${value}</string>`),
14532
+ " </array>"
14533
+ ] : [],
14106
14534
  "</dict>",
14107
14535
  "</plist>",
14108
14536
  ""
14109
14537
  ].join(`
14110
14538
  `);
14111
- await writeFile2(entitlementsPath, body);
14539
+ const currentBody = await Bun.file(entitlementsPath).exists() ? await Bun.file(entitlementsPath).text() : undefined;
14540
+ if (currentBody !== body)
14541
+ await writeFile2(entitlementsPath, body);
14112
14542
  await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
14113
14543
  }
14114
14544
  async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
@@ -14212,7 +14642,7 @@ ${filter}$1`);
14212
14642
  for (const permission of permissions) {
14213
14643
  if (this.#hasPermissionInAndroid(permission)) {
14214
14644
  this.app.logger.info(`${permission} already exists in android`);
14215
- return this;
14645
+ continue;
14216
14646
  }
14217
14647
  this.app.logger.info(`Adding ${permission} to android`);
14218
14648
  this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
@@ -14228,7 +14658,7 @@ ${filter}$1`);
14228
14658
  return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
14229
14659
  }
14230
14660
  #hasPermissionInAndroid(permission) {
14231
- return this.#getPermissionsInAndroid().includes(permission);
14661
+ return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
14232
14662
  }
14233
14663
  }
14234
14664
  // pkgs/@akanjs/devkit/commandDecorators/targetMeta.ts
@@ -15324,6 +15754,42 @@ var CONVENTION_SUFFIXES = [
15324
15754
  ".signal.ts",
15325
15755
  ".store.ts"
15326
15756
  ];
15757
+ var PAGE_RESERVED_EXPORTS = new Set([
15758
+ "pageConfig",
15759
+ "head",
15760
+ "metadata",
15761
+ "generateHead",
15762
+ "generateMetadata",
15763
+ "fonts",
15764
+ "manifest",
15765
+ "theme",
15766
+ "reconnect",
15767
+ "wsConnect",
15768
+ "layoutStyle",
15769
+ "gaTrackingId"
15770
+ ]);
15771
+ var RULE_FIXES = {
15772
+ "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.",
15773
+ "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
15774
+ "akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
15775
+ "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
15776
+ "akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
15777
+ "akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
15778
+ "akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
15779
+ "akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
15780
+ "akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
15781
+ "akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
15782
+ "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`.",
15783
+ "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.",
15784
+ "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
15785
+ "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
15786
+ "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."
15787
+ };
15788
+ function getRuleFix(rule) {
15789
+ if (rule.startsWith("akan.convention"))
15790
+ return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
15791
+ return RULE_FIXES[rule];
15792
+ }
15327
15793
 
15328
15794
  class AkanQualityScanner {
15329
15795
  async scan(workspaceRoot) {
@@ -15332,13 +15798,14 @@ class AkanQualityScanner {
15332
15798
  const warnings = [
15333
15799
  ...this.#scanGlobalQuality(sourceFiles),
15334
15800
  ...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
15801
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
15335
15802
  ...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
15336
15803
  ...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
15337
15804
  ];
15338
15805
  return {
15339
15806
  workspaceRoot,
15340
15807
  scannedFiles: sourceFiles.length,
15341
- warnings: warnings.sort(compareWarnings),
15808
+ warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
15342
15809
  suggestedRules: SUGGESTED_RULES
15343
15810
  };
15344
15811
  }
@@ -15388,7 +15855,8 @@ class AkanQualityScanner {
15388
15855
  #scanGlobalQuality(sourceFiles) {
15389
15856
  const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15390
15857
  const warnings = [];
15391
- for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15858
+ const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
15859
+ for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
15392
15860
  if (declarations.length < 2)
15393
15861
  continue;
15394
15862
  warnings.push({
@@ -15459,6 +15927,49 @@ class AkanQualityScanner {
15459
15927
  }
15460
15928
  return warnings;
15461
15929
  }
15930
+ #scanComponentQuality(sourceFile2) {
15931
+ if (!isComponentDeclarationFile(sourceFile2.file))
15932
+ return [];
15933
+ const isPage = isPageRouteFile(sourceFile2.file);
15934
+ const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
15935
+ const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
15936
+ const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
15937
+ const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
15938
+ const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
15939
+ const warnings = [];
15940
+ for (const declaration of declarations) {
15941
+ if (declaration.isDefaultExport)
15942
+ continue;
15943
+ if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
15944
+ continue;
15945
+ if (declaration.exported) {
15946
+ if (isAllowedComponentExport(declaration, isPage))
15947
+ continue;
15948
+ warnings.push({
15949
+ rule: "akan.file.component-export",
15950
+ scope: "file",
15951
+ severity: "warning",
15952
+ file: sourceFile2.file,
15953
+ line: declaration.line,
15954
+ message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
15955
+ });
15956
+ continue;
15957
+ }
15958
+ if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
15959
+ continue;
15960
+ if (isRestrictedInternalKind(declaration.kind)) {
15961
+ warnings.push({
15962
+ rule: "akan.file.component-internal-declaration",
15963
+ scope: "file",
15964
+ severity: "warning",
15965
+ file: sourceFile2.file,
15966
+ line: declaration.line,
15967
+ message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
15968
+ });
15969
+ }
15970
+ }
15971
+ return warnings;
15972
+ }
15462
15973
  #scanConventionQuality(sourceFile2) {
15463
15974
  const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
15464
15975
  if (!suffix)
@@ -15534,6 +16045,8 @@ function formatQualityWarnings(warnings) {
15534
16045
  if (warning.locations?.length) {
15535
16046
  lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15536
16047
  }
16048
+ if (warning.fix)
16049
+ lines.push(` fix: ${warning.fix}`);
15537
16050
  return lines;
15538
16051
  });
15539
16052
  }
@@ -15542,6 +16055,7 @@ function formatQualityLocation(file, line) {
15542
16055
  }
15543
16056
  function getExportedFunctionLikes(sourceFile2) {
15544
16057
  const declarations = [];
16058
+ const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
15545
16059
  for (const statement of sourceFile2.sourceFile.statements) {
15546
16060
  if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15547
16061
  declarations.push({
@@ -15549,7 +16063,8 @@ function getExportedFunctionLikes(sourceFile2) {
15549
16063
  kind: "function",
15550
16064
  file: sourceFile2.file,
15551
16065
  line: getLine(sourceFile2.sourceFile, statement),
15552
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
16066
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
16067
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15553
16068
  });
15554
16069
  }
15555
16070
  if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
@@ -15558,7 +16073,8 @@ function getExportedFunctionLikes(sourceFile2) {
15558
16073
  kind: "class",
15559
16074
  file: sourceFile2.file,
15560
16075
  line: getLine(sourceFile2.sourceFile, statement),
15561
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
16076
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
16077
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
15562
16078
  });
15563
16079
  }
15564
16080
  if (ts11.isVariableStatement(statement) && isExported(statement)) {
@@ -15570,16 +16086,127 @@ function getExportedFunctionLikes(sourceFile2) {
15570
16086
  kind: "function-variable",
15571
16087
  file: sourceFile2.file,
15572
16088
  line: getLine(sourceFile2.sourceFile, declaration),
15573
- bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
16089
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
16090
+ duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
15574
16091
  });
15575
16092
  }
15576
16093
  }
15577
16094
  }
15578
16095
  return declarations;
15579
16096
  }
16097
+ function isPageRouteFile(file) {
16098
+ const segments = file.split("/");
16099
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
16100
+ }
16101
+ function isUiComponentFile(file) {
16102
+ const segments = file.split("/");
16103
+ return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
16104
+ }
16105
+ function isConventionDuplicateNameExempt(file, isEnumClass) {
16106
+ if (!isInLibModule(file))
16107
+ return false;
16108
+ if (file.endsWith(".tsx"))
16109
+ return true;
16110
+ if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
16111
+ return true;
16112
+ if (file.endsWith(".constant.ts"))
16113
+ return !isEnumClass;
16114
+ return false;
16115
+ }
16116
+ function isInLibModule(file) {
16117
+ const segments = file.split("/");
16118
+ return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
16119
+ }
16120
+ function isEnumClassStatement(sourceFile2, statement) {
16121
+ if (!ts11.isClassDeclaration(statement))
16122
+ return false;
16123
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
16124
+ const expression = heritageClause?.types[0]?.expression;
16125
+ return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
16126
+ }
15580
16127
  function getExportedClassNames(sourceFile2) {
15581
16128
  return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15582
16129
  }
16130
+ function isComponentDeclarationFile(file) {
16131
+ if (!file.endsWith(".tsx"))
16132
+ return false;
16133
+ const segments = file.split("/");
16134
+ const [root, , area] = segments;
16135
+ if (root !== "apps" && root !== "libs")
16136
+ return false;
16137
+ if (area === "lib" || area === "ui")
16138
+ return true;
16139
+ return root === "apps" && area === "page";
16140
+ }
16141
+ function getComponentFileDeclarations(sourceFile2) {
16142
+ const reExportedNames = new Set;
16143
+ for (const statement of sourceFile2.statements) {
16144
+ if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
16145
+ for (const element of statement.exportClause.elements)
16146
+ reExportedNames.add((element.propertyName ?? element.name).text);
16147
+ }
16148
+ }
16149
+ const declarations = [];
16150
+ for (const statement of sourceFile2.statements) {
16151
+ const isDefaultExport = isDefaultExportStatement(statement);
16152
+ const inlineExported = isExported(statement);
16153
+ const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
16154
+ if (ts11.isInterfaceDeclaration(statement))
16155
+ add(statement.name.text, "interface", getLine(sourceFile2, statement));
16156
+ else if (ts11.isTypeAliasDeclaration(statement))
16157
+ add(statement.name.text, "type", getLine(sourceFile2, statement));
16158
+ else if (ts11.isEnumDeclaration(statement))
16159
+ add(statement.name.text, "enum", getLine(sourceFile2, statement));
16160
+ else if (ts11.isFunctionDeclaration(statement) && statement.name)
16161
+ add(statement.name.text, "function", getLine(sourceFile2, statement));
16162
+ else if (ts11.isClassDeclaration(statement) && statement.name)
16163
+ add(statement.name.text, "class", getLine(sourceFile2, statement));
16164
+ else if (ts11.isVariableStatement(statement)) {
16165
+ for (const declaration of statement.declarationList.declarations) {
16166
+ if (!ts11.isIdentifier(declaration.name))
16167
+ continue;
16168
+ const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
16169
+ add(declaration.name.text, kind, getLine(sourceFile2, declaration));
16170
+ }
16171
+ }
16172
+ }
16173
+ return declarations;
16174
+ }
16175
+ function getCompoundComponentNames(sourceFile2) {
16176
+ const names = new Set;
16177
+ for (const statement of sourceFile2.statements) {
16178
+ if (!ts11.isExpressionStatement(statement))
16179
+ continue;
16180
+ const { expression } = statement;
16181
+ if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
16182
+ continue;
16183
+ if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
16184
+ continue;
16185
+ names.add(expression.left.name.text);
16186
+ if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
16187
+ names.add(expression.right.text);
16188
+ }
16189
+ return names;
16190
+ }
16191
+ function isComponentValueKind(kind) {
16192
+ return kind === "variable" || kind === "function" || kind === "class";
16193
+ }
16194
+ function isRestrictedInternalKind(kind) {
16195
+ return kind === "interface" || kind === "type" || kind === "function";
16196
+ }
16197
+ function isAllowedComponentExport(declaration, isPage) {
16198
+ if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
16199
+ return true;
16200
+ return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
16201
+ }
16202
+ function isPascalCaseName(name) {
16203
+ return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
16204
+ }
16205
+ function isDefaultExportStatement(statement) {
16206
+ if (ts11.isExportAssignment(statement))
16207
+ return !statement.isExportEquals;
16208
+ return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
16209
+ }
15583
16210
  function getPlaceholderExportWarnings(sourceFile2) {
15584
16211
  if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
15585
16212
  return [];
@@ -15595,11 +16222,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
15595
16222
  function getDictionaryTextWarnings(sourceFile2) {
15596
16223
  if (!sourceFile2.file.endsWith(".dictionary.ts"))
15597
16224
  return [];
15598
- const stalePatterns = [
15599
- { pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
15600
- { pattern: /settting/, label: "misspelling: settting" },
15601
- { pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
15602
- ];
16225
+ const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
15603
16226
  return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15604
16227
  rule: "akan.file.dictionary-stale-text",
15605
16228
  scope: "file",
@@ -15720,10 +16343,8 @@ function getConventionDescription(suffix, modelName) {
15720
16343
  }
15721
16344
  function getLibRootFile(file) {
15722
16345
  const segments = file.split("/");
15723
- if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
16346
+ if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
15724
16347
  return segments[3];
15725
- if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
15726
- return segments[4];
15727
16348
  return null;
15728
16349
  }
15729
16350
  function getModuleUiWarning(file) {