@akanjs/cli 3.1.0-rc.1 → 3.1.0-rc.11

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 (98) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-kyprmy06.js → CodeAgent-rkjyxgec.js} +2 -2
  3. package/{CodeTui-21mtze3j.js → CodeTui-h7pyj6xa.js} +1 -1
  4. package/TunnelShare-661cxz5e.js +14 -0
  5. package/{agent.command-f3gsc1gd.js → agent.command-3qxf4xcj.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-5s780vnz.js → application.command-m1kcd7jf.js} +54 -42
  8. package/{applicationBuildRunner-1yaqsrdq.js → applicationBuildRunner-dj0xkrep.js} +26 -33
  9. package/buildBatch.proc.js +24 -15
  10. package/{cloud.command-yj6xb2a7.js → cloud.command-9cp1t0f5.js} +60 -67
  11. package/cloud.script-dgstqt1m.js +24 -0
  12. package/{code.command-3wkn4ja5.js → code.command-0ad6gw2a.js} +6 -6
  13. package/{context.command-9jjn3gcw.js → context.command-0qx447mb.js} +17 -13
  14. package/context.runner-1vsjztjg.js +25 -0
  15. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  16. package/{guideline.command-dtczrqpf.js → guideline.command-cv5bmrx2.js} +2 -2
  17. package/guidelines/agentRule/agentRule.instruction.md +3 -3
  18. package/guidelines/conventions/conventions.instruction.md +457 -1071
  19. package/guidelines/cssRule/cssRule.instruction.md +28 -7
  20. package/guidelines/fieldRule/fieldRule.instruction.md +62 -0
  21. package/guidelines/framework/framework.instruction.md +4 -7
  22. package/guidelines/mcpRule/mcpRule.instruction.md +43 -20
  23. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  24. package/guidelines/modelStore/modelStore.instruction.md +9 -0
  25. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  26. package/guidelines/queryRule/queryRule.instruction.md +44 -11
  27. package/guidelines/runtimeRule/runtimeRule.instruction.md +113 -17
  28. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  29. package/guidelines/transportRule/transportRule.instruction.md +135 -13
  30. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  31. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -7
  32. package/incrementalBuilder.proc.js +10 -7
  33. package/index-192ha4xh.js +1173 -0
  34. package/{index-c76k723e.js → index-1p98exm2.js} +2 -2
  35. package/{index-rwq3ekqc.js → index-1xj2t7cb.js} +7 -7
  36. package/{index-rtct7d43.js → index-20bxxyvn.js} +80 -80
  37. package/{index-qt22xstj.js → index-35hc95h5.js} +5 -3
  38. package/{index-8r4d7cmt.js → index-4mhy6qme.js} +4 -4
  39. package/{index-ww9bhp67.js → index-4pf8g7ff.js} +1 -1
  40. package/{index-2mx692qq.js → index-5edzv8fn.js} +5 -5
  41. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  42. package/index-62md4dwy.js +260 -0
  43. package/index-665zp9rs.js +271 -0
  44. package/{index-2etcv0sc.js → index-86jt2a0e.js} +1 -1
  45. package/{index-krq7btbt.js → index-8fp3b47h.js} +419 -418
  46. package/{index-c2yrsxdh.js → index-9z426msb.js} +109 -328
  47. package/{index-ctanya41.js → index-awbdfcf2.js} +4 -4
  48. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  49. package/{index-rc5rwyps.js → index-bspm21y1.js} +71 -60
  50. package/index-cpb3nvma.js +667 -0
  51. package/{index-jx4bzvwg.js → index-d47ypebv.js} +97 -55
  52. package/{index-ae5pwhgs.js → index-d72hfmdm.js} +15 -13
  53. package/{index-e0cvze1s.js → index-fmzcs8v6.js} +4 -4
  54. package/{index-8ggztp8k.js → index-fyq1z6kt.js} +68 -201
  55. package/{index-1w2qaee2.js → index-g1mbppa9.js} +331 -259
  56. package/{index-gnyf1fqy.js → index-j237xtew.js} +6 -6
  57. package/{index-fm46xrf1.js → index-j4mkhz93.js} +3 -3
  58. package/{index-6jjtnhkw.js → index-jw1zm273.js} +74 -34
  59. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  60. package/index-mdjc93e1.js +33 -0
  61. package/{index-c1x0be5j.js → index-pydbnfez.js} +2 -2
  62. package/{index-56d2g8ms.js → index-qwa5k1j8.js} +1 -1
  63. package/{index-v29746vb.js → index-tamvyvj3.js} +3 -3
  64. package/{index-05g3jayg.js → index-trpasv9q.js} +260 -259
  65. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  66. package/{index-55mynnqs.js → index-va06nr10.js} +1434 -1173
  67. package/index-vk0pa0mn.js +240 -0
  68. package/{index-aprt7pkz.js → index-w44cy4vs.js} +2271 -3369
  69. package/index.js +22 -22
  70. package/{library.command-gaga5bmq.js → library.command-erp2f56n.js} +3 -3
  71. package/{localRegistry.command-vsreyy42.js → localRegistry.command-y176p558.js} +15 -11
  72. package/{module.command-7vpf6db4.js → module.command-gs2r615e.js} +5 -5
  73. package/{package.command-twmvvf69.js → package.command-j10r76ax.js} +4 -4
  74. package/package.json +21 -21
  75. package/{page.command-5asq7fdf.js → page.command-4h4a8pnh.js} +3 -3
  76. package/{pipeline.command-d0v38572.js → pipeline.command-a2nrk26m.js} +221 -220
  77. package/{primitive.command-dyrs2fm8.js → primitive.command-w6t0asqz.js} +6 -6
  78. package/{projectTypecheck-cvtkrzp2.js → projectTypecheck-f534enqj.js} +1 -1
  79. package/{quality.command-6emsqpbg.js → quality.command-77x7vbbx.js} +4 -4
  80. package/{repair.command-8t6aegv5.js → repair.command-y6sstev7.js} +5 -5
  81. package/{scalar.command-bnggy1x7.js → scalar.command-20b8cy0k.js} +4 -4
  82. package/{subspace.command-md2teygq.js → subspace.command-5c7yasd7.js} +424 -364
  83. package/templates/appSample/lib/task/Task.Util.tsx +7 -7
  84. package/templates/appSample/lib/task/Task.Zone.tsx +1 -1
  85. package/templates/appSample/ui/QuantityControl.tsx +2 -2
  86. package/templates/crudSinglePage/page.tsx +1 -1
  87. package/templates/lib/cnst.ts +0 -1
  88. package/templates/workspaceRoot/.gitignore.template +23 -52
  89. package/templates/workspaceRoot/biome.json.template +1 -1
  90. package/{tunnel.command-jx3tdm2d.js → tunnel.command-2kw8vt9g.js} +8 -4
  91. package/{workflow.command-jx0wag5r.js → workflow.command-2djwmpqf.js} +10 -10
  92. package/{workspace.command-yxd7rr7g.js → workspace.command-6qfgq4b4.js} +38 -34
  93. package/TunnelShare-q4jq08qg.js +0 -9
  94. package/cloud.script-h6pvcpa2.js +0 -20
  95. package/context.runner-atcrzz5b.js +0 -21
  96. package/index-6w0rbr9d.js +0 -68
  97. package/index-n68jx79w.js +0 -120
  98. package/index-pgj2rt78.js +0 -404
@@ -4,916 +4,599 @@ import {
4
4
  FileSys2
5
5
  } from "./index-52w0fvh8.js";
6
6
 
7
- // pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
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";
39
+
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;
58
+ }
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)}`;
68
+ }
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(`
99
+ `);
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`));
115
+ }
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
125
+
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}
133
+
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.`;
137
+ }
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
8
179
  import path from "path";
9
- var secretKey = /password|passwd|passphrase|^pass$|token|jwt|authorization|cookie|secret|api[-_]?key|private[-_]?key|access[-_]?key|auth[-_]?key|hash[-_]?key|webhook|cred|iap[-_]?verify|service[-_]?account/i;
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)]));
194
+ }
195
+ static isArchive(source) {
196
+ return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
197
+ }
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";
10
260
 
11
261
  class ServerEnvProfile {
12
262
  static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
263
+ static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
13
264
  static fileName(environment, profile) {
14
265
  return `env.server.${environment}.${profile}.ts`;
15
266
  }
16
267
  static profileFileOf(file, profile) {
17
268
  return file.replace(/\.ts$/, `.${profile}.ts`);
18
269
  }
19
- static strip(value, at = "", underSecret = false) {
20
- if (typeof value === "string")
21
- return underSecret && value.trim() ? { value: undefined, stripped: [at] } : { value, stripped: [] };
22
- if (!value || typeof value !== "object")
23
- return { value, stripped: [] };
24
- const stripped = [];
25
- const strip = (key, child) => {
26
- const result = ServerEnvProfile.strip(child, at ? `${at}.${key}` : key, underSecret || secretKey.test(key));
27
- stripped.push(...result.stripped);
28
- return result.value;
29
- };
30
- if (Array.isArray(value))
31
- return { value: value.map((child, idx) => strip(String(idx), child)), stripped };
32
- const kept = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, strip(key, child)]).filter(([, child]) => child !== undefined));
33
- return { value: kept, stripped };
34
- }
35
- static secretValues(value, at = "", underSecret = false) {
36
- if (typeof value === "string") {
37
- const named = secretKey.test(at.split(".").at(-1) ?? "");
38
- const distinct = value.length >= 16 && !/^https?:\/\//.test(value) && !/^[a-z]+(?:[._-][a-z]+)*$/.test(value);
39
- return underSecret && value.length >= 8 && (named || distinct) ? [{ path: at, value }] : [];
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;
40
277
  }
41
- if (!value || typeof value !== "object")
42
- return [];
43
- return Object.entries(value).flatMap(([key, child]) => ServerEnvProfile.secretValues(child, at ? `${at}.${key}` : key, underSecret || secretKey.test(key)));
44
278
  }
45
- static secretPaths(value, at = "", underSecret = false) {
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);
285
+ }
286
+ static #isSecret(value, underSecret) {
287
+ return underSecret && !!value.trim() || ServerEnvProfile.#isCredentialUrl(value);
288
+ }
289
+ static #fold(value, fold, at = "", underSecret = false) {
46
290
  if (typeof value === "string")
47
- return underSecret && value.trim() ? [at] : [];
291
+ return fold.text(value, at, underSecret);
48
292
  if (!value || typeof value !== "object")
49
- return [];
50
- return Object.entries(value).flatMap(([key, child]) => ServerEnvProfile.secretPaths(child, at ? `${at}.${key}` : key, underSecret || secretKey.test(key)));
293
+ return fold.other(value);
294
+ return fold.node(value, Object.entries(value).map(([key, child]) => [
295
+ key,
296
+ ServerEnvProfile.#fold(child, fold, at ? `${at}.${key}` : key, underSecret || Logger.isSecretKey(key))
297
+ ]));
298
+ }
299
+ static #collect(text) {
300
+ return { text, other: () => [], node: (_, children) => children.flatMap(([, found]) => found) };
301
+ }
302
+ static strip(value) {
303
+ return ServerEnvProfile.#fold(value, {
304
+ text: (text, at, underSecret) => ServerEnvProfile.#isSecret(text, underSecret) ? { value: undefined, stripped: [at] } : { value: text, stripped: [] },
305
+ other: (other) => ({ value: other, stripped: [] }),
306
+ node: (node, children) => {
307
+ const stripped = children.flatMap(([, result]) => result.stripped);
308
+ if (Array.isArray(node))
309
+ return {
310
+ value: children.flatMap(([, result]) => result.value === undefined || result.stripped.length && typeof result.value === "object" && !Object.keys(result.value ?? {}).length ? [] : [result.value]),
311
+ stripped
312
+ };
313
+ return {
314
+ value: Object.fromEntries(children.flatMap(([key, result]) => result.value === undefined ? [] : [[key, result.value]])),
315
+ stripped
316
+ };
317
+ }
318
+ });
319
+ }
320
+ static secretValues(value) {
321
+ return ServerEnvProfile.#fold(value, ServerEnvProfile.#collect((text, at, underSecret) => {
322
+ const url = ServerEnvProfile.#url(text);
323
+ if (url)
324
+ return ServerEnvProfile.#carriesCredential(url) || underSecret && (/webhook/i.test(at) || ServerEnvProfile.distinctive(text)) ? [...new Set([text, url.password])].filter((part) => part.length >= 8).map((part) => ({ path: at, value: part })) : [];
325
+ const named = Logger.isSecretKey(at.split(".").at(-1) ?? "");
326
+ return underSecret && text.length >= 8 && (named || ServerEnvProfile.distinctive(text)) ? [{ path: at, value: text }] : [];
327
+ }));
328
+ }
329
+ static distinctive(value) {
330
+ const url = ServerEnvProfile.#url(value);
331
+ if (url)
332
+ return ServerEnvProfile.#carriesCredential(url) || [...url.pathname.split("/"), ...url.searchParams.values()].some((part) => ServerEnvProfile.distinctive(part));
333
+ return value.length >= 16 && !/^https?:\/\//.test(value) && !/^[a-z]+(?:[._-][a-z]+)*$/.test(value);
334
+ }
335
+ static secretPaths(value) {
336
+ return ServerEnvProfile.#fold(value, ServerEnvProfile.#collect((text, at, underSecret) => ServerEnvProfile.#isSecret(text, underSecret) ? [at] : []));
51
337
  }
52
- static async evaluate(file, cwd) {
338
+ static async evaluate(file, cwd, load) {
339
+ return (await ServerEnvProfile.#evaluated(file, cwd, load)).value;
340
+ }
341
+ static async#evaluated(file, cwd, load) {
342
+ const pattern = (regex, flags = "") => `new RegExp(${JSON.stringify(regex.source)}, "${flags}")`;
343
+ const literal = /(["'`])((?:\\[\s\S]|(?!\1)[^\\])*)\1/;
344
+ const frame = /^\s*at (?:[^(]*\()?(.+):(\d+):(\d+)\)?\s*$/;
53
345
  const script = [
54
- "const { env } = await import(process.env.AKAN_PROFILE_ENV_FILE);",
55
- 'const plain = (_, value) => (typeof value === "function" || typeof value === "symbol" ? undefined : typeof value === "bigint" ? String(value) : value);',
56
- "process.stdout.write(JSON.stringify(env ?? null, plain));"
346
+ "const waiting = new Map();",
347
+ 'process.on("message", ({ load, error, loader, contents }) => {',
348
+ " const reply = waiting.get(load);",
349
+ " waiting.delete(load);",
350
+ " if (error === undefined) reply?.resolve({ loader, contents });",
351
+ " else reply?.reject(new Error(error));",
352
+ "});",
353
+ "if (process.env.AKAN_PROFILE_ENV_LOAD)",
354
+ " Bun.plugin({",
355
+ ' name: "akan-server-env",',
356
+ " setup(build) {",
357
+ " build.onLoad({ filter: new RegExp(process.env.AKAN_PROFILE_ENV_LOAD) }, (args) =>",
358
+ " new Promise((resolve, reject) => {",
359
+ " waiting.set(args.path, { resolve, reject });",
360
+ " process.send({ load: args.path });",
361
+ " }));",
362
+ " },",
363
+ " });",
364
+ `const literal = ${pattern(literal, "g")};`,
365
+ `const interpolation = ${pattern(/\$\{[^}]*\}/)};`,
366
+ `const frameOf = ${pattern(frame)};`,
367
+ `const evaluating = ${pattern(/\s*\(evaluating '[\s\S]*'\)\s*$/)};`,
368
+ "const literalsOf = async (file) =>",
369
+ ' [...(await Bun.file(file).text().catch(() => "")).matchAll(literal)].flatMap((match) => match[2].split(interpolation));',
370
+ "const describe = async (error) => {",
371
+ " const first = Array.isArray(error?.errors) && error.errors.length ? error.errors[0] : error;",
372
+ ' const frame = String(first?.stack ?? "").split("\\n").map((line) => frameOf.exec(line)).find(Boolean);',
373
+ " const at = first?.position?.file ? [first.position.file, first.position.line, first.position.column] : frame ? frame.slice(1, 4) : [];",
374
+ " const files = [process.env.AKAN_PROFILE_ENV_FILE, at[0]].filter(Boolean);",
375
+ " const literals = (await Promise.all(files.map(literalsOf))).flat().filter((part) => part.length >= 4);",
376
+ ' let message = String(first?.message ?? first).replace(evaluating, "");',
377
+ " for (const part of literals.sort((a, b) => b.length - a.length))",
378
+ ' message = message.split(part).join("[redacted]");',
379
+ ' const name = typeof first?.name === "string" && /^[A-Za-z]+$/.test(first.name) ? first.name : "Error";',
380
+ ' return name + ": " + message.slice(0, 200) + (at.length ? " (at " + at.join(":") + ")" : "");',
381
+ "};",
382
+ "try {",
383
+ " const { env } = await import(process.env.AKAN_PROFILE_ENV_FILE);",
384
+ " const lossy = [];",
385
+ " const walk = (value, at) => {",
386
+ ' if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint" || (typeof value === "number" && !Number.isFinite(value))) return lossy.push(at);',
387
+ ' if (!value || typeof value !== "object") return;',
388
+ " const proto = Object.getPrototypeOf(value);",
389
+ " if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return lossy.push(at);",
390
+ ' for (const [key, child] of Object.entries(value)) walk(child, at ? at + "." + key : key);',
391
+ " };",
392
+ ' walk(env, "");',
393
+ ' const plain = (_, value) => (typeof value === "function" || typeof value === "symbol" ? undefined : typeof value === "bigint" ? String(value) : value);',
394
+ ' process.send({ env: JSON.stringify(env ?? null, plain) ?? "null", lossy }, undefined, undefined, () => process.disconnect());',
395
+ "} catch (error) {",
396
+ " process.exitCode = 1;",
397
+ " process.send({ failure: await describe(error) }, undefined, undefined, () => process.disconnect());",
398
+ "}"
57
399
  ].join(`
58
400
  `);
401
+ let sent;
402
+ let failure;
403
+ let loadError;
59
404
  const child = Bun.spawn([process.execPath, "--eval", script], {
60
405
  cwd,
61
- env: { PATH: process.env.PATH ?? "", HOME: process.env.HOME ?? "", AKAN_PROFILE_ENV_FILE: file },
62
- stdout: "pipe",
63
- stderr: "pipe"
406
+ env: {
407
+ PATH: process.env.PATH ?? "",
408
+ HOME: process.env.HOME ?? "",
409
+ AKAN_PROFILE_ENV_FILE: file,
410
+ ...load ? { AKAN_PROFILE_ENV_LOAD: ServerEnvProfile.envFilePattern.source } : {}
411
+ },
412
+ stdout: "ignore",
413
+ stderr: "ignore",
414
+ serialization: "json",
415
+ ipc: (message, subprocess) => {
416
+ if ("env" in message) {
417
+ sent = message;
418
+ return;
419
+ }
420
+ if ("failure" in message) {
421
+ failure = message.failure;
422
+ return;
423
+ }
424
+ if (!load)
425
+ return;
426
+ const requested = message.load;
427
+ load(requested).then(({ loader, contents }) => ({ load: requested, loader, contents }), (error) => {
428
+ loadError ??= error;
429
+ return { load: requested, error: toError(error).message };
430
+ }).then((reply) => subprocess.send(reply)).catch(() => {
431
+ return;
432
+ });
433
+ }
64
434
  });
65
- const [stdout, stderr, exitCode] = await Promise.all([
66
- new Response(child.stdout).text(),
67
- new Response(child.stderr).text(),
68
- child.exited
69
- ]);
70
- if (exitCode !== 0)
71
- throw new Error(`${path.basename(file)} could not be evaluated to check it for secrets:
72
- ${stderr.trim()}`);
73
- return JSON.parse(stdout || "null");
435
+ const exitCode = await child.exited;
436
+ if (exitCode !== 0 || !sent) {
437
+ if (loadError)
438
+ throw loadError;
439
+ throw new Error(`${path2.basename(file)} could not be evaluated to check it for secrets: ${failure ?? `its process exited with code ${exitCode} and sent nothing`}`);
440
+ }
441
+ return { value: JSON.parse(sent.env), lossy: sent.lossy };
442
+ }
443
+ static async sourceInPlaceOf(profileFile, replacedFile, profile) {
444
+ const contents = await Bun.file(profileFile).text();
445
+ const dir = path2.dirname(profileFile);
446
+ const replaced = ServerEnvProfile.#real(replacedFile);
447
+ const selfImports = new Bun.Transpiler({ loader: "ts" }).scanImports(contents).flatMap(({ path: specifier }) => {
448
+ try {
449
+ return ServerEnvProfile.#real(Bun.resolveSync(specifier, dir)) === replaced ? [specifier] : [];
450
+ } catch {
451
+ return [];
452
+ }
453
+ });
454
+ if (selfImports.length)
455
+ throw new Error(`${path2.basename(profileFile)} imports ${selfImports.join(", ")}, the file a "${profile}" build bundles it in place of, so it would import itself. Write the values it needs into it instead.`);
456
+ return contents;
457
+ }
458
+ static #real(file) {
459
+ return existsSync(file) ? realpathSync.native(file) : path2.resolve(file);
460
+ }
461
+ static async libEnvSource(file, profile, chain = []) {
462
+ if (chain.includes(file))
463
+ throw new Error(`${[...chain, file].join(" -> ")}: lib env files reach each other, which a "${profile}" build cannot swap.`);
464
+ const profileFile = ServerEnvProfile.profileFileOf(file, profile);
465
+ const libRoot = path2.dirname(path2.dirname(file));
466
+ if (await Bun.file(profileFile).exists()) {
467
+ const contents = await ServerEnvProfile.sourceInPlaceOf(profileFile, file, profile);
468
+ const reached = [...chain, file];
469
+ const value = await ServerEnvProfile.evaluate(profileFile, libRoot, (envFile) => ServerEnvProfile.#underProfile(envFile, profile, new Set, reached));
470
+ const secrets = ServerEnvProfile.secretPaths(value);
471
+ if (secrets.length)
472
+ throw new Error(`${profileFile} holds secret-named values, which the "${profile}" bundle carries to every computer it runs on: ${secrets.join(", ")}.`);
473
+ return { loader: "ts", contents, profileFile: true, stripped: [] };
474
+ }
475
+ const { exports } = new Bun.Transpiler({ loader: "ts" }).scan(await Bun.file(file).text());
476
+ if (exports.some((name) => name !== "env"))
477
+ throw new Error(`${file} exports ${exports.join(", ")}: a "${profile}" build carries only a lib env's \`env\`, so give the lib ${path2.basename(profileFile)}.`);
478
+ const evaluated = await ServerEnvProfile.#evaluated(file, libRoot);
479
+ if (evaluated.lossy.length)
480
+ throw new Error(`${file} holds ${evaluated.lossy.join(", ")}, which a "${profile}" build cannot carry: it bundles a lib env's defaults as JSON, which keeps no function, Map, Set, Date or class instance. Give the lib ${path2.basename(profileFile)}.`);
481
+ const { value, stripped } = ServerEnvProfile.strip(evaluated.value);
482
+ return {
483
+ loader: "js",
484
+ contents: `export const env = ${JSON.stringify(value ?? {})};
485
+ `,
486
+ profileFile: false,
487
+ stripped
488
+ };
489
+ }
490
+ static async#underProfile(envFile, profile, appEnvDirs, chain) {
491
+ const name = path2.basename(envFile, ".ts").slice("env.server.".length);
492
+ if (appEnvDirs.has(path2.dirname(envFile)) || name === "type" || name === "example")
493
+ return { loader: "ts", contents: await Bun.file(envFile).text() };
494
+ const { loader, contents } = await ServerEnvProfile.libEnvSource(envFile, profile, chain);
495
+ return { loader, contents };
74
496
  }
75
497
  static async assertShippable({ envDir, environment, profile }) {
76
498
  if (!ServerEnvProfile.namePattern.test(profile))
77
499
  throw new Error(`The server env profile "${profile}" must be a lowercase-first name of letters, digits and dashes.`);
78
500
  const name = ServerEnvProfile.fileName(environment, profile);
79
- const file = path.join(envDir, name);
501
+ const file = path2.join(envDir, name);
80
502
  if (!await Bun.file(file).exists())
81
503
  throw new Error(`The server env profile "${profile}" bundles env/${name} in place of env/env.server.${environment}.ts, and there is no such file.`);
82
- const secrets = ServerEnvProfile.secretPaths(await ServerEnvProfile.evaluate(file, path.dirname(envDir)));
504
+ await ServerEnvProfile.sourceInPlaceOf(file, path2.join(envDir, `env.server.${environment}.ts`), profile);
505
+ const appEnvDirs = new Set([path2.resolve(envDir), realpathSync.native(envDir)]);
506
+ const value = await ServerEnvProfile.evaluate(file, path2.dirname(envDir), (envFile) => ServerEnvProfile.#underProfile(envFile, profile, appEnvDirs, []));
507
+ const secrets = ServerEnvProfile.secretPaths(value);
83
508
  if (secrets.length)
84
509
  throw new Error(`env/${name} holds secret-named values, which a profile's bundle carries to every computer it runs on: ${secrets.join(", ")}. Leave them out, or read them from the process env of the server that needs them.`);
85
510
  }
86
511
  }
87
512
 
88
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
89
- import { readdir, rm } from "fs/promises";
90
- import path3 from "path";
91
-
92
- // pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
93
- import fs from "fs";
94
- import path2 from "path";
95
-
96
- class AsyncDefaultExportDetector {
97
- static #typescriptLoad;
98
- static #loadTypescript() {
99
- AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
100
- return AsyncDefaultExportDetector.#typescriptLoad;
101
- }
102
- static async detect(moduleAbsPath) {
103
- try {
104
- const typescript = await AsyncDefaultExportDetector.#loadTypescript();
105
- const source = fs.readFileSync(path2.resolve(moduleAbsPath), "utf8");
106
- const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
107
- return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
108
- } catch {
109
- return false;
110
- }
111
- }
112
- #ts;
113
- constructor(typescript) {
114
- this.#ts = typescript;
115
- }
116
- detectInSourceFile(sourceFile) {
117
- const ts = this.#ts;
118
- const asyncBindings = new Map;
119
- let defaultIdentifier = null;
120
- for (const statement of sourceFile.statements) {
121
- if (ts.isFunctionDeclaration(statement)) {
122
- if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
123
- return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
124
- }
125
- if (statement.name) {
126
- asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
127
- }
128
- continue;
129
- }
130
- if (ts.isVariableStatement(statement)) {
131
- for (const declaration of statement.declarationList.declarations) {
132
- if (!ts.isIdentifier(declaration.name))
133
- continue;
134
- asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
135
- }
136
- continue;
137
- }
138
- if (ts.isExportAssignment(statement)) {
139
- if (this.#isAsyncFunctionExpression(statement.expression))
140
- return true;
141
- if (ts.isCallExpression(statement.expression))
142
- return true;
143
- if (ts.isIdentifier(statement.expression))
144
- defaultIdentifier = statement.expression.text;
145
- continue;
146
- }
147
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
148
- for (const specifier of statement.exportClause.elements) {
149
- if (specifier.name.text !== "default")
150
- continue;
151
- defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
152
- }
153
- }
154
- }
155
- return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
156
- }
157
- #hasModifier(node, kind) {
158
- const ts = this.#ts;
159
- return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
160
- }
161
- #isAsyncFunctionExpression(node) {
162
- const ts = this.#ts;
163
- 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();
164
565
  }
165
- }
166
-
167
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
168
- var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
169
- var IMPLICIT_LAYOUT_DIR = path3.join(".akan", "generated", "root-layouts");
170
- var IMPLICIT_DICT_DIR = path3.join(".akan", "generated", "dict");
171
- var IMPLICIT_OVERRIDES_DIR = path3.join(".akan", "generated", "overrides");
172
- var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
173
- var importSpecifier = (fromAbsPath, toAbsPath) => {
174
- const rel = path3.relative(path3.dirname(fromAbsPath), toAbsPath).split(path3.sep).join("/");
175
- return rel.startsWith(".") ? rel : `./${rel}`;
176
566
  };
177
- async function writeGeneratedOverridesLayoutFile(opts) {
178
- const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
179
- const absPath = path3.join(path3.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
180
- const source = `"use client";
181
- import { UiOverrideProvider } from "akanjs/ui";
182
- import { createElement, type ReactNode } from "react";
183
- import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
184
-
185
- export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
186
- return createElement(UiOverrideProvider, { value }, children);
187
- }
188
- `;
189
- await Bun.write(absPath, source);
190
- return absPath;
191
- }
192
- async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
193
- const keep = new Set(keepAbsPaths);
194
- const names = await readdir(dirAbsPath).catch(() => []);
195
- await Promise.all(names.map((name) => path3.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm(absPath, { force: true })));
196
- }
197
- function getRootBoundarySegments(key) {
198
- const match = LAYOUT_KEY_RE.exec(key);
199
- if (!match)
200
- return null;
201
- const prefix = match[1]?.replace(/\/$/, "");
202
- if (!prefix)
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)
203
583
  return [];
204
- return prefix.split("/").filter(Boolean);
205
- }
206
- function implicitRootLayoutKey(segments) {
207
- return `./${[...segments, "__root_layout"].join("/")}.tsx`;
208
- }
209
- function implicitRootLayoutAbsPath(appCwdPath, segments) {
210
- const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
211
- return path3.join(path3.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
212
- }
213
- function isRootBoundarySegments(segments, basePaths) {
214
- const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
215
- if (firstVisibleIndex === -1)
216
- return segments.length <= 1;
217
- if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
218
- return false;
219
- const visible = segments.slice(firstVisibleIndex);
220
- const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
221
- return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
222
- }
223
- function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
224
- const boundaries = new Map;
225
- for (const key of pageKeys) {
226
- const segments = getRootBoundarySegments(key);
227
- if (!segments)
228
- continue;
229
- if (!isRootBoundarySegments(segments, basePaths))
230
- continue;
231
- const id = segments.join("/");
232
- boundaries.set(id, {
233
- sourceKey: key,
234
- sourceAbsPath: path3.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
235
- segments
236
- });
237
- }
238
- if (boundaries.size === 0)
239
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
240
- return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
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
+ };
597
+
598
+ class LibConfigRefusal extends Error {
241
599
  }
242
- function hasAncestorRootBoundary(boundary, boundaries) {
243
- return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
244
- }
245
- function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
246
- const rootLayoutKey = pageKeys.find((key) => {
247
- const segments = getRootBoundarySegments(key);
248
- return segments !== null && segments.length === 0;
249
- });
250
- return rootLayoutKey ? path3.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
251
- }
252
- async function assertEnvClientConvention(appCwdPath, appName) {
253
- const envPath = path3.join(appCwdPath, "env", "env.client.ts");
254
- if (!await Bun.file(envPath).exists()) {
255
- throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
256
- }
257
- }
258
- async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
259
- const absPath = path3.join(path3.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
260
- await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
261
-
262
- export const allDictionary = getAllDictionary();
263
- `);
264
- return absPath;
265
- }
266
- async function writeGeneratedRootLayoutFile(opts) {
267
- await assertEnvClientConvention(opts.appCwdPath, opts.appName);
268
- const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
269
- const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
270
- const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
271
- const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
272
- const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
273
- const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
274
- const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
275
- void st;
276
- ` : `import "@apps/${opts.appName}/client";
277
- `;
278
- const inheritedLabel = inheritedSourceAbsPath ? path3.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
279
- const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
280
- const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
281
- ` : `const inheritedLayout: LayoutModule = {};
282
- `;
283
- const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
284
- const userLabel = opts.boundary.sourceAbsPath ? path3.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
285
- const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
286
- const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
287
- const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
288
- ` : `const UserLayout = ({ children }: LayoutProps) => children;
289
- const userLayout: LayoutModule = {};
290
- `;
291
- const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
292
- const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
293
- const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
294
- const layoutBinding = isAsyncUserLayout ? ` const layout =
295
- process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
296
- ? await UserLayout({ params, searchParams, children })
297
- : ${userLayoutElement};
298
- ` : "";
299
- const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
300
- const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
301
- const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
302
- const layoutExports = `export async function generateHead(props: PageProps) {
303
- const heads = await Promise.all(
304
- ${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
305
- );
306
- return RouteHead.mergeNodes(heads);
307
- }
308
-
309
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
310
- export const Error = userLayout.Error ?? inheritedLayout.Error;
311
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
312
-
313
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
314
- ${layoutBinding} return `;
315
- const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
316
- import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
317
- import type { ReactNode } from "react";
318
- import { System } from "akanjs/ui";
319
- import { env } from "@apps/${opts.appName}/env/env.client";
320
- import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
321
- ${clientImport}${inheritedImport}${userImport}
322
- const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
323
- const defaultFonts = userFonts.filter((font) => font.default);
324
- if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
325
- const defaultFont = defaultFonts[0];
326
- const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
327
- export const theme = userLayout.theme ?? inheritedLayout.theme;
328
- export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
329
- export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
330
-
331
- ${layoutExports}(
332
- <System.Provider
333
- of={GeneratedLayout as never}
334
- appName=${JSON.stringify(opts.appName)}
335
- ${prefix ? `prefix=${JSON.stringify(prefix)}
336
- ` : ""}params={params}
337
- manifest={manifest}
338
- env={env}
339
- theme={theme}
340
- viewport={viewport}
341
- fonts={loadFonts(userFonts)}
342
- className={defaultFontClassName}
343
- layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
344
- reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
345
- wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
346
- allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
347
- >
348
- ${layoutChild}
349
- </System.Provider>
350
- );
351
- }
352
- ` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
353
- import { resolveRouteModule, RouteHead } from "akanjs/client";
354
- import type { ReactNode } from "react";
355
- ${inheritedImport}${userImport}
356
- ${layoutExports}${layoutReturn};
357
- }
358
- `;
359
- await Bun.write(absPath, source);
360
- return absPath;
361
- }
362
- async function resolveSsrPageEntries(opts) {
363
- const absPageDir = path3.resolve(opts.appCwdPath, "page");
364
- const hasSt = await Bun.file(path3.join(opts.appCwdPath, "lib", "st.ts")).exists();
365
- const basePaths = opts.basePaths ?? [];
366
- const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
367
- const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
368
- const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
369
- const segments = getRootBoundarySegments(key);
370
- return segments !== null && isRootBoundarySegments(segments, basePaths);
371
- }));
372
- const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
373
- const userAbsPath = path3.resolve(absPageDir, key);
374
- if (OVERRIDES_KEY_RE.test(key)) {
375
- const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
376
- appCwdPath: opts.appCwdPath,
377
- key,
378
- userAbsPath
379
- });
380
- return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
381
- }
382
- return { key, moduleAbsPath: userAbsPath };
383
- }));
384
- const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
385
- key: implicitRootLayoutKey(boundary.segments),
386
- moduleAbsPath: await writeGeneratedRootLayoutFile({
387
- appCwdPath: opts.appCwdPath,
388
- appName: opts.appName,
389
- boundary,
390
- rootSourceAbsPath,
391
- includeStInit: hasSt && boundary.segments.length === 0,
392
- includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
393
- }),
394
- seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
395
- })));
396
- const appCwdAbsPath = path3.resolve(opts.appCwdPath);
397
- await Promise.all([
398
- pruneGeneratedFiles(path3.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
399
- pruneGeneratedFiles(path3.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
400
- ]);
401
- return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
402
- }
403
- async function resolveSsrPageEntriesForApp(app, pageKeys) {
404
- const config = await app.getConfig();
405
- return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
406
- }
407
-
408
- // pkgs/@akanjs/devkit/codegenLock.ts
409
- import { mkdir, open, readFile, rm as rm2, stat } from "fs/promises";
410
- import path4 from "path";
411
- import { Logger } from "akanjs/common";
412
-
413
- class CodegenLock {
414
- static fileName = "codegen.lock";
415
- static waitTimeoutMs = 1e4;
416
- static unknownHolderStaleMs = 60000;
417
- static #pollMs = 25;
418
- static #logger = new Logger("CodegenLock");
419
- static #queue = Promise.resolve();
420
- static pathIn(workspaceRoot) {
421
- return path4.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
422
- }
423
- static async run(workspaceRoot, label, fn) {
424
- const ahead = CodegenLock.#queue;
425
- let done;
426
- CodegenLock.#queue = new Promise((resolve) => {
427
- done = resolve;
428
- });
429
- try {
430
- await CodegenLock.#waitForQueue(ahead, label);
431
- return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
432
- } finally {
433
- done();
434
- }
435
- }
436
- static async#waitForQueue(ahead, label) {
437
- let timer = null;
438
- const expired = new Promise((resolve) => {
439
- timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
440
- });
441
- try {
442
- if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
443
- CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
444
- } finally {
445
- if (timer)
446
- clearTimeout(timer);
447
- }
448
- }
449
- static async#withFileLock(workspaceRoot, label, fn) {
450
- const lockPath = CodegenLock.pathIn(workspaceRoot);
451
- await mkdir(path4.dirname(lockPath), { recursive: true }).catch(() => {
452
- return;
453
- });
454
- const held = await CodegenLock.#acquire(lockPath, label);
455
- try {
456
- return await fn();
457
- } finally {
458
- if (held)
459
- await rm2(lockPath, { force: true }).catch(() => {
460
- return;
461
- });
462
- }
463
- }
464
- static async#acquire(lockPath, label) {
465
- const deadline = Date.now() + CodegenLock.waitTimeoutMs;
466
- for (;; ) {
467
- const handle = await open(lockPath, "wx").catch(() => null);
468
- if (handle) {
469
- await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
470
- return;
471
- });
472
- await handle.close().catch(() => {
473
- return;
474
- });
475
- return true;
476
- }
477
- if (await CodegenLock.#reclaimIfAbandoned(lockPath))
478
- continue;
479
- if (Date.now() >= deadline) {
480
- CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
481
- return false;
482
- }
483
- await Bun.sleep(CodegenLock.#pollMs);
484
- }
485
- }
486
- static async#reclaimIfAbandoned(lockPath) {
487
- const info = await stat(lockPath).catch(() => null);
488
- if (!info)
489
- return true;
490
- const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
491
- if (holder) {
492
- if (CodegenLock.#isAlive(holder.pid))
493
- return false;
494
- } else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
495
- return false;
496
- await rm2(lockPath, { force: true }).catch(() => {
497
- return;
498
- });
499
- return true;
500
- }
501
- static #parseHolder(raw) {
502
- try {
503
- const parsed = JSON.parse(raw);
504
- if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
505
- return null;
506
- return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
507
- } catch {
508
- return null;
509
- }
510
- }
511
- static #isAlive(pid) {
512
- if (!Number.isInteger(pid) || pid <= 0)
513
- return false;
514
- try {
515
- process.kill(pid, 0);
516
- return true;
517
- } catch (error) {
518
- return error.code === "EPERM";
519
- }
520
- }
521
- }
522
-
523
- // pkgs/@akanjs/devkit/spinner.ts
524
- import ora from "ora";
525
-
526
- class Spinner {
527
- static padding = 12;
528
- static oraOptions = { discardStdin: false };
529
- static canAnimate(stream = process.stderr) {
530
- return !stream.isTTY || stream.columns > 0;
531
- }
532
- spinner;
533
- stopWatch = null;
534
- startAt = new Date;
535
- prefix;
536
- message;
537
- enableSpin;
538
- constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
539
- Spinner.padding = Math.max(Spinner.padding, prefix.length);
540
- this.prefix = prefix;
541
- this.message = message;
542
- this.spinner = ora({ ...Spinner.oraOptions, text: message });
543
- this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
544
- this.spinner.indent = indent;
545
- this.enableSpin = enableSpin && Spinner.canAnimate();
546
- }
547
- start() {
548
- this.startAt = new Date;
549
- if (this.enableSpin) {
550
- this.spinner.start();
551
- this.stopWatch = setInterval(() => {
552
- this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
553
- this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
554
- }, 1000);
555
- } else
556
- this.spinner.info();
557
- return this;
558
- }
559
- succeed(message) {
560
- this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
561
- this.#reset();
562
- }
563
- fail(message) {
564
- this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
565
- this.#reset();
566
- }
567
- isSpinning() {
568
- return this.spinner.isSpinning;
569
- }
570
- #reset() {
571
- if (this.stopWatch)
572
- clearInterval(this.stopWatch);
573
- this.stopWatch = null;
574
- }
575
- #getElapsedTimeStr({ floor = false } = {}) {
576
- const ms = Date.now() - this.startAt.getTime();
577
- if (ms < 1000)
578
- return `${ms}ms`;
579
- const s = Math.floor(ms / 1000);
580
- if (s < 60)
581
- return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
582
- const m = Math.floor(s / 60);
583
- return `${m}m ${s % 60}s`;
584
- }
585
- }
586
-
587
- // pkgs/@akanjs/devkit/executors.ts
588
- import {
589
- exec,
590
- fork,
591
- spawn as spawn2
592
- } from "child_process";
593
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
594
- import {
595
- copyFile,
596
- cp as cpEntry,
597
- lstat,
598
- mkdir as mkdir2,
599
- readdir as readDirEntries,
600
- realpath,
601
- rm as rm3,
602
- stat as stat2,
603
- symlink
604
- } from "fs/promises";
605
- import path12 from "path";
606
- import { pathToFileURL } from "url";
607
- import {
608
- capitalize,
609
- getPageSourceFileViolation,
610
- isRouteSourceFile,
611
- Logger as Logger2,
612
- parseRouteModuleKey,
613
- validatePageSourceFile,
614
- validateSubRoutePageKey
615
- } from "akanjs/common";
616
- var {$ } = globalThis.Bun;
617
- import chalk2 from "chalk";
618
-
619
- // pkgs/@akanjs/devkit/agentsIndex.ts
620
- var recipeScannerLoad = null;
621
- var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
622
- var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
623
- var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
624
- var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
625
- var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
626
-
627
- ${block}`;
628
- var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
629
- var readDevkitVersion = async () => {
630
- const { readFile } = await import("fs/promises");
631
- const { getDirname } = await import("./fileSys-wzzrntkw.js");
632
- try {
633
- const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
634
- return JSON.parse(raw).version ?? null;
635
- } catch {
636
- return null;
637
- }
638
- };
639
- var upsertAgentBlock = (existing, block) => {
640
- const managed = `${AGENT_BLOCK_START}
641
- ${block}
642
- ${AGENT_BLOCK_END}`;
643
- const startIndex = existing.indexOf(AGENT_BLOCK_START);
644
- const endIndex = existing.indexOf(AGENT_BLOCK_END);
645
- if (startIndex >= 0 && endIndex > startIndex) {
646
- return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
647
- }
648
- return `${existing.replace(/\s*$/, "")}
649
-
650
- ${managed}
651
- `;
652
- };
653
- var extractAgentBlock = (content) => {
654
- const startIndex = content.indexOf(AGENT_BLOCK_START);
655
- const endIndex = content.indexOf(AGENT_BLOCK_END);
656
- if (startIndex < 0 || endIndex <= startIndex)
657
- return null;
658
- return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
659
- };
660
- var signatureOf = (recipe) => {
661
- const entries = Object.entries(recipe.variants);
662
- if (entries.length === 0)
663
- return "";
664
- const parts = entries.map(([key, values]) => {
665
- if (values.length === 1 && values[0] === "true")
666
- return `${key}?`;
667
- const def = recipe.defaultVariants?.[key];
668
- return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
669
- });
670
- return `(${parts.join(" \xB7 ")})`;
671
- };
672
- var renderRecipeEntries = (recipes) => {
673
- const groups = new Map;
674
- for (const recipe of recipes)
675
- groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
676
- return [...groups.entries()].map(([importFrom, list]) => {
677
- const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
678
- `);
679
- return `Import from \`${importFrom}\`:
680
- ${items}`;
681
- }).join(`
682
-
683
- `);
684
- };
685
- var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
686
- const { collectRecipeSources } = await loadRecipeScanner();
687
- const sources = [
688
- ...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
689
- ];
690
- for (const lib of [...new Set(libDeps)].sort()) {
691
- if (scope.type === "lib" && lib === scope.name)
692
- continue;
693
- sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
694
- }
695
- return sources;
696
- };
697
- var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
698
- const { scanRecipes } = await loadRecipeScanner();
699
- return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
700
- };
701
- var renderScopeAgentBlock = (scope, recipes) => {
702
- const scopePath = `${scope.type}s/${scope.name}`;
703
- const intro = `## Recipes In Scope
704
-
705
- UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
706
- \`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
707
- arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
708
- boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
709
- and verified by \`akan lint ${scope.name}\`.`;
710
- if (recipes.length === 0) {
711
- return `${intro}
712
-
713
- No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
714
- root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
715
- folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
716
- }
717
- return `${intro}
718
-
719
- ${renderRecipeEntries(recipes)}`;
720
- };
721
- var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
722
-
723
- Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
724
- root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
725
- section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
726
- markers freely.
727
-
728
- ${AGENT_BLOCK_START}
729
- ${block}
730
- ${AGENT_BLOCK_END}
731
- `;
732
- var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
733
-
734
- Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
735
- is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
736
-
737
- Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
738
-
739
- - Restates the identifier, the signature, or the line under it \u2192 delete it.
740
- - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
741
- - JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
742
- - Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
743
- - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
744
- derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
745
- keep it, one line.
746
-
747
- That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
748
- subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
749
- surrounding code carries none, your diff carries none.`;
750
- var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
751
-
752
- @AGENTS.md
753
-
754
- ${CLAUDE_COMMENT_RULE}
755
- `;
756
-
757
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
758
- import path5 from "path";
759
-
760
- // pkgs/@akanjs/devkit/akanConfig/types.ts
761
- import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
762
-
763
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
764
- class AkanBin {
765
- static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
766
- static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
767
- static parse(declared, owner, baseDir) {
768
- if (declared === undefined)
769
- return {};
770
- if (!declared || typeof declared !== "object" || Array.isArray(declared))
771
- throw new Error(`${owner}: bin maps each executable's name to its platforms`);
772
- return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
773
- }
774
- static isArchive(source) {
775
- return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
776
- }
777
- static #platforms(name, platforms, owner, baseDir) {
778
- if (!AkanBin.fileName.test(name))
779
- throw new Error(`${owner}: bin.${name} is not a file name`);
780
- if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
781
- throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
782
- const resolved = {};
783
- for (const [platform, source] of Object.entries(platforms)) {
784
- if (!binPlatforms.includes(platform))
785
- throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
786
- resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
787
- }
788
- return resolved;
789
- }
790
- static #source(at, source, baseDir) {
791
- if (!source || typeof source !== "object" || Array.isArray(source))
792
- throw new Error(`${at} must be { url, sha256 } or { path }`);
793
- const { url, sha256, path: localPath, file, ...rest } = source;
794
- const extra = Object.keys(rest);
795
- if (extra.length)
796
- throw new Error(`${at}: unknown key ${extra.join(", ")}`);
797
- if (url === undefined === (localPath === undefined))
798
- throw new Error(`${at} takes either url and sha256, or path`);
799
- const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
800
- const archive = AkanBin.isArchive(resolved);
801
- if (file === undefined) {
802
- if (archive)
803
- throw new Error(`${at} names an archive: file picks the executable inside it`);
804
- return resolved;
805
- }
806
- if (!archive)
807
- throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
808
- const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
809
- if (!parts.length || path5.isAbsolute(file) || parts.includes(".."))
810
- throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
811
- return { ...resolved, file: parts.join("/") };
812
- }
813
- static #urlSource(at, url, sha256) {
814
- if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
815
- throw new Error(`${at}: url must be an http(s) address`);
816
- if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
817
- throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
818
- return { url, sha256: sha256.toLowerCase() };
819
- }
820
- static #pathSource(at, localPath, sha256, baseDir) {
821
- if (typeof localPath !== "string" || !localPath)
822
- throw new Error(`${at}: path must name a file`);
823
- if (sha256 !== undefined)
824
- throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
825
- return { path: path5.resolve(baseDir, localPath) };
826
- }
827
- }
828
- // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
829
- import fs2 from "fs";
830
- import path6 from "path";
831
- import { fileURLToPath } from "url";
832
- import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
833
- import { resolveAkanI18nConfig } from "akanjs/common";
834
- var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
835
- var DEFAULT_OPTIMIZE_IMPORTS = [
836
- "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
837
- "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
838
- "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
839
- "@material-ui/icons @tabler/icons-react mui-core react-icons/*"
840
- ].flatMap((line) => line.split(" "));
841
- var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
842
- var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
843
- var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
844
- var DEFAULT_AKAN_IMAGE_CONFIG = {
845
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
846
- imageSizes: [32, 48, 64, 96, 128, 256, 384],
847
- formats: ["image/webp"],
848
- qualities: [75],
849
- minimumCacheTTL: 14400,
850
- remotePatterns: [],
851
- localPatterns: [{ pathname: "/**" }],
852
- dangerouslyAllowSVG: false,
853
- maximumRedirects: 3,
854
- fetchTimeoutMs: 7000,
855
- maxRemoteBytes: 25 * 1024 * 1024,
856
- maxConcurrency: 0
857
- };
858
- var normalizeIndexPath = (indexPath) => {
859
- const normalized = indexPath?.trim();
860
- if (!normalized)
861
- return;
862
- const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
863
- return path === "/" ? "/" : path;
864
- };
865
- var normalizeStringList = (values) => {
866
- const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
867
- return normalized.length > 0 ? [...new Set(normalized)] : undefined;
868
- };
869
- var sanitizeAppIdSegment = (value) => {
870
- const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
871
- if (!cleaned)
872
- return "app";
873
- return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
874
- };
875
- var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
876
- var normalizeDeepLinkDomain = (domain) => {
877
- const normalized = domain.trim();
878
- if (!normalized)
879
- return "";
880
- try {
881
- const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
882
- return url.host.toLowerCase();
883
- } catch {
884
- return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
885
- }
886
- };
887
- var normalizeDeepLinks = (deepLinks) => {
888
- const schemes = normalizeStringList(deepLinks?.schemes);
889
- const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
890
- if (!schemes && !domains)
891
- return;
892
- return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
893
- };
894
- var emptyLibContributions = () => ({
895
- externalLibs: [],
896
- trustedDependencies: [],
897
- bin: [],
898
- docker: { preRuns: [], postRuns: [] },
899
- keepFonts: []
900
- });
901
- var normalizePackageNames = (names, owner) => {
902
- if (names === undefined)
903
- return [];
904
- if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
905
- throw new Error(`${owner}: trustedDependencies lists package names`);
906
- return [...new Set(names.map((name) => name.trim()))];
907
- };
908
- var normalizeKeepFonts = (keepFonts) => [
909
- ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
910
- ];
911
- var dedupeDockerRuns = (runs) => {
912
- const byKey = new Map;
913
- for (const run of runs)
914
- byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
915
- return [...byKey.values()];
916
- };
917
600
 
918
601
  class AkanAppConfig {
919
602
  app;
@@ -1025,6 +708,8 @@ class AkanAppConfig {
1025
708
  const { updates } = merged;
1026
709
  if (updates && (!updates.url || !(updates.publicKey || updates.publicKeys?.length)))
1027
710
  throw new Error(`${where}.updates in ${configPath} has no url or publicKeys${named ? "; give them in native.updates or the target's own" : ""}.`);
711
+ if (updates?.publicKey !== undefined && updates.publicKeys !== undefined)
712
+ throw new Error(`${where}.updates in ${configPath} has both publicKey and publicKeys${named ? ", native.updates merged in: give one, in native.updates' own form when overriding its keys" : ": give one"}.`);
1028
713
  AkanAppConfig.#assertFiles(own ?? {}, where, configPath);
1029
714
  const {
1030
715
  basePath: _basePath,
@@ -1100,6 +785,8 @@ class AkanAppConfig {
1100
785
  "AKAN_DATABASE_MODES",
1101
786
  "AKAN_SSR",
1102
787
  "AKAN_CSR",
788
+ "AKAN_MCP",
789
+ "AKAN_SHUTDOWN_TIMEOUT_MS",
1103
790
  "AKAN_MODULES"
1104
791
  ]);
1105
792
  static #desktopServerEnv(env, where, configPath) {
@@ -1118,11 +805,14 @@ class AkanAppConfig {
1118
805
  static #desktopServerWatchdog(watchdog, where, configPath) {
1119
806
  if (watchdog === undefined || watchdog === false)
1120
807
  return null;
1121
- const { intervalMs = 1e4, failures = 3 } = watchdog === true ? {} : watchdog;
1122
- const whole = (value) => Number.isInteger(value) && value > 0;
1123
- if (watchdog !== true && !AkanAppConfig.#isPlainObject(watchdog) || !whole(intervalMs) || !whole(failures))
808
+ const options = watchdog === true ? {} : watchdog;
809
+ const whole = (value) => value === undefined || Number.isInteger(value) && value > 0;
810
+ if (!AkanAppConfig.#isPlainObject(options) || Object.keys(options).some((key) => key !== "intervalMs" && key !== "failures") || !whole(options.intervalMs) || !whole(options.failures))
1124
811
  throw new Error(`${where}.watchdog in ${configPath} must be true or { intervalMs, failures }, whole numbers from 1.`);
1125
- return { intervalMs, failures };
812
+ return {
813
+ intervalMs: options.intervalMs ?? 1e4,
814
+ failures: options.failures ?? 3
815
+ };
1126
816
  }
1127
817
  static #platformIndexPath(section) {
1128
818
  const { indexPath: rawIndexPath, ...rest } = section;
@@ -1242,7 +932,7 @@ class AkanAppConfig {
1242
932
  }
1243
933
  #applyRoutes(routes = []) {
1244
934
  for (const route of routes) {
1245
- const basePath = route.basePath ? route.basePath.replace(/^\/+|\/+$/g, "") : null;
935
+ const basePath = route.basePath ? trimSlashes(route.basePath) : null;
1246
936
  if (basePath !== null)
1247
937
  this.basePaths.add(basePath);
1248
938
  const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
@@ -1341,232 +1031,670 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1341
1031
  const importPath = bustImportCache ? `${configPath}?akanConfigGeneration=${++AkanAppConfig.#importGeneration}` : configPath;
1342
1032
  return await import(importPath).then((mod) => mod.default);
1343
1033
  }
1344
- static async from(app, { bustImportCache = false } = {}) {
1345
- const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
1346
- AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
1347
- WorkspaceExecutor.getBaseDevEnv(path6.join(app.workspace.workspaceRoot, ".env")),
1348
- app.workspace.getLibs(),
1349
- app.workspace.getPackageJson()
1350
- ]);
1351
- const resolved = typeof configImp === "function" ? configImp(app) : configImp;
1352
- const {
1353
- plugins,
1354
- pipeline: _pipeline,
1355
- ...config
1356
- } = resolved ?? {};
1034
+ static async from(app, { bustImportCache = false } = {}) {
1035
+ const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
1036
+ AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
1037
+ WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
1038
+ app.workspace.getLibs(),
1039
+ app.workspace.getPackageJson()
1040
+ ]);
1041
+ const resolved = typeof configImp === "function" ? configImp(app) : configImp;
1042
+ const {
1043
+ plugins,
1044
+ pipeline: _pipeline,
1045
+ ...config
1046
+ } = resolved ?? {};
1047
+ if ("subspace" in config)
1048
+ throw new Error(`apps/${app.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1049
+ const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
1050
+ return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
1051
+ }
1052
+ static async#collectLibContributions(app, libs, bustImportCache) {
1053
+ const libConfigs = await Promise.all(libs.map(async (libName) => LibExecutor.from(app, libName).getConfig({ refresh: bustImportCache }).catch((error) => {
1054
+ if (error instanceof LibConfigRefusal)
1055
+ throw error;
1056
+ app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
1057
+ return null;
1058
+ })));
1059
+ return {
1060
+ externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
1061
+ trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
1062
+ bin: libConfigs.flatMap((libConfig) => libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : []),
1063
+ docker: {
1064
+ preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
1065
+ postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? [])
1066
+ },
1067
+ keepFonts: libConfigs.flatMap((libConfig) => (libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`))
1068
+ };
1069
+ }
1070
+ #resolveProductionDependencyVersion(lib) {
1071
+ const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
1072
+ if (rootVersion)
1073
+ return rootVersion;
1074
+ const akanPackageJson = getAkanPackageJson();
1075
+ const version = akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1076
+ if (!version)
1077
+ throw new Error(`Dependency ${lib} not found in package.json`);
1078
+ return version;
1079
+ }
1080
+ #getProductionRuntimePackages() {
1081
+ return [
1082
+ ...this.externalLibs,
1083
+ ...SSR_RUNTIME_PACKAGES,
1084
+ ...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode))
1085
+ ];
1086
+ }
1087
+ getDatabaseModeRuntimePackages(databaseMode) {
1088
+ return [...DatabaseModes.drivers[databaseMode]];
1089
+ }
1090
+ getMissingDatabaseModeDependencySpecs(databaseMode) {
1091
+ return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
1092
+ }
1093
+ resolveDatabaseMode() {
1094
+ return DatabaseModes.resolve({
1095
+ requested: process.env.AKAN_DATABASE_MODE,
1096
+ declared: this.database.modes.join(","),
1097
+ local: true
1098
+ });
1099
+ }
1100
+ static #database(app, config) {
1101
+ const declared = config?.database?.modes?.filter((mode) => !!mode);
1102
+ const modes = declared?.length ? declared : ["single"];
1103
+ return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
1104
+ }
1105
+ #getMissingDependencySpecs(libs) {
1106
+ const rootDependencies = {
1107
+ ...this.rootPackageJson.dependencies,
1108
+ ...this.rootPackageJson.devDependencies
1109
+ };
1110
+ return libs.filter((lib) => !rootDependencies[lib]).map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
1111
+ }
1112
+ get akanVersion() {
1113
+ return AkanAppConfig.akanVersion;
1114
+ }
1115
+ static get akanVersion() {
1116
+ return getAkanPackageJson().version;
1117
+ }
1118
+ getProductionPackageJson(data = {}) {
1119
+ return {
1120
+ name: this.app.name,
1121
+ description: this.app.name,
1122
+ version: "1.0.0",
1123
+ main: "./main.js",
1124
+ dependencies: Object.fromEntries([...new Set(this.#getProductionRuntimePackages())].map((lib) => [
1125
+ lib,
1126
+ this.#resolveProductionDependencyVersion(lib)
1127
+ ])),
1128
+ ...this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {},
1129
+ ...data
1130
+ };
1131
+ }
1132
+ }
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.`);
1357
1197
  if ("subspace" in config)
1358
- throw new Error(`apps/${app.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1359
- const libContributions = await AkanAppConfig.#collectLibContributions(app, libs, bustImportCache);
1360
- return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libContributions);
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 ?? []);
1361
1200
  }
1362
- static async#collectLibContributions(app, libs, bustImportCache) {
1363
- const libConfigs = await Promise.all(libs.map(async (libName) => LibExecutor.from(app, libName).getConfig({ refresh: bustImportCache }).catch((error) => {
1364
- app.logger.warn(`Skipped libs/${libName}/akan.config.ts contributions: ${String(error)}`);
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)
1365
1242
  return null;
1366
- })));
1367
- return {
1368
- externalLibs: libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []),
1369
- trustedDependencies: libConfigs.flatMap((libConfig) => libConfig?.trustedDependencies ?? []),
1370
- bin: libConfigs.flatMap((libConfig) => libConfig && Object.keys(libConfig.bin).length ? [{ lib: libConfig.lib.name, bin: libConfig.bin }] : []),
1371
- docker: {
1372
- preRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.preRuns ?? []),
1373
- postRuns: libConfigs.flatMap((libConfig) => libConfig?.docker.postRuns ?? [])
1374
- },
1375
- keepFonts: libConfigs.flatMap((libConfig) => (libConfig?.assets.keepFonts ?? []).map((glob) => `libs/${libConfig?.lib.name}/${glob}`))
1243
+ const refuse = (why) => {
1244
+ throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1376
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(/\/+$/, "")}`;
1377
1260
  }
1378
- #resolveProductionDependencyVersion(lib) {
1379
- const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
1380
- if (rootVersion)
1381
- return rootVersion;
1382
- const akanPackageJson = getAkanPackageJson();
1383
- const version = akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
1384
- if (!version)
1385
- throw new Error(`Dependency ${lib} not found in package.json`);
1386
- return version;
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;
1387
1275
  }
1388
- #getProductionRuntimePackages() {
1389
- return [
1390
- ...this.externalLibs,
1391
- ...SSR_RUNTIME_PACKAGES,
1392
- ...this.database.modes.flatMap((mode) => this.getDatabaseModeRuntimePackages(mode))
1393
- ];
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
+ }
1394
1285
  }
1395
- getDatabaseModeRuntimePackages(databaseMode) {
1396
- return [...DatabaseModes.drivers[databaseMode]];
1286
+ #ts;
1287
+ constructor(typescript) {
1288
+ this.#ts = typescript;
1397
1289
  }
1398
- getMissingDatabaseModeDependencySpecs(databaseMode) {
1399
- return this.#getMissingDependencySpecs(this.getDatabaseModeRuntimePackages(databaseMode));
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;
1400
1330
  }
1401
- resolveDatabaseMode() {
1402
- return DatabaseModes.resolve({
1403
- requested: process.env.AKAN_DATABASE_MODE,
1404
- declared: this.database.modes.join(","),
1405
- local: true
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
1406
1410
  });
1407
1411
  }
1408
- static #database(app, config) {
1409
- const declared = config?.database?.modes?.filter((mode) => !!mode);
1410
- const modes = declared?.length ? declared : ["single"];
1411
- return { modes: DatabaseModes.parseList(modes.join(","), `database.modes in apps/${app.name}/akan.config.ts`) };
1412
- }
1413
- #getMissingDependencySpecs(libs) {
1414
- const rootDependencies = {
1415
- ...this.rootPackageJson.dependencies,
1416
- ...this.rootPackageJson.devDependencies
1417
- };
1418
- return libs.filter((lib) => !rootDependencies[lib]).map((lib) => `${lib}@${this.#resolveProductionDependencyVersion(lib)}`);
1419
- }
1420
- get akanVersion() {
1421
- return AkanAppConfig.akanVersion;
1422
- }
1423
- static get akanVersion() {
1424
- return getAkanPackageJson().version;
1425
- }
1426
- getProductionPackageJson(data = {}) {
1427
- return {
1428
- name: this.app.name,
1429
- description: this.app.name,
1430
- version: "1.0.0",
1431
- main: "./main.js",
1432
- dependencies: Object.fromEntries([...new Set(this.#getProductionRuntimePackages())].map((lib) => [
1433
- lib,
1434
- this.#resolveProductionDependencyVersion(lib)
1435
- ])),
1436
- ...this.trustedDependencies.length ? { trustedDependencies: this.trustedDependencies } : {},
1437
- ...data
1438
- };
1439
- }
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("/")));
1440
1415
  }
1441
- var akanPackageJson = null;
1442
- function getAkanPackageJson() {
1443
- if (akanPackageJson)
1444
- return akanPackageJson;
1445
- const sourceDir = path6.dirname(fileURLToPath(import.meta.url));
1446
- const packageJsonPaths = [
1447
- path6.join(sourceDir, "../../../akanjs/package.json"),
1448
- path6.join(process.cwd(), "pkgs/akanjs/package.json"),
1449
- path6.join(path6.dirname(Bun.main), "node_modules/akanjs/package.json")
1450
- ];
1451
- try {
1452
- packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
1453
- } catch {}
1454
- for (const packageJsonPath of packageJsonPaths) {
1455
- try {
1456
- akanPackageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
1457
- return akanPackageJson;
1458
- } catch {}
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`);
1459
1430
  }
1460
- akanPackageJson = {
1461
- name: "akanjs",
1462
- version: "0.0.0",
1463
- description: "akanjs",
1464
- dependencies: {}
1465
- };
1466
- return akanPackageJson;
1467
1431
  }
1468
- function mergeImageConfig(config = {}) {
1469
- return {
1470
- ...DEFAULT_AKAN_IMAGE_CONFIG,
1471
- ...config,
1472
- deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
1473
- imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
1474
- formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
1475
- qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
1476
- remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
1477
- localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
1478
- };
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);
1479
1481
  }
1480
1482
 
1481
- class AkanLibConfig {
1482
- lib;
1483
- externalLibs;
1484
- trustedDependencies;
1485
- bin;
1486
- docker;
1487
- assets;
1488
- plugins;
1489
- constructor(lib, config, plugins = []) {
1490
- this.lib = lib;
1491
- this.externalLibs = config?.externalLibs ?? [];
1492
- const owner = `libs/${lib.name}/akan.config.ts`;
1493
- this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
1494
- this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
1495
- this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
1496
- this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
1497
- 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);
1498
1597
  }
1499
- static async from(lib, { bustImportCache = false } = {}) {
1500
- const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
1501
- const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1502
- const { plugins, ...config } = resolved ?? {};
1503
- if ("pipeline" in config)
1504
- throw new Error(`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.`);
1505
- if ("subspace" in config)
1506
- throw new Error(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1507
- 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
+ }
1508
1610
  }
1509
- }
1510
- //! need to refactor
1511
- var shiftBuildNum = async (app, delta) => {
1512
- const { buildNum } = (await AkanAppConfig.from(app)).native;
1513
- const akanConfigPath = path6.join(app.cwdPath, "akan.config.ts");
1514
- const akanConfig = fs2.readFileSync(akanConfigPath, "utf8");
1515
- fs2.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
1516
- };
1517
- var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
1518
- var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
1519
- // pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
1520
- import path7 from "path";
1521
- class AkanWorkspaceConfig {
1522
- static fileName = "akan.config.ts";
1523
- static #keys = ["subspace", "pipeline", "cloud"];
1524
- static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
1525
- static async from(workspace, { bustImportCache = false } = {}) {
1526
- const file = path7.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
1527
- if (!await FileSys2.fileExists(file))
1528
- return new AkanWorkspaceConfig(null, {});
1529
- const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
1530
- return new AkanWorkspaceConfig(file, input);
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
+ }
1531
1623
  }
1532
- file;
1533
- subspace;
1534
- pipeline;
1535
- cloudHost;
1536
- constructor(file, input) {
1537
- this.file = file;
1538
- if (typeof input !== "object" || input === null || Array.isArray(input))
1539
- throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
1540
- for (const key of Object.keys(input))
1541
- if (!AkanWorkspaceConfig.#keys.includes(key))
1542
- 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.`);
1543
- const config = input;
1544
- this.subspace = config.subspace ?? null;
1545
- this.pipeline = config.pipeline ?? null;
1546
- this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
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
+ }
1547
1638
  }
1548
- static #cloudHost(cloud) {
1549
- if (cloud === undefined)
1550
- return null;
1551
- const refuse = (why) => {
1552
- throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1553
- };
1554
- if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
1555
- return refuse("that is not an object");
1556
- const unknown = Object.keys(cloud).filter((key) => key !== "host");
1557
- if (unknown.length)
1558
- return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
1559
- const { host } = cloud;
1560
- if (host === undefined)
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
+ }
1660
+ }
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 {
1561
1683
  return null;
1562
- const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
1563
- if (!url || url.search || url.hash || url.username || url.password)
1564
- return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
1565
- if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
1566
- return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
1567
- 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
+ }
1568
1695
  }
1569
1696
  }
1697
+
1570
1698
  // pkgs/@akanjs/devkit/jsonFormatter.ts
1571
1699
  import path8 from "path";
1572
1700
 
@@ -1644,7 +1772,7 @@ ${" ".repeat(depth)}]`;
1644
1772
 
1645
1773
  // pkgs/@akanjs/devkit/linter.ts
1646
1774
  import { spawn } from "child_process";
1647
- import { existsSync, readFileSync } from "fs";
1775
+ import { existsSync as existsSync2, readFileSync } from "fs";
1648
1776
  import path9 from "path";
1649
1777
  import chalk from "chalk";
1650
1778
  var parseBiomeReport = (output) => {
@@ -1713,12 +1841,12 @@ class Linter {
1713
1841
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
1714
1842
  this.configPath = Linter.#configPathIn(this.lintRoot) ?? path9.join(this.lintRoot, "biome.json");
1715
1843
  const localBiomeBin = path9.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
1716
- this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
1844
+ this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
1717
1845
  }
1718
1846
  static #configPathIn(dir) {
1719
1847
  for (const fileName of BIOME_CONFIG_FILES) {
1720
1848
  const configPath = path9.join(dir, fileName);
1721
- if (existsSync(configPath))
1849
+ if (existsSync2(configPath))
1722
1850
  return configPath;
1723
1851
  }
1724
1852
  return null;
@@ -1806,7 +1934,7 @@ class Linter {
1806
1934
  };
1807
1935
  }
1808
1936
  async#checkFile(filePath, { write = false } = {}) {
1809
- const originalContent = existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
1937
+ const originalContent = existsSync2(filePath) ? readFileSync(filePath, "utf8") : "";
1810
1938
  const { stdout, stderr } = await this.#runBiome([
1811
1939
  "check",
1812
1940
  ...write ? ["--write"] : [],
@@ -1820,7 +1948,7 @@ class Linter {
1820
1948
  const report = parseBiomeReport(stdout || stderr);
1821
1949
  const results = this.#toLintResults(report, filePath);
1822
1950
  const { errors, warnings } = this.#splitMessages(results);
1823
- const output = write && existsSync(filePath) ? readFileSync(filePath, "utf8") : undefined;
1951
+ const output = write && existsSync2(filePath) ? readFileSync(filePath, "utf8") : undefined;
1824
1952
  return {
1825
1953
  fixed: write && output !== originalContent,
1826
1954
  output,
@@ -1836,7 +1964,7 @@ class Linter {
1836
1964
  }
1837
1965
  async lintFile(filePath) {
1838
1966
  const resolvedFilePath = this.#resolveFilePath(filePath);
1839
- if (!existsSync(resolvedFilePath))
1967
+ if (!existsSync2(resolvedFilePath))
1840
1968
  throw new Error(`File not found: ${filePath}`);
1841
1969
  return await this.#checkFile(resolvedFilePath);
1842
1970
  }
@@ -1853,7 +1981,7 @@ class Linter {
1853
1981
  output.push(`
1854
1982
  ${chalk.cyan(result.filePath)}`);
1855
1983
  let sourceLines = [];
1856
- if (existsSync(result.filePath)) {
1984
+ if (existsSync2(result.filePath)) {
1857
1985
  try {
1858
1986
  const sourceContent = readFileSync(result.filePath, "utf8");
1859
1987
  sourceLines = sourceContent.split(`
@@ -1925,7 +2053,7 @@ ${errorText}, ${warningText} found`];
1925
2053
  }
1926
2054
  async fixFile(filePath, dryRun = false) {
1927
2055
  const resolvedFilePath = this.#resolveFilePath(filePath);
1928
- if (!existsSync(resolvedFilePath))
2056
+ if (!existsSync2(resolvedFilePath))
1929
2057
  throw new Error(`File not found: ${filePath}`);
1930
2058
  if (!dryRun)
1931
2059
  return await this.#checkFile(resolvedFilePath, { write: true });
@@ -1935,7 +2063,7 @@ ${errorText}, ${warningText} found`];
1935
2063
  return { ...lintResult, fixed: stdout !== source, output: stdout };
1936
2064
  }
1937
2065
  async fixFiles(filePaths) {
1938
- const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
2066
+ const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync2);
1939
2067
  if (resolved.length === 0)
1940
2068
  return { fixed: [] };
1941
2069
  const before = new Map(resolved.map((filePath) => [filePath, readFileSync(filePath, "utf8")]));
@@ -1955,7 +2083,7 @@ ${errorText}, ${warningText} found`];
1955
2083
  }
1956
2084
  async getConfigForFile(filePath) {
1957
2085
  const resolvedFilePath = this.#resolveFilePath(filePath);
1958
- if (!existsSync(resolvedFilePath))
2086
+ if (!existsSync2(resolvedFilePath))
1959
2087
  throw new Error(`File not found: ${filePath}`);
1960
2088
  return JSON.parse(readFileSync(this.configPath, "utf8"));
1961
2089
  }
@@ -2244,7 +2372,7 @@ var moduleUiFileTypes = {
2244
2372
  service: new Set(["Util", "Zone"]),
2245
2373
  scalar: new Set(["Template", "Unit"])
2246
2374
  };
2247
- var createDependencyScanner = async (exec) => (await import("./dependencyScanner-06z2dmrr.js")).TypeScriptDependencyScanner.from(exec);
2375
+ var createDependencyScanner = async (exec) => (await import("./dependencyScanner-3afj2vt1.js")).TypeScriptDependencyScanner.from(exec);
2248
2376
  var getScanPath = (exec, relativePath) => path11.posix.join(`${exec.type}s`, exec.name, relativePath.split(path11.sep).join("/"));
2249
2377
  var getModuleNameFromPath = (kind, modulePath) => {
2250
2378
  const dirname = path11.basename(modulePath);
@@ -2289,7 +2417,7 @@ async function assertScanConvention(exec, libRoot) {
2289
2417
  ]);
2290
2418
  if (violations.length > 0) {
2291
2419
  throw new Error(`[scan-convention]
2292
- ${violations.sort().map((violation) => `- ${violation}`).join(`
2420
+ ${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
2293
2421
  `)}`);
2294
2422
  }
2295
2423
  }
@@ -2613,6 +2741,136 @@ class WorkspaceInfo {
2613
2741
  }
2614
2742
  }
2615
2743
 
2744
+ // pkgs/@akanjs/devkit/spinner.ts
2745
+ import ora from "ora";
2746
+
2747
+ class Spinner {
2748
+ static padding = 12;
2749
+ static oraOptions = { discardStdin: false };
2750
+ static canAnimate(stream = process.stderr) {
2751
+ return !stream.isTTY || stream.columns > 0;
2752
+ }
2753
+ spinner;
2754
+ stopWatch = null;
2755
+ startAt = new Date;
2756
+ prefix;
2757
+ message;
2758
+ enableSpin;
2759
+ constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
2760
+ Spinner.padding = Math.max(Spinner.padding, prefix.length);
2761
+ this.prefix = prefix;
2762
+ this.message = message;
2763
+ this.spinner = ora({ ...Spinner.oraOptions, text: message });
2764
+ this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
2765
+ this.spinner.indent = indent;
2766
+ this.enableSpin = enableSpin && Spinner.canAnimate();
2767
+ }
2768
+ start() {
2769
+ this.startAt = new Date;
2770
+ if (this.enableSpin) {
2771
+ this.spinner.start();
2772
+ this.stopWatch = setInterval(() => {
2773
+ this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
2774
+ this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
2775
+ }, 1000);
2776
+ } else
2777
+ this.spinner.info();
2778
+ return this;
2779
+ }
2780
+ succeed(message) {
2781
+ this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
2782
+ this.#reset();
2783
+ }
2784
+ fail(message) {
2785
+ this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
2786
+ this.#reset();
2787
+ }
2788
+ isSpinning() {
2789
+ return this.spinner.isSpinning;
2790
+ }
2791
+ #reset() {
2792
+ if (this.stopWatch)
2793
+ clearInterval(this.stopWatch);
2794
+ this.stopWatch = null;
2795
+ }
2796
+ #getElapsedTimeStr({ floor = false } = {}) {
2797
+ const ms = Date.now() - this.startAt.getTime();
2798
+ if (ms < 1000)
2799
+ return `${ms}ms`;
2800
+ const s = Math.floor(ms / 1000);
2801
+ if (s < 60)
2802
+ return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
2803
+ const m = Math.floor(s / 60);
2804
+ return `${m}m ${s % 60}s`;
2805
+ }
2806
+ }
2807
+
2808
+ // pkgs/@akanjs/devkit/typecheck/projectTypecheck.ts
2809
+ import { mkdir as mkdir2, rm as rm3 } from "fs/promises";
2810
+ import path12 from "path";
2811
+
2812
+ class ProjectTypecheck2 {
2813
+ projectDir;
2814
+ workspaceRoot;
2815
+ env;
2816
+ static #testIncludes = ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"];
2817
+ static #excludes = [".akan/**", "node_modules/**", "local/**"];
2818
+ constructor(projectDir, workspaceRoot, env) {
2819
+ this.projectDir = projectDir;
2820
+ this.workspaceRoot = workspaceRoot;
2821
+ this.env = env;
2822
+ }
2823
+ get typecheckDir() {
2824
+ return path12.join(this.projectDir, ".akan", "typecheck");
2825
+ }
2826
+ async run(entries, { clean = false, incremental = true, tests = false } = {}) {
2827
+ const name = tests ? "tsconfig.tests" : "tsconfig";
2828
+ await mkdir2(this.typecheckDir, { recursive: true });
2829
+ const tsconfig = {
2830
+ extends: "../../tsconfig.json",
2831
+ compilerOptions: { noEmit: true, incremental, tsBuildInfoFile: `./${name}.tsbuildinfo` },
2832
+ include: [...entries, ...tests ? ProjectTypecheck2.#testIncludes.map((glob) => `../../${glob}`) : []],
2833
+ ...tests ? { exclude: ProjectTypecheck2.#excludes.map((glob) => `../../${glob}`) } : {},
2834
+ references: []
2835
+ };
2836
+ const tsconfigPath = path12.join(this.typecheckDir, `${name}.json`);
2837
+ await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
2838
+ `);
2839
+ if (clean)
2840
+ await rm3(path12.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
2841
+ await this.#check(tsconfigPath);
2842
+ return tsconfigPath;
2843
+ }
2844
+ async#check(tsconfigPath) {
2845
+ const proc = Bun.spawn([process.execPath, await ProjectTypecheck2.workerEntry(this.workspaceRoot)], {
2846
+ cwd: this.workspaceRoot,
2847
+ env: { ...this.env, AKAN_COMMAND_TYPE: "typecheck", AKAN_TYPECHECK_TSCONFIG: tsconfigPath },
2848
+ stdout: "pipe",
2849
+ stderr: "pipe"
2850
+ });
2851
+ const [stdout, stderr, exitCode] = await Promise.all([
2852
+ new Response(proc.stdout).text(),
2853
+ new Response(proc.stderr).text(),
2854
+ proc.exited
2855
+ ]);
2856
+ if (exitCode !== 0)
2857
+ throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
2858
+ }
2859
+ static async workerEntry(workspaceRoot) {
2860
+ const candidates = [
2861
+ path12.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
2862
+ path12.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
2863
+ path12.join(import.meta.dir, "typecheck.proc.js"),
2864
+ path12.join(import.meta.dir, "typecheck.proc.ts"),
2865
+ path12.join(import.meta.dir, "..", "typecheck.proc.js")
2866
+ ];
2867
+ for (const candidate of candidates)
2868
+ if (await Bun.file(candidate).exists())
2869
+ return candidate;
2870
+ throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
2871
+ }
2872
+ }
2873
+
2616
2874
  // pkgs/@akanjs/devkit/executors.ts
2617
2875
  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(" ")));
2618
2876
  var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
@@ -2709,16 +2967,16 @@ class Executor {
2709
2967
  linter = null;
2710
2968
  constructor(name, cwdPath) {
2711
2969
  this.name = name;
2712
- this.logger = new Logger2(name);
2970
+ this.logger = new Logger3(name);
2713
2971
  this.logs = [];
2714
2972
  this.cwdPath = cwdPath;
2715
2973
  }
2716
2974
  #stdout(data) {
2717
2975
  if (Executor.verbose)
2718
- Logger2.raw(chalk2.dim(data.toString()));
2976
+ Logger3.raw(chalk2.dim(data.toString()));
2719
2977
  }
2720
2978
  #stderr(data) {
2721
- Logger2.raw(chalk2.red(data.toString()));
2979
+ Logger3.raw(chalk2.red(data.toString()));
2722
2980
  }
2723
2981
  #settle(proc, target, { onClose = false, keepLogs = false, redStderr = false } = {}) {
2724
2982
  let stdout = "";
@@ -2767,10 +3025,10 @@ class Executor {
2767
3025
  return this.#settle(proc, target, { redStderr: true }).then(({ code, signal }) => ({ code, signal }));
2768
3026
  }
2769
3027
  getPath(filePath) {
2770
- if (path12.isAbsolute(filePath))
3028
+ if (path13.isAbsolute(filePath))
2771
3029
  return filePath;
2772
3030
  if (filePath.startsWith("."))
2773
- return path12.join(this.cwdPath, filePath);
3031
+ return path13.join(this.cwdPath, filePath);
2774
3032
  const baseParts = this.cwdPath.split(/[\\/]/).filter(Boolean);
2775
3033
  const targetParts = filePath.split(/[\\/]/).filter(Boolean);
2776
3034
  let overlapLength = 0;
@@ -2784,12 +3042,12 @@ class Executor {
2784
3042
  if (isOverlap)
2785
3043
  overlapLength = i;
2786
3044
  }
2787
- return path12.join(this.cwdPath, ...targetParts.slice(overlapLength));
3045
+ return path13.join(this.cwdPath, ...targetParts.slice(overlapLength));
2788
3046
  }
2789
3047
  async mkdir(dirPath) {
2790
3048
  const writePath = this.getPath(dirPath);
2791
3049
  if (!await FileSys2.dirExists(writePath))
2792
- await mkdir2(writePath, { recursive: true });
3050
+ await mkdir3(writePath, { recursive: true });
2793
3051
  this.logger.verbose(`Make directory ${writePath}`);
2794
3052
  return this;
2795
3053
  }
@@ -2797,20 +3055,20 @@ class Executor {
2797
3055
  const readPath = this.getPath(dirPath);
2798
3056
  try {
2799
3057
  const glob = new Bun.Glob("*");
2800
- return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false }));
3058
+ return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false })).sort();
2801
3059
  } catch {
2802
3060
  return [];
2803
3061
  }
2804
3062
  }
2805
3063
  async getAllFiles(pattern = "**/*", { cwd } = {}) {
2806
3064
  const glob = new Bun.Glob(pattern);
2807
- return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true }));
3065
+ return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true })).sort();
2808
3066
  }
2809
3067
  async getFilesAndDirs(dirPath) {
2810
3068
  const fullDirPath = this.getPath(dirPath);
2811
3069
  const glob = new Bun.Glob("*");
2812
- const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
2813
- const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
3070
+ const files = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: true })).sort();
3071
+ const allEntries = Array.from(glob.scanSync({ cwd: fullDirPath, onlyFiles: false })).sort();
2814
3072
  const dirs = allEntries.filter((entry) => !files.includes(entry));
2815
3073
  return { files, dirs };
2816
3074
  }
@@ -2825,26 +3083,28 @@ class Executor {
2825
3083
  return this;
2826
3084
  }
2827
3085
  async removeDir(dirPath) {
2828
- const readPath = path12.join(this.getPath(dirPath), ".");
3086
+ const readPath = path13.join(this.getPath(dirPath), ".");
2829
3087
  if (await FileSys2.entryExists(readPath))
2830
- await rm3(readPath, { recursive: true, force: true });
3088
+ await rm4(readPath, { recursive: true, force: true });
2831
3089
  this.logger.verbose(`Remove directory ${readPath}`);
2832
3090
  return this;
2833
3091
  }
2834
3092
  async writeFile(filePath, content, { overwrite = true, silent = false } = {}) {
2835
3093
  const writePath = this.getPath(filePath);
2836
- const dir = path12.dirname(writePath);
3094
+ const dir = path13.dirname(writePath);
2837
3095
  if (!await FileSys2.dirExists(dir))
2838
- await mkdir2(dir, { recursive: true });
3096
+ await mkdir3(dir, { recursive: true });
2839
3097
  let contentStr = typeof content === "string" ? content : JsonFormatter.stringify(content, writePath);
3098
+ let written = true;
2840
3099
  if (await FileSys2.fileExists(writePath)) {
2841
3100
  const currentContent = await FileSys2.readText(writePath);
2842
3101
  if (currentContent === contentStr || !overwrite) {
2843
3102
  this.logger.verbose(`File ${writePath} is unchanged`);
2844
3103
  contentStr = currentContent;
3104
+ written = false;
2845
3105
  } else {
2846
3106
  await FileSys2.writeText(writePath, contentStr);
2847
- if (Logger2.isVerbose())
3107
+ if (Logger3.isVerbose())
2848
3108
  this.logger.rawLog(chalk2.yellow(`File Update: ${filePath}`));
2849
3109
  }
2850
3110
  } else {
@@ -2852,13 +3112,13 @@ class Executor {
2852
3112
  if (!silent)
2853
3113
  this.logger.rawLog(chalk2.green(`File Create: ${filePath}`));
2854
3114
  }
2855
- return { filePath: writePath, content: contentStr };
3115
+ return { filePath: writePath, content: contentStr, written };
2856
3116
  }
2857
3117
  async writeJson(filePath, content) {
2858
3118
  await this.writeFile(filePath, content);
2859
3119
  }
2860
3120
  async getLocalFile(targetPath) {
2861
- const filePath = path12.isAbsolute(targetPath) ? targetPath : targetPath.replace(this.cwdPath, "");
3121
+ const filePath = path13.isAbsolute(targetPath) ? targetPath : targetPath.replace(this.cwdPath, "");
2862
3122
  const content = await this.readFile(filePath);
2863
3123
  return { filePath, content };
2864
3124
  }
@@ -2875,7 +3135,7 @@ class Executor {
2875
3135
  return;
2876
3136
  const isDirectory = (await stat2(src)).isDirectory();
2877
3137
  if (!await FileSys2.exists(dest) && isDirectory)
2878
- await mkdir2(dest, { recursive: true });
3138
+ await mkdir3(dest, { recursive: true });
2879
3139
  if (dereference || process.platform === "win32")
2880
3140
  await cpEntry(src, dest, { recursive: isDirectory, dereference: true, force: true });
2881
3141
  else
@@ -2945,9 +3205,9 @@ class Executor {
2945
3205
  const result = await getContent.default(scanInfo ?? null, dict, options);
2946
3206
  if (result === null)
2947
3207
  return null;
2948
- const filename = typeof result === "object" ? result.filename : path12.basename(targetPath).replace(".js", ".ts");
3208
+ const filename = typeof result === "object" ? result.filename : path13.basename(targetPath).replace(".js", ".ts");
2949
3209
  const content = typeof result === "object" ? result.content : result;
2950
- const convertedTargetPath = convertPath(`${path12.dirname(targetPath)}/${filename}`);
3210
+ const convertedTargetPath = convertPath(`${path13.dirname(targetPath)}/${filename}`);
2951
3211
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
2952
3212
  return this.writeFile(convertedTargetPath, content, { overwrite });
2953
3213
  } else if (targetPath.endsWith(".template")) {
@@ -2956,15 +3216,17 @@ class Executor {
2956
3216
  const convertedContent = Object.entries(dict).reduce((data, [key, value]) => data.replace(new RegExp(`<%= ${key} %>`, "g"), value), content);
2957
3217
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
2958
3218
  return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
2959
- } else if (staticTemplateFileExtensions.has(path12.extname(targetPath).toLowerCase())) {
3219
+ } else if (staticTemplateFileExtensions.has(path13.extname(targetPath).toLowerCase())) {
2960
3220
  const convertedTargetPath = convertPath(targetPath);
2961
3221
  const writePath = this.getPath(convertedTargetPath);
2962
- const dirname = path12.dirname(writePath);
3222
+ if (!overwrite && await FileSys2.fileExists(writePath))
3223
+ return { filePath: writePath, content: "", written: false };
3224
+ const dirname = path13.dirname(writePath);
2963
3225
  if (!await FileSys2.dirExists(dirname))
2964
- await mkdir2(dirname, { recursive: true });
3226
+ await mkdir3(dirname, { recursive: true });
2965
3227
  await copyFile(templatePath, writePath);
2966
3228
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
2967
- return { filePath: writePath, content: "" };
3229
+ return { filePath: writePath, content: "", written: true };
2968
3230
  } else
2969
3231
  return null;
2970
3232
  }
@@ -2981,7 +3243,7 @@ class Executor {
2981
3243
  if ((await stat2(templatePath)).isFile()) {
2982
3244
  const fileContent = await this.#applyTemplateFile({
2983
3245
  templatePath,
2984
- targetPath: path12.join(basePath, path12.basename(templatePath)),
3246
+ targetPath: path13.join(basePath, path13.basename(templatePath)),
2985
3247
  scanInfo,
2986
3248
  overwrite
2987
3249
  }, dict, options);
@@ -2989,19 +3251,19 @@ class Executor {
2989
3251
  } else {
2990
3252
  const subdirs = await readDirEntries(templatePath);
2991
3253
  const fileContents = (await Promise.all(subdirs.map(async (subdir) => {
2992
- const subpath = path12.join(templatePath, subdir);
3254
+ const subpath = path13.join(templatePath, subdir);
2993
3255
  if ((await stat2(subpath)).isFile()) {
2994
3256
  const fileContent = await this.#applyTemplateFile({
2995
3257
  templatePath: subpath,
2996
- targetPath: path12.join(basePath, subdir),
3258
+ targetPath: path13.join(basePath, subdir),
2997
3259
  scanInfo,
2998
3260
  overwrite
2999
3261
  }, dict, options);
3000
3262
  return fileContent ? [fileContent] : [];
3001
3263
  } else
3002
3264
  return await this._applyTemplate({
3003
- basePath: path12.join(basePath, subdir),
3004
- template: path12.join(template, subdir),
3265
+ basePath: path13.join(basePath, subdir),
3266
+ template: path13.join(template, subdir),
3005
3267
  scanInfo,
3006
3268
  dict,
3007
3269
  overwrite,
@@ -3013,8 +3275,8 @@ class Executor {
3013
3275
  }
3014
3276
  async#resolveTemplateRoot() {
3015
3277
  const candidates = [
3016
- path12.resolve(getDirname2(import.meta.url), "templates"),
3017
- path12.resolve(getDirname2(import.meta.url), "../cli/templates")
3278
+ path13.resolve(getDirname2(import.meta.url), "templates"),
3279
+ path13.resolve(getDirname2(import.meta.url), "../cli/templates")
3018
3280
  ];
3019
3281
  for (const candidate of candidates) {
3020
3282
  if (await FileSys2.dirExists(candidate))
@@ -3024,7 +3286,7 @@ class Executor {
3024
3286
  }
3025
3287
  async applyTemplate(options) {
3026
3288
  const fileContents = await this._applyTemplate({ ...options, dict: withCapitalizedDict(options.dict) });
3027
- await this.#formatAppliedTemplate(fileContents);
3289
+ await this.#formatAppliedTemplate(fileContents.filter((fileContent) => fileContent.written));
3028
3290
  return fileContents;
3029
3291
  }
3030
3292
  async#formatAppliedTemplate(fileContents) {
@@ -3036,7 +3298,7 @@ class Executor {
3036
3298
  if (fixed.length > 0)
3037
3299
  this.logger.verbose(`Formatted ${fixed.length} scaffolded file(s)`);
3038
3300
  } catch (err) {
3039
- this.logger.verbose(`Skipped formatting scaffolded files: ${err instanceof Error ? err.message : String(err)}`);
3301
+ this.logger.verbose(`Skipped formatting scaffolded files: ${toError2(err).message}`);
3040
3302
  }
3041
3303
  }
3042
3304
  async getTypeChecker() {
@@ -3053,7 +3315,7 @@ class Executor {
3053
3315
  }
3054
3316
  async typeCheckAsync(filePath) {
3055
3317
  const path = this.getPath(filePath);
3056
- const entry = await this.#resolveTypecheckWorkerEntry();
3318
+ const entry = await ProjectTypecheck2.workerEntry(process.cwd());
3057
3319
  const proc = Bun.spawn([process.execPath, entry], {
3058
3320
  cwd: this.cwdPath,
3059
3321
  env: {
@@ -3079,20 +3341,6 @@ class Executor {
3079
3341
  message: result.message
3080
3342
  };
3081
3343
  }
3082
- async#resolveTypecheckWorkerEntry() {
3083
- const dirname = getDirname2(import.meta.url);
3084
- const candidates = [
3085
- path12.join(process.cwd(), "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
3086
- path12.join(process.cwd(), "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
3087
- path12.join(dirname, "typecheck/typecheck.proc.ts"),
3088
- path12.join(dirname, "typecheck.proc.js"),
3089
- path12.join(dirname, "typecheck.proc.ts")
3090
- ];
3091
- for (const candidate of candidates)
3092
- if (await Bun.file(candidate).exists())
3093
- return candidate;
3094
- throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
3095
- }
3096
3344
  getLinter() {
3097
3345
  this.linter ??= new Linter(this.cwdPath);
3098
3346
  return this.linter;
@@ -3124,17 +3372,19 @@ class WorkspaceExecutor extends Executor {
3124
3372
  } = {}) {
3125
3373
  return new WorkspaceExecutor({ workspaceRoot, repoName });
3126
3374
  }
3375
+ static publicEnvPin = null;
3376
+ static localServeDomain = "localhost";
3127
3377
  static getBaseDevEnv(envPath) {
3128
- const sourceEnv = envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env;
3378
+ const sourceEnv = {
3379
+ ...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
3380
+ ...WorkspaceExecutor.publicEnvPin
3381
+ };
3129
3382
  const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
3130
3383
  const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
3131
3384
  const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
3132
- const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
3133
- if (!repoName)
3134
- throw new Error("AKAN_PUBLIC_REPO_NAME is not set");
3135
- const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
3136
- if (!serveDomain)
3137
- throw new Error("AKAN_PUBLIC_SERVE_DOMAIN is not set");
3385
+ const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME || resolveRepoName(envPath ? path13.dirname(envPath) : workspaceRoot ?? process.cwd());
3386
+ const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN || WorkspaceExecutor.localServeDomain;
3387
+ const defaulted = ["AKAN_PUBLIC_REPO_NAME", "AKAN_PUBLIC_SERVE_DOMAIN"].filter((key) => !sourceEnv[key]);
3138
3388
  const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
3139
3389
  const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
3140
3390
  if (!env)
@@ -3146,9 +3396,18 @@ class WorkspaceExecutor extends Executor {
3146
3396
  serveDomain,
3147
3397
  env,
3148
3398
  portOffset,
3149
- workspaceId
3399
+ workspaceId,
3400
+ defaulted
3150
3401
  };
3151
3402
  }
3403
+ static assertDeployableEnv(baseDevEnv, environment, purpose) {
3404
+ const defaulted = environment === "local" ? [] : baseDevEnv.defaulted ?? [];
3405
+ if (!defaulted.length)
3406
+ return;
3407
+ const values = { AKAN_PUBLIC_REPO_NAME: baseDevEnv.repoName, AKAN_PUBLIC_SERVE_DOMAIN: baseDevEnv.serveDomain };
3408
+ const standIns = defaulted.map((key) => `${key} (would be "${values[key]}")`).join(" and ");
3409
+ 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.`);
3410
+ }
3152
3411
  getWorkspaceId({
3153
3412
  allowEmpty
3154
3413
  } = {}) {
@@ -3213,7 +3472,7 @@ class WorkspaceExecutor extends Executor {
3213
3472
  await Promise.all(dirs.map(async (dir) => {
3214
3473
  if (dir.includes("_") || AVOID_DIRS.includes(dir))
3215
3474
  return;
3216
- const dirPath = path12.join(dirname, dir);
3475
+ const dirPath = path13.join(dirname, dir);
3217
3476
  if ((await stat2(dirPath)).isDirectory()) {
3218
3477
  results.push(`${prefix}${dir}`);
3219
3478
  if (maxDepth > 0)
@@ -3250,9 +3509,9 @@ class WorkspaceExecutor extends Executor {
3250
3509
  await Promise.all(dirs.map(async (dir) => {
3251
3510
  if (AVOID_DIRS.includes(dir))
3252
3511
  return;
3253
- const dirPath = path12.join(dirname, dir);
3512
+ const dirPath = path13.join(dirname, dir);
3254
3513
  if (await FileSys2.dirExists(dirPath)) {
3255
- const hasTargetFile = await FileSys2.fileExists(path12.join(dirPath, targetFilename));
3514
+ const hasTargetFile = await FileSys2.fileExists(path13.join(dirPath, targetFilename));
3256
3515
  if (hasTargetFile)
3257
3516
  results.push(`${prefix}${dir}`);
3258
3517
  if (maxDepth > 0)
@@ -3311,7 +3570,7 @@ class SysExecutor extends Executor {
3311
3570
  return [];
3312
3571
  const violations = [];
3313
3572
  for (const entry of await this.getAllFiles("**/*", { cwd: this.getPath("page") })) {
3314
- const relativePath = entry.split(path12.sep).join("/");
3573
+ const relativePath = entry.split(path13.sep).join("/");
3315
3574
  const reason = getPageSourceFileViolation(relativePath);
3316
3575
  if (reason)
3317
3576
  violations.push({ relativePath: `page/${relativePath}`, reason });
@@ -3406,21 +3665,21 @@ class SysExecutor extends Executor {
3406
3665
  await this.setPackageJson({ ...packageJson, dependencies, devDependencies });
3407
3666
  }
3408
3667
  async getLocalFile(targetPath) {
3409
- const filePath = path12.isAbsolute(targetPath) ? targetPath : `${this.type}s/${this.name}/${targetPath}`;
3668
+ const filePath = path13.isAbsolute(targetPath) ? targetPath : `${this.type}s/${this.name}/${targetPath}`;
3410
3669
  const content = await this.workspace.readFile(filePath);
3411
3670
  return { filePath, content };
3412
3671
  }
3413
3672
  async getDatabaseModules() {
3414
- return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync2(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
3673
+ return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${name}.constant.ts`));
3415
3674
  }
3416
3675
  async getServiceModules() {
3417
- return (await this.readdir("lib")).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) => existsSync2(`${this.cwdPath}/lib/${name}/${name.slice(1)}.service.ts`));
3676
+ return (await this.readdir("lib")).filter((name) => name.startsWith("_") && !name.startsWith("__")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${name.slice(1)}.service.ts`));
3418
3677
  }
3419
3678
  async getScalarModules() {
3420
- return (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_")).filter((name) => existsSync2(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
3679
+ return (await this.readdir("lib/__scalar")).filter((name) => !name.startsWith("_")).filter((name) => existsSync3(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`));
3421
3680
  }
3422
3681
  async#getComponentModules(role) {
3423
- return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync2(`${this.cwdPath}/lib/${name}/${capitalize(name)}.${role}.tsx`));
3682
+ return (await this.readdir("lib")).filter((name) => !name.startsWith("_") && !name.endsWith(".ts")).filter((name) => existsSync3(`${this.cwdPath}/lib/${name}/${capitalize(name)}.${role}.tsx`));
3424
3683
  }
3425
3684
  async#getComponentSources(role) {
3426
3685
  const modules = await this.#getComponentModules(role);
@@ -3494,19 +3753,19 @@ class AppExecutor extends SysExecutor {
3494
3753
  static from(executor, name) {
3495
3754
  return new AppExecutor({ workspace: executor instanceof WorkspaceExecutor ? executor : executor.workspace, name });
3496
3755
  }
3497
- static #keptOnStart = [path12.join(".akan", "cache", "bin")];
3498
- static #legacyNative = path12.join(".akan", "native");
3756
+ static #keptOnStart = [path13.join(".akan", "cache", "bin")];
3757
+ static #legacyNative = path13.join(".akan", "native");
3499
3758
  async#removeDevOutput(dir = ".akan") {
3500
3759
  const entries = await readDirEntries(this.getPath(dir)).catch(() => []);
3501
3760
  await Promise.all(entries.map(async (name) => {
3502
- const entry = path12.join(dir, name);
3761
+ const entry = path13.join(dir, name);
3503
3762
  if (entry === AppExecutor.#legacyNative) {
3504
3763
  this.logger.warn(`apps/${this.name}/.akan/native holds native builds from before they moved to dist/native/${this.name}: upload any release waiting in its updates folders, then delete it.`);
3505
3764
  return;
3506
3765
  }
3507
3766
  if (AppExecutor.#keptOnStart.includes(entry))
3508
3767
  return;
3509
- if (AppExecutor.#keptOnStart.some((kept) => kept.startsWith(`${entry}${path12.sep}`)))
3768
+ if (AppExecutor.#keptOnStart.some((kept) => kept.startsWith(`${entry}${path13.sep}`)))
3510
3769
  return await this.#removeDevOutput(entry);
3511
3770
  await this.removeDir(entry);
3512
3771
  }));
@@ -3525,10 +3784,12 @@ class AppExecutor extends SysExecutor {
3525
3784
  return basePort + appIndex + portOffset;
3526
3785
  }
3527
3786
  getCommandEnv(env = {}) {
3528
- const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
3787
+ const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
3529
3788
  const PORT = (8282 + portOffset).toString();
3530
3789
  return {
3531
3790
  ...process.env,
3791
+ AKAN_PUBLIC_REPO_NAME: repoName,
3792
+ AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
3532
3793
  AKAN_PUBLIC_APP_NAME: this.name,
3533
3794
  AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
3534
3795
  NODE_NO_WARNINGS: "1",
@@ -3630,13 +3891,13 @@ class AppExecutor extends SysExecutor {
3630
3891
  absolute: false,
3631
3892
  onlyFiles: true
3632
3893
  })) {
3633
- const segments = rel.split(path12.sep);
3894
+ const segments = rel.split(path13.sep);
3634
3895
  if (segments.some((s) => s === "node_modules"))
3635
3896
  continue;
3636
3897
  const posix = `${root.keyPrefix}${segments.join("/")}`;
3637
3898
  if (!root.keyPrefix && libKeyPrefixes.some((prefix) => posix.startsWith(prefix)))
3638
3899
  continue;
3639
- const absPath = path12.join(root.dir, ...segments);
3900
+ const absPath = path13.join(root.dir, ...segments);
3640
3901
  validatePageSourceFile(posix, { filePath: absPath });
3641
3902
  if (!isRouteSourceFile(posix))
3642
3903
  continue;
@@ -3679,7 +3940,7 @@ class AppExecutor extends SysExecutor {
3679
3940
  pageKeys.push(key);
3680
3941
  }
3681
3942
  }
3682
- pageKeys.sort();
3943
+ pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3683
3944
  this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
3684
3945
  return this.#pageKeys;
3685
3946
  }
@@ -3777,12 +4038,12 @@ class AppExecutor extends SysExecutor {
3777
4038
  static async#linkLibAsset(targetPath, linkPath) {
3778
4039
  const isWindows = process.platform === "win32";
3779
4040
  try {
3780
- const target = isWindows ? targetPath : path12.relative(path12.dirname(linkPath), targetPath);
4041
+ const target = isWindows ? targetPath : path13.relative(path13.dirname(linkPath), targetPath);
3781
4042
  await symlink(target, linkPath, isWindows ? "junction" : "dir");
3782
4043
  } catch (error) {
3783
4044
  if (!isWindows)
3784
4045
  throw error;
3785
- await mkdir2(linkPath, { recursive: true });
4046
+ await mkdir3(linkPath, { recursive: true });
3786
4047
  await cpEntry(targetPath, linkPath, { recursive: true, dereference: true, force: true });
3787
4048
  }
3788
4049
  }
@@ -3820,7 +4081,7 @@ class AppExecutor extends SysExecutor {
3820
4081
  await this.writeFile(rel, content, opts);
3821
4082
  },
3822
4083
  readEnvClient: async () => {
3823
- const envClientPath = path12.join(this.cwdPath, "env", "env.client.ts");
4084
+ const envClientPath = path13.join(this.cwdPath, "env", "env.client.ts");
3824
4085
  if (!await FileSys2.fileExists(envClientPath))
3825
4086
  return null;
3826
4087
  try {
@@ -3914,7 +4175,7 @@ class PkgExecutor extends Executor {
3914
4175
  return overrideVersion;
3915
4176
  try {
3916
4177
  const packageJsonPath = `pkgs/${dep}/package.json`;
3917
- if (!await Bun.file(path12.join(this.workspace.workspaceRoot, packageJsonPath)).exists())
4178
+ if (!await Bun.file(path13.join(this.workspace.workspaceRoot, packageJsonPath)).exists())
3918
4179
  return;
3919
4180
  const packageJson = await this.workspace.readJson(packageJsonPath);
3920
4181
  if (packageJson.name === dep)
@@ -3925,7 +4186,7 @@ class PkgExecutor extends Executor {
3925
4186
  }
3926
4187
  async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
3927
4188
  const dependencyNames = [...new Set(dependencies)].sort();
3928
- const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort();
4189
+ const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3929
4190
  const dependencyVersions = new Map;
3930
4191
  const missingDeps = [];
3931
4192
  for (const dep of [...dependencyNames, ...devDependencyNames]) {
@@ -4007,4 +4268,4 @@ class ModuleExecutor extends Executor {
4007
4268
  }
4008
4269
  }
4009
4270
 
4010
- 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, Executor, WorkspaceExecutor, SysExecutor, AppExecutor, LibExecutor, PkgExecutor, ModuleExecutor };
4271
+ 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 };