@akanjs/cli 2.3.9 → 2.3.10-rc.1

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.
@@ -744,6 +744,44 @@ var DEFAULT_AKAN_IMAGE_CONFIG = {
744
744
  fetchTimeoutMs: 7000,
745
745
  maxRemoteBytes: 25 * 1024 * 1024
746
746
  };
747
+ var normalizeIndexPath = (indexPath) => {
748
+ const normalized = indexPath?.trim();
749
+ if (!normalized)
750
+ return;
751
+ const path2 = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
752
+ return path2 === "/" ? "/" : path2;
753
+ };
754
+ var normalizeStringList = (values) => {
755
+ const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
756
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
757
+ };
758
+ var normalizeDeepLinkDomain = (domain) => {
759
+ const normalized = domain.trim();
760
+ if (!normalized)
761
+ return "";
762
+ try {
763
+ const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
764
+ return url.host.toLowerCase();
765
+ } catch {
766
+ return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
767
+ }
768
+ };
769
+ var normalizeDeepLinks = (deepLinks) => {
770
+ if (!deepLinks)
771
+ return;
772
+ const schemes = normalizeStringList(deepLinks.schemes);
773
+ const domains = normalizeStringList(deepLinks.domains?.map(normalizeDeepLinkDomain));
774
+ const teamId = deepLinks.ios?.teamId?.trim();
775
+ const sha256CertFingerprints = normalizeStringList(deepLinks.android?.sha256CertFingerprints);
776
+ if (!schemes && !domains && !teamId && !sha256CertFingerprints)
777
+ return;
778
+ return {
779
+ ...schemes ? { schemes } : {},
780
+ ...domains ? { domains } : {},
781
+ ...teamId ? { ios: { teamId } } : {},
782
+ ...sha256CertFingerprints ? { android: { sha256CertFingerprints } } : {}
783
+ };
784
+ };
747
785
 
748
786
  class AkanAppConfig {
749
787
  app;
@@ -757,6 +795,7 @@ class AkanAppConfig {
757
795
  i18n;
758
796
  publicEnv;
759
797
  mobile;
798
+ secrets;
760
799
  baseDevEnv;
761
800
  libs;
762
801
  domains = new Set;
@@ -783,11 +822,16 @@ class AkanAppConfig {
783
822
  process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
784
823
  process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
785
824
  this.publicEnv = config?.publicEnv ?? [];
825
+ this.secrets = config?.secrets ?? [];
786
826
  this.mobile = this.#resolveMobileConfig(config.mobile);
787
827
  this.docker = this.#makeDockerContent(config?.docker ?? {});
788
828
  }
789
829
  #resolveMobileConfig(mobile) {
790
- const { targets: rawTargets, ...rawMobile } = mobile ?? {};
830
+ const {
831
+ targets: rawTargets,
832
+ indexPath: _indexPath,
833
+ ...rawMobile
834
+ } = mobile ?? {};
791
835
  const appName = rawMobile.appName ?? this.app.name;
792
836
  const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
793
837
  const version = rawMobile.version ?? "0.0.1";
@@ -800,6 +844,8 @@ class AkanAppConfig {
800
844
  const target = rawTarget;
801
845
  const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
802
846
  const basePath2 = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
847
+ const indexPath = normalizeIndexPath(target.indexPath);
848
+ const deepLinks = normalizeDeepLinks(target.deepLinks);
803
849
  if (basePath2 && !this.basePaths.has(basePath2)) {
804
850
  throw new Error(`Mobile target '${name}' uses unknown basePath '${basePath2}' in apps/${this.app.name}/akan.config.ts`);
805
851
  }
@@ -808,6 +854,8 @@ class AkanAppConfig {
808
854
  ...target,
809
855
  name,
810
856
  basePath: basePath2,
857
+ indexPath,
858
+ deepLinks,
811
859
  appName: target.appName ?? appName,
812
860
  appId: target.appId ?? appId,
813
861
  version: target.version ?? version,
@@ -2746,8 +2794,8 @@ class Executor {
2746
2794
  return filePath;
2747
2795
  if (filePath.startsWith("."))
2748
2796
  return path7.join(this.cwdPath, filePath);
2749
- const baseParts = this.cwdPath.split("/").filter(Boolean);
2750
- const targetParts = filePath.split("/").filter(Boolean);
2797
+ const baseParts = this.cwdPath.split(/[\\/]/).filter(Boolean);
2798
+ const targetParts = filePath.split(/[\\/]/).filter(Boolean);
2751
2799
  let overlapLength = 0;
2752
2800
  for (let i = 1;i <= Math.min(baseParts.length, targetParts.length); i++) {
2753
2801
  let isOverlap = true;
@@ -2759,8 +2807,7 @@ class Executor {
2759
2807
  if (isOverlap)
2760
2808
  overlapLength = i;
2761
2809
  }
2762
- const result = overlapLength > 0 ? `/${[...baseParts, ...targetParts.slice(overlapLength)].join("/")}` : `${this.cwdPath}/${filePath}`;
2763
- return result.replace(/\/+/g, "/");
2810
+ return path7.join(this.cwdPath, ...targetParts.slice(overlapLength));
2764
2811
  }
2765
2812
  async mkdir(dirPath) {
2766
2813
  const writePath = this.getPath(dirPath);
@@ -10225,6 +10272,7 @@ import path24 from "path";
10225
10272
  // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
10226
10273
  import fs3 from "fs";
10227
10274
  import path23 from "path";
10275
+ import { pathToFileURL } from "url";
10228
10276
  import ts8 from "typescript";
10229
10277
 
10230
10278
  class PagesEntrySourceGenerator {
@@ -10237,8 +10285,8 @@ class PagesEntrySourceGenerator {
10237
10285
  }
10238
10286
  generate() {
10239
10287
  const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
10240
- const absPath = path23.resolve(moduleAbsPath);
10241
- return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(absPath)}),`;
10288
+ const specifier = pathToFileURL(path23.resolve(moduleAbsPath)).href;
10289
+ return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
10242
10290
  });
10243
10291
  return `export const pages = {
10244
10292
  ${lines.join(`
@@ -10251,8 +10299,8 @@ ${lines.join(`
10251
10299
  }
10252
10300
  generateStatic() {
10253
10301
  const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
10254
- const absPath = path23.resolve(moduleAbsPath);
10255
- return `import * as page${index} from ${JSON.stringify(absPath)};`;
10302
+ const specifier = pathToFileURL(path23.resolve(moduleAbsPath)).href;
10303
+ return `import * as page${index} from ${JSON.stringify(specifier)};`;
10256
10304
  });
10257
10305
  const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
10258
10306
  const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
@@ -12116,7 +12164,14 @@ class SsrBaseArtifactBuilder {
12116
12164
  basePaths: [...akanConfig2.basePaths],
12117
12165
  branches: [...akanConfig2.branches],
12118
12166
  i18n: akanConfig2.i18n,
12119
- imageConfig: akanConfig2.images
12167
+ imageConfig: akanConfig2.images,
12168
+ deepLinkAssociations: Object.values(akanConfig2.mobile.targets).filter((target) => (target.deepLinks?.domains?.length ?? 0) > 0).map((target) => ({
12169
+ targetName: target.name,
12170
+ appId: target.appId,
12171
+ domains: target.deepLinks?.domains ?? [],
12172
+ iosTeamId: target.deepLinks?.ios?.teamId,
12173
+ androidSha256CertFingerprints: target.deepLinks?.android?.sha256CertFingerprints
12174
+ }))
12120
12175
  };
12121
12176
  await Bun.write(path35.join(this.#absArtifactDir, "base-artifact.json"), `${JSON.stringify(artifact, null, 2)}
12122
12177
  `);
@@ -12125,11 +12180,11 @@ class SsrBaseArtifactBuilder {
12125
12180
  }
12126
12181
  async#buildRuntimeClientEntries() {
12127
12182
  const akanServerPath = await this.#resolveAkanServerPath();
12128
- const rscClientEntry = `${akanServerPath}/rscClient.tsx`;
12129
- const rscSegmentOutletEntry = `${akanServerPath}/rscSegmentOutlet.tsx`;
12183
+ const rscClientEntry = path35.resolve(akanServerPath, "rscClient.tsx");
12184
+ const rscSegmentOutletEntry = path35.resolve(akanServerPath, "rscSegmentOutlet.tsx");
12130
12185
  const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
12131
12186
  specifier,
12132
- absPath: `${akanServerPath}/vendor/${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`
12187
+ absPath: path35.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
12133
12188
  }));
12134
12189
  const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
12135
12190
  const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
@@ -12890,7 +12945,7 @@ class Builder {
12890
12945
  }
12891
12946
  }
12892
12947
  // pkgs/@akanjs/devkit/capacitorApp.ts
12893
- import { cp as cp2, mkdir as mkdir11, rm as rm5 } from "fs/promises";
12948
+ import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm5, writeFile as writeFile2 } from "fs/promises";
12894
12949
  import os from "os";
12895
12950
  import path41 from "path";
12896
12951
  import { select as select2 } from "@inquirer/prompts";
@@ -13404,11 +13459,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13404
13459
  const {
13405
13460
  name,
13406
13461
  basePath: _basePath,
13462
+ indexPath: _indexPath,
13407
13463
  version: _version,
13408
13464
  buildNum: _buildNum,
13409
13465
  assets: _assets,
13410
13466
  permissions: _permissions,
13411
- links: _links,
13467
+ deepLinks: _deepLinks,
13412
13468
  files: _files,
13413
13469
  appId,
13414
13470
  appName,
@@ -13530,7 +13586,7 @@ class CapacitorApp {
13530
13586
  await this.#prepareExternalFiles("ios");
13531
13587
  await this.#applyIosMetadata();
13532
13588
  await this.#applyPermissions();
13533
- await this.#applyLinks();
13589
+ await this.#applyDeepLinks("ios", { operation, env });
13534
13590
  await this.project.commit();
13535
13591
  await this.#generateAssets({ operation, env });
13536
13592
  this.app.verbose(`syncing iOS`);
@@ -13673,12 +13729,13 @@ ${error.message}`;
13673
13729
  await this.#prepareExternalFiles("android");
13674
13730
  await this.#applyAndroidMetadata();
13675
13731
  await this.#applyPermissions();
13676
- await this.#applyLinks();
13732
+ await this.#applyDeepLinks("android", { operation, env });
13677
13733
  await this.project.commit();
13678
13734
  await this.#generateAssets({ operation, env });
13679
13735
  await this.#ensureAndroidAssetsDir();
13680
13736
  await this.#ensureAndroidDebugKeystore();
13681
13737
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
13738
+ await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
13682
13739
  }
13683
13740
  async#updateAndroidBuildTypes() {
13684
13741
  const appGradle = await FileEditor.create(path41.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
@@ -13811,7 +13868,11 @@ ${error.message}`;
13811
13868
  }
13812
13869
  #injectMobileTargetMeta(html) {
13813
13870
  const basePath2 = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13814
- const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({ name: this.target.name, basePath: basePath2 })};</script>`;
13871
+ const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
13872
+ name: this.target.name,
13873
+ basePath: basePath2,
13874
+ indexPath: this.target.indexPath
13875
+ })};</script>`;
13815
13876
  if (html.includes("window.__AKAN_MOBILE_TARGET__"))
13816
13877
  return html;
13817
13878
  return html.replace(/<\/head\s*>/i, `${script}
@@ -13894,25 +13955,48 @@ ${error.message}`;
13894
13955
  await this.addPush();
13895
13956
  }
13896
13957
  }
13897
- async#applyLinks() {
13898
- const links = this.target.links;
13899
- if (!links)
13958
+ async#applyDeepLinks(platform, { operation, env }) {
13959
+ const deepLinks = this.target.deepLinks;
13960
+ if (!deepLinks)
13900
13961
  return;
13901
- const schemes = links.schemes ?? [];
13902
- if (schemes.length > 0) {
13903
- await this.#setPermissionInIos({
13904
- appTransportSecurity: ""
13905
- });
13962
+ const schemes = deepLinks.schemes ?? [];
13963
+ const domains = deepLinks.domains ?? [];
13964
+ if (domains.length > 0)
13965
+ this.#assertDeepLinkVerificationConfig(platform, { operation, env });
13966
+ if (platform === "ios") {
13967
+ if (schemes.length > 0) {
13968
+ await this.#setPermissionInIos({
13969
+ appTransportSecurity: ""
13970
+ });
13971
+ await this.#setUrlSchemesInIos(schemes);
13972
+ }
13973
+ if (domains.length > 0)
13974
+ await this.#setAssociatedDomainsInIos(domains);
13975
+ return;
13976
+ }
13977
+ if (platform === "android") {
13906
13978
  for (const scheme of schemes) {
13907
13979
  this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="${scheme}" /></intent-filter>`);
13908
13980
  }
13981
+ for (const domain of domains) {
13982
+ const pathPrefix = resolveMobilePath(this.target, "/");
13983
+ this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" /></intent-filter>`);
13984
+ }
13985
+ await this.#setDeepLinksInAndroid(schemes, domains);
13909
13986
  }
13910
- for (const domain of links.associatedDomains ?? []) {
13911
- this.app.logger.info(`Configure iOS associated domain manually if needed: ${domain}`);
13987
+ }
13988
+ #assertDeepLinkVerificationConfig(platform, { operation, env }) {
13989
+ const deepLinks = this.target.deepLinks;
13990
+ if (!deepLinks?.domains?.length)
13991
+ return;
13992
+ if (platform === "ios" && !deepLinks.ios?.teamId) {
13993
+ throw new Error(`Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.ios.teamId in apps/${this.app.name}/akan.config.ts`);
13912
13994
  }
13913
- for (const host of links.androidHosts ?? []) {
13914
- const pathPrefix = resolveMobilePath(this.target, "/");
13915
- this.project.android.getAndroidManifest().injectFragment("activity", `<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${host}" android:pathPrefix="${pathPrefix}" /></intent-filter>`);
13995
+ if (platform === "android" && !deepLinks.android?.sha256CertFingerprints?.length) {
13996
+ const message = `Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.android.sha256CertFingerprints in apps/${this.app.name}/akan.config.ts`;
13997
+ if (operation === "release" || env === "main")
13998
+ throw new Error(message);
13999
+ this.app.logger.warn(message);
13916
14000
  }
13917
14001
  }
13918
14002
  async#commandEnv(operation, env) {
@@ -13933,6 +14017,8 @@ ${error.message}`;
13933
14017
  const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
13934
14018
  if (basePath2)
13935
14019
  params.set("akanMobileBasePath", basePath2);
14020
+ if (this.target.indexPath)
14021
+ params.set("akanMobileIndexPath", this.target.indexPath);
13936
14022
  return `http://${ip}:${port}/${pathname}?${params}`;
13937
14023
  }
13938
14024
  async#clearRootCapacitorConfigs() {
@@ -13992,6 +14078,112 @@ ${error.message}`;
13992
14078
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
13993
14079
  ]);
13994
14080
  }
14081
+ async#setUrlSchemesInIos(schemes) {
14082
+ const urlTypes = schemes.map((scheme) => ({
14083
+ CFBundleURLName: this.target.appId,
14084
+ CFBundleURLSchemes: [scheme]
14085
+ }));
14086
+ await Promise.all([
14087
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
14088
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
14089
+ ]);
14090
+ }
14091
+ async#setAssociatedDomainsInIos(domains) {
14092
+ const entitlementsRelPath = "App/App.entitlements";
14093
+ const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
14094
+ const values = domains.map((domain) => `applinks:${domain}`);
14095
+ const body = [
14096
+ '<?xml version="1.0" encoding="UTF-8"?>',
14097
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14098
+ '<plist version="1.0">',
14099
+ "<dict>",
14100
+ " <key>com.apple.developer.associated-domains</key>",
14101
+ " <array>",
14102
+ ...values.map((value) => ` <string>${value}</string>`),
14103
+ " </array>",
14104
+ "</dict>",
14105
+ "</plist>",
14106
+ ""
14107
+ ].join(`
14108
+ `);
14109
+ await writeFile2(entitlementsPath, body);
14110
+ await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
14111
+ }
14112
+ async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
14113
+ const pbxprojPath = path41.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
14114
+ const lines = (await readFile2(pbxprojPath, "utf8")).split(`
14115
+ `);
14116
+ let changed = false;
14117
+ for (let index = 0;index < lines.length; index++) {
14118
+ const line = lines[index] ?? "";
14119
+ if (!line.includes(`PRODUCT_BUNDLE_IDENTIFIER = ${this.target.appId};`))
14120
+ continue;
14121
+ let start = index;
14122
+ while (start >= 0 && !lines[start]?.includes("buildSettings = {"))
14123
+ start--;
14124
+ let end = index;
14125
+ while (end < lines.length && !/^\s*\};\s*$/.test(lines[end] ?? ""))
14126
+ end++;
14127
+ const settings = lines.slice(start, end + 1);
14128
+ if (settings.some((setting) => setting.includes("CODE_SIGN_ENTITLEMENTS")))
14129
+ continue;
14130
+ const indent = line.match(/^\s*/)?.[0] ?? "";
14131
+ lines.splice(index, 0, `${indent}CODE_SIGN_ENTITLEMENTS = ${entitlementsRelPath};`);
14132
+ index++;
14133
+ changed = true;
14134
+ }
14135
+ if (changed)
14136
+ await writeFile2(pbxprojPath, lines.join(`
14137
+ `));
14138
+ }
14139
+ async#setUrlSchemesInAndroid(schemes) {
14140
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14141
+ let manifest = await readFile2(manifestPath, "utf8");
14142
+ let changed = false;
14143
+ for (const scheme of schemes) {
14144
+ if (manifest.includes(`android:scheme="${scheme}"`))
14145
+ continue;
14146
+ const filter = [
14147
+ " <intent-filter>",
14148
+ ' <action android:name="android.intent.action.VIEW" />',
14149
+ ' <category android:name="android.intent.category.DEFAULT" />',
14150
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14151
+ ` <data android:scheme="${scheme}" />`,
14152
+ " </intent-filter>"
14153
+ ].join(`
14154
+ `);
14155
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14156
+ ${filter}$1`);
14157
+ changed = true;
14158
+ }
14159
+ if (changed)
14160
+ await writeFile2(manifestPath, manifest);
14161
+ }
14162
+ async#setDeepLinksInAndroid(schemes, domains) {
14163
+ await this.#setUrlSchemesInAndroid(schemes);
14164
+ const manifestPath = path41.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
14165
+ let manifest = await readFile2(manifestPath, "utf8");
14166
+ let changed = false;
14167
+ const pathPrefix = resolveMobilePath(this.target, "/");
14168
+ for (const domain of domains) {
14169
+ if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"'))
14170
+ continue;
14171
+ const filter = [
14172
+ ' <intent-filter android:autoVerify="true">',
14173
+ ' <action android:name="android.intent.action.VIEW" />',
14174
+ ' <category android:name="android.intent.category.DEFAULT" />',
14175
+ ' <category android:name="android.intent.category.BROWSABLE" />',
14176
+ ` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
14177
+ " </intent-filter>"
14178
+ ].join(`
14179
+ `);
14180
+ manifest = manifest.replace(/(\s*<\/activity>)/, `
14181
+ ${filter}$1`);
14182
+ changed = true;
14183
+ }
14184
+ if (changed)
14185
+ await writeFile2(manifestPath, manifest);
14186
+ }
13995
14187
  #setFeaturesInAndroid(features) {
13996
14188
  for (const feature of features) {
13997
14189
  if (this.#hasFeatureInAndroid(feature)) {
@@ -15074,7 +15266,7 @@ ${document}
15074
15266
  }
15075
15267
  // pkgs/@akanjs/devkit/qualityScanner.ts
15076
15268
  import { createHash } from "crypto";
15077
- import { readdir as readdir3, readFile as readFile2, stat as stat4 } from "fs/promises";
15269
+ import { readdir as readdir3, readFile as readFile3, stat as stat4 } from "fs/promises";
15078
15270
  import path43 from "path";
15079
15271
  import ignore2 from "ignore";
15080
15272
  import ts11 from "typescript";
@@ -15163,7 +15355,7 @@ class AkanQualityScanner {
15163
15355
  const gitIgnorePath = path43.join(workspaceRoot, ".gitignore");
15164
15356
  if (!await Bun.file(gitIgnorePath).exists())
15165
15357
  return [];
15166
- return (await readFile2(gitIgnorePath, "utf8")).split(/\r?\n/);
15358
+ return (await readFile3(gitIgnorePath, "utf8")).split(/\r?\n/);
15167
15359
  }
15168
15360
  async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
15169
15361
  const entries = await readdir3(currentPath, { withFileTypes: true });
@@ -15183,7 +15375,7 @@ class AkanQualityScanner {
15183
15375
  }
15184
15376
  async#readSourceFile(workspaceRoot, file) {
15185
15377
  const absolutePath = path43.join(workspaceRoot, file);
15186
- const content = await readFile2(absolutePath, "utf8");
15378
+ const content = await readFile3(absolutePath, "utf8");
15187
15379
  return {
15188
15380
  file,
15189
15381
  absolutePath,
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,
@@ -2744,8 +2792,8 @@ class Executor {
2744
2792
  return filePath;
2745
2793
  if (filePath.startsWith("."))
2746
2794
  return path7.join(this.cwdPath, filePath);
2747
- const baseParts = this.cwdPath.split("/").filter(Boolean);
2748
- const targetParts = filePath.split("/").filter(Boolean);
2795
+ const baseParts = this.cwdPath.split(/[\\/]/).filter(Boolean);
2796
+ const targetParts = filePath.split(/[\\/]/).filter(Boolean);
2749
2797
  let overlapLength = 0;
2750
2798
  for (let i = 1;i <= Math.min(baseParts.length, targetParts.length); i++) {
2751
2799
  let isOverlap = true;
@@ -2757,8 +2805,7 @@ class Executor {
2757
2805
  if (isOverlap)
2758
2806
  overlapLength = i;
2759
2807
  }
2760
- const result = overlapLength > 0 ? `/${[...baseParts, ...targetParts.slice(overlapLength)].join("/")}` : `${this.cwdPath}/${filePath}`;
2761
- return result.replace(/\/+/g, "/");
2808
+ return path7.join(this.cwdPath, ...targetParts.slice(overlapLength));
2762
2809
  }
2763
2810
  async mkdir(dirPath) {
2764
2811
  const writePath = this.getPath(dirPath);
@@ -10223,6 +10270,7 @@ import path24 from "path";
10223
10270
  // pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
10224
10271
  import fs3 from "fs";
10225
10272
  import path23 from "path";
10273
+ import { pathToFileURL } from "url";
10226
10274
  import ts8 from "typescript";
10227
10275
 
10228
10276
  class PagesEntrySourceGenerator {
@@ -10235,8 +10283,8 @@ class PagesEntrySourceGenerator {
10235
10283
  }
10236
10284
  generate() {
10237
10285
  const lines = this.#pageEntries.map(({ key, moduleAbsPath }) => {
10238
- const absPath = path23.resolve(moduleAbsPath);
10239
- return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(absPath)}),`;
10286
+ const specifier = pathToFileURL(path23.resolve(moduleAbsPath)).href;
10287
+ return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(specifier)}),`;
10240
10288
  });
10241
10289
  return `export const pages = {
10242
10290
  ${lines.join(`
@@ -10249,8 +10297,8 @@ ${lines.join(`
10249
10297
  }
10250
10298
  generateStatic() {
10251
10299
  const imports = this.#pageEntries.map(({ moduleAbsPath }, index) => {
10252
- const absPath = path23.resolve(moduleAbsPath);
10253
- return `import * as page${index} from ${JSON.stringify(absPath)};`;
10300
+ const specifier = pathToFileURL(path23.resolve(moduleAbsPath)).href;
10301
+ return `import * as page${index} from ${JSON.stringify(specifier)};`;
10254
10302
  });
10255
10303
  const entries = this.#pageEntries.map(({ key, moduleAbsPath }, index) => {
10256
10304
  const isAsyncDefault = PagesEntrySourceGenerator.#hasAsyncDefaultExport(moduleAbsPath);
@@ -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
  `);
@@ -12123,11 +12178,11 @@ class SsrBaseArtifactBuilder {
12123
12178
  }
12124
12179
  async#buildRuntimeClientEntries() {
12125
12180
  const akanServerPath = await this.#resolveAkanServerPath();
12126
- const rscClientEntry = `${akanServerPath}/rscClient.tsx`;
12127
- const rscSegmentOutletEntry = `${akanServerPath}/rscSegmentOutlet.tsx`;
12181
+ const rscClientEntry = path35.resolve(akanServerPath, "rscClient.tsx");
12182
+ const rscSegmentOutletEntry = path35.resolve(akanServerPath, "rscSegmentOutlet.tsx");
12128
12183
  const vendorEntries = VENDOR_SPECIFIERS.map((specifier) => ({
12129
12184
  specifier,
12130
- absPath: `${akanServerPath}/vendor/${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`
12185
+ absPath: path35.resolve(akanServerPath, "vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`)
12131
12186
  }));
12132
12187
  const entries = [rscClientEntry, rscSegmentOutletEntry, ...vendorEntries.map((v) => v.absPath)];
12133
12188
  const clientBundle = await new ClientEntriesBundler({ app: this.#app, entries, command: this.#command }).bundle();
@@ -12888,7 +12943,7 @@ class Builder {
12888
12943
  }
12889
12944
  }
12890
12945
  // pkgs/@akanjs/devkit/capacitorApp.ts
12891
- 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";
12892
12947
  import os from "os";
12893
12948
  import path41 from "path";
12894
12949
  import { select as select2 } from "@inquirer/prompts";
@@ -13402,11 +13457,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
13402
13457
  const {
13403
13458
  name,
13404
13459
  basePath: _basePath,
13460
+ indexPath: _indexPath,
13405
13461
  version: _version,
13406
13462
  buildNum: _buildNum,
13407
13463
  assets: _assets,
13408
13464
  permissions: _permissions,
13409
- links: _links,
13465
+ deepLinks: _deepLinks,
13410
13466
  files: _files,
13411
13467
  appId,
13412
13468
  appName,
@@ -13528,7 +13584,7 @@ class CapacitorApp {
13528
13584
  await this.#prepareExternalFiles("ios");
13529
13585
  await this.#applyIosMetadata();
13530
13586
  await this.#applyPermissions();
13531
- await this.#applyLinks();
13587
+ await this.#applyDeepLinks("ios", { operation, env });
13532
13588
  await this.project.commit();
13533
13589
  await this.#generateAssets({ operation, env });
13534
13590
  this.app.verbose(`syncing iOS`);
@@ -13671,12 +13727,13 @@ ${error.message}`;
13671
13727
  await this.#prepareExternalFiles("android");
13672
13728
  await this.#applyAndroidMetadata();
13673
13729
  await this.#applyPermissions();
13674
- await this.#applyLinks();
13730
+ await this.#applyDeepLinks("android", { operation, env });
13675
13731
  await this.project.commit();
13676
13732
  await this.#generateAssets({ operation, env });
13677
13733
  await this.#ensureAndroidAssetsDir();
13678
13734
  await this.#ensureAndroidDebugKeystore();
13679
13735
  await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
13736
+ await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
13680
13737
  }
13681
13738
  async#updateAndroidBuildTypes() {
13682
13739
  const appGradle = await FileEditor.create(path41.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
@@ -13809,7 +13866,11 @@ ${error.message}`;
13809
13866
  }
13810
13867
  #injectMobileTargetMeta(html) {
13811
13868
  const basePath2 = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
13812
- 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>`;
13813
13874
  if (html.includes("window.__AKAN_MOBILE_TARGET__"))
13814
13875
  return html;
13815
13876
  return html.replace(/<\/head\s*>/i, `${script}
@@ -13892,25 +13953,48 @@ ${error.message}`;
13892
13953
  await this.addPush();
13893
13954
  }
13894
13955
  }
13895
- async#applyLinks() {
13896
- const links = this.target.links;
13897
- if (!links)
13956
+ async#applyDeepLinks(platform, { operation, env }) {
13957
+ const deepLinks = this.target.deepLinks;
13958
+ if (!deepLinks)
13898
13959
  return;
13899
- const schemes = links.schemes ?? [];
13900
- if (schemes.length > 0) {
13901
- await this.#setPermissionInIos({
13902
- appTransportSecurity: ""
13903
- });
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") {
13904
13976
  for (const scheme of schemes) {
13905
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>`);
13906
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);
13907
13984
  }
13908
- for (const domain of links.associatedDomains ?? []) {
13909
- 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`);
13910
13992
  }
13911
- for (const host of links.androidHosts ?? []) {
13912
- const pathPrefix = resolveMobilePath(this.target, "/");
13913
- 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);
13914
13998
  }
13915
13999
  }
13916
14000
  async#commandEnv(operation, env) {
@@ -13931,6 +14015,8 @@ ${error.message}`;
13931
14015
  const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
13932
14016
  if (basePath2)
13933
14017
  params.set("akanMobileBasePath", basePath2);
14018
+ if (this.target.indexPath)
14019
+ params.set("akanMobileIndexPath", this.target.indexPath);
13934
14020
  return `http://${ip}:${port}/${pathname}?${params}`;
13935
14021
  }
13936
14022
  async#clearRootCapacitorConfigs() {
@@ -13990,6 +14076,112 @@ ${error.message}`;
13990
14076
  this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
13991
14077
  ]);
13992
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
+ }
13993
14185
  #setFeaturesInAndroid(features) {
13994
14186
  for (const feature of features) {
13995
14187
  if (this.#hasFeatureInAndroid(feature)) {
@@ -15072,7 +15264,7 @@ ${document}
15072
15264
  }
15073
15265
  // pkgs/@akanjs/devkit/qualityScanner.ts
15074
15266
  import { createHash } from "crypto";
15075
- import { readdir as readdir3, readFile as readFile2, stat as stat4 } from "fs/promises";
15267
+ import { readdir as readdir3, readFile as readFile3, stat as stat4 } from "fs/promises";
15076
15268
  import path43 from "path";
15077
15269
  import ignore2 from "ignore";
15078
15270
  import ts11 from "typescript";
@@ -15161,7 +15353,7 @@ class AkanQualityScanner {
15161
15353
  const gitIgnorePath = path43.join(workspaceRoot, ".gitignore");
15162
15354
  if (!await Bun.file(gitIgnorePath).exists())
15163
15355
  return [];
15164
- return (await readFile2(gitIgnorePath, "utf8")).split(/\r?\n/);
15356
+ return (await readFile3(gitIgnorePath, "utf8")).split(/\r?\n/);
15165
15357
  }
15166
15358
  async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
15167
15359
  const entries = await readdir3(currentPath, { withFileTypes: true });
@@ -15181,7 +15373,7 @@ class AkanQualityScanner {
15181
15373
  }
15182
15374
  async#readSourceFile(workspaceRoot, file) {
15183
15375
  const absolutePath = path43.join(workspaceRoot, file);
15184
- const content = await readFile2(absolutePath, "utf8");
15376
+ const content = await readFile3(absolutePath, "utf8");
15185
15377
  return {
15186
15378
  file,
15187
15379
  absolutePath,
@@ -17184,7 +17376,10 @@ ${chalk7.green("\u27A4")} Authentication Required`));
17184
17376
  ...appNames.map((appName) => `apps/${appName}/env`),
17185
17377
  ...libNames.map((libName) => `libs/${libName}/env`)
17186
17378
  ];
17187
- 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();
17188
17383
  await workspace.mkdir("local");
17189
17384
  await workspace.remove("local/env.tar");
17190
17385
  if (envFilePaths.length === 0)
@@ -17195,6 +17390,52 @@ ${chalk7.green("\u27A4")} Authentication Required`));
17195
17390
  Logger15.info(`Archived ${envFilePaths.length} environment files to local/env.tar`);
17196
17391
  return { files: envFilePaths, path: "local/env.tar" };
17197
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
+ }
17198
17439
  }
17199
17440
 
17200
17441
  // pkgs/@akanjs/cli/cloud/cloud.script.ts
@@ -17474,7 +17715,7 @@ class RepairRunner extends runner("repair") {
17474
17715
  }
17475
17716
 
17476
17717
  // pkgs/@akanjs/cli/workflow/workflow.runner.ts
17477
- import { mkdir as mkdir12, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
17718
+ import { mkdir as mkdir12, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
17478
17719
  import path46 from "path";
17479
17720
  import { capitalize as capitalize8 } from "akanjs/common";
17480
17721
 
@@ -18116,7 +18357,7 @@ var defaultValidationExecutor = (workspace) => async (command3) => {
18116
18357
  };
18117
18358
  }
18118
18359
  };
18119
- var readJsonFile = async (filePath) => JSON.parse(await readFile3(resolvePath(filePath), "utf8"));
18360
+ var readJsonFile = async (filePath) => JSON.parse(await readFile4(resolvePath(filePath), "utf8"));
18120
18361
  var planInputString2 = (plan2, key) => {
18121
18362
  const value = plan2.inputs[key];
18122
18363
  return typeof value === "string" ? value : "";
@@ -18213,7 +18454,7 @@ class WorkflowRunner extends runner("workflow") {
18213
18454
  if (out) {
18214
18455
  const outPath = resolvePath(out);
18215
18456
  await mkdir12(path46.dirname(outPath), { recursive: true });
18216
- await writeFile2(outPath, jsonText(plan2));
18457
+ await writeFile3(outPath, jsonText(plan2));
18217
18458
  }
18218
18459
  return format === "json" ? jsonText(plan2) : renderWorkflowPlan(plan2);
18219
18460
  }
@@ -18231,7 +18472,7 @@ class WorkflowRunner extends runner("workflow") {
18231
18472
  };
18232
18473
  let plan2;
18233
18474
  try {
18234
- const parsed = JSON.parse(await readFile3(resolvePath(planPath), "utf8"));
18475
+ const parsed = JSON.parse(await readFile4(resolvePath(planPath), "utf8"));
18235
18476
  if (!isWorkflowPlan2(parsed)) {
18236
18477
  const report = failedApplyReport("unknown", [
18237
18478
  {
@@ -20822,6 +21063,12 @@ class WorkspaceRunner extends runner("workspace") {
20822
21063
  dict,
20823
21064
  overwrite
20824
21065
  });
21066
+ created.push(...await workspace.applyTemplate({
21067
+ basePath: ".",
21068
+ template: "workspaceRoot/CLAUDE.md.template",
21069
+ dict,
21070
+ overwrite
21071
+ }));
20825
21072
  if (!cursorRules)
20826
21073
  return created;
20827
21074
  return [
@@ -21116,7 +21363,9 @@ class WorkspaceCommand extends command("workspace", [WorkspaceScript], ({ public
21116
21363
  ...registry ? { registryUrl: registry } : {}
21117
21364
  });
21118
21365
  }),
21119
- 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) {
21120
21369
  await this.workspaceScript.generateAgentRules(workspace, { overwrite, cursorRules });
21121
21370
  }),
21122
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.3.9",
3
+ "version": "2.3.10-rc.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.3.9",
37
+ "akanjs": "2.3.10-rc.1",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -0,0 +1,3 @@
1
+ # <%= repoName %> — Claude Code Guide
2
+
3
+ @AGENTS.md