@akanjs/cli 3.1.0-rc.2 → 3.1.0-rc.21

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.
Files changed (103) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-q50k4s45.js → CodeAgent-gzfc5t52.js} +2 -2
  3. package/{CodeAgentRpcHost-p7h6n2hp.js → CodeAgentRpcHost-9g27hmer.js} +1 -1
  4. package/{CodeTui-62g35ztk.js → CodeTui-bpsjs8wz.js} +47 -4
  5. package/TunnelShare-tfrnftgr.js +14 -0
  6. package/{agent.command-adfd5299.js → agent.command-67ysh3n6.js} +5 -5
  7. package/akanCode.js +7 -5
  8. package/{application.command-hj3gv1rx.js → application.command-q0tqw5pc.js} +38 -11
  9. package/{applicationBuildRunner-by10r9gf.js → applicationBuildRunner-wsszr305.js} +16 -9
  10. package/buildBatch.proc.js +7 -7
  11. package/{cloud.command-k2fhaape.js → cloud.command-e7tkxyx6.js} +17 -16
  12. package/cloud.script-krzh3d7k.js +24 -0
  13. package/{code.command-kt5zgcc2.js → code.command-ttk8tje2.js} +25 -12
  14. package/commandManifest.json +1 -1
  15. package/{context.command-8fgn42jw.js → context.command-bdsk2z2r.js} +17 -16
  16. package/context.runner-bxzv0gm0.js +25 -0
  17. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  18. package/{guideline.command-3yxxr2mm.js → guideline.command-man90n70.js} +2 -2
  19. package/guidelines/agentRule/agentRule.instruction.md +25 -10
  20. package/guidelines/conventions/conventions.instruction.md +486 -1084
  21. package/guidelines/cssRule/cssRule.instruction.md +16 -1
  22. package/guidelines/fieldRule/fieldRule.instruction.md +74 -0
  23. package/guidelines/framework/framework.instruction.md +4 -7
  24. package/guidelines/mcpRule/mcpRule.instruction.md +16 -9
  25. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  26. package/guidelines/modelStore/modelStore.instruction.md +16 -0
  27. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  28. package/guidelines/queryRule/queryRule.instruction.md +47 -12
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
  30. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  31. package/guidelines/transportRule/transportRule.instruction.md +107 -8
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +70 -7
  34. package/incrementalBuilder.proc.js +4 -4
  35. package/{index-rhbabmja.js → index-033wne2j.js} +260 -259
  36. package/{index-xpdgxsj5.js → index-1r5a8bzx.js} +2216 -2216
  37. package/{index-jwqtchha.js → index-27y8t17p.js} +1 -1
  38. package/{index-wa5jbxj1.js → index-2mrn1mrg.js} +1 -0
  39. package/{index-t7nws9mx.js → index-2w8j2fnv.js} +661 -448
  40. package/{index-0909wckt.js → index-35m6bzm7.js} +238 -236
  41. package/{index-sqfdmb5b.js → index-4393nb4b.js} +4 -4
  42. package/{index-sby8fe1p.js → index-48edp6q4.js} +1 -1
  43. package/{index-g0vwzy9m.js → index-4v43h00g.js} +20 -21
  44. package/{index-azh29dc8.js → index-54wc2phm.js} +19 -14
  45. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  46. package/index-62md4dwy.js +260 -0
  47. package/{index-hnby7p9n.js → index-75n5q4ph.js} +7 -7
  48. package/{index-fjsv10hd.js → index-844q7f8z.js} +7 -7
  49. package/{index-tv8fjaba.js → index-8jehkey8.js} +4 -4
  50. package/{index-bea2gwc6.js → index-9mt4smfy.js} +2 -2
  51. package/{index-d2242ksy.js → index-at7bz0xq.js} +3 -0
  52. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  53. package/{index-8zdwpxef.js → index-cae9m5jz.js} +119 -81
  54. package/{index-tz5evhdr.js → index-ebarbgba.js} +6 -6
  55. package/{index-2etqq75t.js → index-g6ax17x4.js} +1114 -1082
  56. package/{index-h85cxfkg.js → index-g9ks2k8c.js} +2 -2
  57. package/{index-nq045px5.js → index-h3r3nr9y.js} +356 -307
  58. package/{index-20cebtth.js → index-h6czey01.js} +79 -24
  59. package/{index-6726qr1m.js → index-jrmzcp66.js} +57 -58
  60. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  61. package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
  62. package/{index-rk9sdys3.js → index-nv79n88g.js} +5 -5
  63. package/{index-kc8z134d.js → index-nx64dz92.js} +38 -15
  64. package/{index-z507nzhg.js → index-p2r64bpa.js} +4 -4
  65. package/{index-nq2ja5e0.js → index-pg1t468s.js} +3 -3
  66. package/{index-3reaaa1h.js → index-rc51f9ht.js} +29 -27
  67. package/{index-e9prnmyt.js → index-ta1dychm.js} +3 -3
  68. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  69. package/{index-31ce9vhg.js → index-vkb9m4z7.js} +80 -38
  70. package/{index-58vvz603.js → index-xsd96yzw.js} +1 -1
  71. package/{index-pyxn1b53.js → index-zv5s8x58.js} +13 -13
  72. package/index.js +22 -22
  73. package/{library.command-8tw59xzj.js → library.command-6amzsrb8.js} +3 -3
  74. package/{localRegistry.command-tf3dmznd.js → localRegistry.command-yqc0nyhh.js} +15 -14
  75. package/{module.command-9vm8hffy.js → module.command-10x6jv6c.js} +5 -5
  76. package/{package.command-erbxe2tz.js → package.command-pwfvecjf.js} +4 -4
  77. package/package.json +21 -21
  78. package/{page.command-jpavcj0p.js → page.command-ahhgx9y1.js} +3 -3
  79. package/{pipeline.command-b8rzvjvv.js → pipeline.command-1ptr3e9y.js} +3 -3
  80. package/{primitive.command-sm32chzk.js → primitive.command-g5xwv6tj.js} +6 -6
  81. package/{projectTypecheck-jamtx95x.js → projectTypecheck-fx8x7cat.js} +1 -1
  82. package/{quality.command-pkd17n6y.js → quality.command-fw9ck061.js} +4 -4
  83. package/{repair.command-8y4mq6hs.js → repair.command-g8kecj96.js} +5 -5
  84. package/{scalar.command-q8khmyx5.js → scalar.command-fprap51w.js} +4 -4
  85. package/skills/akan-code/SKILL.md +4 -0
  86. package/skills/akan-store/SKILL.md +2 -1
  87. package/{subspace.command-51n0ykbx.js → subspace.command-sc0t527y.js} +11 -10
  88. package/templates/appSample/lib/task/Task.Util.tsx +7 -7
  89. package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
  90. package/templates/appSample/ui/QuantityControl.tsx +2 -2
  91. package/templates/crudSinglePage/page.tsx +1 -1
  92. package/templates/lib/cnst.ts +0 -1
  93. package/templates/migration/__migrationId__.ts +23 -0
  94. package/templates/server.ts +8 -1
  95. package/templates/workspaceRoot/.gitignore.template +23 -52
  96. package/templates/workspaceRoot/biome.json.template +1 -1
  97. package/{tunnel.command-7820vtxq.js → tunnel.command-mqnsqbwf.js} +8 -7
  98. package/{workflow.command-n5sv4wqz.js → workflow.command-j0jgkr13.js} +10 -10
  99. package/{workspace.command-zdy60jh8.js → workspace.command-1r9hvvw0.js} +29 -28
  100. package/TunnelShare-f1egrktx.js +0 -13
  101. package/cloud.script-z8p5dz7e.js +0 -23
  102. package/context.runner-hnh0rg7m.js +0 -24
  103. package/index-n68jx79w.js +0 -120
@@ -4,101 +4,284 @@ import {
4
4
  FileSys2
5
5
  } from "./index-52w0fvh8.js";
6
6
 
7
- // pkgs/@akanjs/devkit/typecheck/projectTypecheck.ts
8
- import { mkdir, rm } from "fs/promises";
9
- import path from "path";
7
+ // pkgs/@akanjs/devkit/executors.ts
8
+ import {
9
+ exec,
10
+ fork,
11
+ spawn as spawn2
12
+ } from "child_process";
13
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
14
+ import {
15
+ copyFile,
16
+ cp as cpEntry,
17
+ lstat,
18
+ mkdir as mkdir3,
19
+ readdir as readDirEntries,
20
+ realpath,
21
+ rm as rm4,
22
+ stat as stat2,
23
+ symlink
24
+ } from "fs/promises";
25
+ import path13 from "path";
26
+ import { pathToFileURL } from "url";
27
+ import {
28
+ capitalize,
29
+ getPageSourceFileViolation,
30
+ isRouteSourceFile,
31
+ Logger as Logger3,
32
+ parseRouteModuleKey,
33
+ toError as toError2,
34
+ validatePageSourceFile,
35
+ validateSubRoutePageKey
36
+ } from "akanjs/common";
37
+ var {$ } = globalThis.Bun;
38
+ import chalk2 from "chalk";
10
39
 
11
- class ProjectTypecheck2 {
12
- projectDir;
13
- workspaceRoot;
14
- env;
15
- static #testIncludes = ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"];
16
- static #excludes = [".akan/**", "node_modules/**", "local/**"];
17
- constructor(projectDir, workspaceRoot, env) {
18
- this.projectDir = projectDir;
19
- this.workspaceRoot = workspaceRoot;
20
- this.env = env;
40
+ // pkgs/@akanjs/devkit/agentsIndex.ts
41
+ var recipeScannerLoad = null;
42
+ var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
43
+ var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
44
+ var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
45
+ var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
46
+ var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
47
+
48
+ ${block}`;
49
+ var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
50
+ var readDevkitVersion = async () => {
51
+ const { readFile } = await import("fs/promises");
52
+ const { getDirname } = await import("./fileSys-wzzrntkw.js");
53
+ try {
54
+ const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
55
+ return JSON.parse(raw).version ?? null;
56
+ } catch {
57
+ return null;
21
58
  }
22
- get typecheckDir() {
23
- return path.join(this.projectDir, ".akan", "typecheck");
59
+ };
60
+ var upsertAgentBlock = (existing, block) => {
61
+ const managed = `${AGENT_BLOCK_START}
62
+ ${block}
63
+ ${AGENT_BLOCK_END}`;
64
+ const startIndex = existing.indexOf(AGENT_BLOCK_START);
65
+ const endIndex = existing.indexOf(AGENT_BLOCK_END);
66
+ if (startIndex >= 0 && endIndex > startIndex) {
67
+ return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
24
68
  }
25
- async run(entries, { clean = false, incremental = true, tests = false } = {}) {
26
- const name = tests ? "tsconfig.tests" : "tsconfig";
27
- await mkdir(this.typecheckDir, { recursive: true });
28
- const tsconfig = {
29
- extends: "../../tsconfig.json",
30
- compilerOptions: { noEmit: true, incremental, tsBuildInfoFile: `./${name}.tsbuildinfo` },
31
- include: [...entries, ...tests ? ProjectTypecheck2.#testIncludes.map((glob) => `../../${glob}`) : []],
32
- ...tests ? { exclude: ProjectTypecheck2.#excludes.map((glob) => `../../${glob}`) } : {},
33
- references: []
34
- };
35
- const tsconfigPath = path.join(this.typecheckDir, `${name}.json`);
36
- await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
69
+ return `${existing.replace(/\s*$/, "")}
70
+
71
+ ${managed}
72
+ `;
73
+ };
74
+ var extractAgentBlock = (content) => {
75
+ const startIndex = content.indexOf(AGENT_BLOCK_START);
76
+ const endIndex = content.indexOf(AGENT_BLOCK_END);
77
+ if (startIndex < 0 || endIndex <= startIndex)
78
+ return null;
79
+ return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
80
+ };
81
+ var signatureOf = (recipe) => {
82
+ const entries = Object.entries(recipe.variants);
83
+ if (entries.length === 0)
84
+ return "";
85
+ const parts = entries.map(([key, values]) => {
86
+ if (values.length === 1 && values[0] === "true")
87
+ return `${key}?`;
88
+ const def = recipe.defaultVariants?.[key];
89
+ return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
90
+ });
91
+ return `(${parts.join(" \xB7 ")})`;
92
+ };
93
+ var renderRecipeEntries = (recipes) => {
94
+ const groups = new Map;
95
+ for (const recipe of recipes)
96
+ groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
97
+ return [...groups.entries()].map(([importFrom, list]) => {
98
+ const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
37
99
  `);
38
- if (clean)
39
- await rm(path.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
40
- await this.#check(tsconfigPath);
41
- return tsconfigPath;
42
- }
43
- async#check(tsconfigPath) {
44
- const proc = Bun.spawn([process.execPath, await ProjectTypecheck2.workerEntry(this.workspaceRoot)], {
45
- cwd: this.workspaceRoot,
46
- env: { ...this.env, AKAN_COMMAND_TYPE: "typecheck", AKAN_TYPECHECK_TSCONFIG: tsconfigPath },
47
- stdout: "pipe",
48
- stderr: "pipe"
49
- });
50
- const [stdout, stderr, exitCode] = await Promise.all([
51
- new Response(proc.stdout).text(),
52
- new Response(proc.stderr).text(),
53
- proc.exited
54
- ]);
55
- if (exitCode !== 0)
56
- throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
57
- }
58
- static async workerEntry(workspaceRoot) {
59
- const candidates = [
60
- path.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
61
- path.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
62
- path.join(import.meta.dir, "typecheck.proc.js"),
63
- path.join(import.meta.dir, "typecheck.proc.ts"),
64
- path.join(import.meta.dir, "..", "typecheck.proc.js")
65
- ];
66
- for (const candidate of candidates)
67
- if (await Bun.file(candidate).exists())
68
- return candidate;
69
- throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
100
+ return `Import from \`${importFrom}\`:
101
+ ${items}`;
102
+ }).join(`
103
+
104
+ `);
105
+ };
106
+ var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
107
+ const { collectRecipeSources } = await loadRecipeScanner();
108
+ const sources = [
109
+ ...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
110
+ ];
111
+ for (const lib of [...new Set(libDeps)].sort()) {
112
+ if (scope.type === "lib" && lib === scope.name)
113
+ continue;
114
+ sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
70
115
  }
71
- }
116
+ return sources;
117
+ };
118
+ var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
119
+ const { scanRecipes } = await loadRecipeScanner();
120
+ return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
121
+ };
122
+ var renderScopeAgentBlock = (scope, recipes) => {
123
+ const scopePath = `${scope.type}s/${scope.name}`;
124
+ const intro = `## Recipes In Scope
72
125
 
73
- // pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
74
- import { existsSync, realpathSync } from "fs";
75
- import path2 from "path";
76
- import { Logger, toError } from "akanjs/common";
126
+ UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
127
+ \`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
128
+ arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
129
+ boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
130
+ and verified by \`akan lint ${scope.name}\`.`;
131
+ if (recipes.length === 0) {
132
+ return `${intro}
77
133
 
78
- class ServerEnvProfile {
79
- static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
80
- static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
81
- static fileName(environment, profile) {
82
- return `env.server.${environment}.${profile}.ts`;
83
- }
84
- static profileFileOf(file, profile) {
85
- return file.replace(/\.ts$/, `.${profile}.ts`);
134
+ No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
135
+ root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
136
+ folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
86
137
  }
87
- static #url(value) {
88
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
89
- return null;
90
- try {
91
- return new URL(value);
92
- } catch {
93
- return null;
94
- }
138
+ return `${intro}
139
+
140
+ ${renderRecipeEntries(recipes)}`;
141
+ };
142
+ var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
143
+
144
+ Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
145
+ root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
146
+ section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
147
+ markers freely.
148
+
149
+ ${AGENT_BLOCK_START}
150
+ ${block}
151
+ ${AGENT_BLOCK_END}
152
+ `;
153
+ var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
154
+
155
+ Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
156
+ is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
157
+
158
+ Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
159
+
160
+ - Restates the identifier, the signature, or the line under it \u2192 delete it.
161
+ - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
162
+ - JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
163
+ - Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
164
+ - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
165
+ derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
166
+ keep it, one line.
167
+
168
+ That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
169
+ subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
170
+ surrounding code carries none, your diff carries none.`;
171
+ var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
172
+
173
+ @AGENTS.md
174
+
175
+ ${CLAUDE_COMMENT_RULE}
176
+ `;
177
+
178
+ // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
179
+ import path from "path";
180
+
181
+ // pkgs/@akanjs/devkit/akanConfig/types.ts
182
+ import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
183
+
184
+ // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
185
+ class AkanBin {
186
+ static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
187
+ static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
188
+ static parse(declared, owner, baseDir) {
189
+ if (declared === undefined)
190
+ return {};
191
+ if (!declared || typeof declared !== "object" || Array.isArray(declared))
192
+ throw new Error(`${owner}: bin maps each executable's name to its platforms`);
193
+ return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
95
194
  }
96
- static #carriesCredential(url) {
97
- return !!url.password || [...url.searchParams.keys()].some((name) => Logger.isSecretKey(name));
195
+ static isArchive(source) {
196
+ return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
98
197
  }
99
- static #isCredentialUrl(value) {
100
- const url = ServerEnvProfile.#url(value);
101
- return !!url && ServerEnvProfile.#carriesCredential(url);
198
+ static #platforms(name, platforms, owner, baseDir) {
199
+ if (!AkanBin.fileName.test(name))
200
+ throw new Error(`${owner}: bin.${name} is not a file name`);
201
+ if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
202
+ throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
203
+ const resolved = {};
204
+ for (const [platform, source] of Object.entries(platforms)) {
205
+ if (!binPlatforms.includes(platform))
206
+ throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
207
+ resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
208
+ }
209
+ return resolved;
210
+ }
211
+ static #source(at, source, baseDir) {
212
+ if (!source || typeof source !== "object" || Array.isArray(source))
213
+ throw new Error(`${at} must be { url, sha256 } or { path }`);
214
+ const { url, sha256, path: localPath, file, ...rest } = source;
215
+ const extra = Object.keys(rest);
216
+ if (extra.length)
217
+ throw new Error(`${at}: unknown key ${extra.join(", ")}`);
218
+ if (url === undefined === (localPath === undefined))
219
+ throw new Error(`${at} takes either url and sha256, or path`);
220
+ const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
221
+ const archive = AkanBin.isArchive(resolved);
222
+ if (file === undefined) {
223
+ if (archive)
224
+ throw new Error(`${at} names an archive: file picks the executable inside it`);
225
+ return resolved;
226
+ }
227
+ if (!archive)
228
+ throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
229
+ const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
230
+ if (!parts.length || path.isAbsolute(file) || parts.includes(".."))
231
+ throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
232
+ return { ...resolved, file: parts.join("/") };
233
+ }
234
+ static #urlSource(at, url, sha256) {
235
+ if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
236
+ throw new Error(`${at}: url must be an http(s) address`);
237
+ if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
238
+ throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
239
+ return { url, sha256: sha256.toLowerCase() };
240
+ }
241
+ static #pathSource(at, localPath, sha256, baseDir) {
242
+ if (typeof localPath !== "string" || !localPath)
243
+ throw new Error(`${at}: path must name a file`);
244
+ if (sha256 !== undefined)
245
+ throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
246
+ return { path: path.resolve(baseDir, localPath) };
247
+ }
248
+ }
249
+ // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
250
+ import fs from "fs";
251
+ import path3 from "path";
252
+ import { fileURLToPath } from "url";
253
+ import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
254
+ import { resolveAkanI18nConfig, trimSlashes } from "akanjs/common";
255
+
256
+ // pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
257
+ import { existsSync, realpathSync } from "fs";
258
+ import path2 from "path";
259
+ import { Logger, toError } from "akanjs/common";
260
+
261
+ class ServerEnvProfile {
262
+ static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
263
+ static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
264
+ static fileName(environment, profile) {
265
+ return `env.server.${environment}.${profile}.ts`;
266
+ }
267
+ static profileFileOf(file, profile) {
268
+ return file.replace(/\.ts$/, `.${profile}.ts`);
269
+ }
270
+ static #url(value) {
271
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
272
+ return null;
273
+ try {
274
+ return new URL(value);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ static #carriesCredential(url) {
280
+ return !!url.password || [...url.searchParams.keys()].some((name) => Logger.isSecretKey(name));
281
+ }
282
+ static #isCredentialUrl(value) {
283
+ const url = ServerEnvProfile.#url(value);
284
+ return !!url && ServerEnvProfile.#carriesCredential(url);
102
285
  }
103
286
  static #isSecret(value, underSecret) {
104
287
  return underSecret && !!value.trim() || ServerEnvProfile.#isCredentialUrl(value);
@@ -327,838 +510,92 @@ class ServerEnvProfile {
327
510
  }
328
511
  }
329
512
 
330
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
331
- import { readdir, rm as rm2 } from "fs/promises";
332
- import path4 from "path";
333
-
334
- // pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
335
- import fs from "fs";
336
- import path3 from "path";
337
-
338
- class AsyncDefaultExportDetector {
339
- static #typescriptLoad;
340
- static #loadTypescript() {
341
- AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
342
- return AsyncDefaultExportDetector.#typescriptLoad;
343
- }
344
- static async detect(moduleAbsPath) {
345
- try {
346
- const typescript = await AsyncDefaultExportDetector.#loadTypescript();
347
- const source = fs.readFileSync(path3.resolve(moduleAbsPath), "utf8");
348
- const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
349
- return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
350
- } catch {
351
- return false;
352
- }
353
- }
354
- #ts;
355
- constructor(typescript) {
356
- this.#ts = typescript;
357
- }
358
- detectInSourceFile(sourceFile) {
359
- const ts = this.#ts;
360
- const asyncBindings = new Map;
361
- let defaultIdentifier = null;
362
- for (const statement of sourceFile.statements) {
363
- if (ts.isFunctionDeclaration(statement)) {
364
- if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
365
- return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
366
- }
367
- if (statement.name) {
368
- asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
369
- }
370
- continue;
371
- }
372
- if (ts.isVariableStatement(statement)) {
373
- for (const declaration of statement.declarationList.declarations) {
374
- if (!ts.isIdentifier(declaration.name))
375
- continue;
376
- asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
377
- }
378
- continue;
379
- }
380
- if (ts.isExportAssignment(statement)) {
381
- if (this.#isAsyncFunctionExpression(statement.expression))
382
- return true;
383
- if (ts.isCallExpression(statement.expression))
384
- return true;
385
- if (ts.isIdentifier(statement.expression))
386
- defaultIdentifier = statement.expression.text;
387
- continue;
388
- }
389
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
390
- for (const specifier of statement.exportClause.elements) {
391
- if (specifier.name.text !== "default")
392
- continue;
393
- defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
394
- }
395
- }
396
- }
397
- return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
398
- }
399
- #hasModifier(node, kind) {
400
- const ts = this.#ts;
401
- return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
402
- }
403
- #isAsyncFunctionExpression(node) {
404
- const ts = this.#ts;
405
- return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
513
+ // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
514
+ var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
515
+ var DEFAULT_OPTIMIZE_IMPORTS = [
516
+ "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
517
+ "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
518
+ "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
519
+ "@material-ui/icons @tabler/icons-react mui-core react-icons/*"
520
+ ].flatMap((line) => line.split(" "));
521
+ var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
522
+ var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
523
+ var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
524
+ var DEFAULT_AKAN_IMAGE_CONFIG = {
525
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
526
+ imageSizes: [32, 48, 64, 96, 128, 256, 384],
527
+ formats: ["image/webp"],
528
+ qualities: [75],
529
+ minimumCacheTTL: 14400,
530
+ remotePatterns: [],
531
+ localPatterns: [{ pathname: "/**" }],
532
+ dangerouslyAllowSVG: false,
533
+ maximumRedirects: 3,
534
+ fetchTimeoutMs: 7000,
535
+ maxRemoteBytes: 25 * 1024 * 1024,
536
+ maxConcurrency: 0
537
+ };
538
+ var normalizeIndexPath = (indexPath) => {
539
+ const normalized = indexPath?.trim();
540
+ if (!normalized)
541
+ return;
542
+ const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
543
+ return path === "/" ? "/" : path;
544
+ };
545
+ var normalizeStringList = (values) => {
546
+ const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
547
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
548
+ };
549
+ var sanitizeAppIdSegment = (value) => {
550
+ const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
551
+ if (!cleaned)
552
+ return "app";
553
+ return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
554
+ };
555
+ var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
556
+ var normalizeDeepLinkDomain = (domain) => {
557
+ const normalized = domain.trim();
558
+ if (!normalized)
559
+ return "";
560
+ try {
561
+ const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
562
+ return url.host.toLowerCase();
563
+ } catch {
564
+ return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
406
565
  }
407
- }
408
-
409
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
410
- var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
411
- var IMPLICIT_LAYOUT_DIR = path4.join(".akan", "generated", "root-layouts");
412
- var IMPLICIT_DICT_DIR = path4.join(".akan", "generated", "dict");
413
- var IMPLICIT_OVERRIDES_DIR = path4.join(".akan", "generated", "overrides");
414
- var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
415
- var importSpecifier = (fromAbsPath, toAbsPath) => {
416
- const rel = path4.relative(path4.dirname(fromAbsPath), toAbsPath).split(path4.sep).join("/");
417
- return rel.startsWith(".") ? rel : `./${rel}`;
418
566
  };
419
- async function writeGeneratedOverridesLayoutFile(opts) {
420
- const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
421
- const absPath = path4.join(path4.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
422
- const source = `"use client";
423
- import { UiOverrideProvider } from "akanjs/ui";
424
- import { createElement, type ReactNode } from "react";
425
- import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
567
+ var normalizeDeepLinks = (deepLinks) => {
568
+ const schemes = normalizeStringList(deepLinks?.schemes);
569
+ const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
570
+ if (!schemes && !domains)
571
+ return;
572
+ return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
573
+ };
574
+ var emptyLibContributions = () => ({
575
+ externalLibs: [],
576
+ trustedDependencies: [],
577
+ bin: [],
578
+ docker: { preRuns: [], postRuns: [] },
579
+ keepFonts: []
580
+ });
581
+ var normalizePackageNames = (names, owner) => {
582
+ if (names === undefined)
583
+ return [];
584
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
585
+ throw new Error(`${owner}: trustedDependencies lists package names`);
586
+ return [...new Set(names.map((name) => name.trim()))];
587
+ };
588
+ var normalizeKeepFonts = (keepFonts) => [
589
+ ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
590
+ ];
591
+ var dedupeDockerRuns = (runs) => {
592
+ const byKey = new Map;
593
+ for (const run of runs)
594
+ byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
595
+ return [...byKey.values()];
596
+ };
426
597
 
427
- export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
428
- return createElement(UiOverrideProvider, { value }, children);
429
- }
430
- `;
431
- await Bun.write(absPath, source);
432
- return absPath;
433
- }
434
- async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
435
- const keep = new Set(keepAbsPaths);
436
- const names = await readdir(dirAbsPath).catch(() => []);
437
- await Promise.all(names.map((name) => path4.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm2(absPath, { force: true })));
438
- }
439
- function getRootBoundarySegments(key) {
440
- const match = LAYOUT_KEY_RE.exec(key);
441
- if (!match)
442
- return null;
443
- const prefix = match[1]?.replace(/\/$/, "");
444
- if (!prefix)
445
- return [];
446
- return prefix.split("/").filter(Boolean);
447
- }
448
- function implicitRootLayoutKey(segments) {
449
- return `./${[...segments, "__root_layout"].join("/")}.tsx`;
450
- }
451
- function implicitRootLayoutAbsPath(appCwdPath, segments) {
452
- const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
453
- return path4.join(path4.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
454
- }
455
- function isRootBoundarySegments(segments, basePaths) {
456
- const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
457
- if (firstVisibleIndex === -1)
458
- return segments.length <= 1;
459
- if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
460
- return false;
461
- const visible = segments.slice(firstVisibleIndex);
462
- const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
463
- return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
464
- }
465
- function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
466
- const boundaries = new Map;
467
- for (const key of pageKeys) {
468
- const segments = getRootBoundarySegments(key);
469
- if (!segments)
470
- continue;
471
- if (!isRootBoundarySegments(segments, basePaths))
472
- continue;
473
- const id = segments.join("/");
474
- boundaries.set(id, {
475
- sourceKey: key,
476
- sourceAbsPath: path4.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
477
- segments
478
- });
479
- }
480
- if (boundaries.size === 0)
481
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
482
- return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
483
- }
484
- function hasAncestorRootBoundary(boundary, boundaries) {
485
- return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
486
- }
487
- function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
488
- const rootLayoutKey = pageKeys.find((key) => {
489
- const segments = getRootBoundarySegments(key);
490
- return segments !== null && segments.length === 0;
491
- });
492
- return rootLayoutKey ? path4.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
493
- }
494
- async function assertEnvClientConvention(appCwdPath, appName) {
495
- const envPath = path4.join(appCwdPath, "env", "env.client.ts");
496
- if (!await Bun.file(envPath).exists()) {
497
- throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
498
- }
499
- }
500
- async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
501
- const absPath = path4.join(path4.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
502
- await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
503
-
504
- export const allDictionary = getAllDictionary();
505
- `);
506
- return absPath;
507
- }
508
- async function writeGeneratedRootLayoutFile(opts) {
509
- await assertEnvClientConvention(opts.appCwdPath, opts.appName);
510
- const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
511
- const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
512
- const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
513
- const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
514
- const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
515
- const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
516
- const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
517
- void st;
518
- ` : `import "@apps/${opts.appName}/client";
519
- `;
520
- const inheritedLabel = inheritedSourceAbsPath ? path4.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
521
- const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
522
- const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
523
- ` : `const inheritedLayout: LayoutModule = {};
524
- `;
525
- const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
526
- const userLabel = opts.boundary.sourceAbsPath ? path4.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
527
- const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
528
- const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
529
- const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
530
- ` : `const UserLayout = ({ children }: LayoutProps) => children;
531
- const userLayout: LayoutModule = {};
532
- `;
533
- const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
534
- const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
535
- const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
536
- const layoutBinding = isAsyncUserLayout ? ` const layout =
537
- process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
538
- ? await UserLayout({ params, searchParams, children })
539
- : ${userLayoutElement};
540
- ` : "";
541
- const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
542
- const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
543
- const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
544
- const layoutExports = `export async function generateHead(props: PageProps) {
545
- const heads = await Promise.all(
546
- ${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
547
- );
548
- return RouteHead.mergeNodes(heads);
549
- }
550
-
551
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
552
- export const Error = userLayout.Error ?? inheritedLayout.Error;
553
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
554
-
555
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
556
- ${layoutBinding} return `;
557
- const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
558
- import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
559
- import type { ReactNode } from "react";
560
- import { System } from "akanjs/ui";
561
- import { env } from "@apps/${opts.appName}/env/env.client";
562
- import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
563
- ${clientImport}${inheritedImport}${userImport}
564
- const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
565
- const defaultFonts = userFonts.filter((font) => font.default);
566
- if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
567
- const defaultFont = defaultFonts[0];
568
- const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
569
- export const theme = userLayout.theme ?? inheritedLayout.theme;
570
- export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
571
- export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
572
-
573
- ${layoutExports}(
574
- <System.Provider
575
- of={GeneratedLayout as never}
576
- appName=${JSON.stringify(opts.appName)}
577
- ${prefix ? `prefix=${JSON.stringify(prefix)}
578
- ` : ""}params={params}
579
- manifest={manifest}
580
- env={env}
581
- theme={theme}
582
- viewport={viewport}
583
- fonts={loadFonts(userFonts)}
584
- className={defaultFontClassName}
585
- layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
586
- reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
587
- wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
588
- allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
589
- >
590
- ${layoutChild}
591
- </System.Provider>
592
- );
593
- }
594
- ` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
595
- import { resolveRouteModule, RouteHead } from "akanjs/client";
596
- import type { ReactNode } from "react";
597
- ${inheritedImport}${userImport}
598
- ${layoutExports}${layoutReturn};
599
- }
600
- `;
601
- await Bun.write(absPath, source);
602
- return absPath;
603
- }
604
- async function resolveSsrPageEntries(opts) {
605
- const absPageDir = path4.resolve(opts.appCwdPath, "page");
606
- const hasSt = await Bun.file(path4.join(opts.appCwdPath, "lib", "st.ts")).exists();
607
- const basePaths = opts.basePaths ?? [];
608
- const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
609
- const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
610
- const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
611
- const segments = getRootBoundarySegments(key);
612
- return segments !== null && isRootBoundarySegments(segments, basePaths);
613
- }));
614
- const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
615
- const userAbsPath = path4.resolve(absPageDir, key);
616
- if (OVERRIDES_KEY_RE.test(key)) {
617
- const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
618
- appCwdPath: opts.appCwdPath,
619
- key,
620
- userAbsPath
621
- });
622
- return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
623
- }
624
- return { key, moduleAbsPath: userAbsPath };
625
- }));
626
- const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
627
- key: implicitRootLayoutKey(boundary.segments),
628
- moduleAbsPath: await writeGeneratedRootLayoutFile({
629
- appCwdPath: opts.appCwdPath,
630
- appName: opts.appName,
631
- boundary,
632
- rootSourceAbsPath,
633
- includeStInit: hasSt && boundary.segments.length === 0,
634
- includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
635
- }),
636
- seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
637
- })));
638
- const appCwdAbsPath = path4.resolve(opts.appCwdPath);
639
- await Promise.all([
640
- pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
641
- pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
642
- ]);
643
- return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
644
- }
645
- async function resolveSsrPageEntriesForApp(app, pageKeys) {
646
- const config = await app.getConfig();
647
- return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
648
- }
649
-
650
- // pkgs/@akanjs/devkit/codegenLock.ts
651
- import { mkdir as mkdir2, open, readFile, rm as rm3, stat } from "fs/promises";
652
- import path5 from "path";
653
- import { Logger as Logger2 } from "akanjs/common";
654
-
655
- class CodegenLock {
656
- static fileName = "codegen.lock";
657
- static waitTimeoutMs = 1e4;
658
- static unknownHolderStaleMs = 60000;
659
- static #pollMs = 25;
660
- static #logger = new Logger2("CodegenLock");
661
- static #queue = Promise.resolve();
662
- static pathIn(workspaceRoot) {
663
- return path5.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
664
- }
665
- static async run(workspaceRoot, label, fn) {
666
- const ahead = CodegenLock.#queue;
667
- let done;
668
- CodegenLock.#queue = new Promise((resolve) => {
669
- done = resolve;
670
- });
671
- try {
672
- await CodegenLock.#waitForQueue(ahead, label);
673
- return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
674
- } finally {
675
- done();
676
- }
677
- }
678
- static async#waitForQueue(ahead, label) {
679
- let timer = null;
680
- const expired = new Promise((resolve) => {
681
- timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
682
- });
683
- try {
684
- if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
685
- CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
686
- } finally {
687
- if (timer)
688
- clearTimeout(timer);
689
- }
690
- }
691
- static async#withFileLock(workspaceRoot, label, fn) {
692
- const lockPath = CodegenLock.pathIn(workspaceRoot);
693
- await mkdir2(path5.dirname(lockPath), { recursive: true }).catch(() => {
694
- return;
695
- });
696
- const held = await CodegenLock.#acquire(lockPath, label);
697
- try {
698
- return await fn();
699
- } finally {
700
- if (held)
701
- await rm3(lockPath, { force: true }).catch(() => {
702
- return;
703
- });
704
- }
705
- }
706
- static async#acquire(lockPath, label) {
707
- const deadline = Date.now() + CodegenLock.waitTimeoutMs;
708
- for (;; ) {
709
- const handle = await open(lockPath, "wx").catch(() => null);
710
- if (handle) {
711
- await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
712
- return;
713
- });
714
- await handle.close().catch(() => {
715
- return;
716
- });
717
- return true;
718
- }
719
- if (await CodegenLock.#reclaimIfAbandoned(lockPath))
720
- continue;
721
- if (Date.now() >= deadline) {
722
- CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
723
- return false;
724
- }
725
- await Bun.sleep(CodegenLock.#pollMs);
726
- }
727
- }
728
- static async#reclaimIfAbandoned(lockPath) {
729
- const info = await stat(lockPath).catch(() => null);
730
- if (!info)
731
- return true;
732
- const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
733
- if (holder) {
734
- if (CodegenLock.#isAlive(holder.pid))
735
- return false;
736
- } else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
737
- return false;
738
- await rm3(lockPath, { force: true }).catch(() => {
739
- return;
740
- });
741
- return true;
742
- }
743
- static #parseHolder(raw) {
744
- try {
745
- const parsed = JSON.parse(raw);
746
- if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
747
- return null;
748
- return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
749
- } catch {
750
- return null;
751
- }
752
- }
753
- static #isAlive(pid) {
754
- if (!Number.isInteger(pid) || pid <= 0)
755
- return false;
756
- try {
757
- process.kill(pid, 0);
758
- return true;
759
- } catch (error) {
760
- return error.code === "EPERM";
761
- }
762
- }
763
- }
764
-
765
- // pkgs/@akanjs/devkit/spinner.ts
766
- import ora from "ora";
767
-
768
- class Spinner {
769
- static padding = 12;
770
- static oraOptions = { discardStdin: false };
771
- static canAnimate(stream = process.stderr) {
772
- return !stream.isTTY || stream.columns > 0;
773
- }
774
- spinner;
775
- stopWatch = null;
776
- startAt = new Date;
777
- prefix;
778
- message;
779
- enableSpin;
780
- constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
781
- Spinner.padding = Math.max(Spinner.padding, prefix.length);
782
- this.prefix = prefix;
783
- this.message = message;
784
- this.spinner = ora({ ...Spinner.oraOptions, text: message });
785
- this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
786
- this.spinner.indent = indent;
787
- this.enableSpin = enableSpin && Spinner.canAnimate();
788
- }
789
- start() {
790
- this.startAt = new Date;
791
- if (this.enableSpin) {
792
- this.spinner.start();
793
- this.stopWatch = setInterval(() => {
794
- this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
795
- this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
796
- }, 1000);
797
- } else
798
- this.spinner.info();
799
- return this;
800
- }
801
- succeed(message) {
802
- this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
803
- this.#reset();
804
- }
805
- fail(message) {
806
- this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
807
- this.#reset();
808
- }
809
- isSpinning() {
810
- return this.spinner.isSpinning;
811
- }
812
- #reset() {
813
- if (this.stopWatch)
814
- clearInterval(this.stopWatch);
815
- this.stopWatch = null;
816
- }
817
- #getElapsedTimeStr({ floor = false } = {}) {
818
- const ms = Date.now() - this.startAt.getTime();
819
- if (ms < 1000)
820
- return `${ms}ms`;
821
- const s = Math.floor(ms / 1000);
822
- if (s < 60)
823
- return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
824
- const m = Math.floor(s / 60);
825
- return `${m}m ${s % 60}s`;
826
- }
827
- }
828
-
829
- // pkgs/@akanjs/devkit/executors.ts
830
- import {
831
- exec,
832
- fork,
833
- spawn as spawn2
834
- } from "child_process";
835
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
836
- import {
837
- copyFile,
838
- cp as cpEntry,
839
- lstat,
840
- mkdir as mkdir3,
841
- readdir as readDirEntries,
842
- realpath,
843
- rm as rm4,
844
- stat as stat2,
845
- symlink
846
- } from "fs/promises";
847
- import path13 from "path";
848
- import { pathToFileURL } from "url";
849
- import {
850
- capitalize,
851
- getPageSourceFileViolation,
852
- isRouteSourceFile,
853
- Logger as Logger3,
854
- parseRouteModuleKey,
855
- toError as toError2,
856
- validatePageSourceFile,
857
- validateSubRoutePageKey
858
- } from "akanjs/common";
859
- var {$ } = globalThis.Bun;
860
- import chalk2 from "chalk";
861
-
862
- // pkgs/@akanjs/devkit/agentsIndex.ts
863
- var recipeScannerLoad = null;
864
- var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
865
- var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
866
- var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
867
- var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
868
- var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
869
-
870
- ${block}`;
871
- var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
872
- var readDevkitVersion = async () => {
873
- const { readFile } = await import("fs/promises");
874
- const { getDirname } = await import("./fileSys-wzzrntkw.js");
875
- try {
876
- const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
877
- return JSON.parse(raw).version ?? null;
878
- } catch {
879
- return null;
880
- }
881
- };
882
- var upsertAgentBlock = (existing, block) => {
883
- const managed = `${AGENT_BLOCK_START}
884
- ${block}
885
- ${AGENT_BLOCK_END}`;
886
- const startIndex = existing.indexOf(AGENT_BLOCK_START);
887
- const endIndex = existing.indexOf(AGENT_BLOCK_END);
888
- if (startIndex >= 0 && endIndex > startIndex) {
889
- return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
890
- }
891
- return `${existing.replace(/\s*$/, "")}
892
-
893
- ${managed}
894
- `;
895
- };
896
- var extractAgentBlock = (content) => {
897
- const startIndex = content.indexOf(AGENT_BLOCK_START);
898
- const endIndex = content.indexOf(AGENT_BLOCK_END);
899
- if (startIndex < 0 || endIndex <= startIndex)
900
- return null;
901
- return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
902
- };
903
- var signatureOf = (recipe) => {
904
- const entries = Object.entries(recipe.variants);
905
- if (entries.length === 0)
906
- return "";
907
- const parts = entries.map(([key, values]) => {
908
- if (values.length === 1 && values[0] === "true")
909
- return `${key}?`;
910
- const def = recipe.defaultVariants?.[key];
911
- return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
912
- });
913
- return `(${parts.join(" \xB7 ")})`;
914
- };
915
- var renderRecipeEntries = (recipes) => {
916
- const groups = new Map;
917
- for (const recipe of recipes)
918
- groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
919
- return [...groups.entries()].map(([importFrom, list]) => {
920
- const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
921
- `);
922
- return `Import from \`${importFrom}\`:
923
- ${items}`;
924
- }).join(`
925
-
926
- `);
927
- };
928
- var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
929
- const { collectRecipeSources } = await loadRecipeScanner();
930
- const sources = [
931
- ...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
932
- ];
933
- for (const lib of [...new Set(libDeps)].sort()) {
934
- if (scope.type === "lib" && lib === scope.name)
935
- continue;
936
- sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
937
- }
938
- return sources;
939
- };
940
- var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
941
- const { scanRecipes } = await loadRecipeScanner();
942
- return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
943
- };
944
- var renderScopeAgentBlock = (scope, recipes) => {
945
- const scopePath = `${scope.type}s/${scope.name}`;
946
- const intro = `## Recipes In Scope
947
-
948
- UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
949
- \`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
950
- arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
951
- boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
952
- and verified by \`akan lint ${scope.name}\`.`;
953
- if (recipes.length === 0) {
954
- return `${intro}
955
-
956
- No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
957
- root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
958
- folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
959
- }
960
- return `${intro}
961
-
962
- ${renderRecipeEntries(recipes)}`;
963
- };
964
- var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
965
-
966
- Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
967
- root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
968
- section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
969
- markers freely.
970
-
971
- ${AGENT_BLOCK_START}
972
- ${block}
973
- ${AGENT_BLOCK_END}
974
- `;
975
- var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
976
-
977
- Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
978
- is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
979
-
980
- Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
981
-
982
- - Restates the identifier, the signature, or the line under it \u2192 delete it.
983
- - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
984
- - JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
985
- - Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
986
- - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
987
- derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
988
- keep it, one line.
989
-
990
- That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
991
- subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
992
- surrounding code carries none, your diff carries none.`;
993
- var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
994
-
995
- @AGENTS.md
996
-
997
- ${CLAUDE_COMMENT_RULE}
998
- `;
999
-
1000
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
1001
- import path6 from "path";
1002
-
1003
- // pkgs/@akanjs/devkit/akanConfig/types.ts
1004
- import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
1005
-
1006
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
1007
- class AkanBin {
1008
- static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
1009
- static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1010
- static parse(declared, owner, baseDir) {
1011
- if (declared === undefined)
1012
- return {};
1013
- if (!declared || typeof declared !== "object" || Array.isArray(declared))
1014
- throw new Error(`${owner}: bin maps each executable's name to its platforms`);
1015
- return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
1016
- }
1017
- static isArchive(source) {
1018
- return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
1019
- }
1020
- static #platforms(name, platforms, owner, baseDir) {
1021
- if (!AkanBin.fileName.test(name))
1022
- throw new Error(`${owner}: bin.${name} is not a file name`);
1023
- if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
1024
- throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
1025
- const resolved = {};
1026
- for (const [platform, source] of Object.entries(platforms)) {
1027
- if (!binPlatforms.includes(platform))
1028
- throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
1029
- resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
1030
- }
1031
- return resolved;
1032
- }
1033
- static #source(at, source, baseDir) {
1034
- if (!source || typeof source !== "object" || Array.isArray(source))
1035
- throw new Error(`${at} must be { url, sha256 } or { path }`);
1036
- const { url, sha256, path: localPath, file, ...rest } = source;
1037
- const extra = Object.keys(rest);
1038
- if (extra.length)
1039
- throw new Error(`${at}: unknown key ${extra.join(", ")}`);
1040
- if (url === undefined === (localPath === undefined))
1041
- throw new Error(`${at} takes either url and sha256, or path`);
1042
- const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
1043
- const archive = AkanBin.isArchive(resolved);
1044
- if (file === undefined) {
1045
- if (archive)
1046
- throw new Error(`${at} names an archive: file picks the executable inside it`);
1047
- return resolved;
1048
- }
1049
- if (!archive)
1050
- throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
1051
- const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
1052
- if (!parts.length || path6.isAbsolute(file) || parts.includes(".."))
1053
- throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
1054
- return { ...resolved, file: parts.join("/") };
1055
- }
1056
- static #urlSource(at, url, sha256) {
1057
- if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
1058
- throw new Error(`${at}: url must be an http(s) address`);
1059
- if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
1060
- throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
1061
- return { url, sha256: sha256.toLowerCase() };
1062
- }
1063
- static #pathSource(at, localPath, sha256, baseDir) {
1064
- if (typeof localPath !== "string" || !localPath)
1065
- throw new Error(`${at}: path must name a file`);
1066
- if (sha256 !== undefined)
1067
- throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
1068
- return { path: path6.resolve(baseDir, localPath) };
1069
- }
1070
- }
1071
- // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
1072
- import fs2 from "fs";
1073
- import path7 from "path";
1074
- import { fileURLToPath } from "url";
1075
- import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
1076
- import { resolveAkanI18nConfig } from "akanjs/common";
1077
- var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
1078
- var DEFAULT_OPTIMIZE_IMPORTS = [
1079
- "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
1080
- "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
1081
- "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
1082
- "@material-ui/icons @tabler/icons-react mui-core react-icons/*"
1083
- ].flatMap((line) => line.split(" "));
1084
- var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
1085
- var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
1086
- var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
1087
- var DEFAULT_AKAN_IMAGE_CONFIG = {
1088
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
1089
- imageSizes: [32, 48, 64, 96, 128, 256, 384],
1090
- formats: ["image/webp"],
1091
- qualities: [75],
1092
- minimumCacheTTL: 14400,
1093
- remotePatterns: [],
1094
- localPatterns: [{ pathname: "/**" }],
1095
- dangerouslyAllowSVG: false,
1096
- maximumRedirects: 3,
1097
- fetchTimeoutMs: 7000,
1098
- maxRemoteBytes: 25 * 1024 * 1024,
1099
- maxConcurrency: 0
1100
- };
1101
- var normalizeIndexPath = (indexPath) => {
1102
- const normalized = indexPath?.trim();
1103
- if (!normalized)
1104
- return;
1105
- const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
1106
- return path === "/" ? "/" : path;
1107
- };
1108
- var normalizeStringList = (values) => {
1109
- const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
1110
- return normalized.length > 0 ? [...new Set(normalized)] : undefined;
1111
- };
1112
- var sanitizeAppIdSegment = (value) => {
1113
- const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1114
- if (!cleaned)
1115
- return "app";
1116
- return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
1117
- };
1118
- var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
1119
- var normalizeDeepLinkDomain = (domain) => {
1120
- const normalized = domain.trim();
1121
- if (!normalized)
1122
- return "";
1123
- try {
1124
- const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
1125
- return url.host.toLowerCase();
1126
- } catch {
1127
- return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
1128
- }
1129
- };
1130
- var normalizeDeepLinks = (deepLinks) => {
1131
- const schemes = normalizeStringList(deepLinks?.schemes);
1132
- const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
1133
- if (!schemes && !domains)
1134
- return;
1135
- return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
1136
- };
1137
- var emptyLibContributions = () => ({
1138
- externalLibs: [],
1139
- trustedDependencies: [],
1140
- bin: [],
1141
- docker: { preRuns: [], postRuns: [] },
1142
- keepFonts: []
1143
- });
1144
- var normalizePackageNames = (names, owner) => {
1145
- if (names === undefined)
1146
- return [];
1147
- if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
1148
- throw new Error(`${owner}: trustedDependencies lists package names`);
1149
- return [...new Set(names.map((name) => name.trim()))];
1150
- };
1151
- var normalizeKeepFonts = (keepFonts) => [
1152
- ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
1153
- ];
1154
- var dedupeDockerRuns = (runs) => {
1155
- const byKey = new Map;
1156
- for (const run of runs)
1157
- byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
1158
- return [...byKey.values()];
1159
- };
1160
-
1161
- class LibConfigRefusal extends Error {
598
+ class LibConfigRefusal extends Error {
1162
599
  }
1163
600
 
1164
601
  class AkanAppConfig {
@@ -1495,7 +932,7 @@ class AkanAppConfig {
1495
932
  }
1496
933
  #applyRoutes(routes = []) {
1497
934
  for (const route of routes) {
1498
- const basePath = route.basePath ? route.basePath.replace(/^\/+|\/+$/g, "") : null;
935
+ const basePath = route.basePath ? trimSlashes(route.basePath) : null;
1499
936
  if (basePath !== null)
1500
937
  this.basePaths.add(basePath);
1501
938
  const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
@@ -1597,7 +1034,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1597
1034
  static async from(app, { bustImportCache = false } = {}) {
1598
1035
  const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
1599
1036
  AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
1600
- WorkspaceExecutor.getBaseDevEnv(path7.join(app.workspace.workspaceRoot, ".env")),
1037
+ WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
1601
1038
  app.workspace.getLibs(),
1602
1039
  app.workspace.getPackageJson()
1603
1040
  ]);
@@ -1693,143 +1130,579 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1693
1130
  };
1694
1131
  }
1695
1132
  }
1696
- var akanPackageJson = null;
1697
- function getAkanPackageJson() {
1698
- if (akanPackageJson)
1699
- return akanPackageJson;
1700
- const sourceDir = path7.dirname(fileURLToPath(import.meta.url));
1701
- const packageJsonPaths = [
1702
- path7.join(sourceDir, "../../../akanjs/package.json"),
1703
- path7.join(process.cwd(), "pkgs/akanjs/package.json"),
1704
- path7.join(path7.dirname(Bun.main), "node_modules/akanjs/package.json")
1705
- ];
1706
- try {
1707
- packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
1708
- } catch {}
1709
- for (const packageJsonPath of packageJsonPaths) {
1710
- try {
1711
- akanPackageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
1712
- return akanPackageJson;
1713
- } catch {}
1133
+ var akanPackageJson = null;
1134
+ function getAkanPackageJson() {
1135
+ if (akanPackageJson)
1136
+ return akanPackageJson;
1137
+ const sourceDir = path3.dirname(fileURLToPath(import.meta.url));
1138
+ const packageJsonPaths = [
1139
+ path3.join(sourceDir, "../../../akanjs/package.json"),
1140
+ path3.join(process.cwd(), "pkgs/akanjs/package.json"),
1141
+ path3.join(path3.dirname(Bun.main), "node_modules/akanjs/package.json")
1142
+ ];
1143
+ try {
1144
+ packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
1145
+ } catch {}
1146
+ for (const packageJsonPath of packageJsonPaths) {
1147
+ try {
1148
+ akanPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
1149
+ return akanPackageJson;
1150
+ } catch {}
1151
+ }
1152
+ akanPackageJson = {
1153
+ name: "akanjs",
1154
+ version: "0.0.0",
1155
+ description: "akanjs",
1156
+ dependencies: {}
1157
+ };
1158
+ return akanPackageJson;
1159
+ }
1160
+ function mergeImageConfig(config = {}) {
1161
+ return {
1162
+ ...DEFAULT_AKAN_IMAGE_CONFIG,
1163
+ ...config,
1164
+ deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
1165
+ imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
1166
+ formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
1167
+ qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
1168
+ remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
1169
+ localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
1170
+ };
1171
+ }
1172
+
1173
+ class AkanLibConfig {
1174
+ lib;
1175
+ externalLibs;
1176
+ trustedDependencies;
1177
+ bin;
1178
+ docker;
1179
+ assets;
1180
+ plugins;
1181
+ constructor(lib, config, plugins = []) {
1182
+ this.lib = lib;
1183
+ this.externalLibs = config?.externalLibs ?? [];
1184
+ const owner = `libs/${lib.name}/akan.config.ts`;
1185
+ this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
1186
+ this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
1187
+ this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
1188
+ this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
1189
+ this.plugins = plugins;
1190
+ }
1191
+ static async from(lib, { bustImportCache = false } = {}) {
1192
+ const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
1193
+ const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1194
+ const { plugins, ...config } = resolved ?? {};
1195
+ if ("pipeline" in config)
1196
+ throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`pipeline\`, which a lib does not take: pipelines belong to apps and the workspace root, and a lib's build-time work goes in an akan build plugin.`);
1197
+ if ("subspace" in config)
1198
+ throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1199
+ return new AkanLibConfig(lib, config, plugins ?? []);
1200
+ }
1201
+ }
1202
+ //! need to refactor
1203
+ var shiftBuildNum = async (app, delta) => {
1204
+ const { buildNum } = (await AkanAppConfig.from(app)).native;
1205
+ const akanConfigPath = path3.join(app.cwdPath, "akan.config.ts");
1206
+ const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
1207
+ fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
1208
+ };
1209
+ var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
1210
+ var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
1211
+ // pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
1212
+ import path4 from "path";
1213
+ class AkanWorkspaceConfig {
1214
+ static fileName = "akan.config.ts";
1215
+ static #keys = ["subspace", "pipeline", "cloud"];
1216
+ static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
1217
+ static async from(workspace, { bustImportCache = false } = {}) {
1218
+ const file = path4.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
1219
+ if (!await FileSys2.fileExists(file))
1220
+ return new AkanWorkspaceConfig(null, {});
1221
+ const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
1222
+ return new AkanWorkspaceConfig(file, input);
1223
+ }
1224
+ file;
1225
+ subspace;
1226
+ pipeline;
1227
+ cloudHost;
1228
+ constructor(file, input) {
1229
+ this.file = file;
1230
+ if (typeof input !== "object" || input === null || Array.isArray(input))
1231
+ throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
1232
+ for (const key of Object.keys(input))
1233
+ if (!AkanWorkspaceConfig.#keys.includes(key))
1234
+ throw new Error(`The workspace root akan.config.ts declares \`${key}\`, which it does not take: it holds \`subspace\`, \`pipeline\` and \`cloud\`, and app or lib settings belong in apps/<app>/akan.config.ts or libs/<lib>/akan.config.ts.`);
1235
+ const config = input;
1236
+ this.subspace = config.subspace ?? null;
1237
+ this.pipeline = config.pipeline ?? null;
1238
+ this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
1239
+ }
1240
+ static #cloudHost(cloud) {
1241
+ if (cloud === undefined)
1242
+ return null;
1243
+ const refuse = (why) => {
1244
+ throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1245
+ };
1246
+ if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
1247
+ return refuse("that is not an object");
1248
+ const unknown = Object.keys(cloud).filter((key) => key !== "host");
1249
+ if (unknown.length)
1250
+ return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
1251
+ const { host } = cloud;
1252
+ if (host === undefined)
1253
+ return null;
1254
+ const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
1255
+ if (!url || url.search || url.hash || url.username || url.password)
1256
+ return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
1257
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
1258
+ return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
1259
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
1260
+ }
1261
+ }
1262
+ // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
1263
+ import { readdir, rm } from "fs/promises";
1264
+ import path6 from "path";
1265
+
1266
+ // pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
1267
+ import fs2 from "fs";
1268
+ import path5 from "path";
1269
+
1270
+ class AsyncDefaultExportDetector {
1271
+ static #typescriptLoad;
1272
+ static #loadTypescript() {
1273
+ AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
1274
+ return AsyncDefaultExportDetector.#typescriptLoad;
1275
+ }
1276
+ static async detect(moduleAbsPath) {
1277
+ try {
1278
+ const typescript = await AsyncDefaultExportDetector.#loadTypescript();
1279
+ const source = fs2.readFileSync(path5.resolve(moduleAbsPath), "utf8");
1280
+ const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
1281
+ return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
1282
+ } catch {
1283
+ return false;
1284
+ }
1285
+ }
1286
+ #ts;
1287
+ constructor(typescript) {
1288
+ this.#ts = typescript;
1289
+ }
1290
+ detectInSourceFile(sourceFile) {
1291
+ const ts = this.#ts;
1292
+ const asyncBindings = new Map;
1293
+ let defaultIdentifier = null;
1294
+ for (const statement of sourceFile.statements) {
1295
+ if (ts.isFunctionDeclaration(statement)) {
1296
+ if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
1297
+ return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
1298
+ }
1299
+ if (statement.name) {
1300
+ asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
1301
+ }
1302
+ continue;
1303
+ }
1304
+ if (ts.isVariableStatement(statement)) {
1305
+ for (const declaration of statement.declarationList.declarations) {
1306
+ if (!ts.isIdentifier(declaration.name))
1307
+ continue;
1308
+ asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
1309
+ }
1310
+ continue;
1311
+ }
1312
+ if (ts.isExportAssignment(statement)) {
1313
+ if (this.#isAsyncFunctionExpression(statement.expression))
1314
+ return true;
1315
+ if (ts.isCallExpression(statement.expression))
1316
+ return true;
1317
+ if (ts.isIdentifier(statement.expression))
1318
+ defaultIdentifier = statement.expression.text;
1319
+ continue;
1320
+ }
1321
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
1322
+ for (const specifier of statement.exportClause.elements) {
1323
+ if (specifier.name.text !== "default")
1324
+ continue;
1325
+ defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
1326
+ }
1327
+ }
1328
+ }
1329
+ return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
1330
+ }
1331
+ #hasModifier(node, kind) {
1332
+ const ts = this.#ts;
1333
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
1334
+ }
1335
+ #isAsyncFunctionExpression(node) {
1336
+ const ts = this.#ts;
1337
+ return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
1338
+ }
1339
+ }
1340
+
1341
+ // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
1342
+ var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
1343
+ var IMPLICIT_LAYOUT_DIR = path6.join(".akan", "generated", "root-layouts");
1344
+ var IMPLICIT_DICT_DIR = path6.join(".akan", "generated", "dict");
1345
+ var IMPLICIT_OVERRIDES_DIR = path6.join(".akan", "generated", "overrides");
1346
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
1347
+ var importSpecifier = (fromAbsPath, toAbsPath) => {
1348
+ const rel = path6.relative(path6.dirname(fromAbsPath), toAbsPath).split(path6.sep).join("/");
1349
+ return rel.startsWith(".") ? rel : `./${rel}`;
1350
+ };
1351
+ async function writeGeneratedOverridesLayoutFile(opts) {
1352
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
1353
+ const absPath = path6.join(path6.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
1354
+ const source = `"use client";
1355
+ import { UiOverrideProvider } from "akanjs/ui";
1356
+ import { createElement, type ReactNode } from "react";
1357
+ import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
1358
+
1359
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
1360
+ return createElement(UiOverrideProvider, { value }, children);
1361
+ }
1362
+ `;
1363
+ await Bun.write(absPath, source);
1364
+ return absPath;
1365
+ }
1366
+ async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
1367
+ const keep = new Set(keepAbsPaths);
1368
+ const names = await readdir(dirAbsPath).catch(() => []);
1369
+ await Promise.all(names.map((name) => path6.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm(absPath, { force: true })));
1370
+ }
1371
+ function getRootBoundarySegments(key) {
1372
+ const match = LAYOUT_KEY_RE.exec(key);
1373
+ if (!match)
1374
+ return null;
1375
+ const prefix = match[1]?.replace(/\/$/, "");
1376
+ if (!prefix)
1377
+ return [];
1378
+ return prefix.split("/").filter(Boolean);
1379
+ }
1380
+ function implicitRootLayoutKey(segments) {
1381
+ return `./${[...segments, "__root_layout"].join("/")}.tsx`;
1382
+ }
1383
+ function implicitRootLayoutAbsPath(appCwdPath, segments) {
1384
+ const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
1385
+ return path6.join(path6.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
1386
+ }
1387
+ function isRootBoundarySegments(segments, basePaths) {
1388
+ const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
1389
+ if (firstVisibleIndex === -1)
1390
+ return segments.length <= 1;
1391
+ if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
1392
+ return false;
1393
+ const visible = segments.slice(firstVisibleIndex);
1394
+ const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
1395
+ return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
1396
+ }
1397
+ function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
1398
+ const boundaries = new Map;
1399
+ for (const key of pageKeys) {
1400
+ const segments = getRootBoundarySegments(key);
1401
+ if (!segments)
1402
+ continue;
1403
+ if (!isRootBoundarySegments(segments, basePaths))
1404
+ continue;
1405
+ const id = segments.join("/");
1406
+ boundaries.set(id, {
1407
+ sourceKey: key,
1408
+ sourceAbsPath: path6.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
1409
+ segments
1410
+ });
1411
+ }
1412
+ if (boundaries.size === 0)
1413
+ boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
1414
+ return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
1415
+ }
1416
+ function hasAncestorRootBoundary(boundary, boundaries) {
1417
+ return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
1418
+ }
1419
+ function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
1420
+ const rootLayoutKey = pageKeys.find((key) => {
1421
+ const segments = getRootBoundarySegments(key);
1422
+ return segments !== null && segments.length === 0;
1423
+ });
1424
+ return rootLayoutKey ? path6.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
1425
+ }
1426
+ async function assertEnvClientConvention(appCwdPath, appName) {
1427
+ const envPath = path6.join(appCwdPath, "env", "env.client.ts");
1428
+ if (!await Bun.file(envPath).exists()) {
1429
+ throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
1714
1430
  }
1715
- akanPackageJson = {
1716
- name: "akanjs",
1717
- version: "0.0.0",
1718
- description: "akanjs",
1719
- dependencies: {}
1720
- };
1721
- return akanPackageJson;
1722
1431
  }
1723
- function mergeImageConfig(config = {}) {
1724
- return {
1725
- ...DEFAULT_AKAN_IMAGE_CONFIG,
1726
- ...config,
1727
- deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
1728
- imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
1729
- formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
1730
- qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
1731
- remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
1732
- localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
1733
- };
1432
+ async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
1433
+ const absPath = path6.join(path6.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
1434
+ await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
1435
+
1436
+ export const allDictionary = getAllDictionary();
1437
+ `);
1438
+ return absPath;
1439
+ }
1440
+ async function writeGeneratedRootLayoutFile(opts) {
1441
+ await assertEnvClientConvention(opts.appCwdPath, opts.appName);
1442
+ const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
1443
+ const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
1444
+ const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
1445
+ const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
1446
+ const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
1447
+ const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
1448
+ const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
1449
+ void st;
1450
+ ` : `import "@apps/${opts.appName}/client";
1451
+ `;
1452
+ const inheritedLabel = inheritedSourceAbsPath ? path6.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
1453
+ const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
1454
+ const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
1455
+ ` : `const inheritedLayout: LayoutModule = {};
1456
+ `;
1457
+ const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
1458
+ const userLabel = opts.boundary.sourceAbsPath ? path6.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
1459
+ const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
1460
+ const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
1461
+ const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
1462
+ ` : `const UserLayout = ({ children }: LayoutProps) => children;
1463
+ const userLayout: LayoutModule = {};
1464
+ `;
1465
+ const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
1466
+ const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
1467
+ const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
1468
+ const layoutBinding = isAsyncUserLayout ? ` const layout =
1469
+ process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
1470
+ ? await UserLayout({ params, searchParams, children })
1471
+ : ${userLayoutElement};
1472
+ ` : "";
1473
+ const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
1474
+ const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
1475
+ const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
1476
+ const layoutExports = `export async function generateHead(props: PageProps) {
1477
+ const heads = await Promise.all(
1478
+ ${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
1479
+ );
1480
+ return RouteHead.mergeNodes(heads);
1734
1481
  }
1735
1482
 
1736
- class AkanLibConfig {
1737
- lib;
1738
- externalLibs;
1739
- trustedDependencies;
1740
- bin;
1741
- docker;
1742
- assets;
1743
- plugins;
1744
- constructor(lib, config, plugins = []) {
1745
- this.lib = lib;
1746
- this.externalLibs = config?.externalLibs ?? [];
1747
- const owner = `libs/${lib.name}/akan.config.ts`;
1748
- this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
1749
- this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
1750
- this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
1751
- this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
1752
- this.plugins = plugins;
1483
+ export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
1484
+ export const Error = userLayout.Error ?? inheritedLayout.Error;
1485
+ export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
1486
+ export const Loading = userLayout.Loading;
1487
+
1488
+ ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
1489
+ ${layoutBinding} return `;
1490
+ const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
1491
+ import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
1492
+ import type { ReactNode } from "react";
1493
+ import { System } from "akanjs/ui";
1494
+ import { env } from "@apps/${opts.appName}/env/env.client";
1495
+ import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
1496
+ ${clientImport}${inheritedImport}${userImport}
1497
+ const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
1498
+ const defaultFonts = userFonts.filter((font) => font.default);
1499
+ if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
1500
+ const defaultFont = defaultFonts[0];
1501
+ const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
1502
+ export const theme = userLayout.theme ?? inheritedLayout.theme;
1503
+ export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
1504
+ export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
1505
+
1506
+ ${layoutExports}(
1507
+ <System.Provider
1508
+ of={GeneratedLayout as never}
1509
+ appName=${JSON.stringify(opts.appName)}
1510
+ ${prefix ? `prefix=${JSON.stringify(prefix)}
1511
+ ` : ""}params={params}
1512
+ manifest={manifest}
1513
+ env={env}
1514
+ theme={theme}
1515
+ viewport={viewport}
1516
+ fonts={loadFonts(userFonts)}
1517
+ className={defaultFontClassName}
1518
+ layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
1519
+ reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
1520
+ wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
1521
+ allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
1522
+ >
1523
+ ${layoutChild}
1524
+ </System.Provider>
1525
+ );
1526
+ }
1527
+ ` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
1528
+ import { resolveRouteModule, RouteHead } from "akanjs/client";
1529
+ import type { ReactNode } from "react";
1530
+ ${inheritedImport}${userImport}
1531
+ ${layoutExports}${layoutReturn};
1532
+ }
1533
+ `;
1534
+ await Bun.write(absPath, source);
1535
+ return absPath;
1536
+ }
1537
+ async function resolveSsrPageEntries(opts) {
1538
+ const absPageDir = path6.resolve(opts.appCwdPath, "page");
1539
+ const hasSt = await Bun.file(path6.join(opts.appCwdPath, "lib", "st.ts")).exists();
1540
+ const basePaths = opts.basePaths ?? [];
1541
+ const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
1542
+ const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
1543
+ const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
1544
+ const segments = getRootBoundarySegments(key);
1545
+ return segments !== null && isRootBoundarySegments(segments, basePaths);
1546
+ }));
1547
+ const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
1548
+ const userAbsPath = path6.resolve(absPageDir, key);
1549
+ if (OVERRIDES_KEY_RE.test(key)) {
1550
+ const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
1551
+ appCwdPath: opts.appCwdPath,
1552
+ key,
1553
+ userAbsPath
1554
+ });
1555
+ return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
1556
+ }
1557
+ return { key, moduleAbsPath: userAbsPath };
1558
+ }));
1559
+ const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
1560
+ key: implicitRootLayoutKey(boundary.segments),
1561
+ moduleAbsPath: await writeGeneratedRootLayoutFile({
1562
+ appCwdPath: opts.appCwdPath,
1563
+ appName: opts.appName,
1564
+ boundary,
1565
+ rootSourceAbsPath,
1566
+ includeStInit: hasSt && boundary.segments.length === 0,
1567
+ includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
1568
+ }),
1569
+ seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
1570
+ })));
1571
+ const appCwdAbsPath = path6.resolve(opts.appCwdPath);
1572
+ await Promise.all([
1573
+ pruneGeneratedFiles(path6.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
1574
+ pruneGeneratedFiles(path6.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
1575
+ ]);
1576
+ return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
1577
+ }
1578
+ async function resolveSsrPageEntriesForApp(app, pageKeys) {
1579
+ const config = await app.getConfig();
1580
+ return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
1581
+ }
1582
+
1583
+ // pkgs/@akanjs/devkit/codegenLock.ts
1584
+ import { mkdir, open, readFile, rm as rm2, stat } from "fs/promises";
1585
+ import path7 from "path";
1586
+ import { Logger as Logger2 } from "akanjs/common";
1587
+
1588
+ class CodegenLock {
1589
+ static fileName = "codegen.lock";
1590
+ static waitTimeoutMs = 1e4;
1591
+ static unknownHolderStaleMs = 60000;
1592
+ static #pollMs = 25;
1593
+ static #logger = new Logger2("CodegenLock");
1594
+ static #queue = Promise.resolve();
1595
+ static pathIn(workspaceRoot) {
1596
+ return path7.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
1753
1597
  }
1754
- static async from(lib, { bustImportCache = false } = {}) {
1755
- const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
1756
- const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1757
- const { plugins, ...config } = resolved ?? {};
1758
- if ("pipeline" in config)
1759
- throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`pipeline\`, which a lib does not take: pipelines belong to apps and the workspace root, and a lib's build-time work goes in an akan build plugin.`);
1760
- if ("subspace" in config)
1761
- throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1762
- return new AkanLibConfig(lib, config, plugins ?? []);
1598
+ static async run(workspaceRoot, label, fn) {
1599
+ const ahead = CodegenLock.#queue;
1600
+ let done;
1601
+ CodegenLock.#queue = new Promise((resolve) => {
1602
+ done = resolve;
1603
+ });
1604
+ try {
1605
+ await CodegenLock.#waitForQueue(ahead, label);
1606
+ return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
1607
+ } finally {
1608
+ done();
1609
+ }
1610
+ }
1611
+ static async#waitForQueue(ahead, label) {
1612
+ let timer = null;
1613
+ const expired = new Promise((resolve) => {
1614
+ timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
1615
+ });
1616
+ try {
1617
+ if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
1618
+ CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
1619
+ } finally {
1620
+ if (timer)
1621
+ clearTimeout(timer);
1622
+ }
1763
1623
  }
1764
- }
1765
- //! need to refactor
1766
- var shiftBuildNum = async (app, delta) => {
1767
- const { buildNum } = (await AkanAppConfig.from(app)).native;
1768
- const akanConfigPath = path7.join(app.cwdPath, "akan.config.ts");
1769
- const akanConfig = fs2.readFileSync(akanConfigPath, "utf8");
1770
- fs2.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
1771
- };
1772
- var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
1773
- var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
1774
- // pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
1775
- import path8 from "path";
1776
- class AkanWorkspaceConfig {
1777
- static fileName = "akan.config.ts";
1778
- static #keys = ["subspace", "pipeline", "cloud"];
1779
- static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
1780
- static async from(workspace, { bustImportCache = false } = {}) {
1781
- const file = path8.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
1782
- if (!await FileSys2.fileExists(file))
1783
- return new AkanWorkspaceConfig(null, {});
1784
- const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
1785
- return new AkanWorkspaceConfig(file, input);
1624
+ static async#withFileLock(workspaceRoot, label, fn) {
1625
+ const lockPath = CodegenLock.pathIn(workspaceRoot);
1626
+ await mkdir(path7.dirname(lockPath), { recursive: true }).catch(() => {
1627
+ return;
1628
+ });
1629
+ const held = await CodegenLock.#acquire(lockPath, label);
1630
+ try {
1631
+ return await fn();
1632
+ } finally {
1633
+ if (held)
1634
+ await rm2(lockPath, { force: true }).catch(() => {
1635
+ return;
1636
+ });
1637
+ }
1786
1638
  }
1787
- file;
1788
- subspace;
1789
- pipeline;
1790
- cloudHost;
1791
- constructor(file, input) {
1792
- this.file = file;
1793
- if (typeof input !== "object" || input === null || Array.isArray(input))
1794
- throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
1795
- for (const key of Object.keys(input))
1796
- if (!AkanWorkspaceConfig.#keys.includes(key))
1797
- throw new Error(`The workspace root akan.config.ts declares \`${key}\`, which it does not take: it holds \`subspace\`, \`pipeline\` and \`cloud\`, and app or lib settings belong in apps/<app>/akan.config.ts or libs/<lib>/akan.config.ts.`);
1798
- const config = input;
1799
- this.subspace = config.subspace ?? null;
1800
- this.pipeline = config.pipeline ?? null;
1801
- this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
1639
+ static async#acquire(lockPath, label) {
1640
+ const deadline = Date.now() + CodegenLock.waitTimeoutMs;
1641
+ for (;; ) {
1642
+ const handle = await open(lockPath, "wx").catch(() => null);
1643
+ if (handle) {
1644
+ await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
1645
+ return;
1646
+ });
1647
+ await handle.close().catch(() => {
1648
+ return;
1649
+ });
1650
+ return true;
1651
+ }
1652
+ if (await CodegenLock.#reclaimIfAbandoned(lockPath))
1653
+ continue;
1654
+ if (Date.now() >= deadline) {
1655
+ CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
1656
+ return false;
1657
+ }
1658
+ await Bun.sleep(CodegenLock.#pollMs);
1659
+ }
1802
1660
  }
1803
- static #cloudHost(cloud) {
1804
- if (cloud === undefined)
1805
- return null;
1806
- const refuse = (why) => {
1807
- throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1808
- };
1809
- if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
1810
- return refuse("that is not an object");
1811
- const unknown = Object.keys(cloud).filter((key) => key !== "host");
1812
- if (unknown.length)
1813
- return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
1814
- const { host } = cloud;
1815
- if (host === undefined)
1661
+ static async#reclaimIfAbandoned(lockPath) {
1662
+ const info = await stat(lockPath).catch(() => null);
1663
+ if (!info)
1664
+ return true;
1665
+ const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
1666
+ if (holder) {
1667
+ if (CodegenLock.#isAlive(holder.pid))
1668
+ return false;
1669
+ } else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
1670
+ return false;
1671
+ await rm2(lockPath, { force: true }).catch(() => {
1672
+ return;
1673
+ });
1674
+ return true;
1675
+ }
1676
+ static #parseHolder(raw) {
1677
+ try {
1678
+ const parsed = JSON.parse(raw);
1679
+ if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
1680
+ return null;
1681
+ return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
1682
+ } catch {
1816
1683
  return null;
1817
- const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
1818
- if (!url || url.search || url.hash || url.username || url.password)
1819
- return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
1820
- if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
1821
- return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
1822
- return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
1684
+ }
1685
+ }
1686
+ static #isAlive(pid) {
1687
+ if (!Number.isInteger(pid) || pid <= 0)
1688
+ return false;
1689
+ try {
1690
+ process.kill(pid, 0);
1691
+ return true;
1692
+ } catch (error) {
1693
+ return error.code === "EPERM";
1694
+ }
1823
1695
  }
1824
1696
  }
1697
+
1825
1698
  // pkgs/@akanjs/devkit/jsonFormatter.ts
1826
- import path9 from "path";
1699
+ import path8 from "path";
1827
1700
 
1828
1701
  class JsonFormatter {
1829
1702
  static lineWidth = 120;
1830
1703
  static stringify(value, filePath) {
1831
1704
  const json = JSON.stringify(value, null, 2);
1832
- if (path9.basename(filePath) === "package.json")
1705
+ if (path8.basename(filePath) === "package.json")
1833
1706
  return `${json}
1834
1707
  `;
1835
1708
  return `${JsonFormatter.#print(JSON.parse(json), 0, { column: 0, trailing: 0 })}
@@ -1900,7 +1773,7 @@ ${" ".repeat(depth)}]`;
1900
1773
  // pkgs/@akanjs/devkit/linter.ts
1901
1774
  import { spawn } from "child_process";
1902
1775
  import { existsSync as existsSync2, readFileSync } from "fs";
1903
- import path10 from "path";
1776
+ import path9 from "path";
1904
1777
  import chalk from "chalk";
1905
1778
  var parseBiomeReport = (output) => {
1906
1779
  for (let start = output.indexOf("{");start !== -1; start = output.indexOf("{", start + 1)) {
@@ -1966,13 +1839,13 @@ class Linter {
1966
1839
  #biomeBin;
1967
1840
  constructor(cwdPath) {
1968
1841
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
1969
- this.configPath = Linter.#configPathIn(this.lintRoot) ?? path10.join(this.lintRoot, "biome.json");
1970
- const localBiomeBin = path10.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
1842
+ this.configPath = Linter.#configPathIn(this.lintRoot) ?? path9.join(this.lintRoot, "biome.json");
1843
+ const localBiomeBin = path9.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
1971
1844
  this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
1972
1845
  }
1973
1846
  static #configPathIn(dir) {
1974
1847
  for (const fileName of BIOME_CONFIG_FILES) {
1975
- const configPath = path10.join(dir, fileName);
1848
+ const configPath = path9.join(dir, fileName);
1976
1849
  if (existsSync2(configPath))
1977
1850
  return configPath;
1978
1851
  }
@@ -1981,19 +1854,19 @@ class Linter {
1981
1854
  #findBiomeRootPath(dir) {
1982
1855
  if (Linter.#configPathIn(dir))
1983
1856
  return dir;
1984
- const parentDir = path10.dirname(dir);
1857
+ const parentDir = path9.dirname(dir);
1985
1858
  if (parentDir === dir)
1986
1859
  throw new Error(`biome.json not found from ${dir}`);
1987
1860
  return this.#findBiomeRootPath(parentDir);
1988
1861
  }
1989
1862
  #toBiomePath(filePath) {
1990
- const relativePath = path10.relative(this.lintRoot, filePath);
1991
- if (!relativePath.startsWith("..") && !path10.isAbsolute(relativePath))
1863
+ const relativePath = path9.relative(this.lintRoot, filePath);
1864
+ if (!relativePath.startsWith("..") && !path9.isAbsolute(relativePath))
1992
1865
  return relativePath;
1993
1866
  return filePath;
1994
1867
  }
1995
1868
  #resolveFilePath(filePath) {
1996
- return path10.isAbsolute(filePath) ? filePath : path10.join(this.lintRoot, filePath);
1869
+ return path9.isAbsolute(filePath) ? filePath : path9.join(this.lintRoot, filePath);
1997
1870
  }
1998
1871
  async#runBiome(args, input) {
1999
1872
  return await new Promise((resolve, reject) => {
@@ -2018,7 +1891,7 @@ class Linter {
2018
1891
  const diagnosticPath = diagnostic.location?.path;
2019
1892
  if (!diagnosticPath)
2020
1893
  return fallbackFilePath;
2021
- return path10.isAbsolute(diagnosticPath) ? diagnosticPath : path10.join(this.lintRoot, diagnosticPath);
1894
+ return path9.isAbsolute(diagnosticPath) ? diagnosticPath : path9.join(this.lintRoot, diagnosticPath);
2022
1895
  }
2023
1896
  #createLintMessage(diagnostic) {
2024
1897
  const start = diagnostic.location?.start;
@@ -2354,7 +2227,7 @@ class ManifestDependencies {
2354
2227
 
2355
2228
  // pkgs/@akanjs/devkit/repoIdentity.ts
2356
2229
  import { execFileSync } from "child_process";
2357
- import path11 from "path";
2230
+ import path10 from "path";
2358
2231
  var resolved = new Map;
2359
2232
  var readRemoteName = (workspaceRoot) => {
2360
2233
  try {
@@ -2372,13 +2245,14 @@ var resolveRepoName = (workspaceRoot) => {
2372
2245
  const cached = resolved.get(workspaceRoot);
2373
2246
  if (cached)
2374
2247
  return cached;
2375
- const repoName = readRemoteName(workspaceRoot) ?? path11.basename(workspaceRoot);
2248
+ const repoName = readRemoteName(workspaceRoot) ?? path10.basename(workspaceRoot);
2376
2249
  resolved.set(workspaceRoot, repoName);
2377
2250
  return repoName;
2378
2251
  };
2379
2252
 
2380
2253
  // pkgs/@akanjs/devkit/scanInfo.ts
2381
- import path12 from "path";
2254
+ import { createHash } from "crypto";
2255
+ import path11 from "path";
2382
2256
 
2383
2257
  // pkgs/@akanjs/devkit/workspaceLayout.ts
2384
2258
  var appRootAllowedFiles = new Set([
@@ -2499,10 +2373,10 @@ var moduleUiFileTypes = {
2499
2373
  service: new Set(["Util", "Zone"]),
2500
2374
  scalar: new Set(["Template", "Unit"])
2501
2375
  };
2502
- var createDependencyScanner = async (exec) => (await import("./dependencyScanner-06z2dmrr.js")).TypeScriptDependencyScanner.from(exec);
2503
- var getScanPath = (exec, relativePath) => path12.posix.join(`${exec.type}s`, exec.name, relativePath.split(path12.sep).join("/"));
2376
+ var createDependencyScanner = async (exec) => (await import("./dependencyScanner-3afj2vt1.js")).TypeScriptDependencyScanner.from(exec);
2377
+ var getScanPath = (exec, relativePath) => path11.posix.join(`${exec.type}s`, exec.name, relativePath.split(path11.sep).join("/"));
2504
2378
  var getModuleNameFromPath = (kind, modulePath) => {
2505
- const dirname = path12.basename(modulePath);
2379
+ const dirname = path11.basename(modulePath);
2506
2380
  return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
2507
2381
  };
2508
2382
  async function assertScanConvention(exec, libRoot) {
@@ -2523,28 +2397,28 @@ async function assertScanConvention(exec, libRoot) {
2523
2397
  for (const filename of await exec.readdir(facet)) {
2524
2398
  const shadow = facetIndexShadowOf(`${facet}/${filename}`);
2525
2399
  if (shadow)
2526
- addViolation(path12.join(facet, filename), shadow);
2400
+ addViolation(path11.join(facet, filename), shadow);
2527
2401
  }
2528
2402
  if (exec.type === "lib")
2529
2403
  for (const { relativePath, reason } of await exec.getPageConventionViolations())
2530
2404
  addViolation(relativePath, reason);
2531
2405
  for (const filename of libRoot.files)
2532
2406
  if (!isAllowedLibFacetRootFile(filename))
2533
- addViolation(path12.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
2407
+ addViolation(path11.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
2534
2408
  for (const dirname of libRoot.dirs)
2535
2409
  if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
2536
- addViolation(path12.join("lib", dirname), "unsupported internal lib folder");
2410
+ addViolation(path11.join("lib", dirname), "unsupported internal lib folder");
2537
2411
  const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
2538
2412
  const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
2539
2413
  const scalarDirs = await exec.readdir("lib/__scalar");
2540
2414
  await Promise.all([
2541
- ...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database", path12.join("lib", dirname))),
2542
- ...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service", path12.join("lib", dirname))),
2543
- ...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar", path12.join("lib/__scalar", dirname)))
2415
+ ...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database", path11.join("lib", dirname))),
2416
+ ...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service", path11.join("lib", dirname))),
2417
+ ...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar", path11.join("lib/__scalar", dirname)))
2544
2418
  ]);
2545
2419
  if (violations.length > 0) {
2546
2420
  throw new Error(`[scan-convention]
2547
- ${violations.sort().map((violation) => `- ${violation}`).join(`
2421
+ ${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
2548
2422
  `)}`);
2549
2423
  }
2550
2424
  }
@@ -2552,11 +2426,11 @@ async function validateModuleFiles(exec, violations, kind, modulePath) {
2552
2426
  const { files, dirs } = await exec.getFilesAndDirs(modulePath);
2553
2427
  const moduleName = getModuleNameFromPath(kind, modulePath);
2554
2428
  dirs.forEach((dirname) => {
2555
- violations.push(`${getScanPath(exec, path12.join(modulePath, dirname))}: unsupported module folder`);
2429
+ violations.push(`${getScanPath(exec, path11.join(modulePath, dirname))}: unsupported module folder`);
2556
2430
  });
2557
2431
  const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
2558
2432
  for (const filename of files) {
2559
- const scanPath = getScanPath(exec, path12.join(modulePath, filename));
2433
+ const scanPath = getScanPath(exec, path11.join(modulePath, filename));
2560
2434
  if (filename === "index.ts" || filename === "index.tsx")
2561
2435
  continue;
2562
2436
  if (filename === `${moduleName}.abstract.md`)
@@ -2631,16 +2505,16 @@ class ScanInfo {
2631
2505
  }
2632
2506
  };
2633
2507
  await Promise.all([
2634
- ...databaseDirs.map((name) => collect(path12.join("lib", name), name, "databases")),
2635
- ...serviceDirs.map((dirname) => collect(path12.join("lib", dirname), dirname.slice(1), "services")),
2636
- ...scalarDirs.map((name) => collect(path12.join("lib/__scalar", name), name, "scalars"))
2508
+ ...databaseDirs.map((name) => collect(path11.join("lib", name), name, "databases")),
2509
+ ...serviceDirs.map((dirname) => collect(path11.join("lib", dirname), dirname.slice(1), "services")),
2510
+ ...scalarDirs.map((name) => collect(path11.join("lib/__scalar", name), name, "scalars"))
2637
2511
  ]);
2638
2512
  const routes = exec.type === "lib" ? [] : await exec.getPageKeys();
2639
2513
  const common = {
2640
2514
  name: exec.name,
2641
2515
  type: exec.type,
2642
2516
  repoName: exec.workspace.repoName,
2643
- serveDomain: WorkspaceExecutor.getBaseDevEnv(path12.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
2517
+ serveDomain: WorkspaceExecutor.getBaseDevEnv(path11.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
2644
2518
  files,
2645
2519
  libDeps,
2646
2520
  pkgDeps,
@@ -2701,10 +2575,13 @@ class ScanInfo {
2701
2575
  var isAkanFrameworkDependency = (dep) => dep === "akanjs" || dep.startsWith("akanjs/");
2702
2576
 
2703
2577
  class AppInfo extends ScanInfo {
2578
+ static migrationDir = "script/migration";
2579
+ static #migrationIdRe = /^\d{12}_[A-Za-z][A-Za-z0-9]*$/;
2704
2580
  type = "app";
2705
2581
  exec;
2706
2582
  akanConfig;
2707
2583
  libDeps;
2584
+ migrations;
2708
2585
  static appInfos = new Map;
2709
2586
  static async fromExecutor(exec, options = {}) {
2710
2587
  const existingAppInfo = AppInfo.appInfos.get(exec.name);
@@ -2716,16 +2593,20 @@ class AppInfo extends ScanInfo {
2716
2593
  const libExecutor = LibExecutor.from(exec, libName);
2717
2594
  LibInfo.libInfos.set(libName, await LibInfo.fromExecutor(libExecutor));
2718
2595
  }));
2719
- const libDeps = await AppInfo.#getAllLibDeps(exec, scanResult.libDeps);
2720
- const appInfo = new AppInfo(exec, scanResult, libDeps);
2596
+ const [libDeps, migrations] = await Promise.all([
2597
+ AppInfo.#getAllLibDeps(exec, scanResult.libDeps),
2598
+ AppInfo.scanMigrations(exec)
2599
+ ]);
2600
+ const appInfo = new AppInfo(exec, scanResult, libDeps, migrations);
2721
2601
  AppInfo.appInfos.set(exec.name, appInfo);
2722
2602
  return appInfo;
2723
2603
  }
2724
- constructor(exec, scanResult, libDeps) {
2604
+ constructor(exec, scanResult, libDeps, migrations = []) {
2725
2605
  super(scanResult);
2726
2606
  this.exec = exec;
2727
2607
  this.akanConfig = scanResult.akanConfig;
2728
2608
  this.libDeps = libDeps;
2609
+ this.migrations = migrations;
2729
2610
  }
2730
2611
  getScanResult() {
2731
2612
  return this.scanResult;
@@ -2733,6 +2614,18 @@ class AppInfo extends ScanInfo {
2733
2614
  setRoutes(routes) {
2734
2615
  this.scanResult.routes = routes;
2735
2616
  }
2617
+ static async scanMigrations(exec) {
2618
+ const files = (await exec.readdir(AppInfo.migrationDir)).filter((file) => file.endsWith(".ts") && !/\.(test|spec|d)\.ts$/.test(file));
2619
+ return await Promise.all(files.map(async (file) => {
2620
+ const id = file.slice(0, -".ts".length);
2621
+ if (!AppInfo.#migrationIdRe.test(id))
2622
+ throw new Error(`${AppInfo.migrationDir}/${file}: a migration is named <yyyyMMddHHmm>_<camelName>.ts; create one with \`akan create-migration\``);
2623
+ const source = (await exec.readFile(`${AppInfo.migrationDir}/${file}`)).replace(/^\uFEFF/, "").replaceAll(`\r
2624
+ `, `
2625
+ `);
2626
+ return { id, checksum: createHash("sha256").update(source).digest("hex") };
2627
+ }));
2628
+ }
2736
2629
  static async#getAllLibDeps(exec, libDeps, libSet = new Set) {
2737
2630
  await Promise.all(libDeps.map(async (libName) => {
2738
2631
  if (libSet.has(libName))
@@ -2868,6 +2761,136 @@ class WorkspaceInfo {
2868
2761
  }
2869
2762
  }
2870
2763
 
2764
+ // pkgs/@akanjs/devkit/spinner.ts
2765
+ import ora from "ora";
2766
+
2767
+ class Spinner {
2768
+ static padding = 12;
2769
+ static oraOptions = { discardStdin: false };
2770
+ static canAnimate(stream = process.stderr) {
2771
+ return !stream.isTTY || stream.columns > 0;
2772
+ }
2773
+ spinner;
2774
+ stopWatch = null;
2775
+ startAt = new Date;
2776
+ prefix;
2777
+ message;
2778
+ enableSpin;
2779
+ constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
2780
+ Spinner.padding = Math.max(Spinner.padding, prefix.length);
2781
+ this.prefix = prefix;
2782
+ this.message = message;
2783
+ this.spinner = ora({ ...Spinner.oraOptions, text: message });
2784
+ this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
2785
+ this.spinner.indent = indent;
2786
+ this.enableSpin = enableSpin && Spinner.canAnimate();
2787
+ }
2788
+ start() {
2789
+ this.startAt = new Date;
2790
+ if (this.enableSpin) {
2791
+ this.spinner.start();
2792
+ this.stopWatch = setInterval(() => {
2793
+ this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
2794
+ this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
2795
+ }, 1000);
2796
+ } else
2797
+ this.spinner.info();
2798
+ return this;
2799
+ }
2800
+ succeed(message) {
2801
+ this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
2802
+ this.#reset();
2803
+ }
2804
+ fail(message) {
2805
+ this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
2806
+ this.#reset();
2807
+ }
2808
+ isSpinning() {
2809
+ return this.spinner.isSpinning;
2810
+ }
2811
+ #reset() {
2812
+ if (this.stopWatch)
2813
+ clearInterval(this.stopWatch);
2814
+ this.stopWatch = null;
2815
+ }
2816
+ #getElapsedTimeStr({ floor = false } = {}) {
2817
+ const ms = Date.now() - this.startAt.getTime();
2818
+ if (ms < 1000)
2819
+ return `${ms}ms`;
2820
+ const s = Math.floor(ms / 1000);
2821
+ if (s < 60)
2822
+ return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
2823
+ const m = Math.floor(s / 60);
2824
+ return `${m}m ${s % 60}s`;
2825
+ }
2826
+ }
2827
+
2828
+ // pkgs/@akanjs/devkit/typecheck/projectTypecheck.ts
2829
+ import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
2830
+ import path12 from "path";
2831
+
2832
+ class ProjectTypecheck2 {
2833
+ projectDir;
2834
+ workspaceRoot;
2835
+ env;
2836
+ static #testIncludes = ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"];
2837
+ static #excludes = [".akan/**", "node_modules/**", "local/**"];
2838
+ constructor(projectDir, workspaceRoot, env) {
2839
+ this.projectDir = projectDir;
2840
+ this.workspaceRoot = workspaceRoot;
2841
+ this.env = env;
2842
+ }
2843
+ get typecheckDir() {
2844
+ return path12.join(this.projectDir, ".akan", "typecheck");
2845
+ }
2846
+ async run(entries, { clean = false, incremental = true, tests = false } = {}) {
2847
+ const name = tests ? "tsconfig.tests" : "tsconfig";
2848
+ await mkdir2(this.typecheckDir, { recursive: true });
2849
+ const tsconfig = {
2850
+ extends: "../../tsconfig.json",
2851
+ compilerOptions: { noEmit: true, incremental, tsBuildInfoFile: `./${name}.tsbuildinfo` },
2852
+ include: [...entries, ...tests ? ProjectTypecheck2.#testIncludes.map((glob) => `../../${glob}`) : []],
2853
+ ...tests ? { exclude: ProjectTypecheck2.#excludes.map((glob) => `../../${glob}`) } : {},
2854
+ references: []
2855
+ };
2856
+ const tsconfigPath = path12.join(this.typecheckDir, `${name}.json`);
2857
+ await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
2858
+ `);
2859
+ if (clean)
2860
+ await rm3(path12.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
2861
+ await this.#check(tsconfigPath);
2862
+ return tsconfigPath;
2863
+ }
2864
+ async#check(tsconfigPath) {
2865
+ const proc = Bun.spawn([process.execPath, await ProjectTypecheck2.workerEntry(this.workspaceRoot)], {
2866
+ cwd: this.workspaceRoot,
2867
+ env: { ...this.env, AKAN_COMMAND_TYPE: "typecheck", AKAN_TYPECHECK_TSCONFIG: tsconfigPath },
2868
+ stdout: "pipe",
2869
+ stderr: "pipe"
2870
+ });
2871
+ const [stdout, stderr, exitCode] = await Promise.all([
2872
+ new Response(proc.stdout).text(),
2873
+ new Response(proc.stderr).text(),
2874
+ proc.exited
2875
+ ]);
2876
+ if (exitCode !== 0)
2877
+ throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
2878
+ }
2879
+ static async workerEntry(workspaceRoot) {
2880
+ const candidates = [
2881
+ path12.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
2882
+ path12.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
2883
+ path12.join(import.meta.dir, "typecheck.proc.js"),
2884
+ path12.join(import.meta.dir, "typecheck.proc.ts"),
2885
+ path12.join(import.meta.dir, "..", "typecheck.proc.js")
2886
+ ];
2887
+ for (const candidate of candidates)
2888
+ if (await Bun.file(candidate).exists())
2889
+ return candidate;
2890
+ throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
2891
+ }
2892
+ }
2893
+
2871
2894
  // pkgs/@akanjs/devkit/executors.ts
2872
2895
  var staticTemplateFileExtensions = new Set(".avif .bmp .cjs .css .eot .gif .html .ico .jpeg .jpg .js .json .map .md .mjs .mp3 .mp4 .ogg .otf .pdf .png .svg".split(" ").concat(".ttf .txt .wasm .wav .webm .webp .woff .woff2 .xml".split(" ")));
2873
2896
  var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
@@ -3052,20 +3075,20 @@ class Executor {
3052
3075
  const readPath = this.getPath(dirPath);
3053
3076
  try {
3054
3077
  const glob = new Bun.Glob("*");
3055
- return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false }));
3078
+ return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false })).sort();
3056
3079
  } catch {
3057
3080
  return [];
3058
3081
  }
3059
3082
  }
3060
3083
  async getAllFiles(pattern = "**/*", { cwd } = {}) {
3061
3084
  const glob = new Bun.Glob(pattern);
3062
- return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true }));
3085
+ return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true })).sort();
3063
3086
  }
3064
3087
  async getFilesAndDirs(dirPath) {
3065
3088
  const fullDirPath = this.getPath(dirPath);
3066
3089
  const glob = new Bun.Glob("*");
3067
- const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
3068
- const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
3090
+ const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true })).sort();
3091
+ const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false })).sort();
3069
3092
  const dirs = allEntries.filter((entry) => !files.includes(entry));
3070
3093
  return { files, dirs };
3071
3094
  }
@@ -3370,6 +3393,7 @@ class WorkspaceExecutor extends Executor {
3370
3393
  return new WorkspaceExecutor({ workspaceRoot, repoName });
3371
3394
  }
3372
3395
  static publicEnvPin = null;
3396
+ static localServeDomain = "localhost";
3373
3397
  static getBaseDevEnv(envPath) {
3374
3398
  const sourceEnv = {
3375
3399
  ...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
@@ -3378,12 +3402,9 @@ class WorkspaceExecutor extends Executor {
3378
3402
  const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
3379
3403
  const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
3380
3404
  const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
3381
- const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
3382
- if (!repoName)
3383
- throw new Error("AKAN_PUBLIC_REPO_NAME is not set");
3384
- const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
3385
- if (!serveDomain)
3386
- throw new Error("AKAN_PUBLIC_SERVE_DOMAIN is not set");
3405
+ const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME || resolveRepoName(envPath ? path13.dirname(envPath) : workspaceRoot ?? process.cwd());
3406
+ const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN || WorkspaceExecutor.localServeDomain;
3407
+ const defaulted = ["AKAN_PUBLIC_REPO_NAME", "AKAN_PUBLIC_SERVE_DOMAIN"].filter((key) => !sourceEnv[key]);
3387
3408
  const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
3388
3409
  const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
3389
3410
  if (!env)
@@ -3395,9 +3416,18 @@ class WorkspaceExecutor extends Executor {
3395
3416
  serveDomain,
3396
3417
  env,
3397
3418
  portOffset,
3398
- workspaceId
3419
+ workspaceId,
3420
+ defaulted
3399
3421
  };
3400
3422
  }
3423
+ static assertDeployableEnv(baseDevEnv, environment, purpose) {
3424
+ const defaulted = environment === "local" ? [] : baseDevEnv.defaulted ?? [];
3425
+ if (!defaulted.length)
3426
+ return;
3427
+ const values = { AKAN_PUBLIC_REPO_NAME: baseDevEnv.repoName, AKAN_PUBLIC_SERVE_DOMAIN: baseDevEnv.serveDomain };
3428
+ const standIns = defaulted.map((key) => `${key} (would be "${values[key]}")`).join(" and ");
3429
+ throw new Error(`${purpose} for the "${environment}" environment needs ${standIns} set, not a local default. Set ${defaulted.length > 1 ? "them" : "it"} in the workspace .env or the shell, or use AKAN_PUBLIC_ENV=local.`);
3430
+ }
3401
3431
  getWorkspaceId({
3402
3432
  allowEmpty
3403
3433
  } = {}) {
@@ -3774,10 +3804,12 @@ class AppExecutor extends SysExecutor {
3774
3804
  return basePort + appIndex + portOffset;
3775
3805
  }
3776
3806
  getCommandEnv(env = {}) {
3777
- const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
3807
+ const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
3778
3808
  const PORT = (8282 + portOffset).toString();
3779
3809
  return {
3780
3810
  ...process.env,
3811
+ AKAN_PUBLIC_REPO_NAME: repoName,
3812
+ AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
3781
3813
  AKAN_PUBLIC_APP_NAME: this.name,
3782
3814
  AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
3783
3815
  NODE_NO_WARNINGS: "1",
@@ -3928,7 +3960,7 @@ class AppExecutor extends SysExecutor {
3928
3960
  pageKeys.push(key);
3929
3961
  }
3930
3962
  }
3931
- pageKeys.sort();
3963
+ pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3932
3964
  this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
3933
3965
  return this.#pageKeys;
3934
3966
  }
@@ -4174,7 +4206,7 @@ class PkgExecutor extends Executor {
4174
4206
  }
4175
4207
  async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
4176
4208
  const dependencyNames = [...new Set(dependencies)].sort();
4177
- const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort();
4209
+ const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4178
4210
  const dependencyVersions = new Map;
4179
4211
  const missingDeps = [];
4180
4212
  for (const dep of [...dependencyNames, ...devDependencyNames]) {
@@ -4256,4 +4288,4 @@ class ModuleExecutor extends Executor {
4256
4288
  }
4257
4289
  }
4258
4290
 
4259
- export { AGENT_BLOCK_START, AGENT_BLOCK_END, stampBlockVersion, extractBlockVersion, readDevkitVersion, upsertAgentBlock, extractAgentBlock, renderRecipeEntries, collectScopeRecipeSources, renderScopeAgentBlock, CLAUDE_COMMENT_RULE, binPlatforms, ServerEnvProfile, AkanAppConfig, AkanWorkspaceConfig, AsyncDefaultExportDetector, resolveSsrPageEntriesForApp, CodegenLock, Linter, LibSource, formatLibStatuses, rootAllowedFiles, rootAllowedDirs, isAllowedLibFacetRootFile, libFacetRootHintOf, barrelFacetDirs, facetIndexShadowOf, rootEntryHintOf, isScannedRootEntry, AppInfo, Spinner, ProjectTypecheck2, Executor, WorkspaceExecutor, SysExecutor, AppExecutor, LibExecutor, PkgExecutor, ModuleExecutor };
4291
+ export { AGENT_BLOCK_START, AGENT_BLOCK_END, stampBlockVersion, extractBlockVersion, readDevkitVersion, upsertAgentBlock, extractAgentBlock, renderRecipeEntries, collectScopeRecipeSources, renderScopeAgentBlock, CLAUDE_COMMENT_RULE, binPlatforms, ServerEnvProfile, AkanAppConfig, AkanWorkspaceConfig, AsyncDefaultExportDetector, resolveSsrPageEntriesForApp, CodegenLock, Linter, LibSource, formatLibStatuses, rootAllowedFiles, rootAllowedDirs, isAllowedLibFacetRootFile, libFacetRootHintOf, barrelFacetDirs, facetIndexShadowOf, rootEntryHintOf, isScannedRootEntry, AppInfo, Spinner, ProjectTypecheck2, CommandExecutionError, Executor, WorkspaceExecutor, SysExecutor, AppExecutor, LibExecutor, PkgExecutor, ModuleExecutor };