@akanjs/cli 3.1.0-rc.4 → 3.1.0-rc.5

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 (89) hide show
  1. package/.build-stamp +1 -1
  2. package/{CodeAgent-ymte1tv8.js → CodeAgent-rkjyxgec.js} +2 -2
  3. package/{CodeTui-4rhdm1qw.js → CodeTui-h7pyj6xa.js} +1 -1
  4. package/TunnelShare-661cxz5e.js +14 -0
  5. package/{agent.command-r5x1mch5.js → agent.command-3qxf4xcj.js} +5 -5
  6. package/akanCode.js +2 -2
  7. package/{application.command-3dkr7rdz.js → application.command-m1kcd7jf.js} +12 -11
  8. package/{applicationBuildRunner-h0j2swx1.js → applicationBuildRunner-dj0xkrep.js} +16 -9
  9. package/buildBatch.proc.js +7 -7
  10. package/{cloud.command-4kyxfw0y.js → cloud.command-3jzym8q7.js} +17 -16
  11. package/cloud.script-r4nskbc6.js +24 -0
  12. package/{code.command-sjvzt8fc.js → code.command-0ad6gw2a.js} +6 -6
  13. package/{context.command-j9frz0dz.js → context.command-0qx447mb.js} +17 -16
  14. package/context.runner-1vsjztjg.js +25 -0
  15. package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
  16. package/{guideline.command-1epw6hey.js → guideline.command-cv5bmrx2.js} +2 -2
  17. package/guidelines/conventions/conventions.instruction.md +450 -1084
  18. package/guidelines/cssRule/cssRule.instruction.md +16 -1
  19. package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
  20. package/guidelines/framework/framework.instruction.md +4 -7
  21. package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
  22. package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
  23. package/guidelines/modelStore/modelStore.instruction.md +9 -0
  24. package/guidelines/pageRule/pageRule.instruction.md +80 -0
  25. package/guidelines/queryRule/queryRule.instruction.md +35 -8
  26. package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
  27. package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
  28. package/guidelines/transportRule/transportRule.instruction.md +76 -8
  29. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
  30. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
  31. package/incrementalBuilder.proc.js +4 -4
  32. package/{index-jw6st2rd.js → index-192ha4xh.js} +356 -307
  33. package/{index-1hkr7mrb.js → index-1p98exm2.js} +2 -2
  34. package/{index-8byzcvxp.js → index-1xj2t7cb.js} +7 -7
  35. package/{index-er3hekv9.js → index-20bxxyvn.js} +80 -80
  36. package/{index-xe3x2kw5.js → index-35hc95h5.js} +1 -1
  37. package/{index-p5nrc5he.js → index-4mhy6qme.js} +4 -4
  38. package/{index-0qc5nd61.js → index-5edzv8fn.js} +5 -5
  39. package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
  40. package/index-62md4dwy.js +260 -0
  41. package/{index-z3v7abrk.js → index-665zp9rs.js} +38 -15
  42. package/{index-1t2xw0p0.js → index-6xn2n442.js} +4 -4
  43. package/{index-jg3fj8h0.js → index-86jt2a0e.js} +1 -1
  44. package/{index-k33me5sp.js → index-8fp3b47h.js} +419 -418
  45. package/{index-shastb7m.js → index-9z426msb.js} +17 -12
  46. package/{index-gdybjw3x.js → index-awbdfcf2.js} +4 -4
  47. package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
  48. package/{index-jppthsff.js → index-cpb3nvma.js} +57 -58
  49. package/{index-b60ccq2s.js → index-d47ypebv.js} +17 -15
  50. package/{index-gc05vqd2.js → index-d72hfmdm.js} +13 -13
  51. package/{index-akha197v.js → index-fmzcs8v6.js} +4 -4
  52. package/{index-28ftaz3d.js → index-fyq1z6kt.js} +29 -27
  53. package/{index-vwmv64ad.js → index-g1mbppa9.js} +238 -236
  54. package/{index-hedt951c.js → index-h47cf1gg.js} +7 -7
  55. package/{index-hfqs9jvc.js → index-j4mkhz93.js} +3 -3
  56. package/{index-08a5ga6n.js → index-jw1zm273.js} +28 -30
  57. package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
  58. package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
  59. package/{index-2wyn17fa.js → index-pydbnfez.js} +2 -2
  60. package/{index-bjh4wbtk.js → index-qwa5k1j8.js} +1 -1
  61. package/{index-81a1ywp6.js → index-tamvyvj3.js} +3 -3
  62. package/{index-dez4t5sd.js → index-trpasv9q.js} +260 -259
  63. package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
  64. package/{index-2k0t8p3p.js → index-va06nr10.js} +1086 -1075
  65. package/{index-9c3cd767.js → index-vk0pa0mn.js} +20 -21
  66. package/{index-qyaqzh8e.js → index-w44cy4vs.js} +2216 -2216
  67. package/index.js +22 -22
  68. package/{library.command-n58dgakb.js → library.command-erp2f56n.js} +3 -3
  69. package/{localRegistry.command-f2768sww.js → localRegistry.command-xxpqgxjw.js} +15 -14
  70. package/{module.command-xzjg5k7n.js → module.command-gs2r615e.js} +5 -5
  71. package/{package.command-24sh1j1q.js → package.command-7343f74r.js} +4 -4
  72. package/package.json +2 -2
  73. package/{page.command-fqnm88na.js → page.command-4h4a8pnh.js} +3 -3
  74. package/{pipeline.command-vn6taz75.js → pipeline.command-a2nrk26m.js} +3 -3
  75. package/{primitive.command-4j1g1a0d.js → primitive.command-w6t0asqz.js} +6 -6
  76. package/{projectTypecheck-3y7ayty0.js → projectTypecheck-f534enqj.js} +1 -1
  77. package/{quality.command-5hqmcdwt.js → quality.command-77x7vbbx.js} +4 -4
  78. package/{repair.command-rw5tk460.js → repair.command-y6sstev7.js} +5 -5
  79. package/{scalar.command-0jpap45m.js → scalar.command-20b8cy0k.js} +4 -4
  80. package/{subspace.command-y7n9sghk.js → subspace.command-92xkc0s0.js} +11 -10
  81. package/templates/lib/cnst.ts +0 -1
  82. package/templates/workspaceRoot/.gitignore.template +24 -53
  83. package/{tunnel.command-5gjwh1f8.js → tunnel.command-2kw8vt9g.js} +8 -7
  84. package/{workflow.command-3t22f7eb.js → workflow.command-2djwmpqf.js} +10 -10
  85. package/{workspace.command-cq5xg2jq.js → workspace.command-5se6spqb.js} +28 -27
  86. package/TunnelShare-p8ymkww8.js +0 -13
  87. package/cloud.script-jc3hc5d9.js +0 -23
  88. package/context.runner-v0def5my.js +0 -24
  89. package/index-n68jx79w.js +0 -120
@@ -4,101 +4,284 @@ import {
4
4
  FileSys2
5
5
  } from "./index-52w0fvh8.js";
6
6
 
7
- // pkgs/@akanjs/devkit/typecheck/projectTypecheck.ts
8
- import { mkdir, rm } from "fs/promises";
9
- import path from "path";
7
+ // pkgs/@akanjs/devkit/executors.ts
8
+ import {
9
+ exec,
10
+ fork,
11
+ spawn as spawn2
12
+ } from "child_process";
13
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
14
+ import {
15
+ copyFile,
16
+ cp as cpEntry,
17
+ lstat,
18
+ mkdir as mkdir3,
19
+ readdir as readDirEntries,
20
+ realpath,
21
+ rm as rm4,
22
+ stat as stat2,
23
+ symlink
24
+ } from "fs/promises";
25
+ import path13 from "path";
26
+ import { pathToFileURL } from "url";
27
+ import {
28
+ capitalize,
29
+ getPageSourceFileViolation,
30
+ isRouteSourceFile,
31
+ Logger as Logger3,
32
+ parseRouteModuleKey,
33
+ toError as toError2,
34
+ validatePageSourceFile,
35
+ validateSubRoutePageKey
36
+ } from "akanjs/common";
37
+ var {$ } = globalThis.Bun;
38
+ import chalk2 from "chalk";
10
39
 
11
- class ProjectTypecheck2 {
12
- projectDir;
13
- workspaceRoot;
14
- env;
15
- static #testIncludes = ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"];
16
- static #excludes = [".akan/**", "node_modules/**", "local/**"];
17
- constructor(projectDir, workspaceRoot, env) {
18
- this.projectDir = projectDir;
19
- this.workspaceRoot = workspaceRoot;
20
- this.env = env;
40
+ // pkgs/@akanjs/devkit/agentsIndex.ts
41
+ var recipeScannerLoad = null;
42
+ var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
43
+ var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
44
+ var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
45
+ var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
46
+ var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
47
+
48
+ ${block}`;
49
+ var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
50
+ var readDevkitVersion = async () => {
51
+ const { readFile } = await import("fs/promises");
52
+ const { getDirname } = await import("./fileSys-wzzrntkw.js");
53
+ try {
54
+ const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
55
+ return JSON.parse(raw).version ?? null;
56
+ } catch {
57
+ return null;
21
58
  }
22
- get typecheckDir() {
23
- return path.join(this.projectDir, ".akan", "typecheck");
59
+ };
60
+ var upsertAgentBlock = (existing, block) => {
61
+ const managed = `${AGENT_BLOCK_START}
62
+ ${block}
63
+ ${AGENT_BLOCK_END}`;
64
+ const startIndex = existing.indexOf(AGENT_BLOCK_START);
65
+ const endIndex = existing.indexOf(AGENT_BLOCK_END);
66
+ if (startIndex >= 0 && endIndex > startIndex) {
67
+ return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
24
68
  }
25
- async run(entries, { clean = false, incremental = true, tests = false } = {}) {
26
- const name = tests ? "tsconfig.tests" : "tsconfig";
27
- await mkdir(this.typecheckDir, { recursive: true });
28
- const tsconfig = {
29
- extends: "../../tsconfig.json",
30
- compilerOptions: { noEmit: true, incremental, tsBuildInfoFile: `./${name}.tsbuildinfo` },
31
- include: [...entries, ...tests ? ProjectTypecheck2.#testIncludes.map((glob) => `../../${glob}`) : []],
32
- ...tests ? { exclude: ProjectTypecheck2.#excludes.map((glob) => `../../${glob}`) } : {},
33
- references: []
34
- };
35
- const tsconfigPath = path.join(this.typecheckDir, `${name}.json`);
36
- await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}
69
+ return `${existing.replace(/\s*$/, "")}
70
+
71
+ ${managed}
72
+ `;
73
+ };
74
+ var extractAgentBlock = (content) => {
75
+ const startIndex = content.indexOf(AGENT_BLOCK_START);
76
+ const endIndex = content.indexOf(AGENT_BLOCK_END);
77
+ if (startIndex < 0 || endIndex <= startIndex)
78
+ return null;
79
+ return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
80
+ };
81
+ var signatureOf = (recipe) => {
82
+ const entries = Object.entries(recipe.variants);
83
+ if (entries.length === 0)
84
+ return "";
85
+ const parts = entries.map(([key, values]) => {
86
+ if (values.length === 1 && values[0] === "true")
87
+ return `${key}?`;
88
+ const def = recipe.defaultVariants?.[key];
89
+ return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
90
+ });
91
+ return `(${parts.join(" \xB7 ")})`;
92
+ };
93
+ var renderRecipeEntries = (recipes) => {
94
+ const groups = new Map;
95
+ for (const recipe of recipes)
96
+ groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
97
+ return [...groups.entries()].map(([importFrom, list]) => {
98
+ const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
37
99
  `);
38
- if (clean)
39
- await rm(path.join(this.typecheckDir, `${name}.tsbuildinfo`), { force: true });
40
- await this.#check(tsconfigPath);
41
- return tsconfigPath;
42
- }
43
- async#check(tsconfigPath) {
44
- const proc = Bun.spawn([process.execPath, await ProjectTypecheck2.workerEntry(this.workspaceRoot)], {
45
- cwd: this.workspaceRoot,
46
- env: { ...this.env, AKAN_COMMAND_TYPE: "typecheck", AKAN_TYPECHECK_TSCONFIG: tsconfigPath },
47
- stdout: "pipe",
48
- stderr: "pipe"
49
- });
50
- const [stdout, stderr, exitCode] = await Promise.all([
51
- new Response(proc.stdout).text(),
52
- new Response(proc.stderr).text(),
53
- proc.exited
54
- ]);
55
- if (exitCode !== 0)
56
- throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
57
- }
58
- static async workerEntry(workspaceRoot) {
59
- const candidates = [
60
- path.join(workspaceRoot, "pkgs/@akanjs/devkit/typecheck/typecheck.proc.ts"),
61
- path.join(workspaceRoot, "node_modules/@akanjs/devkit/typecheck/typecheck.proc.ts"),
62
- path.join(import.meta.dir, "typecheck.proc.js"),
63
- path.join(import.meta.dir, "typecheck.proc.ts"),
64
- path.join(import.meta.dir, "..", "typecheck.proc.js")
65
- ];
66
- for (const candidate of candidates)
67
- if (await Bun.file(candidate).exists())
68
- return candidate;
69
- throw new Error(`[devkit] typecheck worker entry not found; looked in: ${candidates.join(", ")}`);
100
+ return `Import from \`${importFrom}\`:
101
+ ${items}`;
102
+ }).join(`
103
+
104
+ `);
105
+ };
106
+ var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
107
+ const { collectRecipeSources } = await loadRecipeScanner();
108
+ const sources = [
109
+ ...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
110
+ ];
111
+ for (const lib of [...new Set(libDeps)].sort()) {
112
+ if (scope.type === "lib" && lib === scope.name)
113
+ continue;
114
+ sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
70
115
  }
71
- }
116
+ return sources;
117
+ };
118
+ var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
119
+ const { scanRecipes } = await loadRecipeScanner();
120
+ return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
121
+ };
122
+ var renderScopeAgentBlock = (scope, recipes) => {
123
+ const scopePath = `${scope.type}s/${scope.name}`;
124
+ const intro = `## Recipes In Scope
72
125
 
73
- // pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
74
- import { existsSync, realpathSync } from "fs";
75
- import path2 from "path";
76
- import { Logger, toError } from "akanjs/common";
126
+ UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
127
+ \`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
128
+ arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
129
+ boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
130
+ and verified by \`akan lint ${scope.name}\`.`;
131
+ if (recipes.length === 0) {
132
+ return `${intro}
77
133
 
78
- class ServerEnvProfile {
79
- static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
80
- static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
81
- static fileName(environment, profile) {
82
- return `env.server.${environment}.${profile}.ts`;
83
- }
84
- static profileFileOf(file, profile) {
85
- return file.replace(/\.ts$/, `.${profile}.ts`);
134
+ No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
135
+ root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
136
+ folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
86
137
  }
87
- static #url(value) {
88
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
89
- return null;
90
- try {
91
- return new URL(value);
92
- } catch {
93
- return null;
94
- }
138
+ return `${intro}
139
+
140
+ ${renderRecipeEntries(recipes)}`;
141
+ };
142
+ var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
143
+
144
+ Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
145
+ root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
146
+ section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
147
+ markers freely.
148
+
149
+ ${AGENT_BLOCK_START}
150
+ ${block}
151
+ ${AGENT_BLOCK_END}
152
+ `;
153
+ var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
154
+
155
+ Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
156
+ is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
157
+
158
+ Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
159
+
160
+ - Restates the identifier, the signature, or the line under it \u2192 delete it.
161
+ - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
162
+ - JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
163
+ - Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
164
+ - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
165
+ derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
166
+ keep it, one line.
167
+
168
+ That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
169
+ subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
170
+ surrounding code carries none, your diff carries none.`;
171
+ var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
172
+
173
+ @AGENTS.md
174
+
175
+ ${CLAUDE_COMMENT_RULE}
176
+ `;
177
+
178
+ // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
179
+ import path from "path";
180
+
181
+ // pkgs/@akanjs/devkit/akanConfig/types.ts
182
+ import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
183
+
184
+ // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
185
+ class AkanBin {
186
+ static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
187
+ static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
188
+ static parse(declared, owner, baseDir) {
189
+ if (declared === undefined)
190
+ return {};
191
+ if (!declared || typeof declared !== "object" || Array.isArray(declared))
192
+ throw new Error(`${owner}: bin maps each executable's name to its platforms`);
193
+ return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
95
194
  }
96
- static #carriesCredential(url) {
97
- return !!url.password || [...url.searchParams.keys()].some((name) => Logger.isSecretKey(name));
195
+ static isArchive(source) {
196
+ return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
98
197
  }
99
- static #isCredentialUrl(value) {
100
- const url = ServerEnvProfile.#url(value);
101
- return !!url && ServerEnvProfile.#carriesCredential(url);
198
+ static #platforms(name, platforms, owner, baseDir) {
199
+ if (!AkanBin.fileName.test(name))
200
+ throw new Error(`${owner}: bin.${name} is not a file name`);
201
+ if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
202
+ throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
203
+ const resolved = {};
204
+ for (const [platform, source] of Object.entries(platforms)) {
205
+ if (!binPlatforms.includes(platform))
206
+ throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
207
+ resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
208
+ }
209
+ return resolved;
210
+ }
211
+ static #source(at, source, baseDir) {
212
+ if (!source || typeof source !== "object" || Array.isArray(source))
213
+ throw new Error(`${at} must be { url, sha256 } or { path }`);
214
+ const { url, sha256, path: localPath, file, ...rest } = source;
215
+ const extra = Object.keys(rest);
216
+ if (extra.length)
217
+ throw new Error(`${at}: unknown key ${extra.join(", ")}`);
218
+ if (url === undefined === (localPath === undefined))
219
+ throw new Error(`${at} takes either url and sha256, or path`);
220
+ const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
221
+ const archive = AkanBin.isArchive(resolved);
222
+ if (file === undefined) {
223
+ if (archive)
224
+ throw new Error(`${at} names an archive: file picks the executable inside it`);
225
+ return resolved;
226
+ }
227
+ if (!archive)
228
+ throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
229
+ const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
230
+ if (!parts.length || path.isAbsolute(file) || parts.includes(".."))
231
+ throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
232
+ return { ...resolved, file: parts.join("/") };
233
+ }
234
+ static #urlSource(at, url, sha256) {
235
+ if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
236
+ throw new Error(`${at}: url must be an http(s) address`);
237
+ if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
238
+ throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
239
+ return { url, sha256: sha256.toLowerCase() };
240
+ }
241
+ static #pathSource(at, localPath, sha256, baseDir) {
242
+ if (typeof localPath !== "string" || !localPath)
243
+ throw new Error(`${at}: path must name a file`);
244
+ if (sha256 !== undefined)
245
+ throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
246
+ return { path: path.resolve(baseDir, localPath) };
247
+ }
248
+ }
249
+ // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
250
+ import fs from "fs";
251
+ import path3 from "path";
252
+ import { fileURLToPath } from "url";
253
+ import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
254
+ import { resolveAkanI18nConfig, trimSlashes } from "akanjs/common";
255
+
256
+ // pkgs/@akanjs/devkit/transforms/serverEnvProfile.ts
257
+ import { existsSync, realpathSync } from "fs";
258
+ import path2 from "path";
259
+ import { Logger, toError } from "akanjs/common";
260
+
261
+ class ServerEnvProfile {
262
+ static namePattern = /^[a-z][a-zA-Z0-9-]*$/;
263
+ static envFilePattern = /[\\/]env[\\/]env\.server\.[A-Za-z0-9-]+\.ts$/;
264
+ static fileName(environment, profile) {
265
+ return `env.server.${environment}.${profile}.ts`;
266
+ }
267
+ static profileFileOf(file, profile) {
268
+ return file.replace(/\.ts$/, `.${profile}.ts`);
269
+ }
270
+ static #url(value) {
271
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value))
272
+ return null;
273
+ try {
274
+ return new URL(value);
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ static #carriesCredential(url) {
280
+ return !!url.password || [...url.searchParams.keys()].some((name) => Logger.isSecretKey(name));
281
+ }
282
+ static #isCredentialUrl(value) {
283
+ const url = ServerEnvProfile.#url(value);
284
+ return !!url && ServerEnvProfile.#carriesCredential(url);
102
285
  }
103
286
  static #isSecret(value, underSecret) {
104
287
  return underSecret && !!value.trim() || ServerEnvProfile.#isCredentialUrl(value);
@@ -327,839 +510,92 @@ class ServerEnvProfile {
327
510
  }
328
511
  }
329
512
 
330
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
331
- import { readdir, rm as rm2 } from "fs/promises";
332
- import path4 from "path";
333
-
334
- // pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
335
- import fs from "fs";
336
- import path3 from "path";
337
-
338
- class AsyncDefaultExportDetector {
339
- static #typescriptLoad;
340
- static #loadTypescript() {
341
- AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
342
- return AsyncDefaultExportDetector.#typescriptLoad;
343
- }
344
- static async detect(moduleAbsPath) {
345
- try {
346
- const typescript = await AsyncDefaultExportDetector.#loadTypescript();
347
- const source = fs.readFileSync(path3.resolve(moduleAbsPath), "utf8");
348
- const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
349
- return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
350
- } catch {
351
- return false;
352
- }
353
- }
354
- #ts;
355
- constructor(typescript) {
356
- this.#ts = typescript;
357
- }
358
- detectInSourceFile(sourceFile) {
359
- const ts = this.#ts;
360
- const asyncBindings = new Map;
361
- let defaultIdentifier = null;
362
- for (const statement of sourceFile.statements) {
363
- if (ts.isFunctionDeclaration(statement)) {
364
- if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
365
- return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
366
- }
367
- if (statement.name) {
368
- asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
369
- }
370
- continue;
371
- }
372
- if (ts.isVariableStatement(statement)) {
373
- for (const declaration of statement.declarationList.declarations) {
374
- if (!ts.isIdentifier(declaration.name))
375
- continue;
376
- asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
377
- }
378
- continue;
379
- }
380
- if (ts.isExportAssignment(statement)) {
381
- if (this.#isAsyncFunctionExpression(statement.expression))
382
- return true;
383
- if (ts.isCallExpression(statement.expression))
384
- return true;
385
- if (ts.isIdentifier(statement.expression))
386
- defaultIdentifier = statement.expression.text;
387
- continue;
388
- }
389
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
390
- for (const specifier of statement.exportClause.elements) {
391
- if (specifier.name.text !== "default")
392
- continue;
393
- defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
394
- }
395
- }
396
- }
397
- return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
398
- }
399
- #hasModifier(node, kind) {
400
- const ts = this.#ts;
401
- return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
402
- }
403
- #isAsyncFunctionExpression(node) {
404
- const ts = this.#ts;
405
- return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
513
+ // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
514
+ var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
515
+ var DEFAULT_OPTIMIZE_IMPORTS = [
516
+ "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
517
+ "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
518
+ "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
519
+ "@material-ui/icons @tabler/icons-react mui-core react-icons/*"
520
+ ].flatMap((line) => line.split(" "));
521
+ var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
522
+ var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
523
+ var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
524
+ var DEFAULT_AKAN_IMAGE_CONFIG = {
525
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
526
+ imageSizes: [32, 48, 64, 96, 128, 256, 384],
527
+ formats: ["image/webp"],
528
+ qualities: [75],
529
+ minimumCacheTTL: 14400,
530
+ remotePatterns: [],
531
+ localPatterns: [{ pathname: "/**" }],
532
+ dangerouslyAllowSVG: false,
533
+ maximumRedirects: 3,
534
+ fetchTimeoutMs: 7000,
535
+ maxRemoteBytes: 25 * 1024 * 1024,
536
+ maxConcurrency: 0
537
+ };
538
+ var normalizeIndexPath = (indexPath) => {
539
+ const normalized = indexPath?.trim();
540
+ if (!normalized)
541
+ return;
542
+ const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
543
+ return path === "/" ? "/" : path;
544
+ };
545
+ var normalizeStringList = (values) => {
546
+ const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
547
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
548
+ };
549
+ var sanitizeAppIdSegment = (value) => {
550
+ const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
551
+ if (!cleaned)
552
+ return "app";
553
+ return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
554
+ };
555
+ var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
556
+ var normalizeDeepLinkDomain = (domain) => {
557
+ const normalized = domain.trim();
558
+ if (!normalized)
559
+ return "";
560
+ try {
561
+ const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
562
+ return url.host.toLowerCase();
563
+ } catch {
564
+ return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
406
565
  }
407
- }
408
-
409
- // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
410
- var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
411
- var IMPLICIT_LAYOUT_DIR = path4.join(".akan", "generated", "root-layouts");
412
- var IMPLICIT_DICT_DIR = path4.join(".akan", "generated", "dict");
413
- var IMPLICIT_OVERRIDES_DIR = path4.join(".akan", "generated", "overrides");
414
- var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
415
- var importSpecifier = (fromAbsPath, toAbsPath) => {
416
- const rel = path4.relative(path4.dirname(fromAbsPath), toAbsPath).split(path4.sep).join("/");
417
- return rel.startsWith(".") ? rel : `./${rel}`;
418
566
  };
419
- async function writeGeneratedOverridesLayoutFile(opts) {
420
- const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
421
- const absPath = path4.join(path4.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
422
- const source = `"use client";
423
- import { UiOverrideProvider } from "akanjs/ui";
424
- import { createElement, type ReactNode } from "react";
425
- import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
567
+ var normalizeDeepLinks = (deepLinks) => {
568
+ const schemes = normalizeStringList(deepLinks?.schemes);
569
+ const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
570
+ if (!schemes && !domains)
571
+ return;
572
+ return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
573
+ };
574
+ var emptyLibContributions = () => ({
575
+ externalLibs: [],
576
+ trustedDependencies: [],
577
+ bin: [],
578
+ docker: { preRuns: [], postRuns: [] },
579
+ keepFonts: []
580
+ });
581
+ var normalizePackageNames = (names, owner) => {
582
+ if (names === undefined)
583
+ return [];
584
+ if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
585
+ throw new Error(`${owner}: trustedDependencies lists package names`);
586
+ return [...new Set(names.map((name) => name.trim()))];
587
+ };
588
+ var normalizeKeepFonts = (keepFonts) => [
589
+ ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
590
+ ];
591
+ var dedupeDockerRuns = (runs) => {
592
+ const byKey = new Map;
593
+ for (const run of runs)
594
+ byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
595
+ return [...byKey.values()];
596
+ };
426
597
 
427
- export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
428
- return createElement(UiOverrideProvider, { value }, children);
429
- }
430
- `;
431
- await Bun.write(absPath, source);
432
- return absPath;
433
- }
434
- async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
435
- const keep = new Set(keepAbsPaths);
436
- const names = await readdir(dirAbsPath).catch(() => []);
437
- await Promise.all(names.map((name) => path4.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm2(absPath, { force: true })));
438
- }
439
- function getRootBoundarySegments(key) {
440
- const match = LAYOUT_KEY_RE.exec(key);
441
- if (!match)
442
- return null;
443
- const prefix = match[1]?.replace(/\/$/, "");
444
- if (!prefix)
445
- return [];
446
- return prefix.split("/").filter(Boolean);
447
- }
448
- function implicitRootLayoutKey(segments) {
449
- return `./${[...segments, "__root_layout"].join("/")}.tsx`;
450
- }
451
- function implicitRootLayoutAbsPath(appCwdPath, segments) {
452
- const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
453
- return path4.join(path4.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
454
- }
455
- function isRootBoundarySegments(segments, basePaths) {
456
- const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
457
- if (firstVisibleIndex === -1)
458
- return segments.length <= 1;
459
- if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
460
- return false;
461
- const visible = segments.slice(firstVisibleIndex);
462
- const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
463
- return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
464
- }
465
- function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
466
- const boundaries = new Map;
467
- for (const key of pageKeys) {
468
- const segments = getRootBoundarySegments(key);
469
- if (!segments)
470
- continue;
471
- if (!isRootBoundarySegments(segments, basePaths))
472
- continue;
473
- const id = segments.join("/");
474
- boundaries.set(id, {
475
- sourceKey: key,
476
- sourceAbsPath: path4.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
477
- segments
478
- });
479
- }
480
- if (boundaries.size === 0)
481
- boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
482
- return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
483
- }
484
- function hasAncestorRootBoundary(boundary, boundaries) {
485
- return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
486
- }
487
- function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
488
- const rootLayoutKey = pageKeys.find((key) => {
489
- const segments = getRootBoundarySegments(key);
490
- return segments !== null && segments.length === 0;
491
- });
492
- return rootLayoutKey ? path4.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
493
- }
494
- async function assertEnvClientConvention(appCwdPath, appName) {
495
- const envPath = path4.join(appCwdPath, "env", "env.client.ts");
496
- if (!await Bun.file(envPath).exists()) {
497
- throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
498
- }
499
- }
500
- async function writeGeneratedDictionaryMacroFile(appCwdPath, appName) {
501
- const absPath = path4.join(path4.resolve(appCwdPath), IMPLICIT_DICT_DIR, "useDict.ts");
502
- await Bun.write(absPath, `import { getAllDictionary } from "@apps/${appName}/lib/dict" with { type: "macro" };
503
-
504
- export const allDictionary = getAllDictionary();
505
- `);
506
- return absPath;
507
- }
508
- async function writeGeneratedRootLayoutFile(opts) {
509
- await assertEnvClientConvention(opts.appCwdPath, opts.appName);
510
- const dictMacroAbsPath = opts.includeSystemProvider ? await writeGeneratedDictionaryMacroFile(opts.appCwdPath, opts.appName) : null;
511
- const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
512
- const dictMacroSpecifier = dictMacroAbsPath ? importSpecifier(absPath, dictMacroAbsPath) : null;
513
- const sourceSpecifier = opts.boundary.sourceAbsPath ? importSpecifier(absPath, opts.boundary.sourceAbsPath) : null;
514
- const inheritedSourceAbsPath = opts.rootSourceAbsPath && opts.rootSourceAbsPath !== opts.boundary.sourceAbsPath ? opts.rootSourceAbsPath : null;
515
- const inheritedSourceSpecifier = inheritedSourceAbsPath ? importSpecifier(absPath, inheritedSourceAbsPath) : null;
516
- const clientImport = opts.includeStInit ? `import { st } from "@apps/${opts.appName}/client";
517
- void st;
518
- ` : `import "@apps/${opts.appName}/client";
519
- `;
520
- const inheritedLabel = inheritedSourceAbsPath ? path4.relative(opts.appCwdPath, inheritedSourceAbsPath) : "";
521
- const inheritedImport = inheritedSourceSpecifier ? `import * as inheritedModule from ${JSON.stringify(inheritedSourceSpecifier)};
522
- const inheritedLayout = resolveRouteModule(inheritedModule as never, ${JSON.stringify(inheritedLabel)}).module as LayoutModule;
523
- ` : `const inheritedLayout: LayoutModule = {};
524
- `;
525
- const prefix = opts.boundary.segments.find((segment) => !/^\(.+\)$/.test(segment)) ?? null;
526
- const userLabel = opts.boundary.sourceAbsPath ? path4.relative(opts.appCwdPath, opts.boundary.sourceAbsPath) : "";
527
- const userImport = sourceSpecifier ? `import * as userModule from ${JSON.stringify(sourceSpecifier)};
528
- const userLayout = resolveRouteModule(userModule as never, ${JSON.stringify(userLabel)}).module as LayoutModule;
529
- const UserLayout = userLayout.default as (props: LayoutProps) => ReactNode | Promise<ReactNode>;
530
- ` : `const UserLayout = ({ children }: LayoutProps) => children;
531
- const userLayout: LayoutModule = {};
532
- `;
533
- const isAsyncUserLayout = opts.boundary.sourceAbsPath ? await AsyncDefaultExportDetector.detect(opts.boundary.sourceAbsPath) : false;
534
- const userLayoutElement = "<UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>";
535
- const layoutSignature = isAsyncUserLayout ? "export default async function GeneratedLayout" : "export default function GeneratedLayout";
536
- const layoutBinding = isAsyncUserLayout ? ` const layout =
537
- process.env.AKAN_PUBLIC_RENDER_ENV === "csr"
538
- ? await UserLayout({ params, searchParams, children })
539
- : ${userLayoutElement};
540
- ` : "";
541
- const layoutChild = isAsyncUserLayout ? "{layout}" : userLayoutElement;
542
- const layoutReturn = isAsyncUserLayout ? "layout" : userLayoutElement;
543
- const headLayouts = opts.includeSystemProvider ? "[inheritedLayout, userLayout]" : "[userLayout]";
544
- const layoutExports = `export async function generateHead(props: PageProps) {
545
- const heads = await Promise.all(
546
- ${headLayouts}.map((layout) => (layout.generateHead ? layout.generateHead(props) : layout.head)),
547
- );
548
- return RouteHead.mergeNodes(heads);
549
- }
550
-
551
- export const NotFound = userLayout.NotFound ?? inheritedLayout.NotFound;
552
- export const Error = userLayout.Error ?? inheritedLayout.Error;
553
- export const pageConfig = userLayout.pageConfig ?? inheritedLayout.pageConfig;
554
- export const Loading = userLayout.Loading;
555
-
556
- ${layoutSignature}({ children, params, searchParams }: LayoutProps) {
557
- ${layoutBinding} return `;
558
- const source = opts.includeSystemProvider ? `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
559
- import { loadFonts, resolveRouteModule, RouteHead } from "akanjs/client";
560
- import type { ReactNode } from "react";
561
- import { System } from "akanjs/ui";
562
- import { env } from "@apps/${opts.appName}/env/env.client";
563
- import { allDictionary } from ${JSON.stringify(dictMacroSpecifier)};
564
- ${clientImport}${inheritedImport}${userImport}
565
- const userFonts = userLayout.fonts ?? inheritedLayout.fonts ?? [];
566
- const defaultFonts = userFonts.filter((font) => font.default);
567
- if (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");
568
- const defaultFont = defaultFonts[0];
569
- const defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;
570
- export const theme = userLayout.theme ?? inheritedLayout.theme;
571
- export const viewport = userLayout.viewport ?? inheritedLayout.viewport;
572
- export const manifest = userLayout.manifest ?? inheritedLayout.manifest;
573
-
574
- ${layoutExports}(
575
- <System.Provider
576
- of={GeneratedLayout as never}
577
- appName=${JSON.stringify(opts.appName)}
578
- ${prefix ? `prefix=${JSON.stringify(prefix)}
579
- ` : ""}params={params}
580
- manifest={manifest}
581
- env={env}
582
- theme={theme}
583
- viewport={viewport}
584
- fonts={loadFonts(userFonts)}
585
- className={defaultFontClassName}
586
- layoutStyle={userLayout.layoutStyle ?? inheritedLayout.layoutStyle}
587
- reconnect={userLayout.reconnect ?? inheritedLayout.reconnect ?? false}
588
- wsConnect={userLayout.wsConnect ?? inheritedLayout.wsConnect ?? true}
589
- allDictionary={process.env.AKAN_PUBLIC_RENDER_ENV === "ssr" ? allDictionary : undefined}
590
- >
591
- ${layoutChild}
592
- </System.Provider>
593
- );
594
- }
595
- ` : `import type { LayoutModule, LayoutProps, PageProps } from "akanjs/client";
596
- import { resolveRouteModule, RouteHead } from "akanjs/client";
597
- import type { ReactNode } from "react";
598
- ${inheritedImport}${userImport}
599
- ${layoutExports}${layoutReturn};
600
- }
601
- `;
602
- await Bun.write(absPath, source);
603
- return absPath;
604
- }
605
- async function resolveSsrPageEntries(opts) {
606
- const absPageDir = path4.resolve(opts.appCwdPath, "page");
607
- const hasSt = await Bun.file(path4.join(opts.appCwdPath, "lib", "st.ts")).exists();
608
- const basePaths = opts.basePaths ?? [];
609
- const rootSourceAbsPath = findExplicitRootLayoutAbsPath(opts.pageKeys, opts.appCwdPath);
610
- const rootBoundaries = findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths);
611
- const rootLayoutKeys = new Set(opts.pageKeys.filter((key) => {
612
- const segments = getRootBoundarySegments(key);
613
- return segments !== null && isRootBoundarySegments(segments, basePaths);
614
- }));
615
- const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
616
- const userAbsPath = path4.resolve(absPageDir, key);
617
- if (OVERRIDES_KEY_RE.test(key)) {
618
- const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
619
- appCwdPath: opts.appCwdPath,
620
- key,
621
- userAbsPath
622
- });
623
- return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
624
- }
625
- return { key, moduleAbsPath: userAbsPath };
626
- }));
627
- const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
628
- key: implicitRootLayoutKey(boundary.segments),
629
- moduleAbsPath: await writeGeneratedRootLayoutFile({
630
- appCwdPath: opts.appCwdPath,
631
- appName: opts.appName,
632
- boundary,
633
- rootSourceAbsPath,
634
- includeStInit: hasSt && boundary.segments.length === 0,
635
- includeSystemProvider: !hasAncestorRootBoundary(boundary, rootBoundaries)
636
- }),
637
- seedAbsPaths: [...new Set([boundary.sourceAbsPath, rootSourceAbsPath].filter((absPath) => absPath !== null))]
638
- })));
639
- const appCwdAbsPath = path4.resolve(opts.appCwdPath);
640
- await Promise.all([
641
- pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_OVERRIDES_DIR), base.filter(({ key }) => OVERRIDES_KEY_RE.test(key)).map(({ moduleAbsPath }) => moduleAbsPath)),
642
- pruneGeneratedFiles(path4.join(appCwdAbsPath, IMPLICIT_LAYOUT_DIR), generated.map(({ moduleAbsPath }) => moduleAbsPath))
643
- ]);
644
- return [...base, ...generated].sort((a, b) => a.key.localeCompare(b.key));
645
- }
646
- async function resolveSsrPageEntriesForApp(app, pageKeys) {
647
- const config = await app.getConfig();
648
- return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
649
- }
650
-
651
- // pkgs/@akanjs/devkit/codegenLock.ts
652
- import { mkdir as mkdir2, open, readFile, rm as rm3, stat } from "fs/promises";
653
- import path5 from "path";
654
- import { Logger as Logger2 } from "akanjs/common";
655
-
656
- class CodegenLock {
657
- static fileName = "codegen.lock";
658
- static waitTimeoutMs = 1e4;
659
- static unknownHolderStaleMs = 60000;
660
- static #pollMs = 25;
661
- static #logger = new Logger2("CodegenLock");
662
- static #queue = Promise.resolve();
663
- static pathIn(workspaceRoot) {
664
- return path5.join(workspaceRoot, "local", ".akan", CodegenLock.fileName);
665
- }
666
- static async run(workspaceRoot, label, fn) {
667
- const ahead = CodegenLock.#queue;
668
- let done;
669
- CodegenLock.#queue = new Promise((resolve) => {
670
- done = resolve;
671
- });
672
- try {
673
- await CodegenLock.#waitForQueue(ahead, label);
674
- return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
675
- } finally {
676
- done();
677
- }
678
- }
679
- static async#waitForQueue(ahead, label) {
680
- let timer = null;
681
- const expired = new Promise((resolve) => {
682
- timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
683
- });
684
- try {
685
- if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
686
- CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
687
- } finally {
688
- if (timer)
689
- clearTimeout(timer);
690
- }
691
- }
692
- static async#withFileLock(workspaceRoot, label, fn) {
693
- const lockPath = CodegenLock.pathIn(workspaceRoot);
694
- await mkdir2(path5.dirname(lockPath), { recursive: true }).catch(() => {
695
- return;
696
- });
697
- const held = await CodegenLock.#acquire(lockPath, label);
698
- try {
699
- return await fn();
700
- } finally {
701
- if (held)
702
- await rm3(lockPath, { force: true }).catch(() => {
703
- return;
704
- });
705
- }
706
- }
707
- static async#acquire(lockPath, label) {
708
- const deadline = Date.now() + CodegenLock.waitTimeoutMs;
709
- for (;; ) {
710
- const handle = await open(lockPath, "wx").catch(() => null);
711
- if (handle) {
712
- await handle.writeFile(JSON.stringify({ pid: process.pid, at: Date.now(), label })).catch(() => {
713
- return;
714
- });
715
- await handle.close().catch(() => {
716
- return;
717
- });
718
- return true;
719
- }
720
- if (await CodegenLock.#reclaimIfAbandoned(lockPath))
721
- continue;
722
- if (Date.now() >= deadline) {
723
- CodegenLock.#logger.warn(`codegen lock at ${lockPath} held past ${CodegenLock.waitTimeoutMs}ms; continuing without it (${label})`);
724
- return false;
725
- }
726
- await Bun.sleep(CodegenLock.#pollMs);
727
- }
728
- }
729
- static async#reclaimIfAbandoned(lockPath) {
730
- const info = await stat(lockPath).catch(() => null);
731
- if (!info)
732
- return true;
733
- const holder = CodegenLock.#parseHolder(await readFile(lockPath, "utf8").catch(() => ""));
734
- if (holder) {
735
- if (CodegenLock.#isAlive(holder.pid))
736
- return false;
737
- } else if (Date.now() - info.mtimeMs < CodegenLock.unknownHolderStaleMs)
738
- return false;
739
- await rm3(lockPath, { force: true }).catch(() => {
740
- return;
741
- });
742
- return true;
743
- }
744
- static #parseHolder(raw) {
745
- try {
746
- const parsed = JSON.parse(raw);
747
- if (typeof parsed.pid !== "number" || typeof parsed.at !== "number")
748
- return null;
749
- return { pid: parsed.pid, at: parsed.at, label: typeof parsed.label === "string" ? parsed.label : "" };
750
- } catch {
751
- return null;
752
- }
753
- }
754
- static #isAlive(pid) {
755
- if (!Number.isInteger(pid) || pid <= 0)
756
- return false;
757
- try {
758
- process.kill(pid, 0);
759
- return true;
760
- } catch (error) {
761
- return error.code === "EPERM";
762
- }
763
- }
764
- }
765
-
766
- // pkgs/@akanjs/devkit/spinner.ts
767
- import ora from "ora";
768
-
769
- class Spinner {
770
- static padding = 12;
771
- static oraOptions = { discardStdin: false };
772
- static canAnimate(stream = process.stderr) {
773
- return !stream.isTTY || stream.columns > 0;
774
- }
775
- spinner;
776
- stopWatch = null;
777
- startAt = new Date;
778
- prefix;
779
- message;
780
- enableSpin;
781
- constructor(message, { prefix = "", indent = 0, enableSpin = true } = {}) {
782
- Spinner.padding = Math.max(Spinner.padding, prefix.length);
783
- this.prefix = prefix;
784
- this.message = message;
785
- this.spinner = ora({ ...Spinner.oraOptions, text: message });
786
- this.spinner.prefixText = prefix.padStart(Spinner.padding, " ");
787
- this.spinner.indent = indent;
788
- this.enableSpin = enableSpin && Spinner.canAnimate();
789
- }
790
- start() {
791
- this.startAt = new Date;
792
- if (this.enableSpin) {
793
- this.spinner.start();
794
- this.stopWatch = setInterval(() => {
795
- this.spinner.prefixText = this.prefix.padStart(Spinner.padding, " ");
796
- this.spinner.text = `${this.message} (${this.#getElapsedTimeStr({ floor: true })})`;
797
- }, 1000);
798
- } else
799
- this.spinner.info();
800
- return this;
801
- }
802
- succeed(message) {
803
- this.spinner.succeed(`${message} (${this.#getElapsedTimeStr()})`);
804
- this.#reset();
805
- }
806
- fail(message) {
807
- this.spinner.fail(`${message} (${this.#getElapsedTimeStr()})`);
808
- this.#reset();
809
- }
810
- isSpinning() {
811
- return this.spinner.isSpinning;
812
- }
813
- #reset() {
814
- if (this.stopWatch)
815
- clearInterval(this.stopWatch);
816
- this.stopWatch = null;
817
- }
818
- #getElapsedTimeStr({ floor = false } = {}) {
819
- const ms = Date.now() - this.startAt.getTime();
820
- if (ms < 1000)
821
- return `${ms}ms`;
822
- const s = Math.floor(ms / 1000);
823
- if (s < 60)
824
- return `${floor ? Math.floor(ms / 1000) : Math.floor(ms / 100) / 10}s`;
825
- const m = Math.floor(s / 60);
826
- return `${m}m ${s % 60}s`;
827
- }
828
- }
829
-
830
- // pkgs/@akanjs/devkit/executors.ts
831
- import {
832
- exec,
833
- fork,
834
- spawn as spawn2
835
- } from "child_process";
836
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
837
- import {
838
- copyFile,
839
- cp as cpEntry,
840
- lstat,
841
- mkdir as mkdir3,
842
- readdir as readDirEntries,
843
- realpath,
844
- rm as rm4,
845
- stat as stat2,
846
- symlink
847
- } from "fs/promises";
848
- import path13 from "path";
849
- import { pathToFileURL } from "url";
850
- import {
851
- capitalize,
852
- getPageSourceFileViolation,
853
- isRouteSourceFile,
854
- Logger as Logger3,
855
- parseRouteModuleKey,
856
- toError as toError2,
857
- validatePageSourceFile,
858
- validateSubRoutePageKey
859
- } from "akanjs/common";
860
- var {$ } = globalThis.Bun;
861
- import chalk2 from "chalk";
862
-
863
- // pkgs/@akanjs/devkit/agentsIndex.ts
864
- var recipeScannerLoad = null;
865
- var loadRecipeScanner = () => recipeScannerLoad ??= import("./recipeScanner-4b69ybe0.js");
866
- var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
867
- var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
868
- var AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
869
- var stampBlockVersion = (block, version) => `${AGENT_VERSION_PREFIX}${version} -->
870
-
871
- ${block}`;
872
- var extractBlockVersion = (content) => content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
873
- var readDevkitVersion = async () => {
874
- const { readFile } = await import("fs/promises");
875
- const { getDirname } = await import("./fileSys-wzzrntkw.js");
876
- try {
877
- const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
878
- return JSON.parse(raw).version ?? null;
879
- } catch {
880
- return null;
881
- }
882
- };
883
- var upsertAgentBlock = (existing, block) => {
884
- const managed = `${AGENT_BLOCK_START}
885
- ${block}
886
- ${AGENT_BLOCK_END}`;
887
- const startIndex = existing.indexOf(AGENT_BLOCK_START);
888
- const endIndex = existing.indexOf(AGENT_BLOCK_END);
889
- if (startIndex >= 0 && endIndex > startIndex) {
890
- return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
891
- }
892
- return `${existing.replace(/\s*$/, "")}
893
-
894
- ${managed}
895
- `;
896
- };
897
- var extractAgentBlock = (content) => {
898
- const startIndex = content.indexOf(AGENT_BLOCK_START);
899
- const endIndex = content.indexOf(AGENT_BLOCK_END);
900
- if (startIndex < 0 || endIndex <= startIndex)
901
- return null;
902
- return content.slice(startIndex + AGENT_BLOCK_START.length, endIndex).trim();
903
- };
904
- var signatureOf = (recipe) => {
905
- const entries = Object.entries(recipe.variants);
906
- if (entries.length === 0)
907
- return "";
908
- const parts = entries.map(([key, values]) => {
909
- if (values.length === 1 && values[0] === "true")
910
- return `${key}?`;
911
- const def = recipe.defaultVariants?.[key];
912
- return `${key}: ${values.map((value) => value === def ? `${value}*` : value).join("|")}`;
913
- });
914
- return `(${parts.join(" \xB7 ")})`;
915
- };
916
- var renderRecipeEntries = (recipes) => {
917
- const groups = new Map;
918
- for (const recipe of recipes)
919
- groups.set(recipe.importFrom, [...groups.get(recipe.importFrom) ?? [], recipe]);
920
- return [...groups.entries()].map(([importFrom, list]) => {
921
- const items = list.sort((a, b) => a.name.localeCompare(b.name)).map((recipe) => `- \`${recipe.name}\`${signatureOf(recipe)}${recipe.doc ? ` \u2014 ${recipe.doc}` : ""}`).join(`
922
- `);
923
- return `Import from \`${importFrom}\`:
924
- ${items}`;
925
- }).join(`
926
-
927
- `);
928
- };
929
- var collectScopeRecipeSources = async (workspaceRoot, scope, libDeps) => {
930
- const { collectRecipeSources } = await loadRecipeScanner();
931
- const sources = [
932
- ...await collectRecipeSources(`${workspaceRoot}/${scope.type}s/${scope.name}/ui`, `@${scope.type}s/${scope.name}/ui`)
933
- ];
934
- for (const lib of [...new Set(libDeps)].sort()) {
935
- if (scope.type === "lib" && lib === scope.name)
936
- continue;
937
- sources.push(...await collectRecipeSources(`${workspaceRoot}/libs/${lib}/ui`, `@libs/${lib}/ui`));
938
- }
939
- return sources;
940
- };
941
- var scanScopeRecipes = async (workspaceRoot, scope, libDeps) => {
942
- const { scanRecipes } = await loadRecipeScanner();
943
- return scanRecipes(await collectScopeRecipeSources(workspaceRoot, scope, libDeps));
944
- };
945
- var renderScopeAgentBlock = (scope, recipes) => {
946
- const scopePath = `${scope.type}s/${scope.name}`;
947
- const intro = `## Recipes In Scope
948
-
949
- UI recipes importable from \`${scopePath}\` code, **in addition to** the framework recipes indexed in the root
950
- \`AGENTS.md\` \`## Recipes\`. Same contract: import by exact name, then \`<name>(variants?, className?)\` \u2014 the second
951
- arg merges internally and takes an array too, so never wrap it in \`cn()\`. \`*\` marks the default, \`key?\` is a
952
- boolean flag. Do not guess recipe names or import paths; this index is regenerated by \`akan sync ${scope.name}\`
953
- and verified by \`akan lint ${scope.name}\`.`;
954
- if (recipes.length === 0) {
955
- return `${intro}
956
-
957
- No scope recipes yet. Before inlining a repeated surface (card, box, tile, \u2026), reuse a framework recipe from the
958
- root \`AGENTS.md\` or author one as \`${scopePath}/ui/Recipe/<name>.ts\` (one recipe per file, re-exported from that
959
- folder's \`index.ts\`) \u2014 see the \`recipeRule\` guideline.`;
960
- }
961
- return `${intro}
962
-
963
- ${renderRecipeEntries(recipes)}`;
964
- };
965
- var renderScopeAgentsMd = (scope, block) => `# ${scope.name} \u2014 Agent Guide
966
-
967
- Scoped guide for coding agents working in \`${scope.type}s/${scope.name}\`. Workspace-wide conventions live in the
968
- root \`AGENTS.md\`; this file carries what is importable from this ${scope.type === "app" ? "app" : "library"}. The
969
- section between the \`akan:agent\` markers is regenerated by \`akan sync ${scope.name}\`; edit anything outside the
970
- markers freely.
971
-
972
- ${AGENT_BLOCK_START}
973
- ${block}
974
- ${AGENT_BLOCK_END}
975
- `;
976
- var CLAUDE_COMMENT_RULE = `## Comments \u2014 Overrides Your Default
977
-
978
- Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
979
- is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
980
-
981
- Before typing \`//\`, \`/*\`, or a doc block, ask \u2014 **does this sentence carry a fact that is nowhere in the code?**
982
-
983
- - Restates the identifier, the signature, or the line under it \u2192 delete it.
984
- - Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) \u2192 delete it.
985
- - JSDoc on an ordinary function, or a why/how preamble on ordinary logic \u2192 delete it.
986
- - Explains the edit you just made, for whoever reads the diff \u2192 say it in your reply, not in the file.
987
- - Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
988
- derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected \u2192
989
- keep it, one line.
990
-
991
- That keep-list is exact \u2014 \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
992
- subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
993
- surrounding code carries none, your diff carries none.`;
994
- var renderScopeClaudeMd = (scope) => `# ${scope.name} \u2014 Claude Code Guide
995
-
996
- @AGENTS.md
997
-
998
- ${CLAUDE_COMMENT_RULE}
999
- `;
1000
-
1001
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
1002
- import path6 from "path";
1003
-
1004
- // pkgs/@akanjs/devkit/akanConfig/types.ts
1005
- import { archs, binPlatforms, defaultAppPipeline } from "akanjs";
1006
-
1007
- // pkgs/@akanjs/devkit/akanConfig/akanBin.ts
1008
- class AkanBin {
1009
- static archive = /\.(zip|tar\.gz|tgz|tar\.xz|txz|tar\.bz2|tbz2|tar)$/i;
1010
- static fileName = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1011
- static parse(declared, owner, baseDir) {
1012
- if (declared === undefined)
1013
- return {};
1014
- if (!declared || typeof declared !== "object" || Array.isArray(declared))
1015
- throw new Error(`${owner}: bin maps each executable's name to its platforms`);
1016
- return Object.fromEntries(Object.entries(declared).map(([name, platforms]) => [name, AkanBin.#platforms(name, platforms, owner, baseDir)]));
1017
- }
1018
- static isArchive(source) {
1019
- return AkanBin.archive.test("url" in source ? new URL(source.url).pathname : source.path);
1020
- }
1021
- static #platforms(name, platforms, owner, baseDir) {
1022
- if (!AkanBin.fileName.test(name))
1023
- throw new Error(`${owner}: bin.${name} is not a file name`);
1024
- if (!platforms || typeof platforms !== "object" || Array.isArray(platforms))
1025
- throw new Error(`${owner}: bin.${name} maps platforms (${binPlatforms.join(", ")}) to where the file comes from`);
1026
- const resolved = {};
1027
- for (const [platform, source] of Object.entries(platforms)) {
1028
- if (!binPlatforms.includes(platform))
1029
- throw new Error(`${owner}: bin.${name}.${platform} is not one of ${binPlatforms.join(", ")}`);
1030
- resolved[platform] = AkanBin.#source(`${owner}: bin.${name}.${platform}`, source, baseDir);
1031
- }
1032
- return resolved;
1033
- }
1034
- static #source(at, source, baseDir) {
1035
- if (!source || typeof source !== "object" || Array.isArray(source))
1036
- throw new Error(`${at} must be { url, sha256 } or { path }`);
1037
- const { url, sha256, path: localPath, file, ...rest } = source;
1038
- const extra = Object.keys(rest);
1039
- if (extra.length)
1040
- throw new Error(`${at}: unknown key ${extra.join(", ")}`);
1041
- if (url === undefined === (localPath === undefined))
1042
- throw new Error(`${at} takes either url and sha256, or path`);
1043
- const resolved = url !== undefined ? AkanBin.#urlSource(at, url, sha256) : AkanBin.#pathSource(at, localPath, sha256, baseDir);
1044
- const archive = AkanBin.isArchive(resolved);
1045
- if (file === undefined) {
1046
- if (archive)
1047
- throw new Error(`${at} names an archive: file picks the executable inside it`);
1048
- return resolved;
1049
- }
1050
- if (!archive)
1051
- throw new Error(`${at}: file picks the executable out of an archive, and this source is none`);
1052
- const parts = typeof file === "string" ? file.split(/[\\/]/).filter((part) => part && part !== ".") : [];
1053
- if (!parts.length || path6.isAbsolute(file) || parts.includes(".."))
1054
- throw new Error(`${at}: file must be a path inside the archive (got ${JSON.stringify(file)})`);
1055
- return { ...resolved, file: parts.join("/") };
1056
- }
1057
- static #urlSource(at, url, sha256) {
1058
- if (typeof url !== "string" || !URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol))
1059
- throw new Error(`${at}: url must be an http(s) address`);
1060
- if (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(sha256))
1061
- throw new Error(`${at}: sha256 must be the downloaded file's 64-digit hex digest`);
1062
- return { url, sha256: sha256.toLowerCase() };
1063
- }
1064
- static #pathSource(at, localPath, sha256, baseDir) {
1065
- if (typeof localPath !== "string" || !localPath)
1066
- throw new Error(`${at}: path must name a file`);
1067
- if (sha256 !== undefined)
1068
- throw new Error(`${at}: sha256 checks a download, and a path source downloads nothing`);
1069
- return { path: path6.resolve(baseDir, localPath) };
1070
- }
1071
- }
1072
- // pkgs/@akanjs/devkit/akanConfig/akanConfig.ts
1073
- import fs2 from "fs";
1074
- import path7 from "path";
1075
- import { fileURLToPath } from "url";
1076
- import { DatabaseModes, normalizeRoutePrefix } from "akanjs/base";
1077
- import { resolveAkanI18nConfig } from "akanjs/common";
1078
- var DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui", "akanjs/server"];
1079
- var DEFAULT_OPTIMIZE_IMPORTS = [
1080
- "lucide-react date-fns lodash-es ramda antd react-bootstrap ahooks @ant-design/icons @headlessui/react",
1081
- "@headlessui-float/react @heroicons/react/20/solid @heroicons/react/24/solid @heroicons/react/24/outline",
1082
- "@visx/visx @tremor/react rxjs @mui/material @mui/icons-material recharts react-use @material-ui/core",
1083
- "@material-ui/icons @tabler/icons-react mui-core react-icons/*"
1084
- ].flatMap((line) => line.split(" "));
1085
- var WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"];
1086
- var DEFAULT_DOCKER_IMAGE = "oven/bun:1-slim";
1087
- var SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"];
1088
- var DEFAULT_AKAN_IMAGE_CONFIG = {
1089
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
1090
- imageSizes: [32, 48, 64, 96, 128, 256, 384],
1091
- formats: ["image/webp"],
1092
- qualities: [75],
1093
- minimumCacheTTL: 14400,
1094
- remotePatterns: [],
1095
- localPatterns: [{ pathname: "/**" }],
1096
- dangerouslyAllowSVG: false,
1097
- maximumRedirects: 3,
1098
- fetchTimeoutMs: 7000,
1099
- maxRemoteBytes: 25 * 1024 * 1024,
1100
- maxConcurrency: 0
1101
- };
1102
- var normalizeIndexPath = (indexPath) => {
1103
- const normalized = indexPath?.trim();
1104
- if (!normalized)
1105
- return;
1106
- const path = `/${normalized.replace(/^\/+|\/+$/g, "")}`;
1107
- return path === "/" ? "/" : path;
1108
- };
1109
- var normalizeStringList = (values) => {
1110
- const normalized = values?.map((value) => value.trim()).filter(Boolean) ?? [];
1111
- return normalized.length > 0 ? [...new Set(normalized)] : undefined;
1112
- };
1113
- var sanitizeAppIdSegment = (value) => {
1114
- const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
1115
- if (!cleaned)
1116
- return "app";
1117
- return /^[a-z]/.test(cleaned) ? cleaned : `app${cleaned}`;
1118
- };
1119
- var deriveDefaultAppId = (orgName, appName) => `com.${sanitizeAppIdSegment(orgName)}.${sanitizeAppIdSegment(appName)}`;
1120
- var normalizeDeepLinkDomain = (domain) => {
1121
- const normalized = domain.trim();
1122
- if (!normalized)
1123
- return "";
1124
- try {
1125
- const url = new URL(normalized.includes("://") ? normalized : `https://${normalized}`);
1126
- return url.host.toLowerCase();
1127
- } catch {
1128
- return normalized.replace(/^https?:\/\//, "").replace(/\/+$/g, "").toLowerCase();
1129
- }
1130
- };
1131
- var normalizeDeepLinks = (deepLinks) => {
1132
- const schemes = normalizeStringList(deepLinks?.schemes);
1133
- const domains = normalizeStringList(deepLinks?.domains?.map(normalizeDeepLinkDomain));
1134
- if (!schemes && !domains)
1135
- return;
1136
- return { ...schemes ? { schemes } : {}, ...domains ? { domains } : {} };
1137
- };
1138
- var emptyLibContributions = () => ({
1139
- externalLibs: [],
1140
- trustedDependencies: [],
1141
- bin: [],
1142
- docker: { preRuns: [], postRuns: [] },
1143
- keepFonts: []
1144
- });
1145
- var normalizePackageNames = (names, owner) => {
1146
- if (names === undefined)
1147
- return [];
1148
- if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || !name.trim()))
1149
- throw new Error(`${owner}: trustedDependencies lists package names`);
1150
- return [...new Set(names.map((name) => name.trim()))];
1151
- };
1152
- var normalizeKeepFonts = (keepFonts) => [
1153
- ...new Set((keepFonts ?? []).map((glob) => glob.trim().replace(/^\/+/, "")).filter(Boolean))
1154
- ];
1155
- var dedupeDockerRuns = (runs) => {
1156
- const byKey = new Map;
1157
- for (const run of runs)
1158
- byKey.set(typeof run === "string" ? run : JSON.stringify(run), run);
1159
- return [...byKey.values()];
1160
- };
1161
-
1162
- class LibConfigRefusal extends Error {
598
+ class LibConfigRefusal extends Error {
1163
599
  }
1164
600
 
1165
601
  class AkanAppConfig {
@@ -1496,7 +932,7 @@ class AkanAppConfig {
1496
932
  }
1497
933
  #applyRoutes(routes = []) {
1498
934
  for (const route of routes) {
1499
- const basePath = route.basePath ? route.basePath.replace(/^\/+|\/+$/g, "") : null;
935
+ const basePath = route.basePath ? trimSlashes(route.basePath) : null;
1500
936
  if (basePath !== null)
1501
937
  this.basePaths.add(basePath);
1502
938
  const domains = basePath !== null ? this.subRoutes.getOrInsert(basePath, new Set) : this.domains;
@@ -1598,7 +1034,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1598
1034
  static async from(app, { bustImportCache = false } = {}) {
1599
1035
  const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
1600
1036
  AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
1601
- WorkspaceExecutor.getBaseDevEnv(path7.join(app.workspace.workspaceRoot, ".env")),
1037
+ WorkspaceExecutor.getBaseDevEnv(path3.join(app.workspace.workspaceRoot, ".env")),
1602
1038
  app.workspace.getLibs(),
1603
1039
  app.workspace.getPackageJson()
1604
1040
  ]);
@@ -1694,143 +1130,579 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
1694
1130
  };
1695
1131
  }
1696
1132
  }
1697
- var akanPackageJson = null;
1698
- function getAkanPackageJson() {
1699
- if (akanPackageJson)
1700
- return akanPackageJson;
1701
- const sourceDir = path7.dirname(fileURLToPath(import.meta.url));
1702
- const packageJsonPaths = [
1703
- path7.join(sourceDir, "../../../akanjs/package.json"),
1704
- path7.join(process.cwd(), "pkgs/akanjs/package.json"),
1705
- path7.join(path7.dirname(Bun.main), "node_modules/akanjs/package.json")
1706
- ];
1707
- try {
1708
- packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
1709
- } catch {}
1710
- for (const packageJsonPath of packageJsonPaths) {
1711
- try {
1712
- akanPackageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
1713
- return akanPackageJson;
1714
- } catch {}
1133
+ var akanPackageJson = null;
1134
+ function getAkanPackageJson() {
1135
+ if (akanPackageJson)
1136
+ return akanPackageJson;
1137
+ const sourceDir = path3.dirname(fileURLToPath(import.meta.url));
1138
+ const packageJsonPaths = [
1139
+ path3.join(sourceDir, "../../../akanjs/package.json"),
1140
+ path3.join(process.cwd(), "pkgs/akanjs/package.json"),
1141
+ path3.join(path3.dirname(Bun.main), "node_modules/akanjs/package.json")
1142
+ ];
1143
+ try {
1144
+ packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
1145
+ } catch {}
1146
+ for (const packageJsonPath of packageJsonPaths) {
1147
+ try {
1148
+ akanPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
1149
+ return akanPackageJson;
1150
+ } catch {}
1151
+ }
1152
+ akanPackageJson = {
1153
+ name: "akanjs",
1154
+ version: "0.0.0",
1155
+ description: "akanjs",
1156
+ dependencies: {}
1157
+ };
1158
+ return akanPackageJson;
1159
+ }
1160
+ function mergeImageConfig(config = {}) {
1161
+ return {
1162
+ ...DEFAULT_AKAN_IMAGE_CONFIG,
1163
+ ...config,
1164
+ deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
1165
+ imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
1166
+ formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
1167
+ qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
1168
+ remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
1169
+ localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
1170
+ };
1171
+ }
1172
+
1173
+ class AkanLibConfig {
1174
+ lib;
1175
+ externalLibs;
1176
+ trustedDependencies;
1177
+ bin;
1178
+ docker;
1179
+ assets;
1180
+ plugins;
1181
+ constructor(lib, config, plugins = []) {
1182
+ this.lib = lib;
1183
+ this.externalLibs = config?.externalLibs ?? [];
1184
+ const owner = `libs/${lib.name}/akan.config.ts`;
1185
+ this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
1186
+ this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
1187
+ this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
1188
+ this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
1189
+ this.plugins = plugins;
1190
+ }
1191
+ static async from(lib, { bustImportCache = false } = {}) {
1192
+ const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
1193
+ const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1194
+ const { plugins, ...config } = resolved ?? {};
1195
+ if ("pipeline" in config)
1196
+ throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`pipeline\`, which a lib does not take: pipelines belong to apps and the workspace root, and a lib's build-time work goes in an akan build plugin.`);
1197
+ if ("subspace" in config)
1198
+ throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1199
+ return new AkanLibConfig(lib, config, plugins ?? []);
1200
+ }
1201
+ }
1202
+ //! need to refactor
1203
+ var shiftBuildNum = async (app, delta) => {
1204
+ const { buildNum } = (await AkanAppConfig.from(app)).native;
1205
+ const akanConfigPath = path3.join(app.cwdPath, "akan.config.ts");
1206
+ const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
1207
+ fs.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
1208
+ };
1209
+ var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
1210
+ var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
1211
+ // pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
1212
+ import path4 from "path";
1213
+ class AkanWorkspaceConfig {
1214
+ static fileName = "akan.config.ts";
1215
+ static #keys = ["subspace", "pipeline", "cloud"];
1216
+ static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
1217
+ static async from(workspace, { bustImportCache = false } = {}) {
1218
+ const file = path4.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
1219
+ if (!await FileSys2.fileExists(file))
1220
+ return new AkanWorkspaceConfig(null, {});
1221
+ const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
1222
+ return new AkanWorkspaceConfig(file, input);
1223
+ }
1224
+ file;
1225
+ subspace;
1226
+ pipeline;
1227
+ cloudHost;
1228
+ constructor(file, input) {
1229
+ this.file = file;
1230
+ if (typeof input !== "object" || input === null || Array.isArray(input))
1231
+ throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
1232
+ for (const key of Object.keys(input))
1233
+ if (!AkanWorkspaceConfig.#keys.includes(key))
1234
+ throw new Error(`The workspace root akan.config.ts declares \`${key}\`, which it does not take: it holds \`subspace\`, \`pipeline\` and \`cloud\`, and app or lib settings belong in apps/<app>/akan.config.ts or libs/<lib>/akan.config.ts.`);
1235
+ const config = input;
1236
+ this.subspace = config.subspace ?? null;
1237
+ this.pipeline = config.pipeline ?? null;
1238
+ this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
1239
+ }
1240
+ static #cloudHost(cloud) {
1241
+ if (cloud === undefined)
1242
+ return null;
1243
+ const refuse = (why) => {
1244
+ throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1245
+ };
1246
+ if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
1247
+ return refuse("that is not an object");
1248
+ const unknown = Object.keys(cloud).filter((key) => key !== "host");
1249
+ if (unknown.length)
1250
+ return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
1251
+ const { host } = cloud;
1252
+ if (host === undefined)
1253
+ return null;
1254
+ const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
1255
+ if (!url || url.search || url.hash || url.username || url.password)
1256
+ return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
1257
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
1258
+ return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
1259
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
1260
+ }
1261
+ }
1262
+ // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
1263
+ import { readdir, rm } from "fs/promises";
1264
+ import path6 from "path";
1265
+
1266
+ // pkgs/@akanjs/devkit/transforms/asyncDefaultExportDetector.ts
1267
+ import fs2 from "fs";
1268
+ import path5 from "path";
1269
+
1270
+ class AsyncDefaultExportDetector {
1271
+ static #typescriptLoad;
1272
+ static #loadTypescript() {
1273
+ AsyncDefaultExportDetector.#typescriptLoad ??= import("typescript").then((mod) => mod.default ?? mod);
1274
+ return AsyncDefaultExportDetector.#typescriptLoad;
1275
+ }
1276
+ static async detect(moduleAbsPath) {
1277
+ try {
1278
+ const typescript = await AsyncDefaultExportDetector.#loadTypescript();
1279
+ const source = fs2.readFileSync(path5.resolve(moduleAbsPath), "utf8");
1280
+ const sourceFile = typescript.createSourceFile(moduleAbsPath, source, typescript.ScriptTarget.Latest, true, /\.[tj]sx$/.test(moduleAbsPath) ? typescript.ScriptKind.TSX : typescript.ScriptKind.TS);
1281
+ return new AsyncDefaultExportDetector(typescript).detectInSourceFile(sourceFile);
1282
+ } catch {
1283
+ return false;
1284
+ }
1285
+ }
1286
+ #ts;
1287
+ constructor(typescript) {
1288
+ this.#ts = typescript;
1289
+ }
1290
+ detectInSourceFile(sourceFile) {
1291
+ const ts = this.#ts;
1292
+ const asyncBindings = new Map;
1293
+ let defaultIdentifier = null;
1294
+ for (const statement of sourceFile.statements) {
1295
+ if (ts.isFunctionDeclaration(statement)) {
1296
+ if (this.#hasModifier(statement, ts.SyntaxKind.DefaultKeyword)) {
1297
+ return this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword);
1298
+ }
1299
+ if (statement.name) {
1300
+ asyncBindings.set(statement.name.text, this.#hasModifier(statement, ts.SyntaxKind.AsyncKeyword));
1301
+ }
1302
+ continue;
1303
+ }
1304
+ if (ts.isVariableStatement(statement)) {
1305
+ for (const declaration of statement.declarationList.declarations) {
1306
+ if (!ts.isIdentifier(declaration.name))
1307
+ continue;
1308
+ asyncBindings.set(declaration.name.text, this.#isAsyncFunctionExpression(declaration.initializer));
1309
+ }
1310
+ continue;
1311
+ }
1312
+ if (ts.isExportAssignment(statement)) {
1313
+ if (this.#isAsyncFunctionExpression(statement.expression))
1314
+ return true;
1315
+ if (ts.isCallExpression(statement.expression))
1316
+ return true;
1317
+ if (ts.isIdentifier(statement.expression))
1318
+ defaultIdentifier = statement.expression.text;
1319
+ continue;
1320
+ }
1321
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
1322
+ for (const specifier of statement.exportClause.elements) {
1323
+ if (specifier.name.text !== "default")
1324
+ continue;
1325
+ defaultIdentifier = specifier.propertyName?.text ?? specifier.name.text;
1326
+ }
1327
+ }
1328
+ }
1329
+ return defaultIdentifier ? asyncBindings.get(defaultIdentifier) === true : false;
1330
+ }
1331
+ #hasModifier(node, kind) {
1332
+ const ts = this.#ts;
1333
+ return ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some((modifier) => modifier.kind === kind) ?? false);
1334
+ }
1335
+ #isAsyncFunctionExpression(node) {
1336
+ const ts = this.#ts;
1337
+ return Boolean(node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && this.#hasModifier(node, ts.SyntaxKind.AsyncKeyword));
1338
+ }
1339
+ }
1340
+
1341
+ // pkgs/@akanjs/devkit/artifact/implicitRootLayout.ts
1342
+ var LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
1343
+ var IMPLICIT_LAYOUT_DIR = path6.join(".akan", "generated", "root-layouts");
1344
+ var IMPLICIT_DICT_DIR = path6.join(".akan", "generated", "dict");
1345
+ var IMPLICIT_OVERRIDES_DIR = path6.join(".akan", "generated", "overrides");
1346
+ var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
1347
+ var importSpecifier = (fromAbsPath, toAbsPath) => {
1348
+ const rel = path6.relative(path6.dirname(fromAbsPath), toAbsPath).split(path6.sep).join("/");
1349
+ return rel.startsWith(".") ? rel : `./${rel}`;
1350
+ };
1351
+ async function writeGeneratedOverridesLayoutFile(opts) {
1352
+ const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
1353
+ const absPath = path6.join(path6.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
1354
+ const source = `"use client";
1355
+ import { UiOverrideProvider } from "akanjs/ui";
1356
+ import { createElement, type ReactNode } from "react";
1357
+ import value from ${JSON.stringify(importSpecifier(absPath, opts.userAbsPath))};
1358
+
1359
+ export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
1360
+ return createElement(UiOverrideProvider, { value }, children);
1361
+ }
1362
+ `;
1363
+ await Bun.write(absPath, source);
1364
+ return absPath;
1365
+ }
1366
+ async function pruneGeneratedFiles(dirAbsPath, keepAbsPaths) {
1367
+ const keep = new Set(keepAbsPaths);
1368
+ const names = await readdir(dirAbsPath).catch(() => []);
1369
+ await Promise.all(names.map((name) => path6.join(dirAbsPath, name)).filter((absPath) => !keep.has(absPath)).map((absPath) => rm(absPath, { force: true })));
1370
+ }
1371
+ function getRootBoundarySegments(key) {
1372
+ const match = LAYOUT_KEY_RE.exec(key);
1373
+ if (!match)
1374
+ return null;
1375
+ const prefix = match[1]?.replace(/\/$/, "");
1376
+ if (!prefix)
1377
+ return [];
1378
+ return prefix.split("/").filter(Boolean);
1379
+ }
1380
+ function implicitRootLayoutKey(segments) {
1381
+ return `./${[...segments, "__root_layout"].join("/")}.tsx`;
1382
+ }
1383
+ function implicitRootLayoutAbsPath(appCwdPath, segments) {
1384
+ const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
1385
+ return path6.join(path6.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
1386
+ }
1387
+ function isRootBoundarySegments(segments, basePaths) {
1388
+ const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
1389
+ if (firstVisibleIndex === -1)
1390
+ return segments.length <= 1;
1391
+ if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment)))
1392
+ return false;
1393
+ const visible = segments.slice(firstVisibleIndex);
1394
+ const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
1395
+ return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
1396
+ }
1397
+ function findRootBoundaries(pageKeys, appCwdPath, basePaths) {
1398
+ const boundaries = new Map;
1399
+ for (const key of pageKeys) {
1400
+ const segments = getRootBoundarySegments(key);
1401
+ if (!segments)
1402
+ continue;
1403
+ if (!isRootBoundarySegments(segments, basePaths))
1404
+ continue;
1405
+ const id = segments.join("/");
1406
+ boundaries.set(id, {
1407
+ sourceKey: key,
1408
+ sourceAbsPath: path6.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
1409
+ segments
1410
+ });
1411
+ }
1412
+ if (boundaries.size === 0)
1413
+ boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
1414
+ return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
1415
+ }
1416
+ function hasAncestorRootBoundary(boundary, boundaries) {
1417
+ return boundaries.some((candidate) => candidate !== boundary && candidate.segments.length < boundary.segments.length && candidate.segments.every((segment, index) => boundary.segments[index] === segment));
1418
+ }
1419
+ function findExplicitRootLayoutAbsPath(pageKeys, appCwdPath) {
1420
+ const rootLayoutKey = pageKeys.find((key) => {
1421
+ const segments = getRootBoundarySegments(key);
1422
+ return segments !== null && segments.length === 0;
1423
+ });
1424
+ return rootLayoutKey ? path6.resolve(appCwdPath, "page", rootLayoutKey.replace(/^\.\//, "")) : null;
1425
+ }
1426
+ async function assertEnvClientConvention(appCwdPath, appName) {
1427
+ const envPath = path6.join(appCwdPath, "env", "env.client.ts");
1428
+ if (!await Bun.file(envPath).exists()) {
1429
+ throw new Error(`[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`);
1715
1430
  }
1716
- akanPackageJson = {
1717
- name: "akanjs",
1718
- version: "0.0.0",
1719
- description: "akanjs",
1720
- dependencies: {}
1721
- };
1722
- return akanPackageJson;
1723
1431
  }
1724
- function mergeImageConfig(config = {}) {
1725
- return {
1726
- ...DEFAULT_AKAN_IMAGE_CONFIG,
1727
- ...config,
1728
- deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
1729
- imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
1730
- formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
1731
- qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
1732
- remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
1733
- localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns
1734
- };
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);
1735
1481
  }
1736
1482
 
1737
- class AkanLibConfig {
1738
- lib;
1739
- externalLibs;
1740
- trustedDependencies;
1741
- bin;
1742
- docker;
1743
- assets;
1744
- plugins;
1745
- constructor(lib, config, plugins = []) {
1746
- this.lib = lib;
1747
- this.externalLibs = config?.externalLibs ?? [];
1748
- const owner = `libs/${lib.name}/akan.config.ts`;
1749
- this.trustedDependencies = normalizePackageNames(config?.trustedDependencies, owner);
1750
- this.bin = AkanBin.parse(config?.bin, owner, lib.cwdPath);
1751
- this.docker = { preRuns: config?.docker?.preRuns ?? [], postRuns: config?.docker?.postRuns ?? [] };
1752
- this.assets = { keepFonts: normalizeKeepFonts(config?.assets?.keepFonts) };
1753
- 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);
1597
+ }
1598
+ static async run(workspaceRoot, label, fn) {
1599
+ const ahead = CodegenLock.#queue;
1600
+ let done;
1601
+ CodegenLock.#queue = new Promise((resolve) => {
1602
+ done = resolve;
1603
+ });
1604
+ try {
1605
+ await CodegenLock.#waitForQueue(ahead, label);
1606
+ return await CodegenLock.#withFileLock(workspaceRoot, label, fn);
1607
+ } finally {
1608
+ done();
1609
+ }
1610
+ }
1611
+ static async#waitForQueue(ahead, label) {
1612
+ let timer = null;
1613
+ const expired = new Promise((resolve) => {
1614
+ timer = setTimeout(() => resolve("expired"), CodegenLock.waitTimeoutMs);
1615
+ });
1616
+ try {
1617
+ if (await Promise.race([ahead.then(() => "done"), expired]) === "expired")
1618
+ CodegenLock.#logger.warn(`codegen lock queued past ${CodegenLock.waitTimeoutMs}ms in this process; continuing without waiting (${label})`);
1619
+ } finally {
1620
+ if (timer)
1621
+ clearTimeout(timer);
1622
+ }
1754
1623
  }
1755
- static async from(lib, { bustImportCache = false } = {}) {
1756
- const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
1757
- const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
1758
- const { plugins, ...config } = resolved ?? {};
1759
- if ("pipeline" in config)
1760
- 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.`);
1761
- if ("subspace" in config)
1762
- throw new LibConfigRefusal(`libs/${lib.name}/akan.config.ts declares \`subspace\`, a workspace setting: it belongs in the workspace root akan.config.ts.`);
1763
- return new AkanLibConfig(lib, config, plugins ?? []);
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
+ }
1764
1638
  }
1765
- }
1766
- //! need to refactor
1767
- var shiftBuildNum = async (app, delta) => {
1768
- const { buildNum } = (await AkanAppConfig.from(app)).native;
1769
- const akanConfigPath = path7.join(app.cwdPath, "akan.config.ts");
1770
- const akanConfig = fs2.readFileSync(akanConfigPath, "utf8");
1771
- fs2.writeFileSync(akanConfigPath, akanConfig.replace(`buildNum: ${buildNum}`, `buildNum: ${buildNum + delta}`));
1772
- };
1773
- var increaseBuildNum = async (app) => await shiftBuildNum(app, 1);
1774
- var decreaseBuildNum = async (app) => await shiftBuildNum(app, -1);
1775
- // pkgs/@akanjs/devkit/akanConfig/akanWorkspaceConfig.ts
1776
- import path8 from "path";
1777
- class AkanWorkspaceConfig {
1778
- static fileName = "akan.config.ts";
1779
- static #keys = ["subspace", "pipeline", "cloud"];
1780
- static #loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
1781
- static async from(workspace, { bustImportCache = false } = {}) {
1782
- const file = path8.join(workspace.workspaceRoot, AkanWorkspaceConfig.fileName);
1783
- if (!await FileSys2.fileExists(file))
1784
- return new AkanWorkspaceConfig(null, {});
1785
- const input = await AkanAppConfig.importConfigModule(workspace.workspaceRoot, { bustImportCache });
1786
- return new AkanWorkspaceConfig(file, input);
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
+ }
1787
1660
  }
1788
- file;
1789
- subspace;
1790
- pipeline;
1791
- cloudHost;
1792
- constructor(file, input) {
1793
- this.file = file;
1794
- if (typeof input !== "object" || input === null || Array.isArray(input))
1795
- throw new Error("The workspace root akan.config.ts must default-export a WorkspaceConfig object.");
1796
- for (const key of Object.keys(input))
1797
- if (!AkanWorkspaceConfig.#keys.includes(key))
1798
- 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.`);
1799
- const config = input;
1800
- this.subspace = config.subspace ?? null;
1801
- this.pipeline = config.pipeline ?? null;
1802
- this.cloudHost = AkanWorkspaceConfig.#cloudHost(config.cloud);
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;
1803
1675
  }
1804
- static #cloudHost(cloud) {
1805
- if (cloud === undefined)
1806
- return null;
1807
- const refuse = (why) => {
1808
- throw new Error(`The workspace root akan.config.ts has cloud ${why}.`);
1809
- };
1810
- if (typeof cloud !== "object" || cloud === null || Array.isArray(cloud))
1811
- return refuse("that is not an object");
1812
- const unknown = Object.keys(cloud).filter((key) => key !== "host");
1813
- if (unknown.length)
1814
- return refuse(`keys it does not take: ${unknown.join(", ")} (only \`host\`)`);
1815
- const { host } = cloud;
1816
- if (host === undefined)
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 {
1817
1683
  return null;
1818
- const url = typeof host === "string" && URL.canParse(host) ? new URL(host) : null;
1819
- if (!url || url.search || url.hash || url.username || url.password)
1820
- return refuse(`.host ${JSON.stringify(host)}, which is not a URL like "https://cloud.example.com"`);
1821
- if (url.protocol !== "https:" && !(url.protocol === "http:" && AkanWorkspaceConfig.#loopbackHosts.includes(url.hostname)))
1822
- return refuse(`.host ${JSON.stringify(host)}: https only, or http on this computer`);
1823
- 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
+ }
1824
1695
  }
1825
1696
  }
1697
+
1826
1698
  // pkgs/@akanjs/devkit/jsonFormatter.ts
1827
- import path9 from "path";
1699
+ import path8 from "path";
1828
1700
 
1829
1701
  class JsonFormatter {
1830
1702
  static lineWidth = 120;
1831
1703
  static stringify(value, filePath) {
1832
1704
  const json = JSON.stringify(value, null, 2);
1833
- if (path9.basename(filePath) === "package.json")
1705
+ if (path8.basename(filePath) === "package.json")
1834
1706
  return `${json}
1835
1707
  `;
1836
1708
  return `${JsonFormatter.#print(JSON.parse(json), 0, { column: 0, trailing: 0 })}
@@ -1901,7 +1773,7 @@ ${" ".repeat(depth)}]`;
1901
1773
  // pkgs/@akanjs/devkit/linter.ts
1902
1774
  import { spawn } from "child_process";
1903
1775
  import { existsSync as existsSync2, readFileSync } from "fs";
1904
- import path10 from "path";
1776
+ import path9 from "path";
1905
1777
  import chalk from "chalk";
1906
1778
  var parseBiomeReport = (output) => {
1907
1779
  for (let start = output.indexOf("{");start !== -1; start = output.indexOf("{", start + 1)) {
@@ -1967,13 +1839,13 @@ class Linter {
1967
1839
  #biomeBin;
1968
1840
  constructor(cwdPath) {
1969
1841
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
1970
- this.configPath = Linter.#configPathIn(this.lintRoot) ?? path10.join(this.lintRoot, "biome.json");
1971
- const localBiomeBin = path10.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
1842
+ this.configPath = Linter.#configPathIn(this.lintRoot) ?? path9.join(this.lintRoot, "biome.json");
1843
+ const localBiomeBin = path9.join(this.lintRoot, "node_modules", ".bin", Linter.biomeBinName);
1972
1844
  this.#biomeBin = existsSync2(localBiomeBin) ? localBiomeBin : "biome";
1973
1845
  }
1974
1846
  static #configPathIn(dir) {
1975
1847
  for (const fileName of BIOME_CONFIG_FILES) {
1976
- const configPath = path10.join(dir, fileName);
1848
+ const configPath = path9.join(dir, fileName);
1977
1849
  if (existsSync2(configPath))
1978
1850
  return configPath;
1979
1851
  }
@@ -1982,19 +1854,19 @@ class Linter {
1982
1854
  #findBiomeRootPath(dir) {
1983
1855
  if (Linter.#configPathIn(dir))
1984
1856
  return dir;
1985
- const parentDir = path10.dirname(dir);
1857
+ const parentDir = path9.dirname(dir);
1986
1858
  if (parentDir === dir)
1987
1859
  throw new Error(`biome.json not found from ${dir}`);
1988
1860
  return this.#findBiomeRootPath(parentDir);
1989
1861
  }
1990
1862
  #toBiomePath(filePath) {
1991
- const relativePath = path10.relative(this.lintRoot, filePath);
1992
- if (!relativePath.startsWith("..") && !path10.isAbsolute(relativePath))
1863
+ const relativePath = path9.relative(this.lintRoot, filePath);
1864
+ if (!relativePath.startsWith("..") && !path9.isAbsolute(relativePath))
1993
1865
  return relativePath;
1994
1866
  return filePath;
1995
1867
  }
1996
1868
  #resolveFilePath(filePath) {
1997
- return path10.isAbsolute(filePath) ? filePath : path10.join(this.lintRoot, filePath);
1869
+ return path9.isAbsolute(filePath) ? filePath : path9.join(this.lintRoot, filePath);
1998
1870
  }
1999
1871
  async#runBiome(args, input) {
2000
1872
  return await new Promise((resolve, reject) => {
@@ -2019,7 +1891,7 @@ class Linter {
2019
1891
  const diagnosticPath = diagnostic.location?.path;
2020
1892
  if (!diagnosticPath)
2021
1893
  return fallbackFilePath;
2022
- return path10.isAbsolute(diagnosticPath) ? diagnosticPath : path10.join(this.lintRoot, diagnosticPath);
1894
+ return path9.isAbsolute(diagnosticPath) ? diagnosticPath : path9.join(this.lintRoot, diagnosticPath);
2023
1895
  }
2024
1896
  #createLintMessage(diagnostic) {
2025
1897
  const start = diagnostic.location?.start;
@@ -2355,7 +2227,7 @@ class ManifestDependencies {
2355
2227
 
2356
2228
  // pkgs/@akanjs/devkit/repoIdentity.ts
2357
2229
  import { execFileSync } from "child_process";
2358
- import path11 from "path";
2230
+ import path10 from "path";
2359
2231
  var resolved = new Map;
2360
2232
  var readRemoteName = (workspaceRoot) => {
2361
2233
  try {
@@ -2373,13 +2245,13 @@ var resolveRepoName = (workspaceRoot) => {
2373
2245
  const cached = resolved.get(workspaceRoot);
2374
2246
  if (cached)
2375
2247
  return cached;
2376
- const repoName = readRemoteName(workspaceRoot) ?? path11.basename(workspaceRoot);
2248
+ const repoName = readRemoteName(workspaceRoot) ?? path10.basename(workspaceRoot);
2377
2249
  resolved.set(workspaceRoot, repoName);
2378
2250
  return repoName;
2379
2251
  };
2380
2252
 
2381
2253
  // pkgs/@akanjs/devkit/scanInfo.ts
2382
- import path12 from "path";
2254
+ import path11 from "path";
2383
2255
 
2384
2256
  // pkgs/@akanjs/devkit/workspaceLayout.ts
2385
2257
  var appRootAllowedFiles = new Set([
@@ -2500,10 +2372,10 @@ var moduleUiFileTypes = {
2500
2372
  service: new Set(["Util", "Zone"]),
2501
2373
  scalar: new Set(["Template", "Unit"])
2502
2374
  };
2503
- var createDependencyScanner = async (exec) => (await import("./dependencyScanner-06z2dmrr.js")).TypeScriptDependencyScanner.from(exec);
2504
- var getScanPath = (exec, relativePath) => path12.posix.join(`${exec.type}s`, exec.name, relativePath.split(path12.sep).join("/"));
2375
+ var createDependencyScanner = async (exec) => (await import("./dependencyScanner-3afj2vt1.js")).TypeScriptDependencyScanner.from(exec);
2376
+ var getScanPath = (exec, relativePath) => path11.posix.join(`${exec.type}s`, exec.name, relativePath.split(path11.sep).join("/"));
2505
2377
  var getModuleNameFromPath = (kind, modulePath) => {
2506
- const dirname = path12.basename(modulePath);
2378
+ const dirname = path11.basename(modulePath);
2507
2379
  return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
2508
2380
  };
2509
2381
  async function assertScanConvention(exec, libRoot) {
@@ -2524,28 +2396,28 @@ async function assertScanConvention(exec, libRoot) {
2524
2396
  for (const filename of await exec.readdir(facet)) {
2525
2397
  const shadow = facetIndexShadowOf(`${facet}/${filename}`);
2526
2398
  if (shadow)
2527
- addViolation(path12.join(facet, filename), shadow);
2399
+ addViolation(path11.join(facet, filename), shadow);
2528
2400
  }
2529
2401
  if (exec.type === "lib")
2530
2402
  for (const { relativePath, reason } of await exec.getPageConventionViolations())
2531
2403
  addViolation(relativePath, reason);
2532
2404
  for (const filename of libRoot.files)
2533
2405
  if (!isAllowedLibFacetRootFile(filename))
2534
- addViolation(path12.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
2406
+ addViolation(path11.join("lib", filename), libFacetRootHintOf(filename) ?? "unsupported lib facet root file");
2535
2407
  for (const dirname of libRoot.dirs)
2536
2408
  if (dirname.startsWith("__") && !internalLibDirs.has(dirname))
2537
- addViolation(path12.join("lib", dirname), "unsupported internal lib folder");
2409
+ addViolation(path11.join("lib", dirname), "unsupported internal lib folder");
2538
2410
  const databaseDirs = libRoot.dirs.filter((dirname) => !dirname.startsWith("_"));
2539
2411
  const serviceDirs = libRoot.dirs.filter((dirname) => dirname.startsWith("_") && !dirname.startsWith("__"));
2540
2412
  const scalarDirs = await exec.readdir("lib/__scalar");
2541
2413
  await Promise.all([
2542
- ...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database", path12.join("lib", dirname))),
2543
- ...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service", path12.join("lib", dirname))),
2544
- ...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar", path12.join("lib/__scalar", dirname)))
2414
+ ...databaseDirs.map((dirname) => validateModuleFiles(exec, violations, "database", path11.join("lib", dirname))),
2415
+ ...serviceDirs.map((dirname) => validateModuleFiles(exec, violations, "service", path11.join("lib", dirname))),
2416
+ ...scalarDirs.map((dirname) => validateModuleFiles(exec, violations, "scalar", path11.join("lib/__scalar", dirname)))
2545
2417
  ]);
2546
2418
  if (violations.length > 0) {
2547
2419
  throw new Error(`[scan-convention]
2548
- ${violations.sort().map((violation) => `- ${violation}`).join(`
2420
+ ${violations.sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((violation) => `- ${violation}`).join(`
2549
2421
  `)}`);
2550
2422
  }
2551
2423
  }
@@ -2553,11 +2425,11 @@ async function validateModuleFiles(exec, violations, kind, modulePath) {
2553
2425
  const { files, dirs } = await exec.getFilesAndDirs(modulePath);
2554
2426
  const moduleName = getModuleNameFromPath(kind, modulePath);
2555
2427
  dirs.forEach((dirname) => {
2556
- violations.push(`${getScanPath(exec, path12.join(modulePath, dirname))}: unsupported module folder`);
2428
+ violations.push(`${getScanPath(exec, path11.join(modulePath, dirname))}: unsupported module folder`);
2557
2429
  });
2558
2430
  const uiModuleName = moduleName[0].toUpperCase() + moduleName.slice(1);
2559
2431
  for (const filename of files) {
2560
- const scanPath = getScanPath(exec, path12.join(modulePath, filename));
2432
+ const scanPath = getScanPath(exec, path11.join(modulePath, filename));
2561
2433
  if (filename === "index.ts" || filename === "index.tsx")
2562
2434
  continue;
2563
2435
  if (filename === `${moduleName}.abstract.md`)
@@ -2632,16 +2504,16 @@ class ScanInfo {
2632
2504
  }
2633
2505
  };
2634
2506
  await Promise.all([
2635
- ...databaseDirs.map((name) => collect(path12.join("lib", name), name, "databases")),
2636
- ...serviceDirs.map((dirname) => collect(path12.join("lib", dirname), dirname.slice(1), "services")),
2637
- ...scalarDirs.map((name) => collect(path12.join("lib/__scalar", name), name, "scalars"))
2507
+ ...databaseDirs.map((name) => collect(path11.join("lib", name), name, "databases")),
2508
+ ...serviceDirs.map((dirname) => collect(path11.join("lib", dirname), dirname.slice(1), "services")),
2509
+ ...scalarDirs.map((name) => collect(path11.join("lib/__scalar", name), name, "scalars"))
2638
2510
  ]);
2639
2511
  const routes = exec.type === "lib" ? [] : await exec.getPageKeys();
2640
2512
  const common = {
2641
2513
  name: exec.name,
2642
2514
  type: exec.type,
2643
2515
  repoName: exec.workspace.repoName,
2644
- serveDomain: WorkspaceExecutor.getBaseDevEnv(path12.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
2516
+ serveDomain: WorkspaceExecutor.getBaseDevEnv(path11.join(exec.workspace.workspaceRoot, ".env")).serveDomain,
2645
2517
  files,
2646
2518
  libDeps,
2647
2519
  pkgDeps,
@@ -2869,6 +2741,136 @@ class WorkspaceInfo {
2869
2741
  }
2870
2742
  }
2871
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
+
2872
2874
  // pkgs/@akanjs/devkit/executors.ts
2873
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(" ")));
2874
2876
  var plainCommandArgPattern = process.platform === "win32" ? /^[\w@%+=:,./\\-]+$/ : /^[\w@%+=:,./-]+$/;
@@ -3371,6 +3373,7 @@ class WorkspaceExecutor extends Executor {
3371
3373
  return new WorkspaceExecutor({ workspaceRoot, repoName });
3372
3374
  }
3373
3375
  static publicEnvPin = null;
3376
+ static localServeDomain = "localhost";
3374
3377
  static getBaseDevEnv(envPath) {
3375
3378
  const sourceEnv = {
3376
3379
  ...envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env,
@@ -3379,12 +3382,9 @@ class WorkspaceExecutor extends Executor {
3379
3382
  const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
3380
3383
  const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
3381
3384
  const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
3382
- const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
3383
- if (!repoName)
3384
- throw new Error("AKAN_PUBLIC_REPO_NAME is not set");
3385
- const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
3386
- if (!serveDomain)
3387
- 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]);
3388
3388
  const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
3389
3389
  const env = sourceEnv.AKAN_PUBLIC_ENV ?? "debug";
3390
3390
  if (!env)
@@ -3396,9 +3396,18 @@ class WorkspaceExecutor extends Executor {
3396
3396
  serveDomain,
3397
3397
  env,
3398
3398
  portOffset,
3399
- workspaceId
3399
+ workspaceId,
3400
+ defaulted
3400
3401
  };
3401
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
+ }
3402
3411
  getWorkspaceId({
3403
3412
  allowEmpty
3404
3413
  } = {}) {
@@ -3775,10 +3784,12 @@ class AppExecutor extends SysExecutor {
3775
3784
  return basePort + appIndex + portOffset;
3776
3785
  }
3777
3786
  getCommandEnv(env = {}) {
3778
- const { portOffset } = WorkspaceExecutor.getBaseDevEnv();
3787
+ const { portOffset, repoName, serveDomain } = WorkspaceExecutor.getBaseDevEnv();
3779
3788
  const PORT = (8282 + portOffset).toString();
3780
3789
  return {
3781
3790
  ...process.env,
3791
+ AKAN_PUBLIC_REPO_NAME: repoName,
3792
+ AKAN_PUBLIC_SERVE_DOMAIN: serveDomain,
3782
3793
  AKAN_PUBLIC_APP_NAME: this.name,
3783
3794
  AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
3784
3795
  NODE_NO_WARNINGS: "1",
@@ -3929,7 +3940,7 @@ class AppExecutor extends SysExecutor {
3929
3940
  pageKeys.push(key);
3930
3941
  }
3931
3942
  }
3932
- pageKeys.sort();
3943
+ pageKeys.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3933
3944
  this.#pageKeys = this.#excludeDevOnlyPages ? this.#dropDevOnlyPages(pageKeys, devOnlyKeys, devOnlyDirs) : pageKeys;
3934
3945
  return this.#pageKeys;
3935
3946
  }
@@ -4175,7 +4186,7 @@ class PkgExecutor extends Executor {
4175
4186
  }
4176
4187
  async#toDependencyMap(rootPackageJson, dependencies = [], devDependencies = []) {
4177
4188
  const dependencyNames = [...new Set(dependencies)].sort();
4178
- 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);
4179
4190
  const dependencyVersions = new Map;
4180
4191
  const missingDeps = [];
4181
4192
  for (const dep of [...dependencyNames, ...devDependencyNames]) {