@akanjs/cli 2.3.9-rc.9 → 2.3.10-rc.0

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.
@@ -2,7 +2,7 @@
2
2
  var __require = import.meta.require;
3
3
 
4
4
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts
5
- import path42 from "path";
5
+ import path44 from "path";
6
6
 
7
7
  // pkgs/@akanjs/devkit/aiEditor.ts
8
8
  import { input, select } from "@inquirer/prompts";
@@ -744,6 +744,44 @@ var DEFAULT_AKAN_IMAGE_CONFIG = {
744
744
  fetchTimeoutMs: 7000,
745
745
  maxRemoteBytes: 25 * 1024 * 1024
746
746
  };
747
+ var normalizeIndexPath = (indexPath) => {
748
+ const normalized = indexPath?.trim();
749
+ if (!normalized)
750
+ return;
751
+ const path2 = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
752
+ return path2 === "/" ? "/" : path2;
753
+ };
754
+ var normalizeStringList = (values) => {
755
+ const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
756
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
757
+ };
758
+ var normalizeDeepLinkDomain = (domain) => {
759
+ const normalized = domain.trim();
760
+ if (!normalized)
761
+ return "";
762
+ try {
763
+ const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
764
+ return url.host.toLowerCase();
765
+ } catch {
766
+ return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
767
+ }
768
+ };
769
+ var normalizeDeepLinks = (deepLinks) => {
770
+ if (!deepLinks)
771
+ return;
772
+ const schemes = normalizeStringList(deepLinks.schemes);
773
+ const domains = normalizeStringList(deepLinks.domains?.map(normalizeDeepLinkDomain));
774
+ const teamId = deepLinks.ios?.teamId?.trim();
775
+ const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints);
776
+ if (!schemes && !domains && !teamId && !sha256CertFingerprints)
777
+ return;
778
+ return {
779
+ ...schemes ? { schemes } : {},
780
+ ...domains ? { domains } : {},
781
+ ...teamId ? { ios: { teamId } } : {},
782
+ ...sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}
783
+ };
784
+ };
747
785
 
748
786
  class AkanAppConfig {
749
787
  app;
@@ -757,6 +795,7 @@ class AkanAppConfig {
757
795
  i18n;
758
796
  publicEnv;
759
797
  mobile;
798
+ secrets;
760
799
  baseDevEnv;
761
800
  libs;
762
801
  domains = new Set;
@@ -783,11 +822,16 @@ class AkanAppConfig {
783
822
  process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
784
823
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
785
824
  this.publicEnv = config?.publicEnv ?? [];
825
+ this.secrets = config?.secrets ?? [];
786
826
  this.mobile = this.#resolveMobileConfig(config.mobile);
787
827
  this.docker = this.#makeDockerContent(config?.docker ?? {});
788
828
  }
789
829
  #resolveMobileConfig(mobile) {
790
- const { targets: rawTargets, ...rawMobile } = mobile ?? {};
830
+ const {
831
+ targets: rawTargets,
832
+ indexPath: _indexPath,
833
+ ...rawMobile
834
+ } = mobile ?? {};
791
835
  const appName = rawMobile.appName ?? this.app.name;
792
836
  const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
793
837
  const version = rawMobile.version ?? "0.0.1";
@@ -800,6 +844,8 @@ class AkanAppConfig {
800
844
  const target = rawTarget;
801
845
  const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
802
846
  const basePath2 = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
847
+ const indexPath = normalizeIndexPath(target.indexPath);
848
+ const deepLinks = normalizeDeepLinks(target.deepLinks);
803
849
  if (basePath2 && !this.basePaths.has(basePath2)) {
804
850
  throw new Error(`Mobile target '${name}' uses unknown basePath '${basePath2}' in apps/${this.app.name}/akan.config.ts`);
805
851
  }
@@ -808,6 +854,8 @@ class AkanAppConfig {
808
854
  ...target,
809
855
  name,
810
856
  basePath: basePath2,
857
+ indexPath,
858
+ deepLinks,
811
859
  appName: target.appName ?? appName,
812
860
  appId: target.appId ?? appId,
813
861
  version: target.version ?? version,
@@ -12116,7 +12164,14 @@ class SsrBaseArtifactBuilder {
12116
12164
  basePaths: [...akanConfig2.basePaths],
12117
12165
  branches: [...akanConfig2.branches],
12118
12166
  i18n: akanConfig2.i18n,
12119
- imageConfig: akanConfig2.images
12167
+ imageConfig: akanConfig2.images,
12168
+ deepLinkAssociations: Object.values(akanConfig2.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
12169
+ targetName: target.name,
12170
+ appId: target.appId,
12171
+ domains: target.deepLinks?.domains ?? [],
12172
+ iosTeamId: target.deepLinks?.ios?.teamId,
12173
+ androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
12174
+ }))
12120
12175
  };
12121
12176
  await Bun.write(path35.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
12122
12177
  `);
@@ -12504,6 +12559,7 @@ void run().catch((error) => {
12504
12559
  }
12505
12560
  // pkgs/@akanjs/devkit/applicationReleasePackager.ts
12506
12561
  import { cp, mkdir as mkdir9, rm as rm4 } from "fs/promises";
12562
+ import path38 from "path";
12507
12563
 
12508
12564
  // pkgs/@akanjs/devkit/uploadRelease.ts
12509
12565
  import { HttpClient as HttpClient2, Logger as Logger9 } from "akanjs/common";
@@ -12619,13 +12675,9 @@ class ApplicationReleasePackager {
12619
12675
  await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
12620
12676
  await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
12621
12677
  await rm4(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
12622
- await this.#app.workspace.spawn("tar", [
12623
- "-zcf",
12624
- `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-release.tar.gz`,
12625
- "-C",
12626
- buildRoot,
12627
- "./"
12628
- ]);
12678
+ const releaseRoot = this.#app.workspace.workspaceRoot;
12679
+ const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
12680
+ await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
12629
12681
  await this.#writeCsrZipIfPresent();
12630
12682
  return { platformVersion };
12631
12683
  }
@@ -12646,22 +12698,18 @@ class ApplicationReleasePackager {
12646
12698
  await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
12647
12699
  const libDeps = ["social", "shared", "platform", "util"];
12648
12700
  await Promise.all(libDeps.map((lib) => cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true })));
12649
- await Promise.all([".next", "ios", "android", "public/libs"].map(async (path38) => {
12650
- const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path38}`;
12701
+ await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
12702
+ const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
12651
12703
  if (await FileSys.dirExists(targetPath))
12652
12704
  await rm4(targetPath, { recursive: true, force: true });
12653
12705
  }));
12654
12706
  const syncPaths = [".husky", ".gitignore", "package.json"];
12655
- await Promise.all(syncPaths.map((path38) => cp(`${this.#app.workspace.cwdPath}/${path38}`, `${sourceRoot}/${path38}`, { recursive: true })));
12707
+ await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
12656
12708
  await this.#writeSourceTsconfig(sourceRoot, libDeps);
12657
12709
  await Bun.write(`${sourceRoot}/README.md`, readme);
12658
- await this.#app.workspace.spawn("tar", [
12659
- "-zcf",
12660
- `${this.#app.workspace.cwdPath}/releases/sources/${this.#app.name}-source.tar.gz`,
12661
- "-C",
12662
- sourceRoot,
12663
- "./"
12664
- ]);
12710
+ const sourceCwd = this.#app.workspace.cwdPath;
12711
+ const sourceArchivePath = path38.relative(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`).split(path38.sep).join("/");
12712
+ await this.#app.workspace.spawn("tar", ["-zcf", sourceArchivePath, "-C", sourceRoot, "./"], { cwd: sourceCwd });
12665
12713
  }
12666
12714
  async#resetSourceRoot(sourceRoot) {
12667
12715
  if (await FileSys.dirExists(sourceRoot)) {
@@ -12743,20 +12791,20 @@ class ApplicationReleasePackager {
12743
12791
  }
12744
12792
  }
12745
12793
  // pkgs/@akanjs/devkit/applicationTestPreload.ts
12746
- import path38 from "path";
12794
+ import path39 from "path";
12747
12795
  var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
12748
12796
  async function resolveSignalTestPreloadPath(target) {
12749
12797
  const candidates = [];
12750
12798
  const addResolvedPackageCandidate = (basePath2) => {
12751
12799
  try {
12752
- candidates.push(path38.join(path38.dirname(Bun.resolveSync("akanjs/package.json", basePath2)), SIGNAL_TEST_PRELOAD_PATH));
12800
+ candidates.push(path39.join(path39.dirname(Bun.resolveSync("akanjs/package.json", basePath2)), SIGNAL_TEST_PRELOAD_PATH));
12753
12801
  } catch {}
12754
12802
  };
12755
12803
  addResolvedPackageCandidate(target.cwdPath);
12756
12804
  addResolvedPackageCandidate(process.cwd());
12757
- addResolvedPackageCandidate(path38.dirname(Bun.main));
12805
+ addResolvedPackageCandidate(path39.dirname(Bun.main));
12758
12806
  addResolvedPackageCandidate(import.meta.dir);
12759
- candidates.push(path38.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path38.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path38.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path38.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path38.join(path38.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path38.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
12807
+ 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));
12760
12808
  for (const candidate of [...new Set(candidates)]) {
12761
12809
  if (await Bun.file(candidate).exists())
12762
12810
  return candidate;
@@ -12766,7 +12814,7 @@ async function resolveSignalTestPreloadPath(target) {
12766
12814
  // pkgs/@akanjs/devkit/builder.ts
12767
12815
  import { existsSync as existsSync2 } from "fs";
12768
12816
  import { mkdir as mkdir10 } from "fs/promises";
12769
- import path39 from "path";
12817
+ import path40 from "path";
12770
12818
  var SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
12771
12819
  var assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
12772
12820
  var assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file"]));
@@ -12783,14 +12831,14 @@ class Builder {
12783
12831
  #globEntrypoints(cwd, pattern) {
12784
12832
  const glob = new Bun.Glob(pattern);
12785
12833
  return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
12786
- const segments = relativePath.split(path39.sep);
12834
+ const segments = relativePath.split(path40.sep);
12787
12835
  return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
12788
- }).map((rel) => path39.join(cwd, rel));
12836
+ }).map((rel) => path40.join(cwd, rel));
12789
12837
  }
12790
12838
  #globFiles(cwd, pattern = "**/*.*") {
12791
12839
  const glob = new Bun.Glob(pattern);
12792
12840
  return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
12793
- const segments = relativePath.split(path39.sep);
12841
+ const segments = relativePath.split(path40.sep);
12794
12842
  return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
12795
12843
  });
12796
12844
  }
@@ -12798,17 +12846,17 @@ class Builder {
12798
12846
  const out = [];
12799
12847
  for (const p of additionalEntryPoints) {
12800
12848
  if (p.includes("*")) {
12801
- const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path39.sep}`) ? p.slice(cwd.length + 1) : p;
12849
+ const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path40.sep}`) ? p.slice(cwd.length + 1) : p;
12802
12850
  out.push(...this.#globEntrypoints(cwd, rel));
12803
12851
  } else
12804
- out.push(path39.isAbsolute(p) ? p : path39.join(cwd, p));
12852
+ out.push(path40.isAbsolute(p) ? p : path40.join(cwd, p));
12805
12853
  }
12806
12854
  return out;
12807
12855
  }
12808
12856
  #getBuildOptions({ bundle = false, additionalEntryPoints = [] } = {}) {
12809
12857
  const cwd = this.#executor.cwdPath;
12810
12858
  const entrypoints = [
12811
- ...bundle ? [path39.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}"),
12859
+ ...bundle ? [path40.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}"),
12812
12860
  ...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints)
12813
12861
  ];
12814
12862
  return {
@@ -12829,9 +12877,9 @@ class Builder {
12829
12877
  for (const relativePath of this.#globFiles(cwd)) {
12830
12878
  if (relativePath === "package.json")
12831
12879
  continue;
12832
- const sourcePath = path39.join(cwd, relativePath);
12833
- const targetPath = path39.join(this.#distExecutor.cwdPath, relativePath);
12834
- await mkdir10(path39.dirname(targetPath), { recursive: true });
12880
+ const sourcePath = path40.join(cwd, relativePath);
12881
+ const targetPath = path40.join(this.#distExecutor.cwdPath, relativePath);
12882
+ await mkdir10(path40.dirname(targetPath), { recursive: true });
12835
12883
  await Bun.write(targetPath, Bun.file(sourcePath));
12836
12884
  }
12837
12885
  }
@@ -12842,13 +12890,13 @@ class Builder {
12842
12890
  return withoutFormatDir;
12843
12891
  if (!hasDotSlash && withoutFormatDir === publishedPath)
12844
12892
  return publishedPath;
12845
- const parsed = path39.posix.parse(withoutFormatDir);
12893
+ const parsed = path40.posix.parse(withoutFormatDir);
12846
12894
  if (![".js", ".mjs", ".cjs"].includes(parsed.ext))
12847
12895
  return withoutFormatDir;
12848
- const withoutExt = path39.posix.join(parsed.dir, parsed.name);
12896
+ const withoutExt = path40.posix.join(parsed.dir, parsed.name);
12849
12897
  const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
12850
12898
  const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
12851
- const matchedSource = sourceCandidates.find((candidate) => existsSync2(path39.join(this.#executor.cwdPath, candidate)));
12899
+ const matchedSource = sourceCandidates.find((candidate) => existsSync2(path40.join(this.#executor.cwdPath, candidate)));
12852
12900
  if (!matchedSource)
12853
12901
  return withoutFormatDir;
12854
12902
  return hasDotSlash ? `./${matchedSource}` : matchedSource;
@@ -12897,9 +12945,9 @@ class Builder {
12897
12945
  }
12898
12946
  }
12899
12947
  // pkgs/@akanjs/devkit/capacitorApp.ts
12900
- import { cp as cp2, mkdir as mkdir11, rm as rm5 } from "fs/promises";
12948
+ import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
12901
12949
  import os from "os";
12902
- import path40 from "path";
12950
+ import path41 from "path";
12903
12951
  import { select as select2 } from "@inquirer/prompts";
12904
12952
  import { MobileProject } from "@trapezedev/project";
12905
12953
  import { capitalize as capitalize5 } from "akanjs/common";
@@ -13106,13 +13154,13 @@ var rootCapacitorConfigFilenames = [
13106
13154
  "capacitor.config.js",
13107
13155
  "capacitor.config.json"
13108
13156
  ];
13109
- var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path40.join(appRoot, file));
13157
+ var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
13110
13158
  async function clearRootCapacitorConfigs(appRoot) {
13111
13159
  await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm5(file, { force: true })));
13112
13160
  }
13113
13161
  async function writeRootCapacitorConfig(appRoot, content) {
13114
13162
  await clearRootCapacitorConfigs(appRoot);
13115
- await Bun.write(path40.join(appRoot, "capacitor.config.json"), content);
13163
+ await Bun.write(path41.join(appRoot, "capacitor.config.json"), content);
13116
13164
  }
13117
13165
  var getLocalIP = () => {
13118
13166
  const interfaces = os.networkInterfaces();
@@ -13221,13 +13269,13 @@ function buildIosNativeRunCommand({
13221
13269
  scheme = "App",
13222
13270
  configuration = "Debug"
13223
13271
  }) {
13224
- const derivedDataPath = path40.join(appRoot, "ios/DerivedData", device.id);
13272
+ const derivedDataPath = path41.join(appRoot, "ios/DerivedData", device.id);
13225
13273
  const productPlatform = device.kind === "device" ? "iphoneos" : "iphonesimulator";
13226
13274
  const destination = device.kind === "device" ? `id=${device.xcodebuildId ?? device.id}` : `platform=iOS Simulator,id=${device.id}`;
13227
13275
  return {
13228
13276
  configuration,
13229
13277
  derivedDataPath,
13230
- appPath: path40.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
13278
+ appPath: path41.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
13231
13279
  xcodebuildArgs: [
13232
13280
  "-project",
13233
13281
  "App.xcodeproj",
@@ -13411,11 +13459,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13411
13459
  const {
13412
13460
  name,
13413
13461
  basePath: _basePath,
13462
+ indexPath: _indexPath,
13414
13463
  version: _version,
13415
13464
  buildNum: _buildNum,
13416
13465
  assets: _assets,
13417
13466
  permissions: _permissions,
13418
- links: _links,
13467
+ deepLinks: _deepLinks,
13419
13468
  files: _files,
13420
13469
  appId,
13421
13470
  appName,
@@ -13437,7 +13486,7 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13437
13486
  ...capacitorConfig,
13438
13487
  appId,
13439
13488
  appName,
13440
- webDir: path40.posix.join(".akan", "mobile", name, "www"),
13489
+ webDir: path41.posix.join(".akan", "mobile", name, "www"),
13441
13490
  plugins: {
13442
13491
  CapacitorCookies: { enabled: true },
13443
13492
  ...pluginsConfig,
@@ -13494,10 +13543,10 @@ class CapacitorApp {
13494
13543
  constructor(app, target) {
13495
13544
  this.app = app;
13496
13545
  this.target = target;
13497
- this.targetRootPath = path40.posix.join(".akan", "mobile", this.target.name);
13498
- this.targetRoot = path40.join(this.app.cwdPath, this.targetRootPath);
13499
- this.targetWebRoot = path40.join(this.targetRoot, "www");
13500
- this.targetAssetRoot = path40.join(this.targetRoot, "assets");
13546
+ this.targetRootPath = path41.posix.join(".akan", "mobile", this.target.name);
13547
+ this.targetRoot = path41.join(this.app.cwdPath, this.targetRootPath);
13548
+ this.targetWebRoot = path41.join(this.targetRoot, "www");
13549
+ this.targetAssetRoot = path41.join(this.targetRoot, "assets");
13501
13550
  this.project = new MobileProject(this.app.cwdPath, {
13502
13551
  android: { path: this.androidRootPath },
13503
13552
  ios: { path: this.iosProjectPath }
@@ -13512,9 +13561,9 @@ class CapacitorApp {
13512
13561
  await mkdir11(this.targetRoot, { recursive: true });
13513
13562
  if (regenerate) {
13514
13563
  if (!platform || platform === "ios")
13515
- await rm5(path40.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13564
+ await rm5(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13516
13565
  if (!platform || platform === "android")
13517
- await rm5(path40.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13566
+ await rm5(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13518
13567
  }
13519
13568
  const project = this.project;
13520
13569
  await this.project.load();
@@ -13537,7 +13586,7 @@ class CapacitorApp {
13537
13586
  await this.#prepareExternalFiles("ios");
13538
13587
  await this.#applyIosMetadata();
13539
13588
  await this.#applyPermissions();
13540
- await this.#applyLinks();
13589
+ await this.#applyDeepLinks("ios", { operation, env });
13541
13590
  await this.project.commit();
13542
13591
  await this.#generateAssets({ operation, env });
13543
13592
  this.app.verbose(`syncing iOS`);
@@ -13637,7 +13686,7 @@ ${textError instanceof Error ? textError.message : ""}`);
13637
13686
  const xcodebuildArgs = noAllowProvisioningUpdates ? command.xcodebuildArgs : [...command.xcodebuildArgs.slice(0, -1), "-allowProvisioningUpdates", ...command.xcodebuildArgs.slice(-1)];
13638
13687
  try {
13639
13688
  await this.#spawn("xcodebuild", xcodebuildArgs, {
13640
- cwd: path40.join(this.app.cwdPath, this.iosProjectPath),
13689
+ cwd: path41.join(this.app.cwdPath, this.iosProjectPath),
13641
13690
  env: mobileEnv
13642
13691
  });
13643
13692
  const devicectlId = runTarget.devicectlId ?? runTarget.id;
@@ -13662,7 +13711,7 @@ ${textError instanceof Error ? textError.message : ""}`);
13662
13711
  return isRecord2(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
13663
13712
  }
13664
13713
  async#getIosDevelopmentTeam() {
13665
- const pbxprojPath = path40.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
13714
+ const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
13666
13715
  if (!await Bun.file(pbxprojPath).exists())
13667
13716
  return;
13668
13717
  return (await Bun.file(pbxprojPath).text()).match(/DEVELOPMENT_TEAM = ([^;]+);/)?.[1]?.trim();
@@ -13680,15 +13729,16 @@ ${error.message}`;
13680
13729
  await this.#prepareExternalFiles("android");
13681
13730
  await this.#applyAndroidMetadata();
13682
13731
  await this.#applyPermissions();
13683
- await this.#applyLinks();
13732
+ await this.#applyDeepLinks("android", { operation, env });
13684
13733
  await this.project.commit();
13685
13734
  await this.#generateAssets({ operation, env });
13686
13735
  await this.#ensureAndroidAssetsDir();
13687
13736
  await this.#ensureAndroidDebugKeystore();
13688
13737
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
13738
+ await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
13689
13739
  }
13690
13740
  async#updateAndroidBuildTypes() {
13691
- const appGradle = await FileEditor.create(path40.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
13741
+ const appGradle = await FileEditor.create(path41.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
13692
13742
  const buildTypesBlock = `
13693
13743
  debug {
13694
13744
  applicationIdSuffix ".debug"
@@ -13732,7 +13782,7 @@ ${error.message}`;
13732
13782
  const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
13733
13783
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
13734
13784
  stdio: "inherit",
13735
- cwd: path40.join(this.app.cwdPath, this.androidRootPath),
13785
+ cwd: path41.join(this.app.cwdPath, this.androidRootPath),
13736
13786
  env: await this.#commandEnv("release", env)
13737
13787
  });
13738
13788
  }
@@ -13740,10 +13790,10 @@ ${error.message}`;
13740
13790
  await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
13741
13791
  }
13742
13792
  async#ensureAndroidAssetsDir() {
13743
- await mkdir11(path40.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
13793
+ await mkdir11(path41.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
13744
13794
  }
13745
13795
  async#ensureAndroidDebugKeystore() {
13746
- const keystorePath = path40.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
13796
+ const keystorePath = path41.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
13747
13797
  if (await Bun.file(keystorePath).exists())
13748
13798
  return;
13749
13799
  await this.#spawn("keytool", [
@@ -13782,7 +13832,7 @@ ${error.message}`;
13782
13832
  await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
13783
13833
  }
13784
13834
  async#assertAndroidReleaseSigningConfig() {
13785
- const gradlePropertiesPath = path40.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
13835
+ const gradlePropertiesPath = path41.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
13786
13836
  const gradleProperties = await Bun.file(gradlePropertiesPath).exists() ? await Bun.file(gradlePropertiesPath).text() : "";
13787
13837
  const missingKeys = getMissingAndroidReleaseSigningKeys({ gradleProperties });
13788
13838
  if (missingKeys.length > 0)
@@ -13809,16 +13859,20 @@ ${error.message}`;
13809
13859
  await this.#prepareAndroid({ operation: "release", env: "main" });
13810
13860
  }
13811
13861
  async prepareWww() {
13812
- const htmlSource = path40.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13862
+ const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13813
13863
  if (!await Bun.file(htmlSource).exists())
13814
13864
  throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
13815
13865
  await rm5(this.targetWebRoot, { recursive: true, force: true });
13816
13866
  await mkdir11(this.targetWebRoot, { recursive: true });
13817
- await Bun.write(path40.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13867
+ await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13818
13868
  }
13819
13869
  #injectMobileTargetMeta(html) {
13820
13870
  const basePath2 = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13821
- const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({ name: this.target.name, basePath: basePath2 })};</script>`;
13871
+ const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
13872
+ name: this.target.name,
13873
+ basePath: basePath2,
13874
+ indexPath: this.target.indexPath
13875
+ })};</script>`;
13822
13876
  if (html.includes("window.__AKAN_MOBILE_TARGET__"))
13823
13877
  return html;
13824
13878
  return html.replace(/<\/head\s*>/i, `${script}
@@ -13834,7 +13888,7 @@ ${error.message}`;
13834
13888
  });
13835
13889
  const content = `${JSON.stringify(config, null, 2)}
13836
13890
  `;
13837
- await Bun.write(path40.join(this.targetRoot, "capacitor.config.json"), content);
13891
+ await Bun.write(path41.join(this.targetRoot, "capacitor.config.json"), content);
13838
13892
  return content;
13839
13893
  }
13840
13894
  async#prepareTargetAssets() {
@@ -13842,11 +13896,11 @@ ${error.message}`;
13842
13896
  return;
13843
13897
  await mkdir11(this.targetAssetRoot, { recursive: true });
13844
13898
  if (this.target.assets.icon)
13845
- await cp2(path40.join(this.app.cwdPath, this.target.assets.icon), path40.join(this.targetAssetRoot, "icon.png"), {
13899
+ await cp2(path41.join(this.app.cwdPath, this.target.assets.icon), path41.join(this.targetAssetRoot, "icon.png"), {
13846
13900
  force: true
13847
13901
  });
13848
13902
  if (this.target.assets.splash)
13849
- await cp2(path40.join(this.app.cwdPath, this.target.assets.splash), path40.join(this.targetAssetRoot, "splash.png"), {
13903
+ await cp2(path41.join(this.app.cwdPath, this.target.assets.splash), path41.join(this.targetAssetRoot, "splash.png"), {
13850
13904
  force: true
13851
13905
  });
13852
13906
  }
@@ -13854,11 +13908,11 @@ ${error.message}`;
13854
13908
  const files = this.target.files?.[platform];
13855
13909
  if (!files)
13856
13910
  return;
13857
- const platformRoot = path40.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
13911
+ const platformRoot = path41.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
13858
13912
  await Promise.all(Object.entries(files).map(async ([to, from]) => {
13859
- const targetPath = path40.join(platformRoot, to);
13860
- await mkdir11(path40.dirname(targetPath), { recursive: true });
13861
- await cp2(path40.join(this.app.cwdPath, from), targetPath, { force: true });
13913
+ const targetPath = path41.join(platformRoot, to);
13914
+ await mkdir11(path41.dirname(targetPath), { recursive: true });
13915
+ await cp2(path41.join(this.app.cwdPath, from), targetPath, { force: true });
13862
13916
  }));
13863
13917
  }
13864
13918
  async#generateAssets({ operation, env }) {
@@ -13868,7 +13922,7 @@ ${error.message}`;
13868
13922
  "@capacitor/assets",
13869
13923
  "generate",
13870
13924
  "--assetPath",
13871
- path40.posix.join(this.targetRootPath, "assets"),
13925
+ path41.posix.join(this.targetRootPath, "assets"),
13872
13926
  "--iosProject",
13873
13927
  this.iosProjectPath,
13874
13928
  "--androidProject",
@@ -13901,25 +13955,48 @@ ${error.message}`;
13901
13955
  await this.addPush();
13902
13956
  }
13903
13957
  }
13904
- async#applyLinks() {
13905
- const links = this.target.links;
13906
- if (!links)
13958
+ async#applyDeepLinks(platform, { operation, env }) {
13959
+ const deepLinks = this.target.deepLinks;
13960
+ if (!deepLinks)
13907
13961
  return;
13908
- const schemes = links.schemes ?? [];
13909
- if (schemes.length > 0) {
13910
- await this.#setPermissionInIos({
13911
- appTransportSecurity: ""
13912
- });
13962
+ const schemes = deepLinks.schemes ?? [];
13963
+ const domains = deepLinks.domains ?? [];
13964
+ if (domains.length > 0)
13965
+ this.#assertDeepLinkVerificationConfig(platform, { operation, env });
13966
+ if (platform === "ios") {
13967
+ if (schemes.length > 0) {
13968
+ await this.#setPermissionInIos({
13969
+ appTransportSecurity: ""
13970
+ });
13971
+ await this.#setUrlSchemesInIos(schemes);
13972
+ }
13973
+ if (domains.length > 0)
13974
+ await this.#setAssociatedDomainsInIos(domains);
13975
+ return;
13976
+ }
13977
+ if (platform === "android") {
13913
13978
  for (const scheme of schemes) {
13914
13979
  this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="${scheme}" /></intent-filter>`);
13915
13980
  }
13981
+ for (const domain of domains) {
13982
+ const pathPrefix = resolveMobilePath(this.target, "/");
13983
+ this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" /></intent-filter>`);
13984
+ }
13985
+ await this.#setDeepLinksInAndroid(schemes, domains);
13916
13986
  }
13917
- for (const domain of links.associatedDomains ?? []) {
13918
- this.app.logger.info(`Configure iOS associated domain manually if needed: ${domain}`);
13987
+ }
13988
+ #assertDeepLinkVerificationConfig(platform, { operation, env }) {
13989
+ const deepLinks = this.target.deepLinks;
13990
+ if (!deepLinks?.domains?.length)
13991
+ return;
13992
+ if (platform === "ios" && !deepLinks.ios?.teamId) {
13993
+ throw new Error(`Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.ios.teamId in apps/${this.app.name}/akan.config.ts`);
13919
13994
  }
13920
- for (const host of links.androidHosts ?? []) {
13921
- const pathPrefix = resolveMobilePath(this.target, "/");
13922
- this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${host}" android:pathPrefix="${pathPrefix}" /></intent-filter>`);
13995
+ if (platform === "android" && !deepLinks.android?.sha256CertFingerprints?.length) {
13996
+ const message = `Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.android.sha256CertFingerprints in apps/${this.app.name}/akan.config.ts`;
13997
+ if (operation === "release" || env === "main")
13998
+ throw new Error(message);
13999
+ this.app.logger.warn(message);
13923
14000
  }
13924
14001
  }
13925
14002
  async#commandEnv(operation, env) {
@@ -13940,6 +14017,8 @@ ${error.message}`;
13940
14017
  const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
13941
14018
  if (basePath2)
13942
14019
  params.set("akanMobileBasePath", basePath2);
14020
+ if (this.target.indexPath)
14021
+ params.set("akanMobileIndexPath", this.target.indexPath);
13943
14022
  return `http://${ip}:${port}/${pathname}?${params}`;
13944
14023
  }
13945
14024
  async#clearRootCapacitorConfigs() {
@@ -13999,6 +14078,112 @@ ${error.message}`;
13999
14078
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
14000
14079
  ]);
14001
14080
  }
14081
+ async#setUrlSchemesInIos(schemes) {
14082
+ const urlTypes = schemes.map((scheme) => ({
14083
+ CFBundleURLName: this.target.appId,
14084
+ CFBundleURLSchemes: [scheme]
14085
+ }));
14086
+ await Promise.all([
14087
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
14088
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14089
+ ]);
14090
+ }
14091
+ async#setAssociatedDomainsInIos(domains) {
14092
+ const entitlementsRelPath = "App/App.entitlements";
14093
+ const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14094
+ const values = domains.map((domain) => `applinks:${domain}`);
14095
+ const body = [
14096
+ '<?xml version="1.0" encoding="UTF-8"?>',
14097
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14098
+ '<plist version="1.0">',
14099
+ "<dict>",
14100
+ " <key>com.apple.developer.associated-domains</key>",
14101
+ " <array>",
14102
+ ...values.map((value) => ` <string>${value}</string>`),
14103
+ " </array>",
14104
+ "</dict>",
14105
+ "</plist>",
14106
+ ""
14107
+ ].join(`
14108
+ `);
14109
+ await writeFile2(entitlementsPath, body);
14110
+ await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
14111
+ }
14112
+ async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
14113
+ const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
14114
+ const lines = (await readFile2(pbxprojPath, "utf8")).split(`
14115
+ `);
14116
+ let changed = false;
14117
+ for (let index = 0;index < lines.length; index++) {
14118
+ const line = lines[index] ?? "";
14119
+ if (!line.includes(`PRODUCT_BUNDLE_IDENTIFIER = ${this.target.appId};`))
14120
+ continue;
14121
+ let start = index;
14122
+ while (start >= 0 && !lines[start]?.includes("buildSettings = {"))
14123
+ start--;
14124
+ let end = index;
14125
+ while (end < lines.length && !/^\s*\};\s*$/.test(lines[end] ?? ""))
14126
+ end++;
14127
+ const settings = lines.slice(start, end + 1);
14128
+ if (settings.some((setting) => setting.includes("CODE_SIGN_ENTITLEMENTS")))
14129
+ continue;
14130
+ const indent = line.match(/^\s*/)?.[0] ?? "";
14131
+ lines.splice(index, 0, `${indent}CODE_SIGN_ENTITLEMENTS = ${entitlementsRelPath};`);
14132
+ index++;
14133
+ changed = true;
14134
+ }
14135
+ if (changed)
14136
+ await writeFile2(pbxprojPath, lines.join(`
14137
+ `));
14138
+ }
14139
+ async#setUrlSchemesInAndroid(schemes) {
14140
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14141
+ let manifest = await readFile2(manifestPath, "utf8");
14142
+ let changed = false;
14143
+ for (const scheme of schemes) {
14144
+ if (manifest.includes(`android:scheme="${scheme}"`))
14145
+ continue;
14146
+ const filter = [
14147
+ " <intent-filter>",
14148
+ ' <action android:name="android.intent.action.VIEW" />',
14149
+ ' <category android:name="android.intent.category.DEFAULT" />',
14150
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14151
+ ` <data android:scheme="${scheme}" />`,
14152
+ " </intent-filter>"
14153
+ ].join(`
14154
+ `);
14155
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14156
+ ${filter}$1`);
14157
+ changed = true;
14158
+ }
14159
+ if (changed)
14160
+ await writeFile2(manifestPath, manifest);
14161
+ }
14162
+ async#setDeepLinksInAndroid(schemes, domains) {
14163
+ await this.#setUrlSchemesInAndroid(schemes);
14164
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14165
+ let manifest = await readFile2(manifestPath, "utf8");
14166
+ let changed = false;
14167
+ const pathPrefix = resolveMobilePath(this.target, "/");
14168
+ for (const domain of domains) {
14169
+ if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"'))
14170
+ continue;
14171
+ const filter = [
14172
+ ' <intent-filter android:autoVerify="true">',
14173
+ ' <action android:name="android.intent.action.VIEW" />',
14174
+ ' <category android:name="android.intent.category.DEFAULT" />',
14175
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14176
+ ` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
14177
+ " </intent-filter>"
14178
+ ].join(`
14179
+ `);
14180
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14181
+ ${filter}$1`);
14182
+ changed = true;
14183
+ }
14184
+ if (changed)
14185
+ await writeFile2(manifestPath, manifest);
14186
+ }
14002
14187
  #setFeaturesInAndroid(features) {
14003
14188
  for (const feature of features) {
14004
14189
  if (this.#hasFeatureInAndroid(feature)) {
@@ -14078,7 +14263,7 @@ var Pkg = createInternalArgToken("Pkg");
14078
14263
  var Module = createInternalArgToken("Module");
14079
14264
  var Workspace = createInternalArgToken("Workspace");
14080
14265
  // pkgs/@akanjs/devkit/commandDecorators/command.ts
14081
- import path41 from "path";
14266
+ import path42 from "path";
14082
14267
  import { confirm, input as input2, select as select3 } from "@inquirer/prompts";
14083
14268
  import { Logger as Logger10 } from "akanjs/common";
14084
14269
  import chalk6 from "chalk";
@@ -14582,7 +14767,7 @@ var runCommands = async (...commands) => {
14582
14767
  process.exit(1);
14583
14768
  });
14584
14769
  const __dirname2 = getDirname(import.meta.url);
14585
- const packageJsonCandidates = [`${path41.dirname(Bun.main)}/package.json`, `${__dirname2}/../package.json`];
14770
+ const packageJsonCandidates = [`${path42.dirname(Bun.main)}/package.json`, `${__dirname2}/../package.json`];
14586
14771
  let cliPackageJson = null;
14587
14772
  for (const packageJsonPath of packageJsonCandidates) {
14588
14773
  if (!await FileSys.fileExists(packageJsonPath))
@@ -14862,8 +15047,8 @@ var scanModuleSpecifiers = (source2, filePath, includeExports) => {
14862
15047
  return importSpecifiers;
14863
15048
  };
14864
15049
  var parseTsConfig = (tsConfigPath = "./tsconfig.json") => {
14865
- const configFile = ts10.readConfigFile(tsConfigPath, (path42) => {
14866
- return ts10.sys.readFile(path42);
15050
+ const configFile = ts10.readConfigFile(tsConfigPath, (path43) => {
15051
+ return ts10.sys.readFile(path43);
14867
15052
  });
14868
15053
  return ts10.parseJsonConfigFileContent(configFile.config, ts10.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
14869
15054
  };
@@ -15079,6 +15264,571 @@ ${document}
15079
15264
  `;
15080
15265
  }
15081
15266
  }
15267
+ // pkgs/@akanjs/devkit/qualityScanner.ts
15268
+ import { createHash } from "crypto";
15269
+ import { readdir as readdir3, readFile as readFile3, stat as stat4 } from "fs/promises";
15270
+ import path43 from "path";
15271
+ import ignore2 from "ignore";
15272
+ import ts11 from "typescript";
15273
+ var MAX_FILE_LINES = 2000;
15274
+ var PLACEHOLDER_EXPORT_NAMES = new Set([
15275
+ "aa",
15276
+ "dumb",
15277
+ "dumb2",
15278
+ "someBaseLogic",
15279
+ "someCommonLogic",
15280
+ "someFrontendLogic",
15281
+ "someBackendLogic"
15282
+ ]);
15283
+ var SUGGESTED_RULES = [
15284
+ "Keep generated scanSync index files out of hand-written changes; generated indexes should only contain one-depth export statements.",
15285
+ "Generated Akan index files should not contain placeholder exports such as aa, dumb, dumb2, or some*Logic.",
15286
+ "Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
15287
+ "Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
15288
+ "Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
15289
+ "Keep app root folders small and conventional: application code belongs under common, env, lib, page, private, public, script, srvkit, ui, or webkit.",
15290
+ "Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
15291
+ "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
15292
+ "Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
15293
+ "Move shared app utilities to apps/*/common instead of creating apps/*/base.",
15294
+ "Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
15295
+ ];
15296
+ var APP_ROOT_FILES = new Set([
15297
+ "akan.app.json",
15298
+ "akan.config.ts",
15299
+ "capacitor.config.ts",
15300
+ "client.ts",
15301
+ "main.ts",
15302
+ "package.json",
15303
+ "server.ts",
15304
+ "tsconfig.json"
15305
+ ]);
15306
+ var LIB_ROOT_FILES = new Set([
15307
+ "cnst.ts",
15308
+ "db.ts",
15309
+ "dict.ts",
15310
+ "option.ts",
15311
+ "sig.ts",
15312
+ "srv.ts",
15313
+ "st.ts",
15314
+ "useClient.ts",
15315
+ "useServer.ts"
15316
+ ]);
15317
+ var CONVENTION_SUFFIXES = [
15318
+ ".constant.ts",
15319
+ ".dictionary.ts",
15320
+ ".document.ts",
15321
+ ".service.ts",
15322
+ ".signal.ts",
15323
+ ".store.ts"
15324
+ ];
15325
+
15326
+ class AkanQualityScanner {
15327
+ async scan(workspaceRoot) {
15328
+ const targetFiles = await this.#collectTargetFiles(workspaceRoot);
15329
+ const sourceFiles = await Promise.all(targetFiles.map((file) => this.#readSourceFile(workspaceRoot, file)));
15330
+ const warnings = [
15331
+ ...this.#scanGlobalQuality(sourceFiles),
15332
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
15333
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
15334
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
15335
+ ];
15336
+ return {
15337
+ workspaceRoot,
15338
+ scannedFiles: sourceFiles.length,
15339
+ warnings: warnings.sort(compareWarnings),
15340
+ suggestedRules: SUGGESTED_RULES
15341
+ };
15342
+ }
15343
+ async#collectTargetFiles(workspaceRoot) {
15344
+ const ignoreFilter = ignore2().add(await this.#readGitIgnore(workspaceRoot));
15345
+ const files = [];
15346
+ for (const targetRoot of ["apps", "libs"]) {
15347
+ const absoluteTargetRoot = path43.join(workspaceRoot, targetRoot);
15348
+ if (!await isDirectory(absoluteTargetRoot))
15349
+ continue;
15350
+ await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
15351
+ }
15352
+ return files.sort();
15353
+ }
15354
+ async#readGitIgnore(workspaceRoot) {
15355
+ const gitIgnorePath = path43.join(workspaceRoot, ".gitignore");
15356
+ if (!await Bun.file(gitIgnorePath).exists())
15357
+ return [];
15358
+ return (await readFile3(gitIgnorePath, "utf8")).split(/\r?\n/);
15359
+ }
15360
+ async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
15361
+ const entries = await readdir3(currentPath, { withFileTypes: true });
15362
+ for (const entry of entries) {
15363
+ const absolutePath = path43.join(currentPath, entry.name);
15364
+ const relativePath = toPosix(path43.relative(workspaceRoot, absolutePath));
15365
+ if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
15366
+ continue;
15367
+ if (entry.isDirectory()) {
15368
+ await this.#walkTargetFiles(workspaceRoot, absolutePath, ignoreFilter, files);
15369
+ continue;
15370
+ }
15371
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
15372
+ files.push(relativePath);
15373
+ }
15374
+ }
15375
+ }
15376
+ async#readSourceFile(workspaceRoot, file) {
15377
+ const absolutePath = path43.join(workspaceRoot, file);
15378
+ const content = await readFile3(absolutePath, "utf8");
15379
+ return {
15380
+ file,
15381
+ absolutePath,
15382
+ content,
15383
+ sourceFile: ts11.createSourceFile(file, content, ts11.ScriptTarget.Latest, true, getScriptKind(file))
15384
+ };
15385
+ }
15386
+ #scanGlobalQuality(sourceFiles) {
15387
+ const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15388
+ const warnings = [];
15389
+ for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15390
+ if (declarations.length < 2)
15391
+ continue;
15392
+ warnings.push({
15393
+ rule: "akan.global.duplicate-exported-function-name",
15394
+ scope: "global",
15395
+ severity: "warning",
15396
+ message: `Exported function or class name "${name}" is declared in ${declarations.length} files.`,
15397
+ locations: declarations.map(({ file, line }) => ({ file, line }))
15398
+ });
15399
+ }
15400
+ const declarationsWithBody = exportedFunctionLikes.filter((declaration) => declaration.bodyFingerprint);
15401
+ for (const [fingerprint, declarations] of groupBy(declarationsWithBody, (declaration) => declaration.bodyFingerprint ?? "")) {
15402
+ const uniqueNames = new Set(declarations.map((declaration) => declaration.name));
15403
+ if (declarations.length < 2 || uniqueNames.size < 2 || fingerprint === "")
15404
+ continue;
15405
+ warnings.push({
15406
+ rule: "akan.global.duplicate-exported-function-body",
15407
+ scope: "global",
15408
+ severity: "warning",
15409
+ message: `Exported functions/classes share the same implementation body: ${declarations.map((declaration) => declaration.name).join(", ")}.`,
15410
+ locations: declarations.map(({ file, line }) => ({ file, line }))
15411
+ });
15412
+ }
15413
+ return warnings;
15414
+ }
15415
+ #scanSingleFileQuality(sourceFile2) {
15416
+ const warnings = [];
15417
+ const lineCount = sourceFile2.content.split(/\r?\n/).length;
15418
+ const recommendedLineLimit = getRecommendedLineLimit(sourceFile2.file);
15419
+ if (recommendedLineLimit && lineCount > recommendedLineLimit) {
15420
+ warnings.push({
15421
+ rule: "akan.file.recommended-max-lines",
15422
+ scope: "file",
15423
+ severity: "warning",
15424
+ file: sourceFile2.file,
15425
+ message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
15426
+ });
15427
+ }
15428
+ if (lineCount > MAX_FILE_LINES) {
15429
+ warnings.push({
15430
+ rule: "akan.file.max-lines",
15431
+ scope: "file",
15432
+ severity: "warning",
15433
+ file: sourceFile2.file,
15434
+ message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
15435
+ });
15436
+ }
15437
+ warnings.push(...getPlaceholderExportWarnings(sourceFile2));
15438
+ warnings.push(...getDictionaryTextWarnings(sourceFile2));
15439
+ warnings.push(...getGlobalMutationWarnings(sourceFile2));
15440
+ const exportedClassNames = getExportedClassNames(sourceFile2.sourceFile);
15441
+ if (exportedClassNames.length === 0)
15442
+ return warnings;
15443
+ const allowedInterfaceNames = new Set(exportedClassNames.map((name) => `${name}Options`));
15444
+ for (const declaration of getTopLevelDeclarations(sourceFile2)) {
15445
+ if (declaration.kind === "class" && exportedClassNames.includes(declaration.name))
15446
+ continue;
15447
+ if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
15448
+ continue;
15449
+ warnings.push({
15450
+ rule: "akan.file.class-export-global-declaration",
15451
+ scope: "file",
15452
+ severity: "warning",
15453
+ file: sourceFile2.file,
15454
+ line: declaration.line,
15455
+ message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
15456
+ });
15457
+ }
15458
+ return warnings;
15459
+ }
15460
+ #scanConventionQuality(sourceFile2) {
15461
+ const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
15462
+ if (!suffix)
15463
+ return [];
15464
+ const modelName = toPascalCase(path43.basename(sourceFile2.file, suffix));
15465
+ const warnings = [];
15466
+ for (const declaration of getTopLevelDeclarations(sourceFile2)) {
15467
+ if (isAllowedConventionDeclaration(suffix, modelName, declaration))
15468
+ continue;
15469
+ warnings.push({
15470
+ rule: `akan.convention${suffix.replace(".ts", "")}`,
15471
+ scope: "convention",
15472
+ severity: "warning",
15473
+ file: sourceFile2.file,
15474
+ line: declaration.line,
15475
+ message: `${path43.basename(sourceFile2.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
15476
+ });
15477
+ }
15478
+ return warnings;
15479
+ }
15480
+ #scanLayoutQuality(sourceFile2) {
15481
+ const segments = sourceFile2.file.split("/");
15482
+ const warnings = [];
15483
+ if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
15484
+ warnings.push({
15485
+ rule: "akan.layout.app-root-file",
15486
+ scope: "layout",
15487
+ severity: "warning",
15488
+ file: sourceFile2.file,
15489
+ message: `Unexpected app root file "${segments[2]}". Keep application code in conventional app folders.`
15490
+ });
15491
+ }
15492
+ const libRootFile = getLibRootFile(sourceFile2.file);
15493
+ if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
15494
+ warnings.push({
15495
+ rule: "akan.layout.lib-root-file",
15496
+ scope: "layout",
15497
+ severity: "warning",
15498
+ file: sourceFile2.file,
15499
+ message: `Unexpected lib root file "${libRootFile}". Keep direct lib root files limited to generated support facets.`
15500
+ });
15501
+ }
15502
+ const moduleUiWarning = getModuleUiWarning(sourceFile2.file);
15503
+ if (moduleUiWarning)
15504
+ warnings.push(moduleUiWarning);
15505
+ return warnings;
15506
+ }
15507
+ }
15508
+ function formatQualityScanResult(result) {
15509
+ const sections = [
15510
+ "Akan Code Quality Scan",
15511
+ `workspace: ${result.workspaceRoot}`,
15512
+ `scanned files: ${result.scannedFiles}`,
15513
+ `warnings: ${result.warnings.length}`,
15514
+ "",
15515
+ "Warnings:",
15516
+ "",
15517
+ ...formatQualityWarnings(result.warnings),
15518
+ "",
15519
+ "Suggested quality rules:",
15520
+ "",
15521
+ ...result.suggestedRules.map((rule) => ` - ${rule}`)
15522
+ ];
15523
+ return sections.join(`
15524
+ `);
15525
+ }
15526
+ function formatQualityWarnings(warnings) {
15527
+ if (warnings.length === 0)
15528
+ return ["No warnings found."];
15529
+ return warnings.flatMap((warning) => {
15530
+ const location = formatQualityLocation(warning.file, warning.line);
15531
+ const lines = [`${location} - warning ${warning.rule}: ${warning.message}`];
15532
+ if (warning.locations?.length) {
15533
+ lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15534
+ }
15535
+ return lines;
15536
+ });
15537
+ }
15538
+ function formatQualityLocation(file, line) {
15539
+ return `${file ?? "<global>"}:${line ?? 1}:1`;
15540
+ }
15541
+ function getExportedFunctionLikes(sourceFile2) {
15542
+ const declarations = [];
15543
+ for (const statement of sourceFile2.sourceFile.statements) {
15544
+ if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15545
+ declarations.push({
15546
+ name: statement.name.text,
15547
+ kind: "function",
15548
+ file: sourceFile2.file,
15549
+ line: getLine(sourceFile2.sourceFile, statement),
15550
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
15551
+ });
15552
+ }
15553
+ if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
15554
+ declarations.push({
15555
+ name: statement.name.text,
15556
+ kind: "class",
15557
+ file: sourceFile2.file,
15558
+ line: getLine(sourceFile2.sourceFile, statement),
15559
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
15560
+ });
15561
+ }
15562
+ if (ts11.isVariableStatement(statement) && isExported(statement)) {
15563
+ for (const declaration of statement.declarationList.declarations) {
15564
+ if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
15565
+ continue;
15566
+ declarations.push({
15567
+ name: declaration.name.text,
15568
+ kind: "function-variable",
15569
+ file: sourceFile2.file,
15570
+ line: getLine(sourceFile2.sourceFile, declaration),
15571
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
15572
+ });
15573
+ }
15574
+ }
15575
+ }
15576
+ return declarations;
15577
+ }
15578
+ function getExportedClassNames(sourceFile2) {
15579
+ return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15580
+ }
15581
+ function getPlaceholderExportWarnings(sourceFile2) {
15582
+ if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
15583
+ return [];
15584
+ return getTopLevelDeclarations(sourceFile2).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
15585
+ rule: "akan.file.placeholder-export",
15586
+ scope: "file",
15587
+ severity: "warning",
15588
+ file: sourceFile2.file,
15589
+ line: declaration.line,
15590
+ message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
15591
+ }));
15592
+ }
15593
+ function getDictionaryTextWarnings(sourceFile2) {
15594
+ if (!sourceFile2.file.endsWith(".dictionary.ts"))
15595
+ 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
+ ];
15601
+ return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15602
+ rule: "akan.file.dictionary-stale-text",
15603
+ scope: "file",
15604
+ severity: "warning",
15605
+ file: sourceFile2.file,
15606
+ line,
15607
+ message: `Dictionary text appears to contain ${label}.`
15608
+ })));
15609
+ }
15610
+ function getGlobalMutationWarnings(sourceFile2) {
15611
+ const warnings = [];
15612
+ for (const statement of sourceFile2.sourceFile.statements) {
15613
+ if (ts11.isModuleDeclaration(statement) && statement.name.getText(sourceFile2.sourceFile) === "global") {
15614
+ warnings.push({
15615
+ rule: "akan.file.global-declaration",
15616
+ scope: "file",
15617
+ severity: "warning",
15618
+ file: sourceFile2.file,
15619
+ line: getLine(sourceFile2.sourceFile, statement),
15620
+ message: "Global declarations require an explicit low-level integration allowlist."
15621
+ });
15622
+ }
15623
+ if (ts11.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
15624
+ warnings.push({
15625
+ rule: "akan.file.window-augmentation",
15626
+ scope: "file",
15627
+ severity: "warning",
15628
+ file: sourceFile2.file,
15629
+ line: getLine(sourceFile2.sourceFile, statement),
15630
+ message: "Window augmentation should be isolated to approved browser integration files."
15631
+ });
15632
+ }
15633
+ if (ts11.isExpressionStatement(statement) && statement.expression.getText(sourceFile2.sourceFile).includes(".prototype.")) {
15634
+ warnings.push({
15635
+ rule: "akan.file.prototype-mutation",
15636
+ scope: "file",
15637
+ severity: "warning",
15638
+ file: sourceFile2.file,
15639
+ line: getLine(sourceFile2.sourceFile, statement),
15640
+ message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
15641
+ });
15642
+ }
15643
+ }
15644
+ return warnings;
15645
+ }
15646
+ function getTopLevelDeclarations(sourceFile2) {
15647
+ return sourceFile2.sourceFile.statements.flatMap((statement) => getTopLevelDeclaration(sourceFile2.sourceFile, statement));
15648
+ }
15649
+ function getTopLevelDeclaration(sourceFile2, statement) {
15650
+ const line = getLine(sourceFile2, statement);
15651
+ if (ts11.isClassDeclaration(statement) && statement.name) {
15652
+ return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
15653
+ }
15654
+ if (ts11.isFunctionDeclaration(statement) && statement.name) {
15655
+ return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
15656
+ }
15657
+ if (ts11.isInterfaceDeclaration(statement)) {
15658
+ return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
15659
+ }
15660
+ if (ts11.isTypeAliasDeclaration(statement)) {
15661
+ return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
15662
+ }
15663
+ if (ts11.isEnumDeclaration(statement)) {
15664
+ return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
15665
+ }
15666
+ if (ts11.isVariableStatement(statement)) {
15667
+ return statement.declarationList.declarations.filter((declaration) => ts11.isIdentifier(declaration.name)).map((declaration) => ({
15668
+ name: declaration.name.text,
15669
+ kind: "variable",
15670
+ line: getLine(sourceFile2, declaration),
15671
+ exported: isExported(statement),
15672
+ node: statement
15673
+ }));
15674
+ }
15675
+ if (ts11.isExportDeclaration(statement)) {
15676
+ return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
15677
+ }
15678
+ return [];
15679
+ }
15680
+ function isAllowedConventionDeclaration(suffix, modelName, declaration) {
15681
+ if (suffix === ".dictionary.ts")
15682
+ return isExportedConst(declaration) && declaration.name === "dictionary";
15683
+ if (suffix === ".constant.ts")
15684
+ return isAllowedConstantDeclaration(modelName, declaration);
15685
+ if (suffix === ".document.ts")
15686
+ return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
15687
+ if (suffix === ".service.ts")
15688
+ return declaration.kind === "class" && declaration.name === `${modelName}Service`;
15689
+ if (suffix === ".signal.ts")
15690
+ return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
15691
+ if (suffix === ".store.ts")
15692
+ return declaration.kind === "class" && declaration.name === `${modelName}Store`;
15693
+ return false;
15694
+ }
15695
+ function isAllowedConstantDeclaration(modelName, declaration) {
15696
+ if (declaration.kind !== "class")
15697
+ return false;
15698
+ if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
15699
+ return true;
15700
+ if (!ts11.isClassDeclaration(declaration.node))
15701
+ return false;
15702
+ const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
15703
+ const expression = heritageClause?.types[0]?.expression;
15704
+ return !!expression && expression.getText().startsWith("enumOf(");
15705
+ }
15706
+ function getConventionDescription(suffix, modelName) {
15707
+ if (suffix === ".dictionary.ts")
15708
+ return "export const dictionary";
15709
+ if (suffix === ".constant.ts")
15710
+ return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
15711
+ if (suffix === ".document.ts")
15712
+ return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
15713
+ if (suffix === ".service.ts")
15714
+ return `${modelName}Service`;
15715
+ if (suffix === ".signal.ts")
15716
+ return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
15717
+ return `${modelName}Store`;
15718
+ }
15719
+ function getLibRootFile(file) {
15720
+ const segments = file.split("/");
15721
+ if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
15722
+ return segments[3];
15723
+ if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
15724
+ return segments[4];
15725
+ return null;
15726
+ }
15727
+ function getModuleUiWarning(file) {
15728
+ if (!file.endsWith(".tsx"))
15729
+ return null;
15730
+ const moduleInfo = getModuleInfo(file);
15731
+ if (!moduleInfo)
15732
+ return null;
15733
+ const { moduleName, fileName, kind } = moduleInfo;
15734
+ const pascalName = toPascalCase(moduleName.replace(/^_+/, ""));
15735
+ const allowedSuffixes = kind === "database" ? ["Template", "Unit", "Util", "View", "Zone"] : kind === "service" ? ["Util", "Zone"] : ["Template", "Unit"];
15736
+ const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
15737
+ if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
15738
+ return null;
15739
+ return {
15740
+ rule: "akan.layout.module-ui-file",
15741
+ scope: "layout",
15742
+ severity: "warning",
15743
+ file,
15744
+ message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
15745
+ };
15746
+ }
15747
+ function getModuleInfo(file) {
15748
+ const segments = file.split("/");
15749
+ const libIndex = segments.indexOf("lib");
15750
+ if (libIndex < 0)
15751
+ return null;
15752
+ const moduleName = segments[libIndex + 1];
15753
+ if (moduleName === "__scalar") {
15754
+ if (segments.length !== libIndex + 4)
15755
+ return null;
15756
+ return { moduleName: segments[libIndex + 2], fileName: segments[libIndex + 3], kind: "scalar" };
15757
+ }
15758
+ if (segments.length !== libIndex + 3)
15759
+ return null;
15760
+ const fileName = segments[libIndex + 2];
15761
+ if (moduleName.startsWith("__")) {
15762
+ const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
15763
+ return { moduleName: scalarName, fileName, kind: "scalar" };
15764
+ }
15765
+ if (moduleName.startsWith("_"))
15766
+ return { moduleName, fileName, kind: "service" };
15767
+ return { moduleName, fileName, kind: "database" };
15768
+ }
15769
+ function isExportedConst(declaration) {
15770
+ return declaration.exported && ts11.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts11.NodeFlags.Const) !== 0;
15771
+ }
15772
+ function isExported(node) {
15773
+ return !!ts11.getCombinedModifierFlags(node) && !!(ts11.getCombinedModifierFlags(node) & ts11.ModifierFlags.Export);
15774
+ }
15775
+ function isFunctionLikeInitializer(node) {
15776
+ return !!node && (ts11.isArrowFunction(node) || ts11.isFunctionExpression(node));
15777
+ }
15778
+ function getBodyFingerprint(sourceFile2, node) {
15779
+ if (!node)
15780
+ return;
15781
+ const normalizedBody = node.getText(sourceFile2).replace(/\s+/g, " ").trim();
15782
+ if (normalizedBody.length < 80)
15783
+ return;
15784
+ return createHash("sha256").update(normalizedBody).digest("hex");
15785
+ }
15786
+ function shouldSkipPath(relativePath, isDirectory, ignoreFilter) {
15787
+ const ignorePath = isDirectory ? `${relativePath}/` : relativePath;
15788
+ return relativePath === ".git" || relativePath.includes("/.git/") || relativePath.includes("/node_modules/") || ignoreFilter.ignores(relativePath) || ignoreFilter.ignores(ignorePath);
15789
+ }
15790
+ async function isDirectory(absolutePath) {
15791
+ try {
15792
+ return (await stat4(absolutePath)).isDirectory();
15793
+ } catch {
15794
+ return false;
15795
+ }
15796
+ }
15797
+ function getScriptKind(file) {
15798
+ return file.endsWith(".tsx") ? ts11.ScriptKind.TSX : ts11.ScriptKind.TS;
15799
+ }
15800
+ function getLine(sourceFile2, node) {
15801
+ return sourceFile2.getLineAndCharacterOfPosition(node.getStart(sourceFile2)).line + 1;
15802
+ }
15803
+ function getRecommendedLineLimit(file) {
15804
+ if (file.endsWith(".service.ts"))
15805
+ return 500;
15806
+ if (file.endsWith(".Template.tsx") || file.endsWith(".Zone.tsx"))
15807
+ return 800;
15808
+ if (file.endsWith(".Util.tsx"))
15809
+ return 1000;
15810
+ return null;
15811
+ }
15812
+ function findPatternLines(content, pattern) {
15813
+ return content.split(/\r?\n/).flatMap((line, index) => pattern.test(line) ? [index + 1] : []);
15814
+ }
15815
+ function toPascalCase(value) {
15816
+ return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
15817
+ }
15818
+ function toPosix(value) {
15819
+ return value.split(path43.sep).join("/");
15820
+ }
15821
+ function groupBy(items, getKey) {
15822
+ const grouped = new Map;
15823
+ for (const item of items) {
15824
+ const key = getKey(item);
15825
+ grouped.set(key, [...grouped.get(key) ?? [], item]);
15826
+ }
15827
+ return grouped;
15828
+ }
15829
+ function compareWarnings(a, b) {
15830
+ return a.scope.localeCompare(b.scope) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule);
15831
+ }
15082
15832
  // pkgs/@akanjs/devkit/selectModel.ts
15083
15833
  import { select as select5 } from "@inquirer/prompts";
15084
15834
  // pkgs/@akanjs/devkit/streamAi.ts
@@ -15196,10 +15946,10 @@ class IncrementalBuilder {
15196
15946
  }
15197
15947
  }
15198
15948
  batchTouchesPagesTree(appDir, batch) {
15199
- const absAppDir = path42.resolve(appDir);
15949
+ const absAppDir = path44.resolve(appDir);
15200
15950
  for (const f of batch.files) {
15201
- const abs = path42.resolve(f);
15202
- if (!abs.startsWith(`${absAppDir}${path42.sep}`) && abs !== absAppDir)
15951
+ const abs = path44.resolve(f);
15952
+ if (!abs.startsWith(`${absAppDir}${path44.sep}`) && abs !== absAppDir)
15203
15953
  continue;
15204
15954
  if (/\.(tsx|ts|jsx|js)$/.test(abs))
15205
15955
  return true;
@@ -15207,15 +15957,15 @@ class IncrementalBuilder {
15207
15957
  return false;
15208
15958
  }
15209
15959
  async batchMayChangePageKeys(appDir, batch) {
15210
- const absAppDir = path42.resolve(appDir);
15211
- const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path42.normalize(key)));
15960
+ const absAppDir = path44.resolve(appDir);
15961
+ const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path44.normalize(key)));
15212
15962
  for (const f of batch.files) {
15213
- const abs = path42.resolve(f);
15214
- if (!abs.startsWith(`${absAppDir}${path42.sep}`) && abs !== absAppDir)
15963
+ const abs = path44.resolve(f);
15964
+ if (!abs.startsWith(`${absAppDir}${path44.sep}`) && abs !== absAppDir)
15215
15965
  continue;
15216
15966
  if (!/\.(tsx|ts|jsx|js)$/.test(abs))
15217
15967
  continue;
15218
- const rel = path42.normalize(path42.relative(absAppDir, abs));
15968
+ const rel = path44.normalize(path44.relative(absAppDir, abs));
15219
15969
  if (!await Bun.file(abs).exists() || !pageKeys.has(rel))
15220
15970
  return true;
15221
15971
  }
@@ -15243,7 +15993,7 @@ class IncrementalBuilder {
15243
15993
  ${cssText}`).toString(36);
15244
15994
  const cssRelPath = `styles/${cssAssetName}-${cssHash}.css`;
15245
15995
  const cssUrl = `/_akan/styles/${cssAssetName}-${cssHash}.css`;
15246
- await Bun.write(path42.join(artifactDir, cssRelPath), cssText);
15996
+ await Bun.write(path44.join(artifactDir, cssRelPath), cssText);
15247
15997
  cssAssetEntries.push([basePath2, { cssUrl, cssRelPath }]);
15248
15998
  cssBase64ByUrl[cssUrl] = Buffer.from(new TextEncoder().encode(cssText)).toString("base64");
15249
15999
  })()
@@ -15311,10 +16061,10 @@ ${cssText}`).toString(36);
15311
16061
  if (changedFiles.length === 0)
15312
16062
  return false;
15313
16063
  return changedFiles.some((file) => {
15314
- const normalized = path42.resolve(file);
16064
+ const normalized = path44.resolve(file);
15315
16065
  if (/\.(woff2?|ttf|otf)$/i.test(normalized))
15316
16066
  return true;
15317
- return this.#optimizedFonts.files.some((fontFile) => path42.resolve(fontFile) === normalized);
16067
+ return this.#optimizedFonts.files.some((fontFile) => path44.resolve(fontFile) === normalized);
15318
16068
  });
15319
16069
  }
15320
16070
  async installWatcher() {