@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.
package/index.js CHANGED
@@ -742,6 +742,44 @@ var DEFAULT_AKAN_IMAGE_CONFIG = {
742
742
  fetchTimeoutMs: 7000,
743
743
  maxRemoteBytes: 25 * 1024 * 1024
744
744
  };
745
+ var normalizeIndexPath = (indexPath) => {
746
+ const normalized = indexPath?.trim();
747
+ if (!normalized)
748
+ return;
749
+ const path2 = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
750
+ return path2 === "/" ? "/" : path2;
751
+ };
752
+ var normalizeStringList = (values) => {
753
+ const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
754
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
755
+ };
756
+ var normalizeDeepLinkDomain = (domain) => {
757
+ const normalized = domain.trim();
758
+ if (!normalized)
759
+ return "";
760
+ try {
761
+ const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
762
+ return url.host.toLowerCase();
763
+ } catch {
764
+ return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
765
+ }
766
+ };
767
+ var normalizeDeepLinks = (deepLinks) => {
768
+ if (!deepLinks)
769
+ return;
770
+ const schemes = normalizeStringList(deepLinks.schemes);
771
+ const domains = normalizeStringList(deepLinks.domains?.map(normalizeDeepLinkDomain));
772
+ const teamId = deepLinks.ios?.teamId?.trim();
773
+ const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints);
774
+ if (!schemes && !domains && !teamId && !sha256CertFingerprints)
775
+ return;
776
+ return {
777
+ ...schemes ? { schemes } : {},
778
+ ...domains ? { domains } : {},
779
+ ...teamId ? { ios: { teamId } } : {},
780
+ ...sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}
781
+ };
782
+ };
745
783
 
746
784
  class AkanAppConfig {
747
785
  app;
@@ -755,6 +793,7 @@ class AkanAppConfig {
755
793
  i18n;
756
794
  publicEnv;
757
795
  mobile;
796
+ secrets;
758
797
  baseDevEnv;
759
798
  libs;
760
799
  domains = new Set;
@@ -781,11 +820,16 @@ class AkanAppConfig {
781
820
  process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
782
821
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
783
822
  this.publicEnv = config?.publicEnv ?? [];
823
+ this.secrets = config?.secrets ?? [];
784
824
  this.mobile = this.#resolveMobileConfig(config.mobile);
785
825
  this.docker = this.#makeDockerContent(config?.docker ?? {});
786
826
  }
787
827
  #resolveMobileConfig(mobile) {
788
- const { targets: rawTargets, ...rawMobile } = mobile ?? {};
828
+ const {
829
+ targets: rawTargets,
830
+ indexPath: _indexPath,
831
+ ...rawMobile
832
+ } = mobile ?? {};
789
833
  const appName = rawMobile.appName ?? this.app.name;
790
834
  const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
791
835
  const version = rawMobile.version ?? "0.0.1";
@@ -798,6 +842,8 @@ class AkanAppConfig {
798
842
  const target = rawTarget;
799
843
  const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
800
844
  const basePath2 = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
845
+ const indexPath = normalizeIndexPath(target.indexPath);
846
+ const deepLinks = normalizeDeepLinks(target.deepLinks);
801
847
  if (basePath2 && !this.basePaths.has(basePath2)) {
802
848
  throw new Error(`Mobile target '${name}' uses unknown basePath '${basePath2}' in apps/${this.app.name}/akan.config.ts`);
803
849
  }
@@ -806,6 +852,8 @@ class AkanAppConfig {
806
852
  ...target,
807
853
  name,
808
854
  basePath: basePath2,
855
+ indexPath,
856
+ deepLinks,
809
857
  appName: target.appName ?? appName,
810
858
  appId: target.appId ?? appId,
811
859
  version: target.version ?? version,
@@ -12114,7 +12162,14 @@ class SsrBaseArtifactBuilder {
12114
12162
  basePaths: [...akanConfig2.basePaths],
12115
12163
  branches: [...akanConfig2.branches],
12116
12164
  i18n: akanConfig2.i18n,
12117
- imageConfig: akanConfig2.images
12165
+ imageConfig: akanConfig2.images,
12166
+ deepLinkAssociations: Object.values(akanConfig2.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
12167
+ targetName: target.name,
12168
+ appId: target.appId,
12169
+ domains: target.deepLinks?.domains ?? [],
12170
+ iosTeamId: target.deepLinks?.ios?.teamId,
12171
+ androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
12172
+ }))
12118
12173
  };
12119
12174
  await Bun.write(path35.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
12120
12175
  `);
@@ -12502,6 +12557,7 @@ void run().catch((error) => {
12502
12557
  }
12503
12558
  // pkgs/@akanjs/devkit/applicationReleasePackager.ts
12504
12559
  import { cp, mkdir as mkdir9, rm as rm4 } from "fs/promises";
12560
+ import path38 from "path";
12505
12561
 
12506
12562
  // pkgs/@akanjs/devkit/uploadRelease.ts
12507
12563
  import { HttpClient as HttpClient2, Logger as Logger9 } from "akanjs/common";
@@ -12617,13 +12673,9 @@ class ApplicationReleasePackager {
12617
12673
  await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
12618
12674
  await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
12619
12675
  await rm4(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
12620
- await this.#app.workspace.spawn("tar", [
12621
- "-zcf",
12622
- `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-release.tar.gz`,
12623
- "-C",
12624
- buildRoot,
12625
- "./"
12626
- ]);
12676
+ const releaseRoot = this.#app.workspace.workspaceRoot;
12677
+ const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
12678
+ await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
12627
12679
  await this.#writeCsrZipIfPresent();
12628
12680
  return { platformVersion };
12629
12681
  }
@@ -12644,22 +12696,18 @@ class ApplicationReleasePackager {
12644
12696
  await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
12645
12697
  const libDeps = ["social", "shared", "platform", "util"];
12646
12698
  await Promise.all(libDeps.map((lib) => cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true })));
12647
- await Promise.all([".next", "ios", "android", "public/libs"].map(async (path38) => {
12648
- const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path38}`;
12699
+ await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
12700
+ const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
12649
12701
  if (await FileSys.dirExists(targetPath))
12650
12702
  await rm4(targetPath, { recursive: true, force: true });
12651
12703
  }));
12652
12704
  const syncPaths = [".husky", ".gitignore", "package.json"];
12653
- await Promise.all(syncPaths.map((path38) => cp(`${this.#app.workspace.cwdPath}/${path38}`, `${sourceRoot}/${path38}`, { recursive: true })));
12705
+ await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
12654
12706
  await this.#writeSourceTsconfig(sourceRoot, libDeps);
12655
12707
  await Bun.write(`${sourceRoot}/README.md`, readme);
12656
- await this.#app.workspace.spawn("tar", [
12657
- "-zcf",
12658
- `${this.#app.workspace.cwdPath}/releases/sources/${this.#app.name}-source.tar.gz`,
12659
- "-C",
12660
- sourceRoot,
12661
- "./"
12662
- ]);
12708
+ const sourceCwd = this.#app.workspace.cwdPath;
12709
+ const sourceArchivePath = path38.relative(sourceCwd, `${sourceCwd}/releases/sources/${this.#app.name}-source.tar.gz`).split(path38.sep).join("/");
12710
+ await this.#app.workspace.spawn("tar", ["-zcf", sourceArchivePath, "-C", sourceRoot, "./"], { cwd: sourceCwd });
12663
12711
  }
12664
12712
  async#resetSourceRoot(sourceRoot) {
12665
12713
  if (await FileSys.dirExists(sourceRoot)) {
@@ -12741,20 +12789,20 @@ class ApplicationReleasePackager {
12741
12789
  }
12742
12790
  }
12743
12791
  // pkgs/@akanjs/devkit/applicationTestPreload.ts
12744
- import path38 from "path";
12792
+ import path39 from "path";
12745
12793
  var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
12746
12794
  async function resolveSignalTestPreloadPath(target) {
12747
12795
  const candidates = [];
12748
12796
  const addResolvedPackageCandidate = (basePath2) => {
12749
12797
  try {
12750
- candidates.push(path38.join(path38.dirname(Bun.resolveSync("akanjs/package.json", basePath2)), SIGNAL_TEST_PRELOAD_PATH));
12798
+ candidates.push(path39.join(path39.dirname(Bun.resolveSync("akanjs/package.json", basePath2)), SIGNAL_TEST_PRELOAD_PATH));
12751
12799
  } catch {}
12752
12800
  };
12753
12801
  addResolvedPackageCandidate(target.cwdPath);
12754
12802
  addResolvedPackageCandidate(process.cwd());
12755
- addResolvedPackageCandidate(path38.dirname(Bun.main));
12803
+ addResolvedPackageCandidate(path39.dirname(Bun.main));
12756
12804
  addResolvedPackageCandidate(import.meta.dir);
12757
- 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));
12805
+ 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));
12758
12806
  for (const candidate of [...new Set(candidates)]) {
12759
12807
  if (await Bun.file(candidate).exists())
12760
12808
  return candidate;
@@ -12764,7 +12812,7 @@ async function resolveSignalTestPreloadPath(target) {
12764
12812
  // pkgs/@akanjs/devkit/builder.ts
12765
12813
  import { existsSync as existsSync2 } from "fs";
12766
12814
  import { mkdir as mkdir10 } from "fs/promises";
12767
- import path39 from "path";
12815
+ import path40 from "path";
12768
12816
  var SKIP_ENTRY_DIR_SET = new Set(["node_modules", "dist", "build", ".git", ".next"]);
12769
12817
  var assetExtensions = [".css", ".md", ".js", ".png", ".ico", ".svg", ".json", ".template"];
12770
12818
  var assetLoader = Object.fromEntries(assetExtensions.map((ext) => [ext, "file"]));
@@ -12781,14 +12829,14 @@ class Builder {
12781
12829
  #globEntrypoints(cwd, pattern) {
12782
12830
  const glob = new Bun.Glob(pattern);
12783
12831
  return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
12784
- const segments = relativePath.split(path39.sep);
12832
+ const segments = relativePath.split(path40.sep);
12785
12833
  return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
12786
- }).map((rel) => path39.join(cwd, rel));
12834
+ }).map((rel) => path40.join(cwd, rel));
12787
12835
  }
12788
12836
  #globFiles(cwd, pattern = "**/*.*") {
12789
12837
  const glob = new Bun.Glob(pattern);
12790
12838
  return Array.from(glob.scanSync({ cwd, onlyFiles: true })).filter((relativePath) => {
12791
- const segments = relativePath.split(path39.sep);
12839
+ const segments = relativePath.split(path40.sep);
12792
12840
  return !segments.some((segment) => SKIP_ENTRY_DIR_SET.has(segment));
12793
12841
  });
12794
12842
  }
@@ -12796,17 +12844,17 @@ class Builder {
12796
12844
  const out = [];
12797
12845
  for (const p of additionalEntryPoints) {
12798
12846
  if (p.includes("*")) {
12799
- const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path39.sep}`) ? p.slice(cwd.length + 1) : p;
12847
+ const rel = p.startsWith(`${cwd}/`) || p.startsWith(`${cwd}${path40.sep}`) ? p.slice(cwd.length + 1) : p;
12800
12848
  out.push(...this.#globEntrypoints(cwd, rel));
12801
12849
  } else
12802
- out.push(path39.isAbsolute(p) ? p : path39.join(cwd, p));
12850
+ out.push(path40.isAbsolute(p) ? p : path40.join(cwd, p));
12803
12851
  }
12804
12852
  return out;
12805
12853
  }
12806
12854
  #getBuildOptions({ bundle = false, additionalEntryPoints = [] } = {}) {
12807
12855
  const cwd = this.#executor.cwdPath;
12808
12856
  const entrypoints = [
12809
- ...bundle ? [path39.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}"),
12857
+ ...bundle ? [path40.join(cwd, "index.ts")] : this.#globEntrypoints(cwd, "**/*.{ts,tsx}"),
12810
12858
  ...this.#resolveAdditionalEntrypoints(cwd, additionalEntryPoints)
12811
12859
  ];
12812
12860
  return {
@@ -12827,9 +12875,9 @@ class Builder {
12827
12875
  for (const relativePath of this.#globFiles(cwd)) {
12828
12876
  if (relativePath === "package.json")
12829
12877
  continue;
12830
- const sourcePath = path39.join(cwd, relativePath);
12831
- const targetPath = path39.join(this.#distExecutor.cwdPath, relativePath);
12832
- await mkdir10(path39.dirname(targetPath), { recursive: true });
12878
+ const sourcePath = path40.join(cwd, relativePath);
12879
+ const targetPath = path40.join(this.#distExecutor.cwdPath, relativePath);
12880
+ await mkdir10(path40.dirname(targetPath), { recursive: true });
12833
12881
  await Bun.write(targetPath, Bun.file(sourcePath));
12834
12882
  }
12835
12883
  }
@@ -12840,13 +12888,13 @@ class Builder {
12840
12888
  return withoutFormatDir;
12841
12889
  if (!hasDotSlash && withoutFormatDir === publishedPath)
12842
12890
  return publishedPath;
12843
- const parsed = path39.posix.parse(withoutFormatDir);
12891
+ const parsed = path40.posix.parse(withoutFormatDir);
12844
12892
  if (![".js", ".mjs", ".cjs"].includes(parsed.ext))
12845
12893
  return withoutFormatDir;
12846
- const withoutExt = path39.posix.join(parsed.dir, parsed.name);
12894
+ const withoutExt = path40.posix.join(parsed.dir, parsed.name);
12847
12895
  const sourcePath = withoutExt.startsWith("./") ? withoutExt.slice(2) : withoutExt;
12848
12896
  const sourceCandidates = [`${sourcePath}.ts`, `${sourcePath}.tsx`];
12849
- const matchedSource = sourceCandidates.find((candidate) => existsSync2(path39.join(this.#executor.cwdPath, candidate)));
12897
+ const matchedSource = sourceCandidates.find((candidate) => existsSync2(path40.join(this.#executor.cwdPath, candidate)));
12850
12898
  if (!matchedSource)
12851
12899
  return withoutFormatDir;
12852
12900
  return hasDotSlash ? `./${matchedSource}` : matchedSource;
@@ -12895,9 +12943,9 @@ class Builder {
12895
12943
  }
12896
12944
  }
12897
12945
  // pkgs/@akanjs/devkit/capacitorApp.ts
12898
- import { cp as cp2, mkdir as mkdir11, rm as rm5 } from "fs/promises";
12946
+ import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
12899
12947
  import os from "os";
12900
- import path40 from "path";
12948
+ import path41 from "path";
12901
12949
  import { select as select2 } from "@inquirer/prompts";
12902
12950
  import { MobileProject } from "@trapezedev/project";
12903
12951
  import { capitalize as capitalize5 } from "akanjs/common";
@@ -13104,13 +13152,13 @@ var rootCapacitorConfigFilenames = [
13104
13152
  "capacitor.config.js",
13105
13153
  "capacitor.config.json"
13106
13154
  ];
13107
- var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path40.join(appRoot, file));
13155
+ var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
13108
13156
  async function clearRootCapacitorConfigs(appRoot) {
13109
13157
  await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm5(file, { force: true })));
13110
13158
  }
13111
13159
  async function writeRootCapacitorConfig(appRoot, content) {
13112
13160
  await clearRootCapacitorConfigs(appRoot);
13113
- await Bun.write(path40.join(appRoot, "capacitor.config.json"), content);
13161
+ await Bun.write(path41.join(appRoot, "capacitor.config.json"), content);
13114
13162
  }
13115
13163
  var getLocalIP = () => {
13116
13164
  const interfaces = os.networkInterfaces();
@@ -13219,13 +13267,13 @@ function buildIosNativeRunCommand({
13219
13267
  scheme = "App",
13220
13268
  configuration = "Debug"
13221
13269
  }) {
13222
- const derivedDataPath = path40.join(appRoot, "ios/DerivedData", device.id);
13270
+ const derivedDataPath = path41.join(appRoot, "ios/DerivedData", device.id);
13223
13271
  const productPlatform = device.kind === "device" ? "iphoneos" : "iphonesimulator";
13224
13272
  const destination = device.kind === "device" ? `id=${device.xcodebuildId ?? device.id}` : `platform=iOS Simulator,id=${device.id}`;
13225
13273
  return {
13226
13274
  configuration,
13227
13275
  derivedDataPath,
13228
- appPath: path40.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
13276
+ appPath: path41.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
13229
13277
  xcodebuildArgs: [
13230
13278
  "-project",
13231
13279
  "App.xcodeproj",
@@ -13409,11 +13457,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13409
13457
  const {
13410
13458
  name,
13411
13459
  basePath: _basePath,
13460
+ indexPath: _indexPath,
13412
13461
  version: _version,
13413
13462
  buildNum: _buildNum,
13414
13463
  assets: _assets,
13415
13464
  permissions: _permissions,
13416
- links: _links,
13465
+ deepLinks: _deepLinks,
13417
13466
  files: _files,
13418
13467
  appId,
13419
13468
  appName,
@@ -13435,7 +13484,7 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13435
13484
  ...capacitorConfig,
13436
13485
  appId,
13437
13486
  appName,
13438
- webDir: path40.posix.join(".akan", "mobile", name, "www"),
13487
+ webDir: path41.posix.join(".akan", "mobile", name, "www"),
13439
13488
  plugins: {
13440
13489
  CapacitorCookies: { enabled: true },
13441
13490
  ...pluginsConfig,
@@ -13492,10 +13541,10 @@ class CapacitorApp {
13492
13541
  constructor(app, target) {
13493
13542
  this.app = app;
13494
13543
  this.target = target;
13495
- this.targetRootPath = path40.posix.join(".akan", "mobile", this.target.name);
13496
- this.targetRoot = path40.join(this.app.cwdPath, this.targetRootPath);
13497
- this.targetWebRoot = path40.join(this.targetRoot, "www");
13498
- this.targetAssetRoot = path40.join(this.targetRoot, "assets");
13544
+ this.targetRootPath = path41.posix.join(".akan", "mobile", this.target.name);
13545
+ this.targetRoot = path41.join(this.app.cwdPath, this.targetRootPath);
13546
+ this.targetWebRoot = path41.join(this.targetRoot, "www");
13547
+ this.targetAssetRoot = path41.join(this.targetRoot, "assets");
13499
13548
  this.project = new MobileProject(this.app.cwdPath, {
13500
13549
  android: { path: this.androidRootPath },
13501
13550
  ios: { path: this.iosProjectPath }
@@ -13510,9 +13559,9 @@ class CapacitorApp {
13510
13559
  await mkdir11(this.targetRoot, { recursive: true });
13511
13560
  if (regenerate) {
13512
13561
  if (!platform || platform === "ios")
13513
- await rm5(path40.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13562
+ await rm5(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
13514
13563
  if (!platform || platform === "android")
13515
- await rm5(path40.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13564
+ await rm5(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
13516
13565
  }
13517
13566
  const project = this.project;
13518
13567
  await this.project.load();
@@ -13535,7 +13584,7 @@ class CapacitorApp {
13535
13584
  await this.#prepareExternalFiles("ios");
13536
13585
  await this.#applyIosMetadata();
13537
13586
  await this.#applyPermissions();
13538
- await this.#applyLinks();
13587
+ await this.#applyDeepLinks("ios", { operation, env });
13539
13588
  await this.project.commit();
13540
13589
  await this.#generateAssets({ operation, env });
13541
13590
  this.app.verbose(`syncing iOS`);
@@ -13635,7 +13684,7 @@ ${textError instanceof Error ? textError.message : ""}`);
13635
13684
  const xcodebuildArgs = noAllowProvisioningUpdates ? command.xcodebuildArgs : [...command.xcodebuildArgs.slice(0, -1), "-allowProvisioningUpdates", ...command.xcodebuildArgs.slice(-1)];
13636
13685
  try {
13637
13686
  await this.#spawn("xcodebuild", xcodebuildArgs, {
13638
- cwd: path40.join(this.app.cwdPath, this.iosProjectPath),
13687
+ cwd: path41.join(this.app.cwdPath, this.iosProjectPath),
13639
13688
  env: mobileEnv
13640
13689
  });
13641
13690
  const devicectlId = runTarget.devicectlId ?? runTarget.id;
@@ -13660,7 +13709,7 @@ ${textError instanceof Error ? textError.message : ""}`);
13660
13709
  return isRecord2(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
13661
13710
  }
13662
13711
  async#getIosDevelopmentTeam() {
13663
- const pbxprojPath = path40.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
13712
+ const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
13664
13713
  if (!await Bun.file(pbxprojPath).exists())
13665
13714
  return;
13666
13715
  return (await Bun.file(pbxprojPath).text()).match(/DEVELOPMENT_TEAM = ([^;]+);/)?.[1]?.trim();
@@ -13678,15 +13727,16 @@ ${error.message}`;
13678
13727
  await this.#prepareExternalFiles("android");
13679
13728
  await this.#applyAndroidMetadata();
13680
13729
  await this.#applyPermissions();
13681
- await this.#applyLinks();
13730
+ await this.#applyDeepLinks("android", { operation, env });
13682
13731
  await this.project.commit();
13683
13732
  await this.#generateAssets({ operation, env });
13684
13733
  await this.#ensureAndroidAssetsDir();
13685
13734
  await this.#ensureAndroidDebugKeystore();
13686
13735
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
13736
+ await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
13687
13737
  }
13688
13738
  async#updateAndroidBuildTypes() {
13689
- const appGradle = await FileEditor.create(path40.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
13739
+ const appGradle = await FileEditor.create(path41.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
13690
13740
  const buildTypesBlock = `
13691
13741
  debug {
13692
13742
  applicationIdSuffix ".debug"
@@ -13730,7 +13780,7 @@ ${error.message}`;
13730
13780
  const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
13731
13781
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
13732
13782
  stdio: "inherit",
13733
- cwd: path40.join(this.app.cwdPath, this.androidRootPath),
13783
+ cwd: path41.join(this.app.cwdPath, this.androidRootPath),
13734
13784
  env: await this.#commandEnv("release", env)
13735
13785
  });
13736
13786
  }
@@ -13738,10 +13788,10 @@ ${error.message}`;
13738
13788
  await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
13739
13789
  }
13740
13790
  async#ensureAndroidAssetsDir() {
13741
- await mkdir11(path40.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
13791
+ await mkdir11(path41.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
13742
13792
  }
13743
13793
  async#ensureAndroidDebugKeystore() {
13744
- const keystorePath = path40.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
13794
+ const keystorePath = path41.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
13745
13795
  if (await Bun.file(keystorePath).exists())
13746
13796
  return;
13747
13797
  await this.#spawn("keytool", [
@@ -13780,7 +13830,7 @@ ${error.message}`;
13780
13830
  await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
13781
13831
  }
13782
13832
  async#assertAndroidReleaseSigningConfig() {
13783
- const gradlePropertiesPath = path40.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
13833
+ const gradlePropertiesPath = path41.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
13784
13834
  const gradleProperties = await Bun.file(gradlePropertiesPath).exists() ? await Bun.file(gradlePropertiesPath).text() : "";
13785
13835
  const missingKeys = getMissingAndroidReleaseSigningKeys({ gradleProperties });
13786
13836
  if (missingKeys.length > 0)
@@ -13807,16 +13857,20 @@ ${error.message}`;
13807
13857
  await this.#prepareAndroid({ operation: "release", env: "main" });
13808
13858
  }
13809
13859
  async prepareWww() {
13810
- const htmlSource = path40.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13860
+ const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
13811
13861
  if (!await Bun.file(htmlSource).exists())
13812
13862
  throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
13813
13863
  await rm5(this.targetWebRoot, { recursive: true, force: true });
13814
13864
  await mkdir11(this.targetWebRoot, { recursive: true });
13815
- await Bun.write(path40.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13865
+ await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
13816
13866
  }
13817
13867
  #injectMobileTargetMeta(html) {
13818
13868
  const basePath2 = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13819
- const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({ name: this.target.name, basePath: basePath2 })};</script>`;
13869
+ const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
13870
+ name: this.target.name,
13871
+ basePath: basePath2,
13872
+ indexPath: this.target.indexPath
13873
+ })};</script>`;
13820
13874
  if (html.includes("window.__AKAN_MOBILE_TARGET__"))
13821
13875
  return html;
13822
13876
  return html.replace(/<\/head\s*>/i, `${script}
@@ -13832,7 +13886,7 @@ ${error.message}`;
13832
13886
  });
13833
13887
  const content = `${JSON.stringify(config, null, 2)}
13834
13888
  `;
13835
- await Bun.write(path40.join(this.targetRoot, "capacitor.config.json"), content);
13889
+ await Bun.write(path41.join(this.targetRoot, "capacitor.config.json"), content);
13836
13890
  return content;
13837
13891
  }
13838
13892
  async#prepareTargetAssets() {
@@ -13840,11 +13894,11 @@ ${error.message}`;
13840
13894
  return;
13841
13895
  await mkdir11(this.targetAssetRoot, { recursive: true });
13842
13896
  if (this.target.assets.icon)
13843
- await cp2(path40.join(this.app.cwdPath, this.target.assets.icon), path40.join(this.targetAssetRoot, "icon.png"), {
13897
+ await cp2(path41.join(this.app.cwdPath, this.target.assets.icon), path41.join(this.targetAssetRoot, "icon.png"), {
13844
13898
  force: true
13845
13899
  });
13846
13900
  if (this.target.assets.splash)
13847
- await cp2(path40.join(this.app.cwdPath, this.target.assets.splash), path40.join(this.targetAssetRoot, "splash.png"), {
13901
+ await cp2(path41.join(this.app.cwdPath, this.target.assets.splash), path41.join(this.targetAssetRoot, "splash.png"), {
13848
13902
  force: true
13849
13903
  });
13850
13904
  }
@@ -13852,11 +13906,11 @@ ${error.message}`;
13852
13906
  const files = this.target.files?.[platform];
13853
13907
  if (!files)
13854
13908
  return;
13855
- const platformRoot = path40.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
13909
+ const platformRoot = path41.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
13856
13910
  await Promise.all(Object.entries(files).map(async ([to, from]) => {
13857
- const targetPath = path40.join(platformRoot, to);
13858
- await mkdir11(path40.dirname(targetPath), { recursive: true });
13859
- await cp2(path40.join(this.app.cwdPath, from), targetPath, { force: true });
13911
+ const targetPath = path41.join(platformRoot, to);
13912
+ await mkdir11(path41.dirname(targetPath), { recursive: true });
13913
+ await cp2(path41.join(this.app.cwdPath, from), targetPath, { force: true });
13860
13914
  }));
13861
13915
  }
13862
13916
  async#generateAssets({ operation, env }) {
@@ -13866,7 +13920,7 @@ ${error.message}`;
13866
13920
  "@capacitor/assets",
13867
13921
  "generate",
13868
13922
  "--assetPath",
13869
- path40.posix.join(this.targetRootPath, "assets"),
13923
+ path41.posix.join(this.targetRootPath, "assets"),
13870
13924
  "--iosProject",
13871
13925
  this.iosProjectPath,
13872
13926
  "--androidProject",
@@ -13899,25 +13953,48 @@ ${error.message}`;
13899
13953
  await this.addPush();
13900
13954
  }
13901
13955
  }
13902
- async#applyLinks() {
13903
- const links = this.target.links;
13904
- if (!links)
13956
+ async#applyDeepLinks(platform, { operation, env }) {
13957
+ const deepLinks = this.target.deepLinks;
13958
+ if (!deepLinks)
13905
13959
  return;
13906
- const schemes = links.schemes ?? [];
13907
- if (schemes.length > 0) {
13908
- await this.#setPermissionInIos({
13909
- appTransportSecurity: ""
13910
- });
13960
+ const schemes = deepLinks.schemes ?? [];
13961
+ const domains = deepLinks.domains ?? [];
13962
+ if (domains.length > 0)
13963
+ this.#assertDeepLinkVerificationConfig(platform, { operation, env });
13964
+ if (platform === "ios") {
13965
+ if (schemes.length > 0) {
13966
+ await this.#setPermissionInIos({
13967
+ appTransportSecurity: ""
13968
+ });
13969
+ await this.#setUrlSchemesInIos(schemes);
13970
+ }
13971
+ if (domains.length > 0)
13972
+ await this.#setAssociatedDomainsInIos(domains);
13973
+ return;
13974
+ }
13975
+ if (platform === "android") {
13911
13976
  for (const scheme of schemes) {
13912
13977
  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>`);
13913
13978
  }
13979
+ for (const domain of domains) {
13980
+ const pathPrefix = resolveMobilePath(this.target, "/");
13981
+ 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>`);
13982
+ }
13983
+ await this.#setDeepLinksInAndroid(schemes, domains);
13914
13984
  }
13915
- for (const domain of links.associatedDomains ?? []) {
13916
- this.app.logger.info(`Configure iOS associated domain manually if needed: ${domain}`);
13985
+ }
13986
+ #assertDeepLinkVerificationConfig(platform, { operation, env }) {
13987
+ const deepLinks = this.target.deepLinks;
13988
+ if (!deepLinks?.domains?.length)
13989
+ return;
13990
+ if (platform === "ios" && !deepLinks.ios?.teamId) {
13991
+ 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`);
13917
13992
  }
13918
- for (const host of links.androidHosts ?? []) {
13919
- const pathPrefix = resolveMobilePath(this.target, "/");
13920
- 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>`);
13993
+ if (platform === "android" && !deepLinks.android?.sha256CertFingerprints?.length) {
13994
+ const message = `Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.android.sha256CertFingerprints in apps/${this.app.name}/akan.config.ts`;
13995
+ if (operation === "release" || env === "main")
13996
+ throw new Error(message);
13997
+ this.app.logger.warn(message);
13921
13998
  }
13922
13999
  }
13923
14000
  async#commandEnv(operation, env) {
@@ -13938,6 +14015,8 @@ ${error.message}`;
13938
14015
  const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
13939
14016
  if (basePath2)
13940
14017
  params.set("akanMobileBasePath", basePath2);
14018
+ if (this.target.indexPath)
14019
+ params.set("akanMobileIndexPath", this.target.indexPath);
13941
14020
  return `http://${ip}:${port}/${pathname}?${params}`;
13942
14021
  }
13943
14022
  async#clearRootCapacitorConfigs() {
@@ -13997,6 +14076,112 @@ ${error.message}`;
13997
14076
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
13998
14077
  ]);
13999
14078
  }
14079
+ async#setUrlSchemesInIos(schemes) {
14080
+ const urlTypes = schemes.map((scheme) => ({
14081
+ CFBundleURLName: this.target.appId,
14082
+ CFBundleURLSchemes: [scheme]
14083
+ }));
14084
+ await Promise.all([
14085
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
14086
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14087
+ ]);
14088
+ }
14089
+ async#setAssociatedDomainsInIos(domains) {
14090
+ const entitlementsRelPath = "App/App.entitlements";
14091
+ const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14092
+ const values = domains.map((domain) => `applinks:${domain}`);
14093
+ const body = [
14094
+ '<?xml version="1.0" encoding="UTF-8"?>',
14095
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14096
+ '<plist version="1.0">',
14097
+ "<dict>",
14098
+ " <key>com.apple.developer.associated-domains</key>",
14099
+ " <array>",
14100
+ ...values.map((value) => ` <string>${value}</string>`),
14101
+ " </array>",
14102
+ "</dict>",
14103
+ "</plist>",
14104
+ ""
14105
+ ].join(`
14106
+ `);
14107
+ await writeFile2(entitlementsPath, body);
14108
+ await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
14109
+ }
14110
+ async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
14111
+ const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
14112
+ const lines = (await readFile2(pbxprojPath, "utf8")).split(`
14113
+ `);
14114
+ let changed = false;
14115
+ for (let index = 0;index < lines.length; index++) {
14116
+ const line = lines[index] ?? "";
14117
+ if (!line.includes(`PRODUCT_BUNDLE_IDENTIFIER = ${this.target.appId};`))
14118
+ continue;
14119
+ let start = index;
14120
+ while (start >= 0 && !lines[start]?.includes("buildSettings = {"))
14121
+ start--;
14122
+ let end = index;
14123
+ while (end < lines.length && !/^\s*\};\s*$/.test(lines[end] ?? ""))
14124
+ end++;
14125
+ const settings = lines.slice(start, end + 1);
14126
+ if (settings.some((setting) => setting.includes("CODE_SIGN_ENTITLEMENTS")))
14127
+ continue;
14128
+ const indent = line.match(/^\s*/)?.[0] ?? "";
14129
+ lines.splice(index, 0, `${indent}CODE_SIGN_ENTITLEMENTS = ${entitlementsRelPath};`);
14130
+ index++;
14131
+ changed = true;
14132
+ }
14133
+ if (changed)
14134
+ await writeFile2(pbxprojPath, lines.join(`
14135
+ `));
14136
+ }
14137
+ async#setUrlSchemesInAndroid(schemes) {
14138
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14139
+ let manifest = await readFile2(manifestPath, "utf8");
14140
+ let changed = false;
14141
+ for (const scheme of schemes) {
14142
+ if (manifest.includes(`android:scheme="${scheme}"`))
14143
+ continue;
14144
+ const filter = [
14145
+ " <intent-filter>",
14146
+ ' <action android:name="android.intent.action.VIEW" />',
14147
+ ' <category android:name="android.intent.category.DEFAULT" />',
14148
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14149
+ ` <data android:scheme="${scheme}" />`,
14150
+ " </intent-filter>"
14151
+ ].join(`
14152
+ `);
14153
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14154
+ ${filter}$1`);
14155
+ changed = true;
14156
+ }
14157
+ if (changed)
14158
+ await writeFile2(manifestPath, manifest);
14159
+ }
14160
+ async#setDeepLinksInAndroid(schemes, domains) {
14161
+ await this.#setUrlSchemesInAndroid(schemes);
14162
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14163
+ let manifest = await readFile2(manifestPath, "utf8");
14164
+ let changed = false;
14165
+ const pathPrefix = resolveMobilePath(this.target, "/");
14166
+ for (const domain of domains) {
14167
+ if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"'))
14168
+ continue;
14169
+ const filter = [
14170
+ ' <intent-filter android:autoVerify="true">',
14171
+ ' <action android:name="android.intent.action.VIEW" />',
14172
+ ' <category android:name="android.intent.category.DEFAULT" />',
14173
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14174
+ ` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
14175
+ " </intent-filter>"
14176
+ ].join(`
14177
+ `);
14178
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14179
+ ${filter}$1`);
14180
+ changed = true;
14181
+ }
14182
+ if (changed)
14183
+ await writeFile2(manifestPath, manifest);
14184
+ }
14000
14185
  #setFeaturesInAndroid(features) {
14001
14186
  for (const feature of features) {
14002
14187
  if (this.#hasFeatureInAndroid(feature)) {
@@ -14076,7 +14261,7 @@ var Pkg = createInternalArgToken("Pkg");
14076
14261
  var Module = createInternalArgToken("Module");
14077
14262
  var Workspace = createInternalArgToken("Workspace");
14078
14263
  // pkgs/@akanjs/devkit/commandDecorators/command.ts
14079
- import path41 from "path";
14264
+ import path42 from "path";
14080
14265
  import { confirm, input as input2, select as select3 } from "@inquirer/prompts";
14081
14266
  import { Logger as Logger10 } from "akanjs/common";
14082
14267
  import chalk6 from "chalk";
@@ -14580,7 +14765,7 @@ var runCommands = async (...commands) => {
14580
14765
  process.exit(1);
14581
14766
  });
14582
14767
  const __dirname2 = getDirname(import.meta.url);
14583
- const packageJsonCandidates = [`${path41.dirname(Bun.main)}/package.json`, `${__dirname2}/../package.json`];
14768
+ const packageJsonCandidates = [`${path42.dirname(Bun.main)}/package.json`, `${__dirname2}/../package.json`];
14584
14769
  let cliPackageJson = null;
14585
14770
  for (const packageJsonPath of packageJsonCandidates) {
14586
14771
  if (!await FileSys.fileExists(packageJsonPath))
@@ -14860,8 +15045,8 @@ var scanModuleSpecifiers = (source2, filePath, includeExports) => {
14860
15045
  return importSpecifiers;
14861
15046
  };
14862
15047
  var parseTsConfig = (tsConfigPath = "./tsconfig.json") => {
14863
- const configFile = ts10.readConfigFile(tsConfigPath, (path42) => {
14864
- return ts10.sys.readFile(path42);
15048
+ const configFile = ts10.readConfigFile(tsConfigPath, (path43) => {
15049
+ return ts10.sys.readFile(path43);
14865
15050
  });
14866
15051
  return ts10.parseJsonConfigFileContent(configFile.config, ts10.sys, realpathSync(tsConfigPath).replace(/[^/\\]+$/, ""));
14867
15052
  };
@@ -15077,6 +15262,571 @@ ${document}
15077
15262
  `;
15078
15263
  }
15079
15264
  }
15265
+ // pkgs/@akanjs/devkit/qualityScanner.ts
15266
+ import { createHash } from "crypto";
15267
+ import { readdir as readdir3, readFile as readFile3, stat as stat4 } from "fs/promises";
15268
+ import path43 from "path";
15269
+ import ignore2 from "ignore";
15270
+ import ts11 from "typescript";
15271
+ var MAX_FILE_LINES = 2000;
15272
+ var PLACEHOLDER_EXPORT_NAMES = new Set([
15273
+ "aa",
15274
+ "dumb",
15275
+ "dumb2",
15276
+ "someBaseLogic",
15277
+ "someCommonLogic",
15278
+ "someFrontendLogic",
15279
+ "someBackendLogic"
15280
+ ]);
15281
+ var SUGGESTED_RULES = [
15282
+ "Keep generated scanSync index files out of hand-written changes; generated indexes should only contain one-depth export statements.",
15283
+ "Generated Akan index files should not contain placeholder exports such as aa, dumb, dumb2, or some*Logic.",
15284
+ "Dictionary text should not contain scaffold wording, misspellings, or stale copied domain nouns.",
15285
+ "Warn earlier on very long Akan files: 500 lines for services, 800 lines for Template/Zone files, and 1000 lines for Util files.",
15286
+ "Global declarations, Window augmentation, and prototype mutation should stay in explicitly approved low-level integration files.",
15287
+ "Keep app root folders small and conventional: application code belongs under common, env, lib, page, private, public, script, srvkit, ui, or webkit.",
15288
+ "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.",
15289
+ "Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
15290
+ "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.",
15291
+ "Move shared app utilities to apps/*/common instead of creating apps/*/base.",
15292
+ "Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
15293
+ ];
15294
+ var APP_ROOT_FILES = new Set([
15295
+ "akan.app.json",
15296
+ "akan.config.ts",
15297
+ "capacitor.config.ts",
15298
+ "client.ts",
15299
+ "main.ts",
15300
+ "package.json",
15301
+ "server.ts",
15302
+ "tsconfig.json"
15303
+ ]);
15304
+ var LIB_ROOT_FILES = new Set([
15305
+ "cnst.ts",
15306
+ "db.ts",
15307
+ "dict.ts",
15308
+ "option.ts",
15309
+ "sig.ts",
15310
+ "srv.ts",
15311
+ "st.ts",
15312
+ "useClient.ts",
15313
+ "useServer.ts"
15314
+ ]);
15315
+ var CONVENTION_SUFFIXES = [
15316
+ ".constant.ts",
15317
+ ".dictionary.ts",
15318
+ ".document.ts",
15319
+ ".service.ts",
15320
+ ".signal.ts",
15321
+ ".store.ts"
15322
+ ];
15323
+
15324
+ class AkanQualityScanner {
15325
+ async scan(workspaceRoot) {
15326
+ const targetFiles = await this.#collectTargetFiles(workspaceRoot);
15327
+ const sourceFiles = await Promise.all(targetFiles.map((file) => this.#readSourceFile(workspaceRoot, file)));
15328
+ const warnings = [
15329
+ ...this.#scanGlobalQuality(sourceFiles),
15330
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
15331
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
15332
+ ...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
15333
+ ];
15334
+ return {
15335
+ workspaceRoot,
15336
+ scannedFiles: sourceFiles.length,
15337
+ warnings: warnings.sort(compareWarnings),
15338
+ suggestedRules: SUGGESTED_RULES
15339
+ };
15340
+ }
15341
+ async#collectTargetFiles(workspaceRoot) {
15342
+ const ignoreFilter = ignore2().add(await this.#readGitIgnore(workspaceRoot));
15343
+ const files = [];
15344
+ for (const targetRoot of ["apps", "libs"]) {
15345
+ const absoluteTargetRoot = path43.join(workspaceRoot, targetRoot);
15346
+ if (!await isDirectory(absoluteTargetRoot))
15347
+ continue;
15348
+ await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
15349
+ }
15350
+ return files.sort();
15351
+ }
15352
+ async#readGitIgnore(workspaceRoot) {
15353
+ const gitIgnorePath = path43.join(workspaceRoot, ".gitignore");
15354
+ if (!await Bun.file(gitIgnorePath).exists())
15355
+ return [];
15356
+ return (await readFile3(gitIgnorePath, "utf8")).split(/\r?\n/);
15357
+ }
15358
+ async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
15359
+ const entries = await readdir3(currentPath, { withFileTypes: true });
15360
+ for (const entry of entries) {
15361
+ const absolutePath = path43.join(currentPath, entry.name);
15362
+ const relativePath = toPosix(path43.relative(workspaceRoot, absolutePath));
15363
+ if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
15364
+ continue;
15365
+ if (entry.isDirectory()) {
15366
+ await this.#walkTargetFiles(workspaceRoot, absolutePath, ignoreFilter, files);
15367
+ continue;
15368
+ }
15369
+ if ((relativePath.endsWith(".ts") || relativePath.endsWith(".tsx")) && !relativePath.endsWith(".d.ts")) {
15370
+ files.push(relativePath);
15371
+ }
15372
+ }
15373
+ }
15374
+ async#readSourceFile(workspaceRoot, file) {
15375
+ const absolutePath = path43.join(workspaceRoot, file);
15376
+ const content = await readFile3(absolutePath, "utf8");
15377
+ return {
15378
+ file,
15379
+ absolutePath,
15380
+ content,
15381
+ sourceFile: ts11.createSourceFile(file, content, ts11.ScriptTarget.Latest, true, getScriptKind(file))
15382
+ };
15383
+ }
15384
+ #scanGlobalQuality(sourceFiles) {
15385
+ const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
15386
+ const warnings = [];
15387
+ for (const [name, declarations] of groupBy(exportedFunctionLikes, (declaration) => declaration.name)) {
15388
+ if (declarations.length < 2)
15389
+ continue;
15390
+ warnings.push({
15391
+ rule: "akan.global.duplicate-exported-function-name",
15392
+ scope: "global",
15393
+ severity: "warning",
15394
+ message: `Exported function or class name "${name}" is declared in ${declarations.length} files.`,
15395
+ locations: declarations.map(({ file, line }) => ({ file, line }))
15396
+ });
15397
+ }
15398
+ const declarationsWithBody = exportedFunctionLikes.filter((declaration) => declaration.bodyFingerprint);
15399
+ for (const [fingerprint, declarations] of groupBy(declarationsWithBody, (declaration) => declaration.bodyFingerprint ?? "")) {
15400
+ const uniqueNames = new Set(declarations.map((declaration) => declaration.name));
15401
+ if (declarations.length < 2 || uniqueNames.size < 2 || fingerprint === "")
15402
+ continue;
15403
+ warnings.push({
15404
+ rule: "akan.global.duplicate-exported-function-body",
15405
+ scope: "global",
15406
+ severity: "warning",
15407
+ message: `Exported functions/classes share the same implementation body: ${declarations.map((declaration) => declaration.name).join(", ")}.`,
15408
+ locations: declarations.map(({ file, line }) => ({ file, line }))
15409
+ });
15410
+ }
15411
+ return warnings;
15412
+ }
15413
+ #scanSingleFileQuality(sourceFile2) {
15414
+ const warnings = [];
15415
+ const lineCount = sourceFile2.content.split(/\r?\n/).length;
15416
+ const recommendedLineLimit = getRecommendedLineLimit(sourceFile2.file);
15417
+ if (recommendedLineLimit && lineCount > recommendedLineLimit) {
15418
+ warnings.push({
15419
+ rule: "akan.file.recommended-max-lines",
15420
+ scope: "file",
15421
+ severity: "warning",
15422
+ file: sourceFile2.file,
15423
+ message: `File has ${lineCount} lines. Recommended limit for this file type is ${recommendedLineLimit} lines.`
15424
+ });
15425
+ }
15426
+ if (lineCount > MAX_FILE_LINES) {
15427
+ warnings.push({
15428
+ rule: "akan.file.max-lines",
15429
+ scope: "file",
15430
+ severity: "warning",
15431
+ file: sourceFile2.file,
15432
+ message: `File has ${lineCount} lines. Keep single files under ${MAX_FILE_LINES} lines.`
15433
+ });
15434
+ }
15435
+ warnings.push(...getPlaceholderExportWarnings(sourceFile2));
15436
+ warnings.push(...getDictionaryTextWarnings(sourceFile2));
15437
+ warnings.push(...getGlobalMutationWarnings(sourceFile2));
15438
+ const exportedClassNames = getExportedClassNames(sourceFile2.sourceFile);
15439
+ if (exportedClassNames.length === 0)
15440
+ return warnings;
15441
+ const allowedInterfaceNames = new Set(exportedClassNames.map((name) => `${name}Options`));
15442
+ for (const declaration of getTopLevelDeclarations(sourceFile2)) {
15443
+ if (declaration.kind === "class" && exportedClassNames.includes(declaration.name))
15444
+ continue;
15445
+ if (declaration.kind === "interface" && allowedInterfaceNames.has(declaration.name))
15446
+ continue;
15447
+ warnings.push({
15448
+ rule: "akan.file.class-export-global-declaration",
15449
+ scope: "file",
15450
+ severity: "warning",
15451
+ file: sourceFile2.file,
15452
+ line: declaration.line,
15453
+ message: `Class export files should not declare top-level ${declaration.kind} "${declaration.name}". Move helpers to another file and import them.`
15454
+ });
15455
+ }
15456
+ return warnings;
15457
+ }
15458
+ #scanConventionQuality(sourceFile2) {
15459
+ const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
15460
+ if (!suffix)
15461
+ return [];
15462
+ const modelName = toPascalCase(path43.basename(sourceFile2.file, suffix));
15463
+ const warnings = [];
15464
+ for (const declaration of getTopLevelDeclarations(sourceFile2)) {
15465
+ if (isAllowedConventionDeclaration(suffix, modelName, declaration))
15466
+ continue;
15467
+ warnings.push({
15468
+ rule: `akan.convention${suffix.replace(".ts", "")}`,
15469
+ scope: "convention",
15470
+ severity: "warning",
15471
+ file: sourceFile2.file,
15472
+ line: declaration.line,
15473
+ message: `${path43.basename(sourceFile2.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
15474
+ });
15475
+ }
15476
+ return warnings;
15477
+ }
15478
+ #scanLayoutQuality(sourceFile2) {
15479
+ const segments = sourceFile2.file.split("/");
15480
+ const warnings = [];
15481
+ if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
15482
+ warnings.push({
15483
+ rule: "akan.layout.app-root-file",
15484
+ scope: "layout",
15485
+ severity: "warning",
15486
+ file: sourceFile2.file,
15487
+ message: `Unexpected app root file "${segments[2]}". Keep application code in conventional app folders.`
15488
+ });
15489
+ }
15490
+ const libRootFile = getLibRootFile(sourceFile2.file);
15491
+ if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
15492
+ warnings.push({
15493
+ rule: "akan.layout.lib-root-file",
15494
+ scope: "layout",
15495
+ severity: "warning",
15496
+ file: sourceFile2.file,
15497
+ message: `Unexpected lib root file "${libRootFile}". Keep direct lib root files limited to generated support facets.`
15498
+ });
15499
+ }
15500
+ const moduleUiWarning = getModuleUiWarning(sourceFile2.file);
15501
+ if (moduleUiWarning)
15502
+ warnings.push(moduleUiWarning);
15503
+ return warnings;
15504
+ }
15505
+ }
15506
+ function formatQualityScanResult(result) {
15507
+ const sections = [
15508
+ "Akan Code Quality Scan",
15509
+ `workspace: ${result.workspaceRoot}`,
15510
+ `scanned files: ${result.scannedFiles}`,
15511
+ `warnings: ${result.warnings.length}`,
15512
+ "",
15513
+ "Warnings:",
15514
+ "",
15515
+ ...formatQualityWarnings(result.warnings),
15516
+ "",
15517
+ "Suggested quality rules:",
15518
+ "",
15519
+ ...result.suggestedRules.map((rule) => ` - ${rule}`)
15520
+ ];
15521
+ return sections.join(`
15522
+ `);
15523
+ }
15524
+ function formatQualityWarnings(warnings) {
15525
+ if (warnings.length === 0)
15526
+ return ["No warnings found."];
15527
+ return warnings.flatMap((warning) => {
15528
+ const location = formatQualityLocation(warning.file, warning.line);
15529
+ const lines = [`${location} - warning ${warning.rule}: ${warning.message}`];
15530
+ if (warning.locations?.length) {
15531
+ lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
15532
+ }
15533
+ return lines;
15534
+ });
15535
+ }
15536
+ function formatQualityLocation(file, line) {
15537
+ return `${file ?? "<global>"}:${line ?? 1}:1`;
15538
+ }
15539
+ function getExportedFunctionLikes(sourceFile2) {
15540
+ const declarations = [];
15541
+ for (const statement of sourceFile2.sourceFile.statements) {
15542
+ if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
15543
+ declarations.push({
15544
+ name: statement.name.text,
15545
+ kind: "function",
15546
+ file: sourceFile2.file,
15547
+ line: getLine(sourceFile2.sourceFile, statement),
15548
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
15549
+ });
15550
+ }
15551
+ if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
15552
+ declarations.push({
15553
+ name: statement.name.text,
15554
+ kind: "class",
15555
+ file: sourceFile2.file,
15556
+ line: getLine(sourceFile2.sourceFile, statement),
15557
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
15558
+ });
15559
+ }
15560
+ if (ts11.isVariableStatement(statement) && isExported(statement)) {
15561
+ for (const declaration of statement.declarationList.declarations) {
15562
+ if (!ts11.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
15563
+ continue;
15564
+ declarations.push({
15565
+ name: declaration.name.text,
15566
+ kind: "function-variable",
15567
+ file: sourceFile2.file,
15568
+ line: getLine(sourceFile2.sourceFile, declaration),
15569
+ bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
15570
+ });
15571
+ }
15572
+ }
15573
+ }
15574
+ return declarations;
15575
+ }
15576
+ function getExportedClassNames(sourceFile2) {
15577
+ return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
15578
+ }
15579
+ function getPlaceholderExportWarnings(sourceFile2) {
15580
+ if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
15581
+ return [];
15582
+ return getTopLevelDeclarations(sourceFile2).filter((declaration) => declaration.exported && PLACEHOLDER_EXPORT_NAMES.has(declaration.name)).map((declaration) => ({
15583
+ rule: "akan.file.placeholder-export",
15584
+ scope: "file",
15585
+ severity: "warning",
15586
+ file: sourceFile2.file,
15587
+ line: declaration.line,
15588
+ message: `Generated or barrel index file should not export placeholder "${declaration.name}".`
15589
+ }));
15590
+ }
15591
+ function getDictionaryTextWarnings(sourceFile2) {
15592
+ if (!sourceFile2.file.endsWith(".dictionary.ts"))
15593
+ return [];
15594
+ const stalePatterns = [
15595
+ { pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
15596
+ { pattern: /settting/, label: "misspelling: settting" },
15597
+ { pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
15598
+ ];
15599
+ return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
15600
+ rule: "akan.file.dictionary-stale-text",
15601
+ scope: "file",
15602
+ severity: "warning",
15603
+ file: sourceFile2.file,
15604
+ line,
15605
+ message: `Dictionary text appears to contain ${label}.`
15606
+ })));
15607
+ }
15608
+ function getGlobalMutationWarnings(sourceFile2) {
15609
+ const warnings = [];
15610
+ for (const statement of sourceFile2.sourceFile.statements) {
15611
+ if (ts11.isModuleDeclaration(statement) && statement.name.getText(sourceFile2.sourceFile) === "global") {
15612
+ warnings.push({
15613
+ rule: "akan.file.global-declaration",
15614
+ scope: "file",
15615
+ severity: "warning",
15616
+ file: sourceFile2.file,
15617
+ line: getLine(sourceFile2.sourceFile, statement),
15618
+ message: "Global declarations require an explicit low-level integration allowlist."
15619
+ });
15620
+ }
15621
+ if (ts11.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
15622
+ warnings.push({
15623
+ rule: "akan.file.window-augmentation",
15624
+ scope: "file",
15625
+ severity: "warning",
15626
+ file: sourceFile2.file,
15627
+ line: getLine(sourceFile2.sourceFile, statement),
15628
+ message: "Window augmentation should be isolated to approved browser integration files."
15629
+ });
15630
+ }
15631
+ if (ts11.isExpressionStatement(statement) && statement.expression.getText(sourceFile2.sourceFile).includes(".prototype.")) {
15632
+ warnings.push({
15633
+ rule: "akan.file.prototype-mutation",
15634
+ scope: "file",
15635
+ severity: "warning",
15636
+ file: sourceFile2.file,
15637
+ line: getLine(sourceFile2.sourceFile, statement),
15638
+ message: "Prototype mutation should be avoided or isolated to approved low-level integration files."
15639
+ });
15640
+ }
15641
+ }
15642
+ return warnings;
15643
+ }
15644
+ function getTopLevelDeclarations(sourceFile2) {
15645
+ return sourceFile2.sourceFile.statements.flatMap((statement) => getTopLevelDeclaration(sourceFile2.sourceFile, statement));
15646
+ }
15647
+ function getTopLevelDeclaration(sourceFile2, statement) {
15648
+ const line = getLine(sourceFile2, statement);
15649
+ if (ts11.isClassDeclaration(statement) && statement.name) {
15650
+ return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
15651
+ }
15652
+ if (ts11.isFunctionDeclaration(statement) && statement.name) {
15653
+ return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
15654
+ }
15655
+ if (ts11.isInterfaceDeclaration(statement)) {
15656
+ return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
15657
+ }
15658
+ if (ts11.isTypeAliasDeclaration(statement)) {
15659
+ return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
15660
+ }
15661
+ if (ts11.isEnumDeclaration(statement)) {
15662
+ return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
15663
+ }
15664
+ if (ts11.isVariableStatement(statement)) {
15665
+ return statement.declarationList.declarations.filter((declaration) => ts11.isIdentifier(declaration.name)).map((declaration) => ({
15666
+ name: declaration.name.text,
15667
+ kind: "variable",
15668
+ line: getLine(sourceFile2, declaration),
15669
+ exported: isExported(statement),
15670
+ node: statement
15671
+ }));
15672
+ }
15673
+ if (ts11.isExportDeclaration(statement)) {
15674
+ return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
15675
+ }
15676
+ return [];
15677
+ }
15678
+ function isAllowedConventionDeclaration(suffix, modelName, declaration) {
15679
+ if (suffix === ".dictionary.ts")
15680
+ return isExportedConst(declaration) && declaration.name === "dictionary";
15681
+ if (suffix === ".constant.ts")
15682
+ return isAllowedConstantDeclaration(modelName, declaration);
15683
+ if (suffix === ".document.ts")
15684
+ return declaration.kind === "class" && [`${modelName}Filter`, modelName, `${modelName}Model`].includes(declaration.name);
15685
+ if (suffix === ".service.ts")
15686
+ return declaration.kind === "class" && declaration.name === `${modelName}Service`;
15687
+ if (suffix === ".signal.ts")
15688
+ return declaration.kind === "class" && [`${modelName}Internal`, `${modelName}Slice`, `${modelName}Endpoint`].includes(declaration.name);
15689
+ if (suffix === ".store.ts")
15690
+ return declaration.kind === "class" && declaration.name === `${modelName}Store`;
15691
+ return false;
15692
+ }
15693
+ function isAllowedConstantDeclaration(modelName, declaration) {
15694
+ if (declaration.kind !== "class")
15695
+ return false;
15696
+ if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
15697
+ return true;
15698
+ if (!ts11.isClassDeclaration(declaration.node))
15699
+ return false;
15700
+ const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
15701
+ const expression = heritageClause?.types[0]?.expression;
15702
+ return !!expression && expression.getText().startsWith("enumOf(");
15703
+ }
15704
+ function getConventionDescription(suffix, modelName) {
15705
+ if (suffix === ".dictionary.ts")
15706
+ return "export const dictionary";
15707
+ if (suffix === ".constant.ts")
15708
+ return `${modelName}Input, ${modelName}Object, ${modelName}, Light${modelName}, ${modelName}Insight, or enumOf classes`;
15709
+ if (suffix === ".document.ts")
15710
+ return `${modelName}Filter, ${modelName}, or ${modelName}Model`;
15711
+ if (suffix === ".service.ts")
15712
+ return `${modelName}Service`;
15713
+ if (suffix === ".signal.ts")
15714
+ return `${modelName}Internal, ${modelName}Slice, or ${modelName}Endpoint`;
15715
+ return `${modelName}Store`;
15716
+ }
15717
+ function getLibRootFile(file) {
15718
+ const segments = file.split("/");
15719
+ if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
15720
+ return segments[3];
15721
+ if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
15722
+ return segments[4];
15723
+ return null;
15724
+ }
15725
+ function getModuleUiWarning(file) {
15726
+ if (!file.endsWith(".tsx"))
15727
+ return null;
15728
+ const moduleInfo = getModuleInfo(file);
15729
+ if (!moduleInfo)
15730
+ return null;
15731
+ const { moduleName, fileName, kind } = moduleInfo;
15732
+ const pascalName = toPascalCase(moduleName.replace(/^_+/, ""));
15733
+ const allowedSuffixes = kind === "database" ? ["Template", "Unit", "Util", "View", "Zone"] : kind === "service" ? ["Util", "Zone"] : ["Template", "Unit"];
15734
+ const allowedFileNames = new Set(allowedSuffixes.map((suffix) => `${pascalName}.${suffix}.tsx`));
15735
+ if (allowedFileNames.has(fileName) || fileName.endsWith(".test.tsx") || fileName.endsWith(".spec.tsx"))
15736
+ return null;
15737
+ return {
15738
+ rule: "akan.layout.module-ui-file",
15739
+ scope: "layout",
15740
+ severity: "warning",
15741
+ file,
15742
+ message: `Unexpected ${kind} module UI filename "${fileName}". Expected one of: ${[...allowedFileNames].join(", ")}.`
15743
+ };
15744
+ }
15745
+ function getModuleInfo(file) {
15746
+ const segments = file.split("/");
15747
+ const libIndex = segments.indexOf("lib");
15748
+ if (libIndex < 0)
15749
+ return null;
15750
+ const moduleName = segments[libIndex + 1];
15751
+ if (moduleName === "__scalar") {
15752
+ if (segments.length !== libIndex + 4)
15753
+ return null;
15754
+ return { moduleName: segments[libIndex + 2], fileName: segments[libIndex + 3], kind: "scalar" };
15755
+ }
15756
+ if (segments.length !== libIndex + 3)
15757
+ return null;
15758
+ const fileName = segments[libIndex + 2];
15759
+ if (moduleName.startsWith("__")) {
15760
+ const scalarName = moduleName === "__scalar" ? segments[libIndex + 2] : moduleName;
15761
+ return { moduleName: scalarName, fileName, kind: "scalar" };
15762
+ }
15763
+ if (moduleName.startsWith("_"))
15764
+ return { moduleName, fileName, kind: "service" };
15765
+ return { moduleName, fileName, kind: "database" };
15766
+ }
15767
+ function isExportedConst(declaration) {
15768
+ return declaration.exported && ts11.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts11.NodeFlags.Const) !== 0;
15769
+ }
15770
+ function isExported(node) {
15771
+ return !!ts11.getCombinedModifierFlags(node) && !!(ts11.getCombinedModifierFlags(node) & ts11.ModifierFlags.Export);
15772
+ }
15773
+ function isFunctionLikeInitializer(node) {
15774
+ return !!node && (ts11.isArrowFunction(node) || ts11.isFunctionExpression(node));
15775
+ }
15776
+ function getBodyFingerprint(sourceFile2, node) {
15777
+ if (!node)
15778
+ return;
15779
+ const normalizedBody = node.getText(sourceFile2).replace(/\s+/g, " ").trim();
15780
+ if (normalizedBody.length < 80)
15781
+ return;
15782
+ return createHash("sha256").update(normalizedBody).digest("hex");
15783
+ }
15784
+ function shouldSkipPath(relativePath, isDirectory, ignoreFilter) {
15785
+ const ignorePath = isDirectory ? `${relativePath}/` : relativePath;
15786
+ return relativePath === ".git" || relativePath.includes("/.git/") || relativePath.includes("/node_modules/") || ignoreFilter.ignores(relativePath) || ignoreFilter.ignores(ignorePath);
15787
+ }
15788
+ async function isDirectory(absolutePath) {
15789
+ try {
15790
+ return (await stat4(absolutePath)).isDirectory();
15791
+ } catch {
15792
+ return false;
15793
+ }
15794
+ }
15795
+ function getScriptKind(file) {
15796
+ return file.endsWith(".tsx") ? ts11.ScriptKind.TSX : ts11.ScriptKind.TS;
15797
+ }
15798
+ function getLine(sourceFile2, node) {
15799
+ return sourceFile2.getLineAndCharacterOfPosition(node.getStart(sourceFile2)).line + 1;
15800
+ }
15801
+ function getRecommendedLineLimit(file) {
15802
+ if (file.endsWith(".service.ts"))
15803
+ return 500;
15804
+ if (file.endsWith(".Template.tsx") || file.endsWith(".Zone.tsx"))
15805
+ return 800;
15806
+ if (file.endsWith(".Util.tsx"))
15807
+ return 1000;
15808
+ return null;
15809
+ }
15810
+ function findPatternLines(content, pattern) {
15811
+ return content.split(/\r?\n/).flatMap((line, index) => pattern.test(line) ? [index + 1] : []);
15812
+ }
15813
+ function toPascalCase(value) {
15814
+ return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
15815
+ }
15816
+ function toPosix(value) {
15817
+ return value.split(path43.sep).join("/");
15818
+ }
15819
+ function groupBy(items, getKey) {
15820
+ const grouped = new Map;
15821
+ for (const item of items) {
15822
+ const key = getKey(item);
15823
+ grouped.set(key, [...grouped.get(key) ?? [], item]);
15824
+ }
15825
+ return grouped;
15826
+ }
15827
+ function compareWarnings(a, b) {
15828
+ return a.scope.localeCompare(b.scope) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule);
15829
+ }
15080
15830
  // pkgs/@akanjs/devkit/selectModel.ts
15081
15831
  import { select as select5 } from "@inquirer/prompts";
15082
15832
  // pkgs/@akanjs/devkit/streamAi.ts
@@ -16038,7 +16788,7 @@ class ApplicationCommand extends command("application", [ApplicationScript], ({
16038
16788
  import { Logger as Logger16 } from "akanjs/common";
16039
16789
 
16040
16790
  // pkgs/@akanjs/cli/package/package.runner.ts
16041
- import path42 from "path";
16791
+ import path44 from "path";
16042
16792
  import { Logger as Logger14 } from "akanjs/common";
16043
16793
  var {$: $2 } = globalThis.Bun;
16044
16794
 
@@ -16058,11 +16808,11 @@ class PackageRunner extends runner("package") {
16058
16808
  }
16059
16809
  async#getInstalledPackageJson() {
16060
16810
  const packageJsonCandidates = [
16061
- `${path42.dirname(Bun.main)}/package.json`,
16811
+ `${path44.dirname(Bun.main)}/package.json`,
16062
16812
  `${process.cwd()}/node_modules/akanjs/package.json`
16063
16813
  ];
16064
16814
  try {
16065
- packageJsonCandidates.unshift(Bun.resolveSync("akanjs/package.json", path42.dirname(Bun.main)));
16815
+ packageJsonCandidates.unshift(Bun.resolveSync("akanjs/package.json", path44.dirname(Bun.main)));
16066
16816
  } catch {}
16067
16817
  for (const packageJsonPath of packageJsonCandidates) {
16068
16818
  if (!await Bun.file(packageJsonPath).exists())
@@ -16071,7 +16821,7 @@ class PackageRunner extends runner("package") {
16071
16821
  if (packageJson.name === "akanjs" || packageJson.name === "@akanjs/cli")
16072
16822
  return packageJson;
16073
16823
  }
16074
- throw new Error(`[package] failed to locate akanjs package.json from ${path42.dirname(Bun.main)}`);
16824
+ throw new Error(`[package] failed to locate akanjs package.json from ${path44.dirname(Bun.main)}`);
16075
16825
  }
16076
16826
  async createPackage(workspace, pkgName) {
16077
16827
  await workspace.applyTemplate({ basePath: `pkgs/${pkgName}`, template: "pkgRoot", dict: { pkgName } });
@@ -16246,7 +16996,7 @@ class PackageScript extends script("package", [PackageRunner]) {
16246
16996
  }
16247
16997
 
16248
16998
  // pkgs/@akanjs/cli/cloud/cloud.runner.ts
16249
- import path43 from "path";
16999
+ import path45 from "path";
16250
17000
  import { confirm as confirm4, input as input5, select as select8 } from "@inquirer/prompts";
16251
17001
  import { Logger as Logger15, sleep } from "akanjs/common";
16252
17002
  import chalk7 from "chalk";
@@ -16517,7 +17267,7 @@ ${chalk7.green("\u27A4")} Authentication Required`));
16517
17267
  await Promise.all(akanPkgs.map(async (library) => {
16518
17268
  Logger15.info(`Publishing ${library}@${nextVersion} to ${registry ?? "npm"}...`);
16519
17269
  await workspace.spawn("npm", ["publish", "--tag", tag, ...this.#getRegistryArgs(registry), ...this.#getLocalRegistryAuthArgs(registry)], {
16520
- cwd: path43.join(workspace.workspaceRoot, "dist/pkgs", library),
17270
+ cwd: path45.join(workspace.workspaceRoot, "dist/pkgs", library),
16521
17271
  env: this.#getRegistryEnv(registry),
16522
17272
  stdio: "inherit"
16523
17273
  });
@@ -16571,11 +17321,12 @@ ${chalk7.green("\u27A4")} Authentication Required`));
16571
17321
  async downloadEnv(cloudApi2, workspace, workspaceId) {
16572
17322
  await workspace.mkdir("local");
16573
17323
  const localPath = await cloudApi2.downloadEnv(workspaceId);
16574
- await workspace.spawn("tar", ["-xf", localPath], { cwd: workspace.workspaceRoot });
17324
+ const relativePath = path45.relative(workspace.workspaceRoot, localPath).split(path45.sep).join("/");
17325
+ await workspace.spawn("tar", ["-xf", relativePath], { cwd: workspace.workspaceRoot });
16575
17326
  await workspace.remove(localPath);
16576
17327
  }
16577
17328
  async uploadEnv(cloudApi2, workspaceId, filePath) {
16578
- const file = new File([Bun.file(filePath)], path43.basename(filePath));
17329
+ const file = new File([Bun.file(filePath)], path45.basename(filePath));
16579
17330
  await cloudApi2.uploadEnv(workspaceId, file);
16580
17331
  }
16581
17332
  async downloadEnvByScp(workspace) {
@@ -16625,7 +17376,10 @@ ${chalk7.green("\u27A4")} Authentication Required`));
16625
17376
  ...appNames.map((appName) => `apps/${appName}/env`),
16626
17377
  ...libNames.map((libName) => `libs/${libName}/env`)
16627
17378
  ];
16628
- const envFilePaths = (await Promise.all(envDirs.map(async (envDir) => (await workspace.readdir(envDir)).filter((fileName) => envFilePattern.test(fileName)).map((fileName) => `${envDir}/${fileName}`)))).flat().sort();
17379
+ const defaultEnvFilePaths = (await Promise.all(envDirs.map(async (envDir) => (await workspace.readdir(envDir)).filter((fileName) => envFilePattern.test(fileName)).map((fileName) => `${envDir}/${fileName}`)))).flat();
17380
+ await this.#syncSecretGitignore(workspace, appNames);
17381
+ const customSecretPaths = await this.#gatherCustomSecretFiles(workspace, appNames);
17382
+ const envFilePaths = [...new Set([...defaultEnvFilePaths, ...customSecretPaths])].sort();
16629
17383
  await workspace.mkdir("local");
16630
17384
  await workspace.remove("local/env.tar");
16631
17385
  if (envFilePaths.length === 0)
@@ -16636,6 +17390,52 @@ ${chalk7.green("\u27A4")} Authentication Required`));
16636
17390
  Logger15.info(`Archived ${envFilePaths.length} environment files to local/env.tar`);
16637
17391
  return { files: envFilePaths, path: "local/env.tar" };
16638
17392
  }
17393
+ async#gatherCustomSecretFiles(workspace, appNames) {
17394
+ const secretPaths = await Promise.all(appNames.map(async (appName) => {
17395
+ const config = await AppExecutor.from(workspace, appName).getConfig();
17396
+ const secretGlobs = config.secrets ?? [];
17397
+ if (secretGlobs.length === 0)
17398
+ return [];
17399
+ const appDir = path45.join(workspace.workspaceRoot, "apps", appName);
17400
+ return secretGlobs.flatMap((pattern) => Array.from(new Bun.Glob(pattern).scanSync({ cwd: appDir, onlyFiles: true })).map((match) => `apps/${appName}/${match.split(path45.sep).join("/")}`));
17401
+ }));
17402
+ return secretPaths.flat();
17403
+ }
17404
+ async#syncSecretGitignore(workspace, appNames) {
17405
+ const patterns = (await Promise.all(appNames.map(async (appName) => {
17406
+ const config = await AppExecutor.from(workspace, appName).getConfig();
17407
+ return (config.secrets ?? []).map((pattern) => `apps/${appName}/${pattern.replace(/^\/+/, "")}`);
17408
+ }))).flat();
17409
+ const uniquePatterns = [...new Set(patterns)].sort();
17410
+ const existing = await workspace.exists(".gitignore") ? await workspace.readFile(".gitignore") : "";
17411
+ const nextContent = this.#applySecretGitignoreBlock(existing, uniquePatterns);
17412
+ if (nextContent === existing)
17413
+ return;
17414
+ await workspace.writeFile(".gitignore", nextContent);
17415
+ Logger15.info(uniquePatterns.length ? `Synced ${uniquePatterns.length} secret pattern(s) from akan.config.ts to .gitignore` : "Removed managed secret patterns from .gitignore");
17416
+ }
17417
+ #applySecretGitignoreBlock(content, patterns) {
17418
+ const beginMarker = "# akan:secrets (managed by akan.config.ts \u2014 do not edit)";
17419
+ const endMarker = "# akan:secrets:end";
17420
+ const lines = content.split(`
17421
+ `);
17422
+ const beginIdx = lines.indexOf(beginMarker);
17423
+ const endIdx = lines.indexOf(endMarker);
17424
+ const stripped = beginIdx !== -1 && endIdx !== -1 && endIdx >= beginIdx ? [...lines.slice(0, beginIdx), ...lines.slice(endIdx + 1)] : [...lines];
17425
+ while (stripped.length && stripped[stripped.length - 1]?.trim() === "")
17426
+ stripped.pop();
17427
+ if (patterns.length === 0)
17428
+ return stripped.length ? `${stripped.join(`
17429
+ `)}
17430
+ ` : "";
17431
+ const body = stripped.length ? `${stripped.join(`
17432
+ `)}
17433
+
17434
+ ` : "";
17435
+ return `${body}${[beginMarker, ...patterns, endMarker].join(`
17436
+ `)}
17437
+ `;
17438
+ }
16639
17439
  }
16640
17440
 
16641
17441
  // pkgs/@akanjs/cli/cloud/cloud.script.ts
@@ -16667,14 +17467,14 @@ class CloudScript extends script("cloud", [CloudRunner, ApplicationScript, Packa
16667
17467
  }
16668
17468
  async uploadEnv(workspace) {
16669
17469
  const workspaceId = workspace.getWorkspaceId({ allowEmpty: true });
16670
- const { path: path44 } = await this.cloudRunner.gatherEnvFiles(workspace);
17470
+ const { path: path46 } = await this.cloudRunner.gatherEnvFiles(workspace);
16671
17471
  if (workspaceId) {
16672
17472
  await this.login(workspace);
16673
17473
  const cloudApi2 = await CloudApi.fromHost(workspace);
16674
- await this.cloudRunner.uploadEnv(cloudApi2, workspaceId, path44);
17474
+ await this.cloudRunner.uploadEnv(cloudApi2, workspaceId, path46);
16675
17475
  return;
16676
17476
  }
16677
- await this.cloudRunner.uploadEnvByScp(workspace, path44);
17477
+ await this.cloudRunner.uploadEnvByScp(workspace, path46);
16678
17478
  }
16679
17479
  async deployAkan(workspace, { test = true, registryUrl } = {}) {
16680
17480
  const akanPkgs = await this.cloudRunner.getAkanPkgs(workspace);
@@ -16915,8 +17715,8 @@ class RepairRunner extends runner("repair") {
16915
17715
  }
16916
17716
 
16917
17717
  // pkgs/@akanjs/cli/workflow/workflow.runner.ts
16918
- import { mkdir as mkdir12, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
16919
- import path44 from "path";
17718
+ import { mkdir as mkdir12, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
17719
+ import path46 from "path";
16920
17720
  import { capitalize as capitalize8 } from "akanjs/common";
16921
17721
 
16922
17722
  // pkgs/@akanjs/cli/workflows/shared.ts
@@ -17459,7 +18259,7 @@ var workflowSpecs = [
17459
18259
  ];
17460
18260
 
17461
18261
  // pkgs/@akanjs/cli/workflow/workflow.runner.ts
17462
- var resolvePath = (filePath) => path44.isAbsolute(filePath) ? filePath : path44.join(process.cwd(), filePath);
18262
+ var resolvePath = (filePath) => path46.isAbsolute(filePath) ? filePath : path46.join(process.cwd(), filePath);
17463
18263
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17464
18264
  var isWorkflowPlan2 = (value) => {
17465
18265
  if (!isRecord3(value))
@@ -17557,7 +18357,7 @@ var defaultValidationExecutor = (workspace) => async (command3) => {
17557
18357
  };
17558
18358
  }
17559
18359
  };
17560
- var readJsonFile = async (filePath) => JSON.parse(await readFile2(resolvePath(filePath), "utf8"));
18360
+ var readJsonFile = async (filePath) => JSON.parse(await readFile4(resolvePath(filePath), "utf8"));
17561
18361
  var planInputString2 = (plan2, key) => {
17562
18362
  const value = plan2.inputs[key];
17563
18363
  return typeof value === "string" ? value : "";
@@ -17653,8 +18453,8 @@ class WorkflowRunner extends runner("workflow") {
17653
18453
  const plan2 = createWorkflowPlan(spec, compactWorkflowInputs(inputs));
17654
18454
  if (out) {
17655
18455
  const outPath = resolvePath(out);
17656
- await mkdir12(path44.dirname(outPath), { recursive: true });
17657
- await writeFile2(outPath, jsonText(plan2));
18456
+ await mkdir12(path46.dirname(outPath), { recursive: true });
18457
+ await writeFile3(outPath, jsonText(plan2));
17658
18458
  }
17659
18459
  return format === "json" ? jsonText(plan2) : renderWorkflowPlan(plan2);
17660
18460
  }
@@ -17672,7 +18472,7 @@ class WorkflowRunner extends runner("workflow") {
17672
18472
  };
17673
18473
  let plan2;
17674
18474
  try {
17675
- const parsed = JSON.parse(await readFile2(resolvePath(planPath), "utf8"));
18475
+ const parsed = JSON.parse(await readFile4(resolvePath(planPath), "utf8"));
17676
18476
  if (!isWorkflowPlan2(parsed)) {
17677
18477
  const report = failedApplyReport("unknown", [
17678
18478
  {
@@ -19477,14 +20277,14 @@ class GuidelinePrompt extends Prompter {
19477
20277
  async#getScanFilePaths(matchPattern, { avoidDirs = ["node_modules", ".next"], filterText } = {}) {
19478
20278
  const glob = new Bun.Glob(matchPattern);
19479
20279
  const paths = [];
19480
- for await (const path45 of glob.scan({ cwd: this.workspace.workspaceRoot, absolute: true })) {
19481
- if (avoidDirs.some((dir) => path45.includes(dir)))
20280
+ for await (const path47 of glob.scan({ cwd: this.workspace.workspaceRoot, absolute: true })) {
20281
+ if (avoidDirs.some((dir) => path47.includes(dir)))
19482
20282
  continue;
19483
- const fileContent = await FileSys.readText(path45);
20283
+ const fileContent = await FileSys.readText(path47);
19484
20284
  const textFilter = filterText ? new RegExp(filterText) : null;
19485
20285
  if (filterText && !textFilter?.test(fileContent))
19486
20286
  continue;
19487
- paths.push(path45);
20287
+ paths.push(path47);
19488
20288
  }
19489
20289
  return paths;
19490
20290
  }
@@ -19804,7 +20604,7 @@ class LibraryCommand extends command("library", [LibraryScript], ({ public: targ
19804
20604
 
19805
20605
  // pkgs/@akanjs/cli/localRegistry/localRegistry.runner.ts
19806
20606
  import { mkdir as mkdir13, rm as rm6 } from "fs/promises";
19807
- import path45 from "path";
20607
+ import path47 from "path";
19808
20608
  import { Logger as Logger19 } from "akanjs/common";
19809
20609
  var defaultLocalRegistryUrl = "http://127.0.0.1:4873";
19810
20610
  var containerName = "akan-verdaccio";
@@ -19822,8 +20622,8 @@ class LocalRegistryRunner extends runner("localRegistry") {
19822
20622
  Logger19.info(`Local registry is already running at ${registry}`);
19823
20623
  return registry;
19824
20624
  } catch {}
19825
- const configPath2 = path45.join(workspace.workspaceRoot, "pkgs/@akanjs/cli/localRegistry/verdaccio.yaml");
19826
- const storagePath = path45.join(workspace.workspaceRoot, ".akan/verdaccio/storage");
20625
+ const configPath2 = path47.join(workspace.workspaceRoot, "pkgs/@akanjs/cli/localRegistry/verdaccio.yaml");
20626
+ const storagePath = path47.join(workspace.workspaceRoot, ".akan/verdaccio/storage");
19827
20627
  await mkdir13(storagePath, { recursive: true });
19828
20628
  await workspace.spawn("docker", [
19829
20629
  "run",
@@ -19846,13 +20646,13 @@ class LocalRegistryRunner extends runner("localRegistry") {
19846
20646
  try {
19847
20647
  await workspace.spawn("docker", ["rm", "-f", containerName], { stdio: "inherit" });
19848
20648
  } catch {}
19849
- await rm6(path45.join(workspace.workspaceRoot, ".akan/verdaccio"), { recursive: true, force: true });
20649
+ await rm6(path47.join(workspace.workspaceRoot, ".akan/verdaccio"), { recursive: true, force: true });
19850
20650
  Logger19.info("Local registry storage has been reset");
19851
20651
  }
19852
20652
  async smoke(workspace, { registryUrl } = {}) {
19853
20653
  const registry = this.getRegistryUrl(registryUrl);
19854
- const smokeRoot = path45.join(workspace.workspaceRoot, ".akan/e2e");
19855
- await rm6(path45.join(smokeRoot, smokeRepoName), { recursive: true, force: true });
20654
+ const smokeRoot = path47.join(workspace.workspaceRoot, ".akan/e2e");
20655
+ await rm6(path47.join(smokeRoot, smokeRepoName), { recursive: true, force: true });
19856
20656
  await workspace.spawn(process.execPath, [
19857
20657
  "dist/pkgs/create-akan-workspace/index.js",
19858
20658
  smokeRepoName,
@@ -19869,12 +20669,12 @@ class LocalRegistryRunner extends runner("localRegistry") {
19869
20669
  stdio: "inherit"
19870
20670
  });
19871
20671
  await workspace.spawn("akan", ["typecheck", smokeAppName], {
19872
- cwd: path45.join(smokeRoot, smokeRepoName),
20672
+ cwd: path47.join(smokeRoot, smokeRepoName),
19873
20673
  env: { ...process.env, AKAN_NPM_REGISTRY: registry, NPM_CONFIG_REGISTRY: registry },
19874
20674
  stdio: "inherit"
19875
20675
  });
19876
20676
  await workspace.spawn("akan", ["build", smokeAppName], {
19877
- cwd: path45.join(smokeRoot, smokeRepoName),
20677
+ cwd: path47.join(smokeRoot, smokeRepoName),
19878
20678
  env: { ...process.env, AKAN_NPM_REGISTRY: registry, NPM_CONFIG_REGISTRY: registry },
19879
20679
  stdio: "inherit"
19880
20680
  });
@@ -20053,8 +20853,46 @@ class PrimitiveCommand extends command("primitive", [PrimitiveScript], ({ public
20053
20853
  })) {
20054
20854
  }
20055
20855
 
20056
- // pkgs/@akanjs/cli/repair/repair.script.ts
20856
+ // pkgs/@akanjs/cli/quality/quality.script.ts
20057
20857
  import { Logger as Logger22 } from "akanjs/common";
20858
+
20859
+ // pkgs/@akanjs/cli/quality/quality.runner.ts
20860
+ class QualityRunner extends runner("quality") {
20861
+ async scan(workspace) {
20862
+ return await new AkanQualityScanner().scan(workspace.workspaceRoot);
20863
+ }
20864
+ }
20865
+
20866
+ // pkgs/@akanjs/cli/quality/quality.script.ts
20867
+ class QualityScript extends script("quality", [QualityRunner]) {
20868
+ async scan(workspace, format = "text") {
20869
+ const spinner2 = workspace.spinning("Scanning Akan code quality...");
20870
+ const result = await this.qualityRunner.scan(workspace);
20871
+ spinner2.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
20872
+ Logger22.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
20873
+ }
20874
+ }
20875
+
20876
+ // pkgs/@akanjs/cli/quality/quality.command.ts
20877
+ class QualityCommand extends command("quality", [QualityScript], ({ public: target }) => ({
20878
+ quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
20879
+ desc: "quality action",
20880
+ default: "scan",
20881
+ enum: ["scan"]
20882
+ }).option("format", String, {
20883
+ desc: "output format",
20884
+ default: "text",
20885
+ enum: ["text", "json"]
20886
+ }).with(Workspace).exec(async function(action, format, workspace) {
20887
+ if (action !== "scan")
20888
+ throw new Error(`Unknown quality action: ${action}. Use "scan".`);
20889
+ await this.qualityScript.scan(workspace, format);
20890
+ })
20891
+ })) {
20892
+ }
20893
+
20894
+ // pkgs/@akanjs/cli/repair/repair.script.ts
20895
+ import { Logger as Logger23 } from "akanjs/common";
20058
20896
  class RepairScript extends script("repair", [RepairRunner]) {
20059
20897
  async repair(kind, {
20060
20898
  workspace,
@@ -20063,7 +20901,7 @@ class RepairScript extends script("repair", [RepairRunner]) {
20063
20901
  target = null,
20064
20902
  format = "markdown"
20065
20903
  }) {
20066
- Logger22.rawLog(await this.repairRunner.repair(kind, {
20904
+ Logger23.rawLog(await this.repairRunner.repair(kind, {
20067
20905
  workspace,
20068
20906
  app,
20069
20907
  module,
@@ -20103,12 +20941,12 @@ class RepairCommand extends command("repair", [RepairScript], ({ public: target
20103
20941
  }
20104
20942
 
20105
20943
  // pkgs/@akanjs/cli/scalar/scalar.command.ts
20106
- import { Logger as Logger23, lowerlize as lowerlize3 } from "akanjs/common";
20944
+ import { Logger as Logger24, lowerlize as lowerlize3 } from "akanjs/common";
20107
20945
  class ScalarCommand extends command("scalar", [ScalarScript], ({ public: target }) => ({
20108
20946
  createScalar: target({ desc: "Create a new scalar type (simple data model without DB)" }).arg("scalarName", String, { desc: "name of scalar" }).with(Sys).option("ai", Boolean, { default: false, desc: "use ai to create scalar" }).option("format", String, { flag: "o", desc: "output format", default: "markdown", enum: ["markdown", "json"] }).exec(async function(scalarName, sys3, ai, format) {
20109
20947
  const name = lowerlize3(scalarName.replace(/ /g, ""));
20110
20948
  const report = ai ? await this.scalarScript.createScalarWithAi(sys3, name) : await this.scalarScript.createScalar(sys3, name);
20111
- Logger23.rawLog(renderPrimitiveReport(report, format));
20949
+ Logger24.rawLog(renderPrimitiveReport(report, format));
20112
20950
  }),
20113
20951
  removeScalar: target({ desc: "Remove a scalar type from an app or library" }).arg("scalarName", String, { desc: "name of scalar" }).with(Sys).exec(async function(scalarName, sys3) {
20114
20952
  await this.scalarScript.removeScalar(sys3, scalarName);
@@ -20117,7 +20955,7 @@ class ScalarCommand extends command("scalar", [ScalarScript], ({ public: target
20117
20955
  }
20118
20956
 
20119
20957
  // pkgs/@akanjs/cli/workflow/workflow.script.ts
20120
- import { Logger as Logger24 } from "akanjs/common";
20958
+ import { Logger as Logger25 } from "akanjs/common";
20121
20959
  class WorkflowScript extends script("workflow", [WorkflowRunner, ModuleScript, ScalarScript, PrimitiveScript]) {
20122
20960
  async workflow(action, workflow2, inputs, {
20123
20961
  format = "markdown",
@@ -20126,23 +20964,23 @@ class WorkflowScript extends script("workflow", [WorkflowRunner, ModuleScript, S
20126
20964
  workspace
20127
20965
  } = {}) {
20128
20966
  if (action === "list") {
20129
- Logger24.rawLog(this.workflowRunner.list({ format }));
20967
+ Logger25.rawLog(this.workflowRunner.list({ format }));
20130
20968
  return;
20131
20969
  }
20132
20970
  if (!workflow2)
20133
20971
  throw new Error(`Workflow name is required for "${action}".`);
20134
20972
  if (action === "explain") {
20135
- Logger24.rawLog(this.workflowRunner.explain(workflow2, { format }));
20973
+ Logger25.rawLog(this.workflowRunner.explain(workflow2, { format }));
20136
20974
  return;
20137
20975
  }
20138
20976
  if (action === "plan") {
20139
- Logger24.rawLog(await this.workflowRunner.plan(workflow2, inputs, { format, out }));
20977
+ Logger25.rawLog(await this.workflowRunner.plan(workflow2, inputs, { format, out }));
20140
20978
  return;
20141
20979
  }
20142
20980
  if (action === "apply") {
20143
20981
  if (!workspace)
20144
20982
  throw new Error("Workspace is required for workflow apply.");
20145
- Logger24.rawLog(await this.workflowRunner.apply(workflow2, {
20983
+ Logger25.rawLog(await this.workflowRunner.apply(workflow2, {
20146
20984
  dryRun,
20147
20985
  format,
20148
20986
  workspace,
@@ -20160,13 +20998,13 @@ class WorkflowScript extends script("workflow", [WorkflowRunner, ModuleScript, S
20160
20998
  if (action === "validate") {
20161
20999
  if (!workspace)
20162
21000
  throw new Error("Workspace is required for workflow validate.");
20163
- Logger24.rawLog(await this.workflowRunner.validate(workflow2, { format, workspace }));
21001
+ Logger25.rawLog(await this.workflowRunner.validate(workflow2, { format, workspace }));
20164
21002
  return;
20165
21003
  }
20166
21004
  if (action === "report") {
20167
21005
  if (!workspace)
20168
21006
  throw new Error("Workspace is required for workflow report.");
20169
- Logger24.rawLog(await this.workflowRunner.report(workflow2, { format, workspace }));
21007
+ Logger25.rawLog(await this.workflowRunner.report(workflow2, { format, workspace }));
20170
21008
  return;
20171
21009
  }
20172
21010
  throw new Error(`Unknown workflow action: ${action}. Use list, explain, plan, apply, validate, or report.`);
@@ -20190,11 +21028,11 @@ class WorkflowCommand extends command("workflow", [WorkflowScript], ({ public: t
20190
21028
  }
20191
21029
 
20192
21030
  // pkgs/@akanjs/cli/workspace/workspace.script.ts
20193
- import path47 from "path";
20194
- import { Logger as Logger25 } from "akanjs/common";
21031
+ import path49 from "path";
21032
+ import { Logger as Logger26 } from "akanjs/common";
20195
21033
 
20196
21034
  // pkgs/@akanjs/cli/workspace/workspace.runner.ts
20197
- import path46 from "path";
21035
+ import path48 from "path";
20198
21036
  var defaultWorkspacePeerDependencies = new Set([
20199
21037
  "@react-spring/web",
20200
21038
  "@use-gesture/react",
@@ -20225,6 +21063,12 @@ class WorkspaceRunner extends runner("workspace") {
20225
21063
  dict,
20226
21064
  overwrite
20227
21065
  });
21066
+ created.push(...await workspace.applyTemplate({
21067
+ basePath: ".",
21068
+ template: "workspaceRoot/CLAUDE.md.template",
21069
+ dict,
21070
+ overwrite
21071
+ }));
20228
21072
  if (!cursorRules)
20229
21073
  return created;
20230
21074
  return [
@@ -20245,7 +21089,7 @@ class WorkspaceRunner extends runner("workspace") {
20245
21089
  owner = ""
20246
21090
  }) {
20247
21091
  const cwdPath = process.cwd();
20248
- const workspaceRoot = path46.join(cwdPath, dirname2, repoName);
21092
+ const workspaceRoot = path48.join(cwdPath, dirname2, repoName);
20249
21093
  const normalizedRegistryUrl = registryUrl ? getNpmRegistryUrl(registryUrl) : undefined;
20250
21094
  const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
20251
21095
  const templateSpinner = workspace.spinning(`Creating workspace template files in ${dirname2}/${repoName}...`);
@@ -20301,9 +21145,9 @@ class WorkspaceRunner extends runner("workspace") {
20301
21145
  }
20302
21146
  async#getCliPackageJson() {
20303
21147
  const packageJsonCandidates = [
20304
- path46.join(import.meta.dir, "../package.json"),
20305
- path46.join(import.meta.dir, "package.json"),
20306
- path46.join(path46.dirname(Bun.main), "package.json")
21148
+ path48.join(import.meta.dir, "../package.json"),
21149
+ path48.join(import.meta.dir, "package.json"),
21150
+ path48.join(path48.dirname(Bun.main), "package.json")
20307
21151
  ];
20308
21152
  try {
20309
21153
  packageJsonCandidates.unshift(Bun.resolveSync("@akanjs/cli/package.json", import.meta.dir));
@@ -20319,9 +21163,9 @@ class WorkspaceRunner extends runner("workspace") {
20319
21163
  }
20320
21164
  async#getAkanPackageJson() {
20321
21165
  const packageJsonCandidates = [
20322
- path46.join(import.meta.dir, "../../../akanjs/package.json"),
20323
- path46.join(process.cwd(), "pkgs/akanjs/package.json"),
20324
- path46.join(path46.dirname(Bun.main), "node_modules/akanjs/package.json")
21166
+ path48.join(import.meta.dir, "../../../akanjs/package.json"),
21167
+ path48.join(process.cwd(), "pkgs/akanjs/package.json"),
21168
+ path48.join(path48.dirname(Bun.main), "node_modules/akanjs/package.json")
20325
21169
  ];
20326
21170
  try {
20327
21171
  packageJsonCandidates.unshift(Bun.resolveSync("akanjs/package.json", import.meta.dir));
@@ -20335,13 +21179,13 @@ class WorkspaceRunner extends runner("workspace") {
20335
21179
  }
20336
21180
  let current = import.meta.dir;
20337
21181
  for (let depth = 0;depth < 6; depth++) {
20338
- const packageJsonPath = path46.join(current, "package.json");
21182
+ const packageJsonPath = path48.join(current, "package.json");
20339
21183
  if (await Bun.file(packageJsonPath).exists()) {
20340
21184
  const packageJson = await FileSys.readJson(packageJsonPath);
20341
21185
  if (packageJson.name === "akanjs")
20342
21186
  return packageJson;
20343
21187
  }
20344
- const parent = path46.dirname(current);
21188
+ const parent = path48.dirname(current);
20345
21189
  if (parent === current)
20346
21190
  break;
20347
21191
  current = parent;
@@ -20418,11 +21262,11 @@ class WorkspaceScript extends script("workspace", [
20418
21262
  } catch (_) {
20419
21263
  gitSpinner.fail("Git repository initialization failed. It's not fatal, you can commit manually");
20420
21264
  }
20421
- const workspacePath2 = path47.join(dirname2, repoName);
20422
- Logger25.rawLog(`
21265
+ const workspacePath2 = path49.join(dirname2, repoName);
21266
+ Logger26.rawLog(`
20423
21267
  \uD83C\uDF89 Welcome aboard! Workspace created in ${dirname2}/${repoName}`);
20424
- Logger25.rawLog(`\uD83D\uDE80 Run \`cd ${workspacePath2} && akan start ${appName}\` to start the development server.`);
20425
- Logger25.rawLog(`
21268
+ Logger26.rawLog(`\uD83D\uDE80 Run \`cd ${workspacePath2} && akan start ${appName}\` to start the development server.`);
21269
+ Logger26.rawLog(`
20426
21270
  \uD83D\uDC4B Happy coding!`);
20427
21271
  }
20428
21272
  async generateAgentRules(workspace, { overwrite = false, cursorRules = true } = {}) {
@@ -20519,7 +21363,9 @@ class WorkspaceCommand extends command("workspace", [WorkspaceScript], ({ public
20519
21363
  ...registry ? { registryUrl: registry } : {}
20520
21364
  });
20521
21365
  }),
20522
- generateAgentRules: target({ desc: "Generate AGENTS.md and optional Cursor rules for Akan coding agents" }).option("overwrite", Boolean, { desc: "Overwrite existing agent rule files", default: false }).option("cursorRules", Boolean, { desc: "Generate .cursor/rules/akan.mdc", default: true }).with(Workspace).exec(async function(overwrite, cursorRules, workspace) {
21366
+ generateAgentRules: target({
21367
+ desc: "Generate AGENTS.md, CLAUDE.md, and optional Cursor rules for Akan coding agents"
21368
+ }).option("overwrite", Boolean, { desc: "Overwrite existing agent rule files", default: false }).option("cursorRules", Boolean, { desc: "Generate .cursor/rules/akan.mdc", default: true }).with(Workspace).exec(async function(overwrite, cursorRules, workspace) {
20523
21369
  await this.workspaceScript.generateAgentRules(workspace, { overwrite, cursorRules });
20524
21370
  }),
20525
21371
  lint: target({ desc: "Lint and fix code in a specific app/lib/pkg" }).with(Exec).option("fix", Boolean, { default: true }).with(Workspace).exec(async function(exec2, fix, workspace) {
@@ -20538,4 +21384,4 @@ class WorkspaceCommand extends command("workspace", [WorkspaceScript], ({ public
20538
21384
  }
20539
21385
 
20540
21386
  // pkgs/@akanjs/cli/index.ts
20541
- runCommands(WorkspaceCommand, AgentCommand, ApplicationCommand, LibraryCommand, LocalRegistryCommand, PackageCommand, ModuleCommand, PageCommand, ContextCommand, CloudCommand, GuidelineCommand, ScalarCommand, PrimitiveCommand, RepairCommand, WorkflowCommand);
21387
+ runCommands(WorkspaceCommand, AgentCommand, ApplicationCommand, LibraryCommand, LocalRegistryCommand, PackageCommand, ModuleCommand, PageCommand, ContextCommand, CloudCommand, GuidelineCommand, ScalarCommand, PrimitiveCommand, QualityCommand, RepairCommand, WorkflowCommand);