@akanjs/cli 3.0.0-beta.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.build-stamp +1 -1
  2. package/CodeAgent-2vmp9qe0.js +9 -0
  3. package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
  4. package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
  5. package/CodeTui-5ed6bg2a.js +1457 -0
  6. package/README.ko.md +83 -28
  7. package/README.md +70 -27
  8. package/TunnelShare-cwh9ezsm.js +9 -0
  9. package/{agent.command-0q17pnrs.js → agent.command-gn73rhe7.js} +8 -9
  10. package/akanCode.js +102 -0
  11. package/application.command-e968t0hw.js +246 -0
  12. package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-64taxseg.js} +121 -77
  13. package/buildBatch.proc.js +120 -32
  14. package/cloud.command-0jm301rr.js +95 -0
  15. package/cloud.script-nemnzjpv.js +20 -0
  16. package/code.command-18kcm7hq.js +154 -0
  17. package/commandManifest.json +1 -1
  18. package/consoleToStderr-h5bryj94.js +7 -0
  19. package/context.command-trd9bn18.js +48 -0
  20. package/context.runner-y5hwh673.js +21 -0
  21. package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
  22. package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
  23. package/fileSys-wzzrntkw.js +9 -0
  24. package/guideline.command-bdmdn2de.js +42 -0
  25. package/guidelines/agentRule/agentRule.instruction.md +154 -26
  26. package/guidelines/conventions/conventions.instruction.md +134 -48
  27. package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
  28. package/guidelines/queryRule/queryRule.instruction.md +21 -7
  29. package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
  30. package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
  31. package/guidelines/transportRule/transportRule.instruction.md +10 -8
  32. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
  33. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
  34. package/incrementalBuilder.proc.js +564 -109
  35. package/index-0wpybxtr.js +2729 -0
  36. package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
  37. package/index-2wc41k5w.js +49 -0
  38. package/index-3hx1z96c.js +87 -0
  39. package/{index-2tv3fdx4.js → index-3w94e7n3.js} +1228 -1458
  40. package/{index-hwkygn9s.js → index-48k96p55.js} +325 -36
  41. package/index-4npz05nm.js +187 -0
  42. package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
  43. package/index-6he6gvfg.js +4300 -0
  44. package/index-7md2g9vw.js +6 -0
  45. package/{index-04bk599g.js → index-93j60em0.js} +10 -17
  46. package/index-94s080qm.js +266 -0
  47. package/index-9dp031by.js +12 -0
  48. package/{index-ktb5njgy.js → index-9z771ptz.js} +49 -99
  49. package/{index-y6n8nmny.js → index-abxpehf8.js} +75 -203
  50. package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
  51. package/{index-9077wzd2.js → index-by9pxkr0.js} +43 -26
  52. package/index-c6eggbee.js +123 -0
  53. package/index-d2242ksy.js +99 -0
  54. package/index-drehwpdt.js +1119 -0
  55. package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
  56. package/{index-ffvk10zz.js → index-f3c78nd6.js} +107 -241
  57. package/index-gbggbqcp.js +1058 -0
  58. package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
  59. package/index-hsmtysz4.js +53 -0
  60. package/{index-j9yhg404.js → index-jmrn54tt.js} +4 -7
  61. package/{index-a64esmtb.js → index-kcdj1z5v.js} +523 -1022
  62. package/{index-d43r08tf.js → index-kqabytj7.js} +77 -150
  63. package/{index-vyvre9en.js → index-mbxbhay5.js} +6 -2
  64. package/{index-vxtad5bd.js → index-mgxpx3f2.js} +5 -8
  65. package/index-p6pbz9n5.js +25 -0
  66. package/{index-sj5vxr8f.js → index-p993efkq.js} +981 -897
  67. package/{index-2xp0kfkt.js → index-pmhqttv3.js} +37 -25
  68. package/index-tqm7jjcn.js +700 -0
  69. package/{index-0g7r8xz3.js → index-vpj42xwt.js} +114 -232
  70. package/index-wa5jbxj1.js +760 -0
  71. package/{index-k2j1hc4r.js → index-wprnsjms.js} +12 -12
  72. package/{index-gyd220p2.js → index-y3mphr1m.js} +554 -577
  73. package/index-yg7jhz8x.js +69 -0
  74. package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
  75. package/index.js +23 -22
  76. package/{library.command-qzzbj6d8.js → library.command-9h749a1x.js} +4 -5
  77. package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-2bqqhxyz.js} +23 -43
  78. package/{module.command-rq5hq3ax.js → module.command-nv8nzgft.js} +8 -12
  79. package/{package.command-d9hd6v9n.js → package.command-mq4bggp6.js} +6 -7
  80. package/package.json +15 -7
  81. package/{page.command-126d56te.js → page.command-j639n707.js} +4 -5
  82. package/{primitive.command-taxd1538.js → primitive.command-kz6849th.js} +9 -23
  83. package/prompter-76k56b37.js +8 -0
  84. package/{quality.command-6gsna0a3.js → quality.command-4hyabwkb.js} +205 -377
  85. package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
  86. package/{repair.command-tq2dgy72.js → repair.command-a9awpbqr.js} +11 -35
  87. package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
  88. package/{scalar.command-3bcmrrkv.js → scalar.command-ma4fpm9m.js} +7 -11
  89. package/skills/akan-code/SKILL.md +130 -0
  90. package/skills/akan-debug/SKILL.md +42 -0
  91. package/skills/akan-deps/SKILL.md +50 -0
  92. package/skills/akan-err/SKILL.md +39 -0
  93. package/skills/akan-module/SKILL.md +45 -0
  94. package/skills/akan-scaffold/SKILL.md +76 -0
  95. package/skills/akan-scalar/SKILL.md +52 -0
  96. package/skills/akan-service/SKILL.md +34 -0
  97. package/skills/akan-store/SKILL.md +67 -0
  98. package/skills/akan-ui/SKILL.md +63 -0
  99. package/skills/akan-validate/SKILL.md +44 -0
  100. package/{subspace.command-eddzqpx3.js → subspace.command-3k2pmrs4.js} +53 -62
  101. package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
  102. package/templates/app/page/_index.tsx +257 -258
  103. package/templates/app/page/_layout.tsx +5 -1
  104. package/templates/app/page/_overrides.tsx +12 -0
  105. package/templates/app/page/styles.css.template +316 -14
  106. package/templates/app/public/apple-touch-icon.png +0 -0
  107. package/templates/app/public/favicon.ico +0 -0
  108. package/templates/app/public/jelly/cloud.webp +0 -0
  109. package/templates/app/public/jelly/comet.webp +0 -0
  110. package/templates/app/public/jelly/moon.webp +0 -0
  111. package/templates/app/public/jelly/planet.webp +0 -0
  112. package/templates/app/public/jelly/rocket.webp +0 -0
  113. package/templates/app/public/jelly/star.webp +0 -0
  114. package/templates/app/public/jelly/sugar.webp +0 -0
  115. package/templates/app/public/logo.png +0 -0
  116. package/templates/app/ui/Recipe/index.ts +9 -0
  117. package/templates/app/ui/Recipe/jellyButton.ts +63 -0
  118. package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
  119. package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
  120. package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
  121. package/templates/appSample/lib/task/Task.Util.tsx +10 -13
  122. package/templates/appSample/lib/task/Task.View.tsx +2 -2
  123. package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
  124. package/templates/appSample/lib/task/task.abstract.ts +12 -32
  125. package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
  126. package/templates/appSample/lib/task/task.signal.test.ts +15 -1
  127. package/templates/appSample/lib/task/task.signal.ts +10 -8
  128. package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
  129. package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
  130. package/templates/appSample/page/task/_index.tsx +4 -3
  131. package/templates/appSample/page/task/_layout.tsx +1 -1
  132. package/templates/appSample/page/task/new.tsx +5 -4
  133. package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
  134. package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
  135. package/templates/appSample/ui/GlobalLoading.tsx +2 -1
  136. package/templates/appSample/ui/QuantityControl.tsx +3 -3
  137. package/templates/client.ts +7 -30
  138. package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
  139. package/templates/crudPages/[__model__Id]/page.tsx +2 -1
  140. package/templates/crudPages/new/page.tsx +2 -1
  141. package/templates/crudPages/page.tsx +2 -1
  142. package/templates/crudSinglePage/page.tsx +2 -1
  143. package/templates/lib/__lib/lib.dictionary.ts +0 -2
  144. package/templates/lib/__lib/lib.document.ts +0 -2
  145. package/templates/lib/sig.ts +8 -30
  146. package/templates/localDev/docker-compose.yaml.template +3 -14
  147. package/templates/module/__model__.signal.ts +12 -4
  148. package/templates/pkgRoot/tsconfig.json.template +2 -7
  149. package/templates/server.ts +1 -3
  150. package/templates/service/__model__.abstract.md.template +8 -0
  151. package/templates/workspaceRoot/.gitignore.template +0 -1
  152. package/templates/workspaceRoot/AGENTS.md.template +6 -0
  153. package/templates/workspaceRoot/package.json.template +1 -3
  154. package/tunnel.command-xwr46b66.js +129 -0
  155. package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
  156. package/typecheck.proc.js +1 -1
  157. package/workflow.command-t7wqe4h7.js +79 -0
  158. package/{workspace.command-06wvstgr.js → workspace.command-4derhzrh.js} +79 -109
  159. package/abstractCompactor-gmkc0z8j.js +0 -102
  160. package/application.command-18y0s485.js +0 -204
  161. package/applicationReleasePackager-4yasxwv9.js +0 -244
  162. package/capacitorApp-36rbr858.js +0 -12
  163. package/cloud.command-av97ndh6.js +0 -94
  164. package/cloud.script-cm11xjdz.js +0 -20
  165. package/context.command-5v37ef3n.js +0 -81
  166. package/devTuiView-0hc2emhe.js +0 -9
  167. package/getDirname-dr3jz1xg.js +0 -7
  168. package/guideline.command-4pz9g1rt.js +0 -360
  169. package/guidelines/agentRule/agentRule.generate.json +0 -31
  170. package/guidelines/componentRule/componentRule.generate.json +0 -31
  171. package/guidelines/conventions/conventions.generate.json +0 -25
  172. package/guidelines/cssRule/cssRule.generate.json +0 -31
  173. package/guidelines/docPageRule/docPageRule.generate.json +0 -31
  174. package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
  175. package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
  176. package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
  177. package/guidelines/enumConstant/enumConstant.generate.json +0 -31
  178. package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
  179. package/guidelines/fieldRule/fieldRule.generate.json +0 -31
  180. package/guidelines/framework/framework.generate.json +0 -32
  181. package/guidelines/mcpRule/mcpRule.generate.json +0 -35
  182. package/guidelines/modelConstant/modelConstant.generate.json +0 -32
  183. package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
  184. package/guidelines/modelDocument/modelDocument.generate.json +0 -31
  185. package/guidelines/modelService/modelService.generate.json +0 -31
  186. package/guidelines/modelSignal/modelSignal.generate.json +0 -31
  187. package/guidelines/modelStore/modelStore.generate.json +0 -31
  188. package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
  189. package/guidelines/modelUnit/modelUnit.generate.json +0 -31
  190. package/guidelines/modelUtil/modelUtil.generate.json +0 -31
  191. package/guidelines/modelView/modelView.generate.json +0 -31
  192. package/guidelines/modelZone/modelZone.generate.json +0 -31
  193. package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
  194. package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
  195. package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
  196. package/guidelines/queryRule/queryRule.generate.json +0 -31
  197. package/guidelines/recipeRule/recipeRule.generate.json +0 -31
  198. package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
  199. package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
  200. package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
  201. package/guidelines/scalarModule/scalarModule.generate.json +0 -31
  202. package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
  203. package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
  204. package/guidelines/ssrRule/ssrRule.generate.json +0 -31
  205. package/guidelines/transportRule/transportRule.generate.json +0 -31
  206. package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
  207. package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
  208. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
  209. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
  210. package/index-47pj3aee.js +0 -76
  211. package/index-5ystnsd5.js +0 -841
  212. package/index-6wwzs1bc.js +0 -7
  213. package/index-a17x49vg.js +0 -229
  214. package/index-m0xccmmm.js +0 -228
  215. package/index-meh558jy.js +0 -65
  216. package/index-rtxxan88.js +0 -471
  217. package/index-te54x9zm.js +0 -338
  218. package/index-w8kmx1b6.js +0 -2176
  219. package/index-wt7qrtd4.js +0 -1516
  220. package/templates/__scalar/__model__/__model__.abstract.md +0 -28
  221. package/templates/module/__model__.abstract.md +0 -28
  222. package/templates/service/__model__.abstract.md +0 -28
  223. package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
  224. package/workflow.command-gtdnzvbz.js +0 -108
@@ -1,178 +1,80 @@
1
1
  // @bun
2
+ import {
3
+ ApplicationBuildReporter,
4
+ BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS,
5
+ SOURCE_EXTS,
6
+ shouldRestartBackendByDevPlan,
7
+ shouldRestartBuilderByDevPlan,
8
+ shouldAbandonBackendRecovery,
9
+ normalizeBackendReportedGeneration,
10
+ shouldRestartDevHostByDevPlan,
11
+ devHostStateOf,
12
+ isLegacyBackendFallbackFile,
13
+ shouldMarkBuildPhaseRecovered,
14
+ shouldKeepBuildFailure,
15
+ createBackendBuildStatus,
16
+ backendRestartReasonFromMessage,
17
+ mergeBackendRestartReasons,
18
+ shouldReplaceLastGoodMessage,
19
+ shouldQueueBuildStatusReplay,
20
+ hasBuildFailureForGeneration,
21
+ decideBuilderRssRecycle,
22
+ decideBuilderRssSettle,
23
+ decideIdleSuspend,
24
+ resolveIdleSuspendMs,
25
+ hasAnyBuildFailure,
26
+ shouldRefreshConfigOnIdleWake,
27
+ shouldHoldForReturningBuilder,
28
+ shouldWarnBuilderRssCeilingTight,
29
+ isRssCeilingUnreachable,
30
+ shouldRelayRecycledFrontendState,
31
+ mergeInvalidateMessages,
32
+ buildStatusReplaySequence,
33
+ filesChangedSince,
34
+ HmrWatcher,
35
+ WatchRootResolver
36
+ } from "./index-48k96p55.js";
2
37
  import {
3
38
  runner,
4
39
  script
5
- } from "./index-0g7r8xz3.js";
40
+ } from "./index-vpj42xwt.js";
6
41
  import {
7
42
  WorkspaceExecutor,
8
43
  AppExecutor,
9
44
  LibExecutor,
10
45
  PkgExecutor
11
- } from "./index-2tv3fdx4.js";
46
+ } from "./index-3w94e7n3.js";
47
+ import {
48
+ DesktopServerStage,
49
+ resolveMobileTargets,
50
+ resolveAppId,
51
+ NativeApp
52
+ } from "./index-gbggbqcp.js";
12
53
  import {
13
54
  LibraryScript
14
- } from "./index-vxtad5bd.js";
55
+ } from "./index-mgxpx3f2.js";
15
56
  import {
16
- HmrWatcher,
17
- WatchRootResolver
18
- } from "./index-hwkygn9s.js";
57
+ stripAnsi
58
+ } from "./index-7md2g9vw.js";
19
59
  import {
20
60
  SlicePlanner,
21
61
  formatSlicePlan
22
- } from "./index-j9yhg404.js";
62
+ } from "./index-jmrn54tt.js";
23
63
  import {
24
- resolveMobileTargets
25
- } from "./index-47pj3aee.js";
26
-
27
- // pkgs/@akanjs/cli/openBrowser.ts
28
- import { spawn } from "child_process";
29
- function openBrowser(url) {
30
- const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
31
- const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
32
- child.on("error", () => {});
33
- child.unref();
34
- return Promise.resolve();
35
- }
36
-
37
- // pkgs/@akanjs/cli/application/devLogBuffer.ts
38
- var ansi = new RegExp(`${String.fromCharCode(27)}(?:[@-Z\\\\-_]|\\[[\\s\\S]*?[@-~])`, "g");
39
- var stripAnsi = (text) => text.replace(ansi, "");
40
- var plainTextOf = (lines, { withApp = false } = {}) => {
41
- const width = withApp ? lines.reduce((max, line) => Math.max(max, line.app.length), 0) : 0;
42
- return lines.map((line) => `${withApp ? `${line.app.padEnd(width)} \u2502 ` : ""}${stripAnsi(line.text)}`).join(`
43
- `);
44
- };
45
- var HOST_SOURCE = "host";
46
- var childPrefix = /^\[child:(\d+) ([a-z-]+)\] \[(?:stdout|stderr)\] /;
47
- var sourceOf = (text) => {
48
- const match = childPrefix.exec(text);
49
- return match ? `#${match[1]} ${match[2]}` : HOST_SOURCE;
50
- };
51
-
52
- class DevLogBuffer {
53
- static defaultLimit = 5000;
54
- #limit;
55
- #lines = [];
56
- #partial = new Map;
57
- #sources = new Map;
58
- #seq = 0;
59
- constructor({ limit = DevLogBuffer.defaultLimit } = {}) {
60
- this.#limit = Math.max(1, limit);
61
- }
62
- get size() {
63
- return this.#lines.length;
64
- }
65
- sourcesOf(app) {
66
- return [...this.#sources.get(app) ?? []];
67
- }
68
- push(app, kind, chunk) {
69
- const key = `${app}:${kind}`;
70
- const parts = `${this.#partial.get(key) ?? ""}${chunk}`.split(`
71
- `);
72
- this.#partial.set(key, parts.pop() ?? "");
73
- return this.#append(app, kind, parts);
74
- }
75
- flushPartials() {
76
- const flushed = [];
77
- for (const [key, remainder] of [...this.#partial]) {
78
- this.#partial.set(key, "");
79
- if (!remainder)
80
- continue;
81
- const split = key.lastIndexOf(":");
82
- const app = key.slice(0, split);
83
- const kind = key.slice(split + 1) === "stderr" ? "stderr" : "stdout";
84
- flushed.push(...this.#append(app, kind, [remainder]));
85
- }
86
- return flushed;
87
- }
88
- select({ app = null, source = null, grep, errorsOnly = false } = {}) {
89
- const needle = grep?.trim().toLowerCase();
90
- return this.#lines.filter((line) => {
91
- if (app !== null && line.app !== app)
92
- return false;
93
- if (source !== null && line.source !== source)
94
- return false;
95
- if (errorsOnly && line.kind !== "stderr")
96
- return false;
97
- if (needle && !stripAnsi(line.text).toLowerCase().includes(needle))
98
- return false;
99
- return true;
100
- });
101
- }
102
- clear(app = null) {
103
- const kept = app === null ? [] : this.#lines.filter((line) => line.app !== app);
104
- this.#lines.length = 0;
105
- this.#lines.push(...kept);
106
- }
107
- #append(app, kind, texts) {
108
- const known = this.#sources.get(app) ?? new Set;
109
- this.#sources.set(app, known);
110
- const added = texts.map((text) => {
111
- const source = sourceOf(text);
112
- known.add(source);
113
- return { seq: ++this.#seq, app, source, kind, text };
114
- });
115
- this.#lines.push(...added);
116
- if (this.#lines.length > this.#limit)
117
- this.#lines.splice(0, this.#lines.length - this.#limit);
118
- return added;
119
- }
120
- }
121
-
122
- // pkgs/@akanjs/devkit/applicationBuildReporter.ts
123
- import { Logger } from "akanjs/common";
124
-
125
- class ApplicationBuildReporter {
126
- static create() {
127
- return {
128
- phaseDone: (phase) => Logger.rawLog(ApplicationBuildReporter.formatPhaseLine(phase))
129
- };
130
- }
131
- static printSummary(result) {
132
- Logger.rawLog("");
133
- Logger.rawLog(`Route artifacts: ${result.artifactDir}`);
134
- Logger.rawLog(`Server output: ${result.outputDir}`);
135
- Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
136
- }
137
- static formatError(error) {
138
- if (error instanceof AggregateError) {
139
- const nestedMessages = error.errors.map((nestedError, index) => ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`).map((message) => message.replace(/^/gm, " ")).join(`
140
- `);
141
- return nestedMessages ? `${error.message}
142
- ${nestedMessages}` : error.message;
143
- }
144
- if (error instanceof Error) {
145
- const causeMessage = error.cause ? `
146
- Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
147
- return `${error.message}${causeMessage}`;
148
- }
149
- if (typeof error === "object" && error !== null && "message" in error)
150
- return String(error.message);
151
- return String(error);
152
- }
153
- static formatDuration(ms) {
154
- if (ms < 1000)
155
- return `${ms}ms`;
156
- if (ms < 60000)
157
- return `${Math.round(ms / 100) / 10}s`;
158
- const seconds = Math.floor(ms / 1000);
159
- return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
160
- }
161
- static formatPhaseLine(phase) {
162
- const summary = phase.summary ? `: ${phase.summary}` : "";
163
- return `\u2713 ${phase.label}${summary} (${ApplicationBuildReporter.formatDuration(phase.durationMs)})`;
164
- }
165
- }
64
+ openBrowser
65
+ } from "./index-9dp031by.js";
166
66
 
167
67
  // pkgs/@akanjs/cli/application/application.script.ts
168
68
  import { confirm } from "@inquirer/prompts";
169
69
  import { Logger as Logger6 } from "akanjs/common";
170
70
 
171
71
  // pkgs/@akanjs/cli/application/application.runner.ts
72
+ import { realpathSync } from "fs";
172
73
  import path5 from "path";
173
74
 
174
75
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
175
- import { Logger as Logger3 } from "akanjs/common";
76
+ import path3 from "path";
77
+ import { Logger as Logger2 } from "akanjs/common";
176
78
 
177
79
  // pkgs/@akanjs/devkit/sshTunnel.ts
178
80
  import { Client } from "ssh2";
@@ -261,7 +163,7 @@ var createSshTunnel = async (options) => {
261
163
  for (const socket of sockets)
262
164
  closeSocket(socket, error);
263
165
  closeQuietly(() => client.end());
264
- closeBunServer(server);
166
+ server.stop();
265
167
  };
266
168
  client.once("error", (error) => closeTunnel(error));
267
169
  client.once("close", () => closeTunnel());
@@ -281,22 +183,10 @@ var closeBunSocket = (socket, error) => {
281
183
  socket.data.stream?.end();
282
184
  closeQuietly(() => socket.end());
283
185
  };
284
- var closeBunServer = (server) => {
285
- server.stop();
286
- };
287
186
 
288
187
  // pkgs/@akanjs/devkit/createTunnel.ts
289
- var getSshTunnelOptions = (app, environment) => {
290
- const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
291
- return {
292
- host: `${app.name}-${environment}.${serveDomain}`,
293
- port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
294
- username: process.env.SSH_TUNNEL_USERNAME ?? "root",
295
- password: process.env.SSH_TUNNEL_PASSWORD ?? repoName
296
- };
297
- };
298
188
  var createTunnel = async (service, { app, environment, port = service === "postgres" ? 5432 : 6379 }) => {
299
- const sshOptions = getSshTunnelOptions(app, environment);
189
+ const { serveDomain, repoName } = WorkspaceExecutor.getBaseDevEnv();
300
190
  await createSshTunnel({
301
191
  localHost: "0.0.0.0",
302
192
  localPort: port,
@@ -304,13 +194,20 @@ var createTunnel = async (service, { app, environment, port = service === "postg
304
194
  srcPort: port,
305
195
  dstHost: `${service}-0.${service}-svc.${app.name}-${environment}.svc.cluster.local`,
306
196
  dstPort: service === "postgres" ? 5432 : 6379,
307
- sshOptions
197
+ sshOptions: {
198
+ host: `${app.name}-${environment}.${serveDomain}`,
199
+ port: process.env.SSH_TUNNEL_PORT ? parseInt(process.env.SSH_TUNNEL_PORT) : 32767,
200
+ username: process.env.SSH_TUNNEL_USERNAME ?? "root",
201
+ password: process.env.SSH_TUNNEL_PASSWORD ?? repoName
202
+ }
308
203
  });
309
204
  return `localhost:${port}`;
310
205
  };
311
206
  // pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.host.ts
207
+ import fs from "fs";
312
208
  import path from "path";
313
- import { Logger as Logger2 } from "akanjs/common";
209
+ import { Logger } from "akanjs/common";
210
+ import { CSR_DEV_DIRNAME, CSR_DEV_PATCHING_MARKER, SSR_DEV_DIRNAME } from "akanjs/server/hmr/csrDevManifest";
314
211
  import { MemoryLimit } from "akanjs/server/memoryLimit";
315
212
  var builderMsgTypeSet = new Set([
316
213
  "build-route-res",
@@ -319,16 +216,19 @@ var builderMsgTypeSet = new Set([
319
216
  "invalidate",
320
217
  "css-updated",
321
218
  "pages-updated",
219
+ "csr-updated",
220
+ "ssr-updated",
322
221
  "build-status",
323
- "builder-metrics"
222
+ "builder-metrics",
223
+ "boot-armed"
324
224
  ]);
325
225
 
326
226
  class IncrementalBuilderHost {
327
227
  static #restartBaseDelayMs = 1000;
328
228
  static #restartMaxDelayMs = 30000;
329
229
  static #recycleDrainTimeoutMs = 30000;
330
- static #devMaxRssBytes = 1200 * 1024 * 1024;
331
- logger = new Logger2("IncrementalBuilderHost");
230
+ static #devMaxRssBytes = 2048 * 1024 * 1024;
231
+ logger = new Logger("IncrementalBuilderHost");
332
232
  entry;
333
233
  env;
334
234
  app;
@@ -343,20 +243,33 @@ class IncrementalBuilderHost {
343
243
  #recycleTimer = null;
344
244
  #recycleRequested = false;
345
245
  #spawnAfterRecycle = false;
246
+ #patcherOff = false;
346
247
  #manualStop = false;
347
248
  #inFlight = new Map;
348
249
  #startOptions = {};
349
- constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }) {
250
+ constructor({
251
+ app,
252
+ entry,
253
+ env,
254
+ stdio = "inherit",
255
+ onMessage,
256
+ onOutput,
257
+ patcherOff = false
258
+ }) {
350
259
  this.app = app;
351
260
  this.entry = entry;
352
261
  this.env = env;
353
262
  this.#stdio = stdio;
354
263
  this.#onMessage = onMessage;
355
264
  this.#onOutput = onOutput ?? null;
265
+ this.#patcherOff = patcherOff;
356
266
  }
357
267
  get status() {
358
268
  return this.#status;
359
269
  }
270
+ get patcherOff() {
271
+ return this.#patcherOff;
272
+ }
360
273
  get pid() {
361
274
  return this.#proc?.pid ?? null;
362
275
  }
@@ -374,10 +287,16 @@ class IncrementalBuilderHost {
374
287
  this.ready = false;
375
288
  const afterRecycle = this.#spawnAfterRecycle;
376
289
  this.#spawnAfterRecycle = false;
290
+ this.#checkPatchingMarker(isRestart && !afterRecycle);
377
291
  let proc;
378
292
  proc = Bun.spawn(["bun", this.entry], {
379
293
  cwd: this.app.cwdPath,
380
- env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
294
+ env: {
295
+ ...this.env,
296
+ AKAN_WATCH: "1",
297
+ ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {},
298
+ ...this.#patcherOff ? { AKAN_DEV_CSR_PATCHER: "off" } : {}
299
+ },
381
300
  stdio: ["ignore", this.#stdio, this.#stdio],
382
301
  ipc: (msg) => {
383
302
  if (this.#proc !== proc)
@@ -432,6 +351,17 @@ class IncrementalBuilderHost {
432
351
  }
433
352
  this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
434
353
  }
354
+ #checkPatchingMarker(afterCrash) {
355
+ const markers = [CSR_DEV_DIRNAME, SSR_DEV_DIRNAME].map((dirName) => path.join(this.app.cwdPath, ".akan/artifact", dirName, CSR_DEV_PATCHING_MARKER)).filter((marker) => fs.existsSync(marker));
356
+ if (markers.length === 0)
357
+ return;
358
+ if (afterCrash && !this.#patcherOff) {
359
+ this.#patcherOff = true;
360
+ this.logger.warn("the builder died while patching a dev module registry; registry saves go to a build worker until the next config or metadata restart (AKAN_DEV_CSR_PATCHER=off)");
361
+ }
362
+ for (const marker of markers)
363
+ fs.rmSync(marker, { force: true });
364
+ }
435
365
  async#drain(stream, kind) {
436
366
  if (!stream)
437
367
  return;
@@ -518,12 +448,8 @@ class IncrementalBuilderHost {
518
448
  const lost = [...this.#inFlight];
519
449
  this.#inFlight.clear();
520
450
  this.logger.warn(`failing ${lost.length} unanswered builder request(s): ${reason}`);
521
- for (const [id, type] of lost) {
522
- if (type === "build-route")
523
- this.#onMessage({ type: "build-route-res", id, ok: false, error: reason });
524
- else
525
- this.#onMessage({ type: "build-csr-res", id, ok: false, error: reason });
526
- }
451
+ for (const [id, type] of lost)
452
+ this.#onMessage({ type: `${type}-res`, id, ok: false, error: reason });
527
453
  }
528
454
  stop() {
529
455
  this.#manualStop = true;
@@ -541,7 +467,8 @@ class IncrementalBuilderHost {
541
467
  }
542
468
  static async create(app, env, onMessage, {
543
469
  stdio = "inherit",
544
- onOutput
470
+ onOutput,
471
+ patcherOff = false
545
472
  } = {}) {
546
473
  const candidates = [
547
474
  path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
@@ -551,7 +478,7 @@ class IncrementalBuilderHost {
551
478
  ];
552
479
  for (const c of candidates)
553
480
  if (await Bun.file(c).exists())
554
- return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
481
+ return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput, patcherOff });
555
482
  throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
556
483
  }
557
484
  }
@@ -592,211 +519,7 @@ class BuilderRequestRouter {
592
519
 
593
520
  // pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
594
521
  import { stat } from "fs/promises";
595
- import path3 from "path";
596
-
597
- // pkgs/@akanjs/devkit/akanApp/devHostPolicy.ts
598
522
  import path2 from "path";
599
- var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
600
- var HELD_BUILDER_REQUEST_LIMIT = 64;
601
- var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
602
- var BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
603
- var BUILDER_RSS_HARD_MULTIPLE = 1.5;
604
- var DEV_IDLE_SUSPEND_MS = 300000;
605
- var DEV_IDLE_MIN_UPTIME_MS = 30000;
606
- var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
607
- var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
608
- var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
609
- var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
610
- var shouldRestartBackendByDevPlan = (message) => {
611
- if (!message.devPlan)
612
- return null;
613
- if (message.devPlan.actions.includes("report-error"))
614
- return false;
615
- if (message.devPlan.actions.includes("restart-builder"))
616
- return false;
617
- return message.devPlan.actions.includes("restart-backend");
618
- };
619
- var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includes("restart-builder") ?? false;
620
- var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
621
- var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
622
- var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
623
- var devHostStateByBackendState = {
624
- starting: "starting",
625
- ready: "ready",
626
- "restart-pending": "restarting",
627
- recovering: "recovering",
628
- stopping: "stopped",
629
- stopped: "stopped"
630
- };
631
- var devHostStateOf = (state, gaveUp) => state === "stopped" && gaveUp ? "failed" : devHostStateByBackendState[state];
632
- var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
633
- var generationValue = (generation) => generation ?? -1;
634
- var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
635
- const abs = path2.resolve(file);
636
- const ext = path2.extname(abs).toLowerCase();
637
- if (!SOURCE_EXTS.has(ext))
638
- return false;
639
- const rel = path2.relative(path2.resolve(workspaceRoot), abs);
640
- if (!rel || rel.startsWith("..") || path2.isAbsolute(rel))
641
- return false;
642
- const parts = rel.split(path2.sep).filter(Boolean);
643
- const [scope] = parts;
644
- if (scope !== "apps" && scope !== "libs" && scope !== "pkgs")
645
- return false;
646
- const base = path2.basename(abs);
647
- return parts.includes("srvkit") || parts.includes("common") || SERVER_SUFFIXES.some((suffix) => base.endsWith(suffix)) || SHARED_SUFFIXES.some((suffix) => base.endsWith(suffix)) || RUNTIME_METADATA_BASENAMES.has(base) || base === "main.ts" || base === "server.ts";
648
- };
649
- var shouldMarkBuildPhaseRecovered = (previousByPhase, status) => {
650
- const previous = previousByPhase.get(status.phase);
651
- return Boolean(previous && status.ok && !previous.ok && generationValue(status.generation) >= previous.generation);
652
- };
653
- var createBackendBuildStatus = ({
654
- generation,
655
- ok,
656
- files = [],
657
- message
658
- }) => ({
659
- generation,
660
- phase: "backend",
661
- ok,
662
- files,
663
- message
664
- });
665
- var backendRestartReasonFromMessage = (message) => {
666
- const roleSet = new Set;
667
- for (const role of message.devPlan?.roles ?? []) {
668
- if (role === "server" || role === "shared" || role === "barrel" || role === "config")
669
- roleSet.add(role);
670
- }
671
- return {
672
- generation: message.devPlan?.generation ?? message.generation,
673
- files: [...new Set(message.files)].sort(),
674
- roles: RESTART_ROLE_ORDER.filter((role) => roleSet.has(role))
675
- };
676
- };
677
- var mergeBackendRestartReasons = (current, next) => ({
678
- generation: generationValue(next.generation) >= generationValue(current?.generation) ? next.generation : current?.generation,
679
- files: [...new Set([...current?.files ?? [], ...next.files])].sort(),
680
- roles: RESTART_ROLE_ORDER.filter((role) => current?.roles.includes(role) || next.roles.includes(role))
681
- });
682
- var shouldReplaceLastGoodMessage = (current, next) => !current || generationValue(next.data.generation) >= generationValue(current.data.generation);
683
- var shouldQueueBuildStatusReplay = (backendReady, pendingReplayCount) => !backendReady || pendingReplayCount > 0;
684
- var hasBuildFailureForGeneration = (statusByPhase, generation) => {
685
- if (typeof generation !== "number")
686
- return false;
687
- for (const status of statusByPhase.values()) {
688
- if (!status.ok && status.generation === generation)
689
- return true;
690
- }
691
- return false;
692
- };
693
- var decideBuilderRssRecycle = ({
694
- rssBytes,
695
- ceilingBytes,
696
- buildFailed,
697
- msSinceLastRecycle,
698
- minIntervalMs = BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS
699
- }) => {
700
- if (!ceilingBytes)
701
- return "unbounded";
702
- if (rssBytes < ceilingBytes)
703
- return "below-ceiling";
704
- if (buildFailed)
705
- return "build-failed";
706
- if (msSinceLastRecycle !== null && msSinceLastRecycle < minIntervalMs)
707
- return "too-soon";
708
- return "recycle";
709
- };
710
- var decideBuilderRssSettle = ({
711
- rssBytes,
712
- ceilingBytes,
713
- hardMultiple = BUILDER_RSS_HARD_MULTIPLE
714
- }) => rssBytes >= ceilingBytes * hardMultiple ? "recycle-now" : "wait-and-recheck";
715
- var decideIdleSuspend = ({
716
- enabled,
717
- suspended,
718
- builderReady,
719
- backendReady,
720
- buildFailed,
721
- restartPending,
722
- msSinceWake,
723
- minUptimeMs = DEV_IDLE_MIN_UPTIME_MS
724
- }) => {
725
- if (!enabled)
726
- return "disabled";
727
- if (suspended)
728
- return "already-suspended";
729
- if (!builderReady)
730
- return "builder-not-ready";
731
- if (!backendReady)
732
- return "backend-not-ready";
733
- if (buildFailed)
734
- return "build-failed";
735
- if (restartPending)
736
- return "restart-pending";
737
- if (msSinceWake !== null && msSinceWake < minUptimeMs)
738
- return "too-soon";
739
- return "suspend";
740
- };
741
- var resolveIdleSuspendMs = (raw) => {
742
- if (raw === undefined || raw === "")
743
- return DEV_IDLE_SUSPEND_MS;
744
- const parsed = Number(raw);
745
- if (!Number.isFinite(parsed) || parsed <= 0)
746
- return null;
747
- return Math.round(parsed);
748
- };
749
- var hasAnyBuildFailure = (statusByPhase) => [...statusByPhase.values()].some((status) => !status.ok);
750
- var shouldRefreshConfigOnIdleWake = (batch) => !!batch && batch.kinds.has("config");
751
- var RETURNING_BUILDER_STATUSES = new Set(["starting", "recycling", "restarting"]);
752
- var shouldHoldForReturningBuilder = ({
753
- status,
754
- heldCount,
755
- limit = HELD_BUILDER_REQUEST_LIMIT
756
- }) => RETURNING_BUILDER_STATUSES.has(status) && heldCount < limit;
757
- var shouldWarnBuilderRssCeilingTight = (reportsSinceRecycle, limit = BUILDER_TIGHT_RSS_REPORT_LIMIT) => reportsSinceRecycle >= limit;
758
- var isRssCeilingUnreachable = (freshRssBytes, ceilingBytes) => freshRssBytes !== null && ceilingBytes !== null && freshRssBytes >= ceilingBytes;
759
- var shouldRelayRecycledFrontendState = (current, next) => {
760
- if (!current || current.type !== next.type)
761
- return true;
762
- if (current.type === "pages-updated" && next.type === "pages-updated")
763
- return current.data.bundlePath !== next.data.bundlePath;
764
- if (current.type === "css-updated" && next.type === "css-updated")
765
- return JSON.stringify(current.data.cssAssets) !== JSON.stringify(next.data.cssAssets);
766
- return true;
767
- };
768
- var mergeDevPlans = (current, next) => {
769
- if (!current)
770
- return next;
771
- if (!next)
772
- return current;
773
- const reasonByFile = { ...current.reasonByFile };
774
- for (const [file, reasons] of Object.entries(next.reasonByFile)) {
775
- reasonByFile[file] = [...new Set([...reasonByFile[file] ?? [], ...reasons])].sort();
776
- }
777
- return {
778
- generation: Math.max(current.generation, next.generation),
779
- files: [...new Set([...current.files, ...next.files])].sort(),
780
- generatedFiles: [...new Set([...current.generatedFiles, ...next.generatedFiles])].sort(),
781
- roles: [...new Set([...current.roles, ...next.roles])].sort(),
782
- actions: [...new Set([...current.actions, ...next.actions])].sort(),
783
- reasonByFile
784
- };
785
- };
786
- var mergeInvalidateMessages = (current, next) => {
787
- const generation = Math.max(generationValue(current.generation), generationValue(next.generation));
788
- return {
789
- type: "invalidate",
790
- kinds: [...new Set([...current.kinds, ...next.kinds])].sort(),
791
- files: [...new Set([...current.files, ...next.files])].sort(),
792
- generation: generation >= 0 ? generation : undefined,
793
- devPlan: mergeDevPlans(current.devPlan, next.devPlan)
794
- };
795
- };
796
- var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
797
- var filesChangedSince = (before, after) => [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
798
-
799
- // pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
800
523
  var NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
801
524
  var GRAPH_IMPORT_KINDS = new Set([
802
525
  "import-statement",
@@ -827,7 +550,7 @@ class BackendImportGraph {
827
550
  return this.#lastRefreshSucceeded;
828
551
  }
829
552
  has(file) {
830
- return this.#files.has(path3.resolve(file));
553
+ return this.#files.has(path2.resolve(file));
831
554
  }
832
555
  async fingerprint() {
833
556
  const stamps = await Promise.all([...this.#files].map(async (file) => {
@@ -855,9 +578,9 @@ class BackendImportGraph {
855
578
  const roots = await this.#entrypoints();
856
579
  const files = new Set;
857
580
  const queue = [...roots];
858
- const workspaceRoot = path3.resolve(this.#app.workspace.workspaceRoot);
581
+ const workspaceRoot = path2.resolve(this.#app.workspace.workspaceRoot);
859
582
  while (queue.length > 0) {
860
- const current = path3.resolve(queue.pop());
583
+ const current = path2.resolve(queue.pop());
861
584
  if (files.has(current))
862
585
  continue;
863
586
  if (!this.#isWorkspaceSource(current, workspaceRoot))
@@ -866,7 +589,7 @@ class BackendImportGraph {
866
589
  if (!imports)
867
590
  continue;
868
591
  files.add(current);
869
- const importerDir = path3.dirname(current);
592
+ const importerDir = path2.dirname(current);
870
593
  for (const imp of imports) {
871
594
  if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
872
595
  continue;
@@ -897,7 +620,7 @@ class BackendImportGraph {
897
620
  const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
898
621
  const existing = [];
899
622
  for (const root of roots) {
900
- const abs = path3.resolve(root);
623
+ const abs = path2.resolve(root);
901
624
  if (await Bun.file(abs).exists())
902
625
  existing.push(abs);
903
626
  }
@@ -906,25 +629,25 @@ class BackendImportGraph {
906
629
  #resolve(specifier, importerDir) {
907
630
  try {
908
631
  const resolved = Bun.resolveSync(specifier, importerDir);
909
- if (!path3.isAbsolute(resolved))
632
+ if (!path2.isAbsolute(resolved))
910
633
  return null;
911
- if (!SOURCE_EXTS.has(path3.extname(resolved).toLowerCase()))
634
+ if (!SOURCE_EXTS.has(path2.extname(resolved).toLowerCase()))
912
635
  return null;
913
- return path3.resolve(resolved);
636
+ return path2.resolve(resolved);
914
637
  } catch {
915
638
  return null;
916
639
  }
917
640
  }
918
641
  #isWorkspaceSource(file, workspaceRoot) {
919
- const rel = path3.relative(workspaceRoot, file);
920
- if (rel.startsWith("..") || path3.isAbsolute(rel))
642
+ const rel = path2.relative(workspaceRoot, file);
643
+ if (rel.startsWith("..") || path2.isAbsolute(rel))
921
644
  return false;
922
- if (rel.includes(`${path3.sep}node_modules${path3.sep}`) || rel.includes(`${path3.sep}.akan${path3.sep}`))
645
+ if (rel.includes(`${path2.sep}node_modules${path2.sep}`) || rel.includes(`${path2.sep}.akan${path2.sep}`))
923
646
  return false;
924
- return SOURCE_EXTS.has(path3.extname(file).toLowerCase());
647
+ return SOURCE_EXTS.has(path2.extname(file).toLowerCase());
925
648
  }
926
649
  #scanImports(file, source) {
927
- const ext = path3.extname(file).toLowerCase();
650
+ const ext = path2.extname(file).toLowerCase();
928
651
  if (ext === ".tsx")
929
652
  return this.#tsxTranspiler.scanImports(source);
930
653
  if (ext === ".jsx")
@@ -935,8 +658,44 @@ class BackendImportGraph {
935
658
  }
936
659
  }
937
660
 
661
+ // pkgs/@akanjs/devkit/akanApp/devBootLatch.ts
662
+ class DevBootLatch {
663
+ #onBooted;
664
+ #waitMs;
665
+ #ready = false;
666
+ #armed = false;
667
+ #booted = false;
668
+ #timer = null;
669
+ constructor(onBooted, { waitMs = 30000 } = {}) {
670
+ this.#onBooted = onBooted;
671
+ this.#waitMs = waitMs;
672
+ }
673
+ ready() {
674
+ this.#ready = true;
675
+ if (!this.#armed && !this.#timer) {
676
+ this.#timer = setTimeout(() => this.armed(), this.#waitMs);
677
+ this.#timer.unref?.();
678
+ }
679
+ this.#release();
680
+ }
681
+ armed() {
682
+ this.#armed = true;
683
+ this.#release();
684
+ }
685
+ #release() {
686
+ if (this.#booted || !this.#ready || !this.#armed)
687
+ return;
688
+ this.#booted = true;
689
+ if (this.#timer)
690
+ clearTimeout(this.#timer);
691
+ this.#timer = null;
692
+ this.#onBooted();
693
+ }
694
+ }
695
+
938
696
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
939
697
  var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
698
+ var asMib = (bytes) => Math.round(bytes / 1024 / 1024);
940
699
  var BACKEND_RESTART_DEBOUNCE_MS = 120;
941
700
  var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
942
701
  var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
@@ -952,13 +711,15 @@ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
952
711
 
953
712
  class AkanAppHost {
954
713
  app;
955
- logger = new Logger3("AkanAppHost");
714
+ logger = new Logger2("AkanAppHost");
956
715
  stdio;
957
716
  env;
958
717
  #onDevEvent;
959
718
  #lastDevState = null;
719
+ #bootLatch = new DevBootLatch(() => this.#onDevEvent?.({ app: this.app.name, booted: true }));
960
720
  #backend = null;
961
721
  #builder = null;
722
+ #patcherOff = false;
962
723
  #backendReady = false;
963
724
  #plannedBackendStops = new WeakSet;
964
725
  #restartTimer = null;
@@ -967,7 +728,10 @@ class AkanAppHost {
967
728
  #backendGaveUp = false;
968
729
  #backendLifecycleState = "stopped";
969
730
  #pendingRestartReason = null;
731
+ #backendRestart = null;
970
732
  #pendingRecycle = null;
733
+ #restartFailedAfterApply = null;
734
+ #stopping = false;
971
735
  #builderRecoveryTimer = null;
972
736
  #builderRecoveryAttempts = 0;
973
737
  #backendStartStatus = null;
@@ -995,6 +759,7 @@ class AkanAppHost {
995
759
  #builderGapStamp = null;
996
760
  #pendingBuilderMessages = [];
997
761
  #builderRequests = new BuilderRequestRouter;
762
+ #builderGeneration = 0;
998
763
  constructor(app, {
999
764
  env,
1000
765
  stdio = "inherit",
@@ -1011,6 +776,8 @@ class AkanAppHost {
1011
776
  return;
1012
777
  this.#lastDevState = state;
1013
778
  this.#onDevEvent({ app: this.app.name, state, ...detail ? { detail } : {} });
779
+ if (state === "ready")
780
+ this.#bootLatch.ready();
1014
781
  }
1015
782
  async start() {
1016
783
  if (this.#backend)
@@ -1028,25 +795,24 @@ class AkanAppHost {
1028
795
  return this;
1029
796
  }
1030
797
  async stop() {
798
+ this.#stopping = true;
799
+ this.#pendingRecycle = null;
1031
800
  this.#cancelIdleSuspend();
1032
801
  this.#stopIdleWatcher();
1033
- if (this.#restartTimer) {
1034
- clearTimeout(this.#restartTimer);
1035
- this.#restartTimer = null;
1036
- }
1037
- if (this.#backendRecoveryTimer) {
1038
- clearTimeout(this.#backendRecoveryTimer);
1039
- this.#backendRecoveryTimer = null;
1040
- }
1041
- if (this.#builderRecoveryTimer) {
1042
- clearTimeout(this.#builderRecoveryTimer);
1043
- this.#builderRecoveryTimer = null;
1044
- }
802
+ this.#clearRestartTimers();
803
+ this.#pendingRestartReason = null;
1045
804
  this.#failPendingBuilderMessages("dev server is shutting down");
805
+ await this.#backendRestart;
1046
806
  await this.#stopBackend();
1047
807
  this.#stopBuilder();
1048
808
  return this;
1049
809
  }
810
+ #clearRestartTimers() {
811
+ for (const timer of [this.#restartTimer, this.#backendRecoveryTimer, this.#builderRecoveryTimer])
812
+ if (timer)
813
+ clearTimeout(timer);
814
+ this.#restartTimer = this.#backendRecoveryTimer = this.#builderRecoveryTimer = null;
815
+ }
1050
816
  kill() {
1051
817
  this.stop();
1052
818
  }
@@ -1057,6 +823,12 @@ class AkanAppHost {
1057
823
  return await createTunnel(type, { app: this.app, environment });
1058
824
  }
1059
825
  #startBackend(startStatus = null) {
826
+ if (this.#stopping)
827
+ return;
828
+ if (this.#backend) {
829
+ this.logger.warn(`backend pid=${this.#backend.pid} is still running; not starting a second one`);
830
+ return;
831
+ }
1060
832
  this.#builderRequests.startGeneration();
1061
833
  this.#discardPendingBuilderMessages("the backend restarted while the builder was away");
1062
834
  this.#backendStartStatus = startStatus;
@@ -1077,6 +849,7 @@ class AkanAppHost {
1077
849
  this.#setBackendLifecycleState("ready", `pid=${msg.pid}`);
1078
850
  this.#recordBackendReadyStatus();
1079
851
  this.logger.verbose(`backend ready pid=${msg.pid}`);
852
+ this.#forgetRouteBuildStatus();
1080
853
  this.#replayBuilderState();
1081
854
  return;
1082
855
  }
@@ -1088,6 +861,8 @@ class AkanAppHost {
1088
861
  message: msg.data.message
1089
862
  });
1090
863
  this.#sendOrQueueBuildStatus(status);
864
+ if (!status.ok)
865
+ this.#emitDevEvent("failed", status.message);
1091
866
  return;
1092
867
  }
1093
868
  if (backendMsgTypeSet.has(msg.type))
@@ -1236,11 +1011,17 @@ class AkanAppHost {
1236
1011
  clearTimeout(this.#restartTimer);
1237
1012
  this.#restartTimer = setTimeout(() => {
1238
1013
  this.#restartTimer = null;
1014
+ this.#backendRestart ??= this.#drainBackendRestarts().finally(() => {
1015
+ this.#backendRestart = null;
1016
+ });
1017
+ }, BACKEND_RESTART_DEBOUNCE_MS);
1018
+ }
1019
+ async#drainBackendRestarts() {
1020
+ while (this.#pendingRestartReason && !this.#restartTimer && !this.#stopping) {
1239
1021
  const next = this.#pendingRestartReason;
1240
1022
  this.#pendingRestartReason = null;
1241
- if (next)
1242
- this.#restartBackend(next);
1243
- }, BACKEND_RESTART_DEBOUNCE_MS);
1023
+ await this.#restartBackend(next);
1024
+ }
1244
1025
  }
1245
1026
  async#restartBackend(reason) {
1246
1027
  this.logger.verbose(`[backend-reload] restarting backend generation=${reason.generation ?? "(unknown)"} files=${reason.files.length} roles=${reason.roles.join(",") || "(none)"}`);
@@ -1297,17 +1078,26 @@ ${this.#backendStderrTail.join(`
1297
1078
  });
1298
1079
  }
1299
1080
  async#handleBuilderMessage(message) {
1081
+ if (this.#stopping)
1082
+ return;
1300
1083
  this.#markDevActivity();
1084
+ this.#trackBuilderGeneration(message);
1301
1085
  if (message.type === "build-status") {
1302
1086
  this.#recordBuildStatus(message.data);
1303
1087
  this.#sendOrQueueBuildStatus(message.data);
1304
1088
  this.#reviveBackendAfterGreenBuild(message.data);
1089
+ await this.#resumeFailedRecycle(message.data);
1305
1090
  return;
1306
1091
  }
1307
1092
  if (message.type === "builder-metrics") {
1308
1093
  this.#handleBuilderMetrics(message.data);
1309
1094
  return;
1310
1095
  }
1096
+ if (message.type === "boot-armed") {
1097
+ this.logger.verbose("[builder] boot builds settled");
1098
+ this.#bootLatch.armed();
1099
+ return;
1100
+ }
1311
1101
  if (message.type === "pages-updated" || message.type === "css-updated") {
1312
1102
  const recycled = message.data.reason === "builder-recycle";
1313
1103
  if (recycled && !this.#shouldRelayRecycledState(message))
@@ -1329,6 +1119,22 @@ ${this.#backendStderrTail.join(`
1329
1119
  }
1330
1120
  this.#sendToBackend(message);
1331
1121
  }
1122
+ #trackBuilderGeneration(message) {
1123
+ const generation = AkanAppHost.#builderGenerationOf(message);
1124
+ if (generation === undefined || generation <= this.#builderGeneration)
1125
+ return;
1126
+ this.#builderGeneration = generation;
1127
+ Object.assign(this.env, { AKAN_BUILDER_INITIAL_GENERATION: String(generation) });
1128
+ }
1129
+ static #builderGenerationOf(message) {
1130
+ if (message.type === "invalidate")
1131
+ return message.generation;
1132
+ if (message.type === "build-status" || message.type === "builder-metrics")
1133
+ return message.data.generation;
1134
+ if (message.type === "pages-updated" || message.type === "css-updated")
1135
+ return message.data.generation;
1136
+ return;
1137
+ }
1332
1138
  #shouldRelayRecycledState(message) {
1333
1139
  const current = message.type === "pages-updated" ? this.#lastGoodFrontend.pages : this.#lastGoodFrontend.css;
1334
1140
  if (shouldRelayRecycledFrontendState(current, message)) {
@@ -1342,7 +1148,6 @@ ${this.#backendStderrTail.join(`
1342
1148
  if (this.#rssCeilingAbandoned)
1343
1149
  return;
1344
1150
  const ceilingBytes = IncrementalBuilderHost.maxRssBytes();
1345
- const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
1346
1151
  const decision = decideBuilderRssRecycle({
1347
1152
  rssBytes: metrics.rssBytes,
1348
1153
  ceilingBytes,
@@ -1380,7 +1185,6 @@ ${this.#backendStderrTail.join(`
1380
1185
  if (!isRssCeilingUnreachable(freshRssBytes, ceilingBytes))
1381
1186
  return;
1382
1187
  this.#rssCeilingAbandoned = true;
1383
- const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
1384
1188
  this.logger.error(`[builder-recycle] a freshly recycled builder is already at ${asMib(freshRssBytes ?? 0)}MiB with nothing built on demand, so the ${asMib(ceilingBytes)}MiB ceiling cannot be met for this app; no longer enforcing it this session. Raise AKAN_BUILDER_MAX_RSS_MB, or set it to 0 to leave the builder unbounded.`);
1385
1189
  }
1386
1190
  #armRssRecycle(reason, over) {
@@ -1407,7 +1211,6 @@ ${this.#backendStderrTail.join(`
1407
1211
  }, BUILDER_RSS_RECYCLE_QUIET_MS);
1408
1212
  }
1409
1213
  async#recycleBuilderForRssWhenStillOver(reason, { rssBytes, ceilingBytes }) {
1410
- const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
1411
1214
  if (decideBuilderRssSettle({ rssBytes, ceilingBytes }) === "recycle-now") {
1412
1215
  this.#recycleBuilderForRss(reason);
1413
1216
  return;
@@ -1433,16 +1236,18 @@ ${this.#backendStderrTail.join(`
1433
1236
  this.#recycleBuilderForRss(settledBytes === null ? reason : `${reason}; still ${asMib(settledBytes)}MiB after settling`);
1434
1237
  }
1435
1238
  static async readProcessRssBytes(pid) {
1239
+ if (process.platform === "win32")
1240
+ return await AkanAppHost.#readRssKbVia(["tasklist", "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], AkanAppHost.#tasklistRssKb);
1436
1241
  const status = await Bun.file(`/proc/${pid}/status`).text().catch(() => null);
1437
1242
  const vmRssKb = status === null ? null : /VmRSS:\s+(\d+) kB/.exec(status)?.[1];
1438
1243
  if (vmRssKb)
1439
1244
  return Number(vmRssKb) * 1024;
1440
- return await AkanAppHost.#readRssViaPs(pid);
1245
+ return await AkanAppHost.#readRssKbVia(["ps", "-o", "rss=", "-p", String(pid)], (output) => Number(output.trim()));
1441
1246
  }
1442
- static async#readRssViaPs(pid, timeoutMs = PS_RSS_TIMEOUT_MS) {
1247
+ static async#readRssKbVia(command, parseKb, timeoutMs = PS_RSS_TIMEOUT_MS) {
1443
1248
  let proc;
1444
1249
  try {
1445
- proc = Bun.spawn(["ps", "-o", "rss=", "-p", String(pid)], { stdio: ["ignore", "pipe", "ignore"] });
1250
+ proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
1446
1251
  } catch {
1447
1252
  return null;
1448
1253
  }
@@ -1450,7 +1255,7 @@ ${this.#backendStderrTail.join(`
1450
1255
  try {
1451
1256
  const output = await new Response(proc.stdout).text();
1452
1257
  await proc.exited;
1453
- const rssKb = Number(output.trim());
1258
+ const rssKb = parseKb(output);
1454
1259
  return Number.isFinite(rssKb) && rssKb > 0 ? rssKb * 1024 : null;
1455
1260
  } catch {
1456
1261
  return null;
@@ -1458,6 +1263,10 @@ ${this.#backendStderrTail.join(`
1458
1263
  clearTimeout(killer);
1459
1264
  }
1460
1265
  }
1266
+ static #tasklistRssKb(output) {
1267
+ const memUsage = /"([^"]*)"\s*$/m.exec(output)?.[1];
1268
+ return memUsage ? Number(memUsage.replace(/\D/g, "")) : Number.NaN;
1269
+ }
1461
1270
  #cancelRssRecycle() {
1462
1271
  this.#rssRecycleReason = null;
1463
1272
  this.#rssRecycleOver = null;
@@ -1517,7 +1326,7 @@ ${this.#backendStderrTail.join(`
1517
1326
  this.logger.info(`[idle-suspend] no build activity for ${Math.round(idleMs / 1000)}s; released the builder \u2014 the next edit or route request brings it back`);
1518
1327
  }
1519
1328
  get #restartPending() {
1520
- return !!(this.#pendingRecycle || this.#restartTimer || this.#backendRecoveryTimer || this.#builderRecoveryTimer || this.#rssRecycleReason);
1329
+ return !!(this.#pendingRecycle || this.#restartTimer || this.#backendRestart || this.#backendRecoveryTimer || this.#builderRecoveryTimer || this.#rssRecycleReason);
1521
1330
  }
1522
1331
  async#startIdleWatcher() {
1523
1332
  try {
@@ -1584,16 +1393,14 @@ ${this.#backendStderrTail.join(`
1584
1393
  if (shouldRefreshConfigOnIdleWake(batch)) {
1585
1394
  this.logger.verbose("[idle-suspend] config changed while suspended; restarting the dev host");
1586
1395
  this.#discardBuilderGap("config change replaces the backend anyway");
1587
- await this.#recycleDevChildren({ type: "invalidate", kinds: [...batch?.kinds ?? []], files }, {
1588
- refreshConfig: true
1589
- });
1396
+ await this.#applyRecycle({ type: "invalidate", kinds: [...batch?.kinds ?? []], files }, true);
1590
1397
  return;
1591
1398
  }
1592
1399
  if (files.length > 0)
1593
1400
  await this.#backendGraph.refresh();
1594
1401
  await this.#startBuilder({ announceBootState: true });
1595
1402
  const missed = await this.#takeBuilderGapChanges();
1596
- const backendFiles = [...new Set([...files.filter((file) => this.#isBackendFile(file)), ...missed])];
1403
+ const backendFiles = [...new Set([...files.filter((file) => this.#backendGraph.has(file)), ...missed])];
1597
1404
  if (backendFiles.length === 0)
1598
1405
  return;
1599
1406
  this.logger.verbose(`[idle-suspend] ${backendFiles.length} backend file(s) changed while suspended`);
@@ -1648,10 +1455,6 @@ ${this.#backendStderrTail.join(`
1648
1455
  for (const message of pending)
1649
1456
  this.#sendToBuilder(message);
1650
1457
  }
1651
- #holdUntilBuilderReady(message) {
1652
- this.#pendingBuilderMessages.push(message);
1653
- this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
1654
- }
1655
1458
  #discardPendingBuilderMessages(reason) {
1656
1459
  const held = this.#pendingBuilderMessages.splice(0);
1657
1460
  if (held.length === 0)
@@ -1663,12 +1466,17 @@ ${this.#backendStderrTail.join(`
1663
1466
  if (held.length === 0)
1664
1467
  return;
1665
1468
  this.logger.warn(`failing ${held.length} held builder request(s): ${reason}`);
1666
- for (const message of held) {
1667
- if (message.type === "build-route")
1668
- this.#sendToBackend({ type: "build-route-res", id: message.id, ok: false, error: reason });
1669
- else if (message.type === "build-csr")
1670
- this.#sendToBackend({ type: "build-csr-res", id: message.id, ok: false, error: reason });
1671
- }
1469
+ for (const message of held)
1470
+ this.#failBuilderRequest(message, reason);
1471
+ }
1472
+ #failBuilderRequest(message, error) {
1473
+ if (message.type === "build-route")
1474
+ this.#sendToBackend({ type: "build-route-res", id: message.id, ok: false, error });
1475
+ else if (message.type === "build-csr")
1476
+ this.#sendToBackend({ type: "build-csr-res", id: message.id, ok: false, error });
1477
+ else
1478
+ return false;
1479
+ return true;
1672
1480
  }
1673
1481
  #recycleBuilderForRss(reason) {
1674
1482
  if (this.#pendingRecycle || this.#restartTimer) {
@@ -1686,7 +1494,8 @@ ${this.#backendStderrTail.join(`
1686
1494
  this.#armRssRecycle(this.#rssRecycleReason);
1687
1495
  const wantsDevHostRestart = shouldRestartDevHostByDevPlan(message);
1688
1496
  const pending = this.#pendingRecycle;
1689
- if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || pending && message.kinds.includes("code")) {
1497
+ const resumes = !!pending && message.kinds.includes("code") && this.#touchesFailedRecycle(message.files);
1498
+ if (wantsDevHostRestart || shouldRestartBuilderByDevPlan(message) || resumes) {
1690
1499
  const refreshConfig = wantsDevHostRestart || (pending?.refreshConfig ?? false);
1691
1500
  const merged = pending ? mergeInvalidateMessages(pending.message, message) : message;
1692
1501
  const generation = message.devPlan?.generation ?? message.generation;
@@ -1694,16 +1503,7 @@ ${this.#backendStderrTail.join(`
1694
1503
  this.#deferRecycle(merged, { refreshConfig, generation });
1695
1504
  return;
1696
1505
  }
1697
- this.#pendingRecycle = null;
1698
- try {
1699
- if (refreshConfig)
1700
- await this.#restartDevHost(merged);
1701
- else
1702
- await this.#restartDevChildren(merged);
1703
- } catch (err) {
1704
- this.#recordDevHostRestartFailure(merged, err, refreshConfig ? "Config" : "Runtime metadata");
1705
- this.#resurrectDevChildren(merged);
1706
- }
1506
+ await this.#applyRecycle(merged, refreshConfig);
1707
1507
  return;
1708
1508
  }
1709
1509
  if (await this.#shouldRestartBackend(message)) {
@@ -1712,6 +1512,41 @@ ${this.#backendStderrTail.join(`
1712
1512
  }
1713
1513
  this.#sendToBackend(message);
1714
1514
  }
1515
+ async#applyRecycle(message, refreshConfig) {
1516
+ if (this.#stopping)
1517
+ return;
1518
+ this.#pendingRecycle = null;
1519
+ const progress = { applied: !refreshConfig };
1520
+ try {
1521
+ if (refreshConfig)
1522
+ await this.#restartDevHost(message, progress);
1523
+ else
1524
+ await this.#restartDevChildren(message);
1525
+ } catch (err) {
1526
+ const kind = refreshConfig ? "Config" : "Runtime metadata";
1527
+ const generation = this.#recordDevHostRestartFailure(message, err, kind, { applied: progress.applied });
1528
+ if (progress.applied)
1529
+ this.#restartFailedAfterApply = generation;
1530
+ else
1531
+ this.#pendingRecycle = { message, refreshConfig, failed: true, failedGeneration: generation };
1532
+ this.#resurrectDevChildren(message);
1533
+ }
1534
+ }
1535
+ async#resumeFailedRecycle(status) {
1536
+ const pending = this.#pendingRecycle;
1537
+ if (!pending?.failed || !status.ok || status.phase !== "pages" || !this.#touchesFailedRecycle(status.files))
1538
+ return;
1539
+ if (status.generation <= (pending.failedGeneration ?? -1))
1540
+ return;
1541
+ await this.#applyRecycle(pending.message, pending.refreshConfig);
1542
+ }
1543
+ #touchesFailedRecycle(files) {
1544
+ const pending = this.#pendingRecycle;
1545
+ if (!pending?.failed)
1546
+ return true;
1547
+ const changed = new Set(pending.message.files);
1548
+ return files.some((file) => changed.has(file));
1549
+ }
1715
1550
  #deferRecycle(message, { refreshConfig, generation }) {
1716
1551
  this.#pendingRecycle = { message, refreshConfig };
1717
1552
  const kind = refreshConfig ? "Config" : "Runtime metadata";
@@ -1757,8 +1592,10 @@ ${this.#backendStderrTail.join(`
1757
1592
  this.#builderRecoveryAttempts = 0;
1758
1593
  this.logger.info("[builder-recovery] builder recovered");
1759
1594
  this.#restartBackendForGapChanges();
1595
+ const failedAfterApply = this.#restartFailedAfterApply;
1596
+ this.#restartFailedAfterApply = null;
1760
1597
  const status = {
1761
- generation: reason.generation ?? this.#nextBackendBuildStatusGeneration(),
1598
+ generation: failedAfterApply !== null ? this.#nextBackendBuildStatusGeneration(failedAfterApply + 1) : reason.generation ?? this.#nextBackendBuildStatusGeneration(),
1762
1599
  phase: "scan",
1763
1600
  ok: true,
1764
1601
  files: reason.files,
@@ -1782,36 +1619,29 @@ ${this.#backendStderrTail.join(`
1782
1619
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
1783
1620
  await this.#recycleDevChildren(message);
1784
1621
  }
1785
- async#restartDevHost(message) {
1622
+ async#restartDevHost(message, progress) {
1786
1623
  const generation = message.devPlan?.generation ?? message.generation;
1787
1624
  this.logger.warn(`[dev-host] config change detected; restarting dev host generation=${generation ?? "(unknown)"} files=${message.files.length}`);
1788
- await this.#recycleDevChildren(message, { refreshConfig: true });
1625
+ await this.#recycleDevChildren(message, { refreshConfig: true, progress });
1789
1626
  }
1790
- async#recycleDevChildren(message, { refreshConfig = false } = {}) {
1627
+ async#recycleDevChildren(message, { refreshConfig = false, progress } = {}) {
1791
1628
  const generation = message.devPlan?.generation ?? message.generation;
1792
- if (this.#restartTimer) {
1793
- clearTimeout(this.#restartTimer);
1794
- this.#restartTimer = null;
1795
- }
1796
- if (this.#backendRecoveryTimer) {
1797
- clearTimeout(this.#backendRecoveryTimer);
1798
- this.#backendRecoveryTimer = null;
1799
- }
1800
- if (this.#builderRecoveryTimer) {
1801
- clearTimeout(this.#builderRecoveryTimer);
1802
- this.#builderRecoveryTimer = null;
1803
- }
1629
+ this.#clearRestartTimers();
1804
1630
  this.#builderRecoveryAttempts = 0;
1805
1631
  this.#pendingRestartReason = null;
1632
+ await this.#backendRestart;
1806
1633
  this.#lastGoodFrontend = {};
1807
1634
  this.#buildStatusByPhase.clear();
1808
1635
  this.#pendingBuildStatusReplay = [];
1809
1636
  await this.#stopBackend();
1810
1637
  this.#stopBuilder();
1638
+ this.#patcherOff = false;
1811
1639
  if (refreshConfig) {
1812
1640
  await this.app.getConfig({ refresh: true });
1813
1641
  const { env } = await this.app.prepareCommand("start");
1814
1642
  Object.assign(this.env, env);
1643
+ if (progress)
1644
+ progress.applied = true;
1815
1645
  }
1816
1646
  await this.#backendGraph.refresh();
1817
1647
  await this.#startBuilder();
@@ -1830,22 +1660,27 @@ ${this.#backendStderrTail.join(`
1830
1660
  this.#lastGoodFrontend.css = message;
1831
1661
  this.logger.verbose(`[last-good] css generation=${message.data.generation ?? "(unknown)"} assets=${Object.keys(message.data.cssAssets).length}`);
1832
1662
  }
1833
- #recordDevHostRestartFailure(message, err, kind) {
1663
+ #recordDevHostRestartFailure(message, err, kind, { applied }) {
1834
1664
  const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
1835
1665
  const detail = err instanceof Error ? err.message : String(err);
1836
1666
  this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart failed generation=${generation}: ${detail}`);
1667
+ const names = message.files.slice(0, 3).map((file) => path3.basename(file));
1668
+ const saved = names.length > 0 ? names.join(", ") : "the changed file";
1837
1669
  const status = {
1838
1670
  generation,
1839
1671
  phase: "scan",
1840
1672
  ok: false,
1841
1673
  files: message.files,
1842
- message: `${kind} change failed to apply; recovering the dev server automatically: ${detail}`
1674
+ message: applied ? `${kind} change applied, but the dev server failed to restart: ${detail}. It is recovering on its own.` : `${kind} change failed to apply: ${detail}. The dev server keeps running on the previous ${kind.toLowerCase()}; once it is fixed, save ${saved} again or restart the dev server to apply it.`
1843
1675
  };
1844
1676
  this.#recordBuildStatus(status);
1845
1677
  this.#sendOrQueueBuildStatus(status);
1678
+ return generation;
1846
1679
  }
1847
1680
  #recordBuildStatus(status) {
1848
1681
  const recovered = shouldMarkBuildPhaseRecovered(this.#buildStatusByPhase, status);
1682
+ if (shouldKeepBuildFailure(this.#buildStatusByPhase, status))
1683
+ return;
1849
1684
  this.#buildStatusByPhase.set(status.phase, status);
1850
1685
  const label = `[build-status] generation=${status.generation} phase=${status.phase} ok=${status.ok} files=${status.files.length}`;
1851
1686
  if (status.ok)
@@ -1861,6 +1696,10 @@ ${this.#backendStderrTail.join(`
1861
1696
  }
1862
1697
  this.#sendToBackend({ type: "build-status", data: status });
1863
1698
  }
1699
+ #forgetRouteBuildStatus() {
1700
+ this.#buildStatusByPhase.delete("route");
1701
+ this.#pendingBuildStatusReplay = this.#pendingBuildStatusReplay.filter((status) => status.phase !== "route");
1702
+ }
1864
1703
  #replayBuilderState() {
1865
1704
  if (!this.#backendReady)
1866
1705
  return;
@@ -1890,7 +1729,7 @@ ${this.#backendStderrTail.join(`
1890
1729
  }
1891
1730
  if (message.kinds.includes("code"))
1892
1731
  await this.#backendGraph.refresh();
1893
- if (message.files.some((file) => this.#isBackendFile(file)))
1732
+ if (message.files.some((file) => this.#backendGraph.has(file)))
1894
1733
  return true;
1895
1734
  if (!this.#backendGraph.lastRefreshSucceeded) {
1896
1735
  const fallbackFiles = message.files.filter((file) => isLegacyBackendFallbackFile(file, this.app.workspace.workspaceRoot));
@@ -1901,9 +1740,6 @@ ${this.#backendStderrTail.join(`
1901
1740
  }
1902
1741
  return false;
1903
1742
  }
1904
- #isBackendFile(file) {
1905
- return this.#backendGraph.has(file);
1906
- }
1907
1743
  async#startBuilder({
1908
1744
  announceBootState = false
1909
1745
  } = {}) {
@@ -1915,7 +1751,8 @@ ${this.#backendStderrTail.join(`
1915
1751
  this.#enqueueBuilderMessage(msg);
1916
1752
  }, {
1917
1753
  stdio: this.stdio,
1918
- onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
1754
+ onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text),
1755
+ patcherOff: this.#patcherOff
1919
1756
  });
1920
1757
  try {
1921
1758
  await this.#waitForBuilderReady(attempt, { announceBootState });
@@ -1989,33 +1826,20 @@ ${this.#backendStderrTail.join(`
1989
1826
  return;
1990
1827
  const status = this.#builder?.status ?? "stopped";
1991
1828
  if (shouldHoldForReturningBuilder({ status, heldCount: this.#pendingBuilderMessages.length })) {
1992
- this.#holdUntilBuilderReady(message);
1993
- return;
1994
- }
1995
- if (message.type === "build-route") {
1996
- this.#sendToBackend({
1997
- type: "build-route-res",
1998
- id: message.id,
1999
- ok: false,
2000
- error: `builder is ${status}; reload after the builder is ready`
2001
- });
1829
+ this.#pendingBuilderMessages.push(message);
1830
+ this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
2002
1831
  return;
2003
1832
  }
2004
- if (message.type === "build-csr") {
2005
- this.#sendToBackend({
2006
- type: "build-csr-res",
2007
- id: message.id,
2008
- ok: false,
2009
- error: `builder is ${status}; reload after the builder is ready`
2010
- });
1833
+ if (this.#failBuilderRequest(message, `builder is ${status}; reload after the builder is ready`))
2011
1834
  return;
2012
- }
2013
1835
  this.logger.warn("akanAppHost builder is not running");
2014
1836
  }
2015
1837
  #stopBuilder() {
2016
1838
  this.#cancelRssRecycle();
2017
1839
  if (!this.#builder)
2018
1840
  return;
1841
+ if (this.#builder.patcherOff)
1842
+ this.#patcherOff = true;
2019
1843
  this.#builder.stop();
2020
1844
  this.#builder = null;
2021
1845
  }
@@ -2025,21 +1849,16 @@ import path4 from "path";
2025
1849
  var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
2026
1850
  async function resolveSignalTestPreloadPath(target) {
2027
1851
  const candidates = [];
2028
- const addResolvedPackageCandidate = (basePath) => {
1852
+ for (const basePath of [target.cwdPath, process.cwd(), path4.dirname(Bun.main), import.meta.dir]) {
2029
1853
  try {
2030
1854
  candidates.push(path4.join(path4.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
2031
1855
  } catch {}
2032
- };
2033
- addResolvedPackageCandidate(target.cwdPath);
2034
- addResolvedPackageCandidate(process.cwd());
2035
- addResolvedPackageCandidate(path4.dirname(Bun.main));
2036
- addResolvedPackageCandidate(import.meta.dir);
1856
+ }
2037
1857
  candidates.push(path4.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.join(path4.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path4.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
2038
1858
  const uniqueCandidates = [...new Set(candidates)];
2039
- for (const candidate of uniqueCandidates) {
1859
+ for (const candidate of uniqueCandidates)
2040
1860
  if (await Bun.file(candidate).exists())
2041
1861
  return candidate;
2042
- }
2043
1862
  throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
2044
1863
  Probed paths:
2045
1864
  ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
@@ -2047,21 +1866,17 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
2047
1866
  }
2048
1867
 
2049
1868
  // pkgs/@akanjs/cli/application/application.runner.ts
2050
- import { Logger as Logger4 } from "akanjs/common";
2051
- import ora from "ora";
2052
- var loadBuildRunner = async () => (await import("./applicationBuildRunner-kxwbssgn.js")).ApplicationBuildRunner;
2053
- var loadReleasePackager = async () => (await import("./applicationReleasePackager-4yasxwv9.js")).ApplicationReleasePackager;
2054
- var loadCapacitorApp = async () => (await import("./capacitorApp-36rbr858.js")).CapacitorApp;
2055
- var loadAbstractCompactor = async () => (await import("./abstractCompactor-gmkc0z8j.js")).AbstractCompactor;
1869
+ import { Logger as Logger3 } from "akanjs/common";
1870
+ var loadBuildRunner = async () => (await import("./applicationBuildRunner-64taxseg.js")).ApplicationBuildRunner;
2056
1871
  var loadPrompts = async () => await import("@inquirer/prompts");
2057
1872
 
2058
1873
  class ApplicationRunner extends runner("application") {
2059
- async createApplication(appName, workspace, libs = []) {
1874
+ async createApplication(appName, workspace, libs = [], { sample = false } = {}) {
2060
1875
  await workspace.applyTemplate({
2061
1876
  basePath: `apps/${appName}`,
2062
1877
  template: "app",
2063
1878
  dict: { appName },
2064
- options: { libs }
1879
+ options: { libs, sample }
2065
1880
  });
2066
1881
  return AppExecutor.from(workspace, appName);
2067
1882
  }
@@ -2071,9 +1886,6 @@ class ApplicationRunner extends runner("application") {
2071
1886
  async removeApplication(app) {
2072
1887
  await app.workspace.removeDir(`apps/${app.name}`);
2073
1888
  }
2074
- async getConfig(app) {
2075
- return await app.getConfig();
2076
- }
2077
1889
  async getScriptFilename(app) {
2078
1890
  if (!await app.exists("script")) {
2079
1891
  await app.mkdir("script");
@@ -2095,7 +1907,7 @@ class ApplicationRunner extends runner("application") {
2095
1907
  if (!await app.exists(scriptPath))
2096
1908
  throw new Error(`Script file not found: apps/${app.name}/${scriptPath}`);
2097
1909
  await app.spawn("bun", [scriptPath], {
2098
- env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script" }),
1910
+ env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
2099
1911
  stdio: "inherit"
2100
1912
  });
2101
1913
  }
@@ -2110,13 +1922,10 @@ class ApplicationRunner extends runner("application") {
2110
1922
  const { level, grep, endpoint, trace, child, role, origin, since } = options;
2111
1923
  const query = Object.fromEntries(Object.entries({ level, grep, endpoint, trace, child, role, origin, since }).filter(([, value]) => value !== undefined && value !== null && value !== ""));
2112
1924
  const print = (record) => process.stdout.write(options.json ? `${JSON.stringify(record)}
2113
- ` : Logger4.render(record));
1925
+ ` : Logger3.render(record));
2114
1926
  const note = (text) => process.stderr.write(`[akan logs] ${text}
2115
1927
  `);
2116
- let finish = null;
2117
- const closed = new Promise((resolve) => {
2118
- finish = resolve;
2119
- });
1928
+ const { promise: closed, resolve: finish } = Promise.withResolvers();
2120
1929
  let client;
2121
1930
  try {
2122
1931
  client = await LogTailClient.connect(socketPath, {
@@ -2125,7 +1934,7 @@ class ApplicationRunner extends runner("application") {
2125
1934
  if (response.type === "dropped")
2126
1935
  note(`${response.count} records dropped (reader too slow)`);
2127
1936
  },
2128
- onClose: () => finish?.()
1937
+ onClose: () => finish()
2129
1938
  });
2130
1939
  } catch (error) {
2131
1940
  if (error instanceof LogControlUnavailableError)
@@ -2167,7 +1976,7 @@ const serverModule = await import(${JSON.stringify(serverPath)});
2167
1976
  const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
2168
1977
  const server = serverModule.server;
2169
1978
  if (!server?.start) throw new Error("server.ts must export server with start()");
2170
- assertAkanConsoleAllowed(server.env);
1979
+ assertAkanConsoleAllowed();
2171
1980
  await server.start({ listen: false, web: false });
2172
1981
  try {
2173
1982
  await startAkanConsole(server, {
@@ -2186,17 +1995,35 @@ try {
2186
1995
  }
2187
1996
  `;
2188
1997
  await app.spawn("bun", ["-e", code], {
2189
- env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console" }),
1998
+ env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "console", ...await app.getDatabaseModeEnv() }),
1999
+ stdio: "inherit"
2000
+ });
2001
+ }
2002
+ async transferDatabase(app, direction, dir) {
2003
+ const serverPath = `${app.cwdPath}/server.ts`;
2004
+ if (!await app.exists("server.ts"))
2005
+ throw new Error(`Server file not found: apps/${app.name}/server.ts`);
2006
+ const target = path5.resolve(app.workspace.workspaceRoot, dir);
2007
+ const code = `
2008
+ const { server } = await import(${JSON.stringify(serverPath)});
2009
+ const { DatabaseAdaptorRole, DocumentTransfer } = await import("akanjs/service");
2010
+ await server.start({ listen: false, web: false });
2011
+ try {
2012
+ const transfer = new DocumentTransfer(server.get(DatabaseAdaptorRole));
2013
+ const reports = await transfer.${direction === "export" ? "exportTo" : "importFrom"}(${JSON.stringify(target)});
2014
+ for (const { table, rows } of reports) console.info(\`${direction === "export" ? "exported" : "imported"} \${table}: \${rows} rows\`);
2015
+ } finally {
2016
+ await server.stop();
2017
+ }
2018
+ `;
2019
+ await app.spawn("bun", ["-e", code], {
2020
+ env: app.getCommandEnv({ AKAN_COMMAND_TYPE: "script", ...await app.getDatabaseModeEnv() }),
2190
2021
  stdio: "inherit"
2191
2022
  });
2192
2023
  }
2193
2024
  async typecheck(app, options = {}) {
2194
2025
  await new (await loadBuildRunner())(app).typecheck(options);
2195
2026
  }
2196
- async compact(sys, { module, minLines, interactive } = {}) {
2197
- const compactor = new (await loadAbstractCompactor())(sys, { minLines, interactive });
2198
- return await compactor.compactAll({ module });
2199
- }
2200
2027
  async test(exec) {
2201
2028
  const isSignalTarget = exec instanceof AppExecutor || exec instanceof LibExecutor;
2202
2029
  const preloadPath = isSignalTarget ? await resolveSignalTestPreloadPath(exec) : null;
@@ -2232,72 +2059,188 @@ try {
2232
2059
  setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
2233
2060
  return appHost;
2234
2061
  }
2235
- async buildIos(app, { target, env = "debug", regenerate = false } = {}) {
2062
+ async buildMobile(app, platform, { target, env = "debug", profile = "release", installer = false, arch } = {}) {
2236
2063
  const targets = await resolveMobileTargets(app, target);
2237
- await this.#buildMobileCsr(app, env);
2064
+ NativeApp.assertInstaller(platform, installer);
2065
+ if (arch && platform !== "macos" && platform !== "windows" && platform !== "linux")
2066
+ throw new Error(`--arch picks a desktop app's CPU; ${platform} builds for its own.`);
2067
+ if (arch === "x64" && platform === "macos")
2068
+ throw new Error("A macOS app is built for Apple silicon (arm64) only.");
2069
+ const carried = await this.#stageMobile(app, platform, targets, env, arch);
2238
2070
  await this.#runMobileTargets(targets, async (mobileTarget) => {
2239
- await new (await loadCapacitorApp())(app, mobileTarget.config).buildIos({ env, regenerate });
2071
+ this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).build(platform, {
2072
+ profile,
2073
+ ...carried && ApplicationRunner.carriesServer(mobileTarget, platform) ? { server: carried } : {},
2074
+ ...installer ? { installer } : {},
2075
+ ...arch ? { arch } : {}
2076
+ }));
2240
2077
  });
2241
2078
  }
2242
- async startIos(app, {
2243
- open = false,
2244
- operation = "local",
2245
- env = "local",
2246
- target,
2247
- device,
2248
- regenerate = false,
2249
- noAllowProvisioningUpdates = false
2250
- } = {}) {
2251
- const targets = await resolveMobileTargets(app, target);
2252
- if (operation === "release")
2253
- await this.#buildMobileCsr(app, env);
2254
- await this.#runMobileTargets(targets, async (mobileTarget) => {
2255
- const capacitorApp = new (await loadCapacitorApp())(app, mobileTarget.config);
2256
- await capacitorApp.runIos({ operation, env, regenerate, noAllowProvisioningUpdates, iosDeviceId: device });
2257
- if (open)
2258
- await capacitorApp.openIos();
2259
- });
2079
+ async buildDesktop(app, options = {}) {
2080
+ await this.buildMobile(app, NativeApp.desktopPlatform(), options);
2081
+ }
2082
+ async startMobile(app, platform, { target, env = "local", operation = "local", device, teamId, interrupt } = {}) {
2083
+ const mobileTarget = await ApplicationRunner.startTarget(app, platform, target);
2084
+ const nativeApp = new NativeApp(app, mobileTarget, env);
2085
+ const selection = { ...device ? { device } : {}, ...teamId ? { teamId } : {} };
2086
+ if (operation === "release") {
2087
+ const carried = await this.#stageMobile(app, platform, [mobileTarget], env);
2088
+ const running = await nativeApp.run(platform, {
2089
+ ...selection,
2090
+ profile: "release",
2091
+ ...carried ? { server: carried } : {}
2092
+ });
2093
+ await running.exited;
2094
+ return;
2095
+ }
2096
+ const upstream = `http://localhost:${await app.getDevPort()}`;
2097
+ if (!await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
2098
+ throw new Error(`No dev server answers on ${upstream}; run \`akan start ${app.name}\` first.`);
2099
+ const { i18n } = await app.getConfig();
2100
+ const session = await nativeApp.dev(platform, { upstream, lang: i18n.defaultLocale, ...selection });
2101
+ if (interrupt)
2102
+ interrupt.add(async () => await session.stop(), "Abandoning the app's shutdown; its window may stay open.", 130);
2103
+ else
2104
+ process.once("SIGINT", () => {
2105
+ session.stop().finally(() => process.exit(130));
2106
+ });
2107
+ await session.exited;
2260
2108
  }
2261
- async releaseIos(app, { target, env = "main", regenerate = false } = {}) {
2109
+ async startDesktop(app, options = {}) {
2110
+ await this.startMobile(app, NativeApp.desktopPlatform(), options);
2111
+ }
2112
+ static async startTarget(app, platform = NativeApp.desktopPlatform(), target) {
2262
2113
  const targets = await resolveMobileTargets(app, target);
2263
- await this.#buildMobileCsr(app, env);
2264
- for (const mobileTarget of targets) {
2265
- await new (await loadCapacitorApp())(app, mobileTarget.config).buildIos({ env, regenerate });
2114
+ const [mobileTarget] = targets;
2115
+ if (!mobileTarget || targets.length > 1)
2116
+ throw new Error(`start-${platform === "ios" || platform === "android" ? platform : "desktop"} runs one native target at a time; pass --target <name>.`);
2117
+ return mobileTarget;
2118
+ }
2119
+ static async answers(url, appName, workspaceRoot) {
2120
+ const signal = AbortSignal.timeout(3000);
2121
+ const health = await fetch(new URL("/_akan/app/health", url), { signal }).catch(() => null);
2122
+ if (!health)
2123
+ return false;
2124
+ const info = await fetch(new URL("/_akan/app/info", url), { signal }).then(async (res) => res.ok ? await res.json() : null).catch(() => null);
2125
+ const stopIt = `Stop it (\`akan start ${appName} --kill\` takes the port over) or give ${appName} another port with AKAN_DEV_PORT`;
2126
+ if (info?.appName === appName) {
2127
+ if (typeof info.workspaceRoot !== "string" || ApplicationRunner.#sameCheckout(info.workspaceRoot, workspaceRoot))
2128
+ return true;
2129
+ throw new Error(`${url} is the dev server of ${appName} in ${info.workspaceRoot}, not in ${workspaceRoot}. ${stopIt}.`);
2266
2130
  }
2131
+ if (typeof info?.appName === "string")
2132
+ throw new Error(`${url} is the dev server of ${info.appName}, not ${appName}. ${stopIt}.`);
2133
+ const { pid } = await health.json().catch(() => null) ?? {};
2134
+ throw new Error(typeof pid === "number" ? `${url} is an akan dev server (pid ${pid}) too old to say which app it serves. ${stopIt}.` : `${url} answers, but not as an akan dev server. Stop what holds the port or give ${appName} another port with AKAN_DEV_PORT.`);
2267
2135
  }
2268
- async buildAndroid(app, { target, env = "debug", regenerate = false } = {}) {
2136
+ static #sameCheckout(reported, own) {
2137
+ const canonical = (root) => {
2138
+ try {
2139
+ return realpathSync.native(root);
2140
+ } catch {
2141
+ return path5.resolve(root);
2142
+ }
2143
+ };
2144
+ return canonical(reported) === canonical(own);
2145
+ }
2146
+ async releaseIos(app, { target, env = "main", teamId, adHoc = false } = {}) {
2269
2147
  const targets = await resolveMobileTargets(app, target);
2270
2148
  await this.#buildMobileCsr(app, env);
2271
- await this.#runMobileTargets(targets, async (mobileTarget) => {
2272
- await new (await loadCapacitorApp())(app, mobileTarget.config).buildAndroid("apk", { env, regenerate });
2273
- });
2149
+ for (const mobileTarget of targets)
2150
+ this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseIos({ teamId, adHoc }));
2274
2151
  }
2275
- async startAndroid(app, {
2276
- open = false,
2277
- operation = "local",
2278
- env = "local",
2279
- target,
2280
- regenerate = false
2281
- } = {}) {
2152
+ async releaseAndroid(app, format, { target, env = "main" } = {}) {
2282
2153
  const targets = await resolveMobileTargets(app, target);
2283
- if (operation === "release")
2284
- await this.#buildMobileCsr(app, env);
2285
- await this.#runMobileTargets(targets, async (mobileTarget) => {
2286
- const capacitorApp = new (await loadCapacitorApp())(app, mobileTarget.config);
2287
- await capacitorApp.runAndroid({ operation, env, regenerate });
2288
- if (open)
2289
- await capacitorApp.openAndroid();
2290
- });
2154
+ NativeApp.androidSigning();
2155
+ await this.#buildMobileCsr(app, env);
2156
+ for (const mobileTarget of targets)
2157
+ this.#reportBuild(app, mobileTarget, await new NativeApp(app, mobileTarget, env).releaseAndroid({ formats: [format] }));
2291
2158
  }
2292
- async releaseAndroid(app, assembleType, { target, env = "main", regenerate = false } = {}) {
2159
+ async packUpdate(app, platform, { target, env = "main", out, against }) {
2293
2160
  const targets = await resolveMobileTargets(app, target);
2161
+ const [mobileTarget] = targets;
2162
+ if (!mobileTarget || targets.length > 1)
2163
+ throw new Error("pack-update packs one native target at a time; pass --target <name>.");
2294
2164
  await this.#buildMobileCsr(app, env);
2295
- for (const mobileTarget of targets) {
2296
- await new (await loadCapacitorApp())(app, mobileTarget.config).buildAndroid(assembleType, { env, regenerate });
2297
- app.log(`Release Android ${app.name}/${mobileTarget.name} ${assembleType} Completed.`);
2298
- app.log(`Path : ${app.cwdPath}/android/app/build/outputs/${assembleType === "apk" ? "apk" : "bundle"}/release`);
2165
+ const packed = await new NativeApp(app, mobileTarget, env).packUpdate(platform, { out });
2166
+ const size = packed.manifest.files.reduce((sum, file) => sum + file.size, 0);
2167
+ app.logger.info(`packed ${mobileTarget.name} ${platform} for channel '${packed.channel}' to ${packed.out}: ${packed.manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB, native API ${packed.manifest.nativeApi}`);
2168
+ if (!against)
2169
+ return;
2170
+ const bundle = path5.join(packed.out, "bundle.json");
2171
+ const compat = await NativeApp.compareBundles(app.cwdPath, against, bundle);
2172
+ await Bun.write(path5.join(packed.out, "compat.json"), `${JSON.stringify({ against, ...compat }, null, 2)}
2173
+ `);
2174
+ if (!compat.compatible)
2175
+ throw new Error(`This bundle needs a new store build; the one ${against} came from provides another native API:
2176
+ - ${compat.problems.join(`
2177
+ - `)}`);
2178
+ app.logger.info(`runs in the store build of ${against}`);
2179
+ }
2180
+ async updateKeygen(app, platform, target) {
2181
+ const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
2182
+ const seen = new Set;
2183
+ for (const mobileTarget of await resolveMobileTargets(app, target ?? "all")) {
2184
+ const appId = resolveAppId(mobileTarget.config.appId, nativePlatform);
2185
+ if (seen.has(appId))
2186
+ continue;
2187
+ seen.add(appId);
2188
+ const { publicKey, keyPath, created } = await new NativeApp(app, mobileTarget).updateKeygen(nativePlatform);
2189
+ app.log(`${appId}: ${created ? "made" : "read"} ${keyPath}${created ? " (keep it private and backed up: without it, installed apps take no more updates)" : ""}`);
2190
+ app.log(` native: { updates: { url: "https://\u2026/${app.name}", publicKey: ${JSON.stringify(publicKey)} } }`);
2191
+ const declared = mobileTarget.config.updates?.publicKey;
2192
+ if (declared && declared !== publicKey)
2193
+ app.logger.warn(`native.targets.${mobileTarget.name}.updates.publicKey is another key's; releases would not verify.`);
2299
2194
  }
2300
2195
  }
2196
+ async publishUpdate(app, platform, { target, env = "main", channel } = {}) {
2197
+ const targets = await resolveMobileTargets(app, target);
2198
+ const nativePlatform = platform === "desktop" ? NativeApp.desktopPlatform() : platform;
2199
+ for (const mobileTarget of targets)
2200
+ await new NativeApp(app, mobileTarget, env).assertPublishable(nativePlatform, {
2201
+ server: ApplicationRunner.carriesServer(mobileTarget, nativePlatform),
2202
+ ...channel ? { channel } : {}
2203
+ });
2204
+ const carried = await this.#stageMobile(app, nativePlatform, targets, env);
2205
+ for (const mobileTarget of targets) {
2206
+ const nativeApp = new NativeApp(app, mobileTarget, env);
2207
+ const published = await nativeApp.publishUpdate(nativePlatform, {
2208
+ ...channel ? { channel } : {},
2209
+ ...carried && ApplicationRunner.carriesServer(mobileTarget, nativePlatform) ? { server: carried } : {}
2210
+ });
2211
+ this.#reportBuild(app, mobileTarget, published.build);
2212
+ app.log(`${app.name}/${mobileTarget.name} ${published.bundle}: ${published.dir}/${published.channel}.json, ${published.files} files, ${Math.round(published.size / 1024)} KiB`);
2213
+ app.log(`Upload ${nativeApp.updatesDir} to ${mobileTarget.config.updates?.url}, the manifests last.`);
2214
+ }
2215
+ }
2216
+ #reportBuild(app, mobileTarget, { artifacts, warnings, signing }) {
2217
+ for (const warning of warnings)
2218
+ app.logger.warn(warning);
2219
+ if (signing)
2220
+ app.log(`Signed with ${signing.identity} (${signing.profile}, team ${signing.teamId})`);
2221
+ for (const artifact of artifacts)
2222
+ app.log(`${app.name}/${mobileTarget.name} ${artifact.kind}: ${artifact.path}`);
2223
+ }
2224
+ static carriesServer({ config }, platform = NativeApp.desktopPlatform()) {
2225
+ return platform !== "ios" && platform !== "android" && !!config.desktop?.server;
2226
+ }
2227
+ static serverOmit(targets, platform) {
2228
+ const carrying = targets.filter((target) => ApplicationRunner.carriesServer(target, platform));
2229
+ const omits = carrying.map(({ config }) => {
2230
+ const server = config.desktop?.server;
2231
+ return typeof server === "object" ? server.omit ?? [] : [];
2232
+ });
2233
+ const [first = [], ...rest] = omits;
2234
+ if (rest.some((omit) => omit.join("\x00") !== first.join("\x00")))
2235
+ throw new Error(`The targets that carry the server omit different packages (${carrying.map(({ name }, idx) => `${name}: ${omits[idx]?.join(", ") || "none"}`).join("; ")}); build them one --target at a time.`);
2236
+ return first;
2237
+ }
2238
+ async#stageMobile(app, platform, targets, env, arch) {
2239
+ const stage = targets.some((mobileTarget) => ApplicationRunner.carriesServer(mobileTarget, platform)) ? new DesktopServerStage(app) : null;
2240
+ await stage?.install(arch, ApplicationRunner.serverOmit(targets, platform));
2241
+ await this.#buildMobileCsr(app, env);
2242
+ return await stage?.prepare(env);
2243
+ }
2301
2244
  async#buildMobileCsr(app, env) {
2302
2245
  const prevEnv = {
2303
2246
  AKAN_PUBLIC_ENV: process.env.AKAN_PUBLIC_ENV,
@@ -2310,7 +2253,7 @@ try {
2310
2253
  APP_OPERATION_MODE: "release"
2311
2254
  });
2312
2255
  try {
2313
- await new (await loadBuildRunner())(app).build({ spinner: true });
2256
+ await new (await loadBuildRunner())(app, { environment: env }).build({ spinner: true });
2314
2257
  } finally {
2315
2258
  for (const [key, value] of Object.entries(prevEnv)) {
2316
2259
  if (value === undefined)
@@ -2335,24 +2278,23 @@ try {
2335
2278
  return;
2336
2279
  for (const failure of failures) {
2337
2280
  const message = failure.error instanceof Error ? failure.error.message : String(failure.error);
2338
- Logger4.rawLog(`Mobile target ${failure.target} failed: ${message}`, undefined, "error");
2281
+ Logger3.rawLog(`Native target ${failure.target} failed: ${message}`, undefined, "error");
2339
2282
  }
2340
- throw new Error(`${failures.length}/${results.length} mobile targets failed`);
2341
- }
2342
- async codepush(app, os) {
2343
- const [target] = await resolveMobileTargets(app, undefined);
2344
- if (!target)
2345
- throw new Error(`No mobile target configured for ${app.name}`);
2346
- const capacitorApp = new (await loadCapacitorApp())(app, target.config);
2347
- await capacitorApp.init();
2283
+ throw new Error(`${failures.length}/${results.length} native targets failed`);
2348
2284
  }
2349
2285
  #getLocalDatabaseServices(mode) {
2350
2286
  if (mode === "single")
2351
2287
  return [];
2352
2288
  if (mode === "multiple")
2353
- return ["redis", "libsql"];
2289
+ return ["redis"];
2354
2290
  return ["redis", "postgres"];
2355
2291
  }
2292
+ async#assertComposeHas(workspace, services) {
2293
+ const compose = Bun.YAML.parse(await Bun.file(`${workspace.workspaceRoot}/local/docker-compose.yaml`).text());
2294
+ const missing = services.filter((service) => !compose?.services?.[service]);
2295
+ if (missing.length)
2296
+ throw new Error(`local/docker-compose.yaml declares no ${missing.join(" or ")} service. Add it, or move the file aside so the next akan dbup writes the current template.`);
2297
+ }
2356
2298
  async#isLocalDatabaseUp(workspace, mode) {
2357
2299
  const requiredServices = this.#getLocalDatabaseServices(mode);
2358
2300
  if (!requiredServices.length)
@@ -2366,16 +2308,18 @@ try {
2366
2308
  async dbup(workspace, mode = "multiple") {
2367
2309
  if (mode === "single")
2368
2310
  return true;
2311
+ const services = this.#getLocalDatabaseServices(mode);
2312
+ await workspace.applyTemplate({
2313
+ basePath: "local",
2314
+ template: "localDev",
2315
+ dict: { repoName: workspace.repoName },
2316
+ overwrite: false
2317
+ });
2318
+ await this.#assertComposeHas(workspace, services);
2369
2319
  try {
2370
- await workspace.applyTemplate({
2371
- basePath: "local",
2372
- template: "localDev",
2373
- dict: { repoName: workspace.repoName },
2374
- overwrite: false
2375
- });
2376
2320
  const wasAlreadyUp = await this.#isLocalDatabaseUp(workspace, mode);
2377
2321
  if (!wasAlreadyUp)
2378
- await workspace.spawn(`docker`, ["compose", "up", "-d", ...this.#getLocalDatabaseServices(mode)], {
2322
+ await workspace.spawn(`docker`, ["compose", "up", "-d", ...services], {
2379
2323
  cwd: `${workspace.workspaceRoot}/local`
2380
2324
  });
2381
2325
  return wasAlreadyUp;
@@ -2393,74 +2337,182 @@ ${detail}`
2393
2337
  async dbdown(workspace) {
2394
2338
  await workspace.spawn(`docker`, ["compose", "down"], { cwd: `${workspace.workspaceRoot}/local` });
2395
2339
  }
2396
- async configureApp(app) {
2397
- const [target] = await resolveMobileTargets(app, undefined);
2398
- if (!target)
2399
- throw new Error(`No mobile target configured for ${app.name}`);
2400
- const capacitorApp = new (await loadCapacitorApp())(app, target.config);
2401
- await capacitorApp.init();
2402
- if (await (await loadPrompts()).confirm({ message: "want to add camera permission?" }))
2403
- await capacitorApp.addCamera();
2404
- if (await (await loadPrompts()).confirm({ message: "want to add contact permission?" }))
2405
- await capacitorApp.addContact();
2406
- if (await (await loadPrompts()).confirm({ message: "want to add location permission?" }))
2407
- await capacitorApp.addLocation();
2408
- await capacitorApp.save();
2409
- }
2410
- async releaseSource(app, { rebuild, buildNum = 0, environment = "debug", local = true } = {}) {
2411
- await new (await loadReleasePackager())(app, { build: () => this.build(app).then(() => {
2412
- return;
2413
- }) }).releaseSource({
2414
- rebuild,
2415
- buildNum,
2416
- environment,
2417
- local
2418
- });
2419
- return;
2340
+ }
2341
+
2342
+ // pkgs/@akanjs/cli/application/hostProcessProbe.ts
2343
+ import { readdirSync, readFileSync, readlinkSync } from "fs";
2344
+
2345
+ class HostProcessProbe {
2346
+ static timeoutMs = 3000;
2347
+ static win32TimeoutMs = 20000;
2348
+ static #procTcpListen = "0A";
2349
+ static #win32ProcessQuery = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress";
2350
+ platform;
2351
+ #win32Table = null;
2352
+ constructor(platform = process.platform) {
2353
+ this.platform = platform;
2354
+ }
2355
+ async listenersOn(port) {
2356
+ if (this.platform === "win32")
2357
+ return HostProcessProbe.parseNetstat(await HostProcessProbe.run(["netstat", "-ano"]), port);
2358
+ if (this.platform === "linux")
2359
+ return HostProcessProbe.#procListenersOn(port);
2360
+ const output = await HostProcessProbe.run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
2361
+ return [
2362
+ ...new Set(output.split(`
2363
+ `).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
2364
+ ];
2420
2365
  }
2421
- async createApplicationTemplate(workspace, appName) {
2422
- await workspace.applyTemplate({ basePath: `apps/${appName}`, template: "appRoot", dict: { appName } });
2366
+ async parentOf(pid) {
2367
+ if (this.platform === "win32") {
2368
+ const child = await this.#win32Process(pid);
2369
+ const parent = child ? await this.#win32Process(child.ppid) : null;
2370
+ return child && parent ? HostProcessProbe.parentPidOf(child, parent) : null;
2371
+ }
2372
+ if (this.platform === "linux") {
2373
+ const stat = HostProcessProbe.#readProc(pid, "stat");
2374
+ return stat === null ? null : HostProcessProbe.parseProcStatPpid(stat);
2375
+ }
2376
+ const parsed = Number((await HostProcessProbe.run(["ps", "-p", String(pid), "-o", "ppid="])).trim());
2377
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
2423
2378
  }
2424
- async compressProjectFiles(app, { rebuild, buildNum = 0, environment = "debug", local = true } = {}) {
2425
- await new (await loadReleasePackager())(app, {
2426
- build: () => this.build(app).then(() => {
2427
- return;
2428
- })
2429
- }).compressProjectFiles({
2430
- rebuild,
2431
- buildNum,
2432
- environment,
2433
- local
2434
- });
2435
- return;
2379
+ async commandOf(pid) {
2380
+ if (this.platform === "win32")
2381
+ return (await this.#win32Process(pid))?.command ?? null;
2382
+ if (this.platform === "linux") {
2383
+ const cmdline = HostProcessProbe.#readProc(pid, "cmdline");
2384
+ return cmdline?.split("\x00").filter(Boolean).join(" ") || null;
2385
+ }
2386
+ return (await HostProcessProbe.run(["ps", "-ww", "-p", String(pid), "-o", "command="])).trim() || null;
2436
2387
  }
2437
- async generateApplicationTemplate(app) {
2438
- const openAIApiKey = process.env.OPENAI_API_KEY;
2439
- if (!openAIApiKey)
2440
- throw new Error("OPENAI_API_KEY is not set");
2441
- const [{ StringOutputParser }, { PromptTemplate }, { RunnableSequence }, { ChatOpenAI }, prompts] = await Promise.all([
2442
- import("@langchain/core/output_parsers"),
2443
- import("@langchain/core/prompts"),
2444
- import("@langchain/core/runnables"),
2445
- import("@langchain/openai"),
2446
- loadPrompts()
2447
- ]);
2448
- const chatModel = new ChatOpenAI({ modelName: "gpt-4o", openAIApiKey });
2449
- const projectName = await prompts.input({ message: "please enter project name." });
2450
- const projectDesc = await prompts.input({
2451
- message: "please enter project description. (40 ~ 60 characters)"
2452
- });
2453
- const spinner = ora("Gerating project files...");
2454
- const mainPrompt = PromptTemplate.fromTemplate(`prompt.requestApplication()`);
2455
- const chain = RunnableSequence.from([mainPrompt, chatModel, new StringOutputParser]);
2456
- await chain.invoke({ projectName, projectDesc });
2457
- spinner.succeed("Loading complete!");
2388
+ static parentPidOf(child, parent) {
2389
+ if (child.ppid !== parent.pid)
2390
+ return null;
2391
+ if (parent.startedAt !== null && child.startedAt !== null && parent.startedAt > child.startedAt)
2392
+ return null;
2393
+ return parent.pid;
2394
+ }
2395
+ static parseNetstat(output, port) {
2396
+ const pids = new Set;
2397
+ for (const line of output.split(/\r?\n/)) {
2398
+ const fields = line.trim().split(/\s+/);
2399
+ if (fields[0] !== "TCP" || fields.length < 5)
2400
+ continue;
2401
+ const [, local, remote] = fields;
2402
+ if (!local?.endsWith(`:${port}`) || !remote?.endsWith(":0"))
2403
+ continue;
2404
+ const pid = Number(fields.at(-1));
2405
+ if (Number.isInteger(pid) && pid > 0)
2406
+ pids.add(pid);
2407
+ }
2408
+ return [...pids];
2409
+ }
2410
+ static parseProcNetTcp(text, port) {
2411
+ const inodes = [];
2412
+ for (const line of text.split(`
2413
+ `).slice(1)) {
2414
+ const fields = line.trim().split(/\s+/);
2415
+ const [, local, , state, , , , , , inode] = fields;
2416
+ if (!local || !inode || state !== HostProcessProbe.#procTcpListen || inode === "0")
2417
+ continue;
2418
+ if (Number.parseInt(local.slice(local.lastIndexOf(":") + 1), 16) === port)
2419
+ inodes.push(inode);
2420
+ }
2421
+ return inodes;
2422
+ }
2423
+ static parseProcStatPpid(stat) {
2424
+ const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
2425
+ return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
2426
+ }
2427
+ static parseWin32Processes(json) {
2428
+ const table = new Map;
2429
+ let parsed;
2430
+ try {
2431
+ parsed = JSON.parse(json.trim() || "[]");
2432
+ } catch {
2433
+ return table;
2434
+ }
2435
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
2436
+ for (const row of rows) {
2437
+ if (typeof row?.ProcessId !== "number")
2438
+ continue;
2439
+ table.set(row.ProcessId, {
2440
+ pid: row.ProcessId,
2441
+ ppid: row.ParentProcessId ?? 0,
2442
+ command: row.CommandLine?.trim() || null,
2443
+ startedAt: HostProcessProbe.#parseCimDate(row.CreationDate)
2444
+ });
2445
+ }
2446
+ return table;
2447
+ }
2448
+ static async run(command, timeoutMs = HostProcessProbe.timeoutMs) {
2449
+ let proc;
2450
+ try {
2451
+ proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
2452
+ } catch {
2453
+ return "";
2454
+ }
2455
+ const timer = setTimeout(() => proc.kill("SIGKILL"), timeoutMs);
2456
+ try {
2457
+ return await new Response(proc.stdout).text();
2458
+ } catch {
2459
+ return "";
2460
+ } finally {
2461
+ clearTimeout(timer);
2462
+ }
2463
+ }
2464
+ async#win32Process(pid) {
2465
+ this.#win32Table ??= await HostProcessProbe.#queryWin32Processes();
2466
+ if (!this.#win32Table.has(pid))
2467
+ this.#win32Table = await HostProcessProbe.#queryWin32Processes();
2468
+ return this.#win32Table.get(pid) ?? null;
2469
+ }
2470
+ static async#queryWin32Processes() {
2471
+ const output = await HostProcessProbe.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", HostProcessProbe.#win32ProcessQuery], HostProcessProbe.win32TimeoutMs);
2472
+ return HostProcessProbe.parseWin32Processes(output);
2473
+ }
2474
+ static #parseCimDate(value) {
2475
+ if (!value)
2476
+ return null;
2477
+ const legacy = /\/Date\((\d+)/.exec(value)?.[1];
2478
+ const ms = legacy ? Number(legacy) : Date.parse(value);
2479
+ return Number.isFinite(ms) ? ms : null;
2480
+ }
2481
+ static #procListenersOn(port) {
2482
+ const inodes = new Set(["tcp", "tcp6"].flatMap((table) => HostProcessProbe.parseProcNetTcp(HostProcessProbe.#readProc("net", table) ?? "", port)));
2483
+ if (!inodes.size)
2484
+ return [];
2485
+ const pids = [];
2486
+ for (const entry of HostProcessProbe.#listDir("/proc")) {
2487
+ if (!/^\d+$/.test(entry))
2488
+ continue;
2489
+ const holds = HostProcessProbe.#listDir(`/proc/${entry}/fd`).some((fd) => {
2490
+ const link = HostProcessProbe.#orElse(() => readlinkSync(`/proc/${entry}/fd/${fd}`), "");
2491
+ const inode = /^socket:\[(\d+)\]$/.exec(link)?.[1];
2492
+ return !!inode && inodes.has(inode);
2493
+ });
2494
+ if (holds)
2495
+ pids.push(Number(entry));
2496
+ }
2497
+ return pids;
2498
+ }
2499
+ static #readProc(pid, file) {
2500
+ return HostProcessProbe.#orElse(() => readFileSync(`/proc/${pid}/${file}`, "utf8"), null);
2501
+ }
2502
+ static #listDir(dir) {
2503
+ return HostProcessProbe.#orElse(() => readdirSync(dir), []);
2504
+ }
2505
+ static #orElse(read, fallback) {
2506
+ try {
2507
+ return read();
2508
+ } catch {
2509
+ return fallback;
2510
+ }
2458
2511
  }
2459
2512
  }
2460
2513
 
2461
2514
  // pkgs/@akanjs/cli/application/devPortReclaimer.ts
2462
2515
  class DevPortReclaimer {
2463
- static lsofTimeoutMs = 3000;
2464
2516
  static releaseTimeoutMs = 8000;
2465
2517
  static maxAncestorDepth = 8;
2466
2518
  static #releasePollMs = 100;
@@ -2469,7 +2521,12 @@ class DevPortReclaimer {
2469
2521
  return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
2470
2522
  }
2471
2523
  static isAkanCommand(command) {
2472
- return /(^|\/)akan(\s|$)/.test(command) || command.includes("@akanjs/cli") || command.includes("akanjs/server") || /\bapps\/[^/\s]+\/main\.ts\b/.test(command) || command.includes("incrementalBuilder.proc") || command.includes("rscWorker");
2524
+ const normalized = command.replaceAll("\\", "/");
2525
+ return /(^|\/)akan(\.exe)?"?(\s|$)/.test(normalized) || normalized.includes("@akanjs/cli") || normalized.includes("akanjs/server") || /\bapps\/[^/\s]+\/main\.ts\b/.test(normalized) || normalized.includes("incrementalBuilder.proc") || normalized.includes("rscWorker");
2526
+ }
2527
+ #probe;
2528
+ constructor(probe = new HostProcessProbe) {
2529
+ this.#probe = probe;
2473
2530
  }
2474
2531
  async reclaim(ports) {
2475
2532
  const killed = [];
@@ -2494,10 +2551,10 @@ class DevPortReclaimer {
2494
2551
  }
2495
2552
  async holdersOf(port) {
2496
2553
  const holders = [];
2497
- for (const listenerPid of await this.#pidsOn(port)) {
2554
+ for (const listenerPid of await this.#probe.listenersOn(port)) {
2498
2555
  if (listenerPid === process.pid || listenerPid === process.ppid)
2499
2556
  continue;
2500
- const command = await this.#commandOf(listenerPid);
2557
+ const command = await this.#probe.commandOf(listenerPid);
2501
2558
  if (!command)
2502
2559
  continue;
2503
2560
  if (!DevPortReclaimer.isAkanCommand(command)) {
@@ -2512,44 +2569,37 @@ class DevPortReclaimer {
2512
2569
  async#akanRootOf(pid, command) {
2513
2570
  let root = { pid, command };
2514
2571
  for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
2515
- const parentPid = await this.#ppidOf(root.pid);
2572
+ const parentPid = await this.#probe.parentOf(root.pid);
2516
2573
  if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
2517
2574
  break;
2518
- const parentCommand = await this.#commandOf(parentPid);
2575
+ const parentCommand = await this.#probe.commandOf(parentPid);
2519
2576
  if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
2520
2577
  break;
2521
2578
  root = { pid: parentPid, command: parentCommand };
2522
2579
  }
2523
2580
  return root;
2524
2581
  }
2525
- async#ppidOf(pid) {
2526
- const output = await this.#run(["ps", "-p", String(pid), "-o", "ppid="]);
2527
- const parsed = Number(output.trim());
2528
- return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
2529
- }
2530
- async#pidsOn(port) {
2531
- const output = await this.#run(["lsof", "-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
2532
- return [
2533
- ...new Set(output.split(`
2534
- `).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))
2535
- ];
2536
- }
2537
- async#commandOf(pid) {
2538
- const output = await this.#run(["ps", "-p", String(pid), "-o", "command="]);
2539
- return output.trim() || null;
2540
- }
2541
2582
  async#terminate(pid) {
2583
+ if (this.#probe.platform === "win32") {
2584
+ await HostProcessProbe.run(["taskkill", "/PID", String(pid), "/T", "/F"]);
2585
+ return await this.#released(pid);
2586
+ }
2542
2587
  if (!this.#signal(pid, "SIGTERM"))
2543
2588
  return !this.#alive(pid);
2589
+ if (await this.#released(pid))
2590
+ return true;
2591
+ this.#signal(pid, "SIGKILL");
2592
+ await Bun.sleep(DevPortReclaimer.#releasePollMs);
2593
+ return !this.#alive(pid);
2594
+ }
2595
+ async#released(pid) {
2544
2596
  const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
2545
2597
  while (Date.now() < deadline) {
2546
2598
  if (!this.#alive(pid))
2547
2599
  return true;
2548
2600
  await Bun.sleep(DevPortReclaimer.#releasePollMs);
2549
2601
  }
2550
- this.#signal(pid, "SIGKILL");
2551
- await Bun.sleep(DevPortReclaimer.#releasePollMs);
2552
- return !this.#alive(pid);
2602
+ return false;
2553
2603
  }
2554
2604
  #signal(pid, signal) {
2555
2605
  try {
@@ -2567,26 +2617,15 @@ class DevPortReclaimer {
2567
2617
  return error.code === "EPERM";
2568
2618
  }
2569
2619
  }
2570
- async#run(command) {
2571
- const proc = Bun.spawn(command, { stdio: ["ignore", "pipe", "ignore"] });
2572
- const timer = setTimeout(() => proc.kill("SIGKILL"), DevPortReclaimer.lsofTimeoutMs);
2573
- try {
2574
- return await new Response(proc.stdout).text();
2575
- } catch {
2576
- return "";
2577
- } finally {
2578
- clearTimeout(timer);
2579
- }
2580
- }
2581
2620
  }
2582
2621
 
2583
2622
  // pkgs/@akanjs/cli/application/devStreamView.ts
2584
- import { Logger as Logger5 } from "akanjs/common";
2623
+ import { Logger as Logger4 } from "akanjs/common";
2585
2624
  import chalk from "chalk";
2586
2625
  var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
2587
2626
 
2588
2627
  class DevStreamView {
2589
- #logger = new Logger5("akan start");
2628
+ #logger = new Logger4("akan start");
2590
2629
  #partial = new Map;
2591
2630
  #lastState = new Map;
2592
2631
  #color = new Map;
@@ -2656,7 +2695,7 @@ import os from "os";
2656
2695
  import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
2657
2696
 
2658
2697
  class DevBootConcurrency {
2659
- static perAppBytes = 900 * 1024 * 1024;
2698
+ static perAppBytes = 1800 * 1024 * 1024;
2660
2699
  static memoryShare = 0.5;
2661
2700
  static coresPerApp = 4;
2662
2701
  static budget() {
@@ -2692,6 +2731,7 @@ class DevBootConcurrency {
2692
2731
  import { mkdir, rename } from "fs/promises";
2693
2732
  import path6 from "path";
2694
2733
  import dayjs from "dayjs";
2734
+
2695
2735
  class DevSessionLog {
2696
2736
  static fileName = "dev.log";
2697
2737
  static previousFileName = "dev.prev.log";
@@ -2792,6 +2832,16 @@ class DevSessionLog {
2792
2832
 
2793
2833
  // pkgs/@akanjs/cli/application/devSupervisor.ts
2794
2834
  class DevSupervisor {
2835
+ static resolveDevUi(plain, {
2836
+ isTty = !!process.stdout.isTTY,
2837
+ columns = process.stdout.columns ?? 0
2838
+ } = {}) {
2839
+ if (plain)
2840
+ return { mode: "stream", downgraded: false };
2841
+ if (!isTty || columns <= 0)
2842
+ return { mode: "stream", downgraded: true };
2843
+ return { mode: "tui", downgraded: false };
2844
+ }
2795
2845
  static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
2796
2846
  static childHooks() {
2797
2847
  if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
@@ -2806,7 +2856,7 @@ class DevSupervisor {
2806
2856
  }
2807
2857
  };
2808
2858
  }
2809
- static readyTimeoutMs = 180000;
2859
+ static bootTimeoutMs = 180000;
2810
2860
  static shutdownGraceMs = 12000;
2811
2861
  #options;
2812
2862
  #children = new Map;
@@ -2855,23 +2905,21 @@ class DevSupervisor {
2855
2905
  return ports;
2856
2906
  }
2857
2907
  #makeChild(app, port) {
2858
- let markReady;
2859
- const ready = new Promise((resolve) => {
2860
- markReady = resolve;
2861
- });
2908
+ const { promise: booted, resolve: markBooted } = Promise.withResolvers();
2862
2909
  return {
2863
2910
  status: {
2864
2911
  app,
2865
2912
  name: app.name,
2866
2913
  port,
2867
2914
  url: `http://localhost:${port}`,
2915
+ shareUrl: this.#options.shares?.get(app.name) ?? null,
2868
2916
  state: "starting",
2869
2917
  detail: "queued",
2870
2918
  exitCode: null
2871
2919
  },
2872
2920
  proc: null,
2873
- ready,
2874
- markReady,
2921
+ booted,
2922
+ markBooted,
2875
2923
  opened: false
2876
2924
  };
2877
2925
  }
@@ -2890,20 +2938,22 @@ class DevSupervisor {
2890
2938
  for (const child of wave)
2891
2939
  this.#spawnChild(child);
2892
2940
  this.#publishStatus();
2893
- await Promise.all(wave.map(async (child) => await this.#waitForReady(child)));
2941
+ await Promise.all(wave.map(async (child) => await this.#waitForBoot(child)));
2894
2942
  }
2895
2943
  }
2896
- async#waitForReady(child) {
2897
- let timer = null;
2944
+ async#waitForBoot(child) {
2945
+ if (await DevSupervisor.timesOut(child.booted, DevSupervisor.bootTimeoutMs))
2946
+ this.#note(`${child.status.name} has not finished booting after ${Math.round(DevSupervisor.bootTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
2947
+ }
2948
+ static async timesOut(work, ms) {
2949
+ let timer;
2898
2950
  const expired = new Promise((resolve) => {
2899
- timer = setTimeout(() => resolve("expired"), DevSupervisor.readyTimeoutMs);
2951
+ timer = setTimeout(() => resolve(true), ms);
2900
2952
  });
2901
2953
  try {
2902
- if (await Promise.race([child.ready.then(() => "ready"), expired]) === "expired")
2903
- this.#note(`${child.status.name} has not reported ready after ${Math.round(DevSupervisor.readyTimeoutMs / 1000)}s; starting the next app anyway`, "warn");
2954
+ return await Promise.race([work.then(() => false), expired]);
2904
2955
  } finally {
2905
- if (timer)
2906
- clearTimeout(timer);
2956
+ clearTimeout(timer);
2907
2957
  }
2908
2958
  }
2909
2959
  static childArgs(name, { write = true } = {}) {
@@ -2930,9 +2980,12 @@ class DevSupervisor {
2930
2980
  },
2931
2981
  stdio: ["ignore", "pipe", "pipe"],
2932
2982
  ipc: (message) => {
2933
- if (!message || typeof message !== "object" || typeof message.state !== "string")
2983
+ if (!message || typeof message !== "object")
2934
2984
  return;
2935
- this.#applyChildEvent(child, message);
2985
+ if ("booted" in message)
2986
+ child.markBooted();
2987
+ else if (typeof message.state === "string")
2988
+ this.#applyChildEvent(child, message);
2936
2989
  },
2937
2990
  serialization: "advanced",
2938
2991
  onExit: (_proc, exitCode, signalCode) => {
@@ -2942,7 +2995,7 @@ class DevSupervisor {
2942
2995
  return;
2943
2996
  child.status.state = "stopped";
2944
2997
  child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
2945
- child.markReady();
2998
+ child.markBooted();
2946
2999
  this.#publishStatus();
2947
3000
  }
2948
3001
  });
@@ -2956,14 +3009,13 @@ class DevSupervisor {
2956
3009
  child.status.state = event.state;
2957
3010
  child.status.detail = event.detail ?? "";
2958
3011
  if (event.state === "ready") {
2959
- child.markReady();
2960
3012
  if (this.#options.open && !child.opened) {
2961
3013
  child.opened = true;
2962
3014
  openBrowser(child.status.url);
2963
3015
  }
2964
3016
  }
2965
3017
  if (event.state === "failed")
2966
- child.markReady();
3018
+ child.markBooted();
2967
3019
  this.#publishStatus();
2968
3020
  }
2969
3021
  async#drain(stream, name, kind) {
@@ -3025,44 +3077,81 @@ class DevSupervisor {
3025
3077
  for (const child of running)
3026
3078
  child.proc.kill("SIGTERM");
3027
3079
  const exited = Promise.all(running.map(async (child) => await child.proc.exited));
3028
- let timer = null;
3029
- const expired = new Promise((resolve) => {
3030
- timer = setTimeout(() => resolve("expired"), DevSupervisor.shutdownGraceMs);
3031
- });
3032
- try {
3033
- if (await Promise.race([exited.then(() => "exited"), expired]) === "expired") {
3034
- for (const child of running) {
3035
- if (!child.proc.killed) {
3036
- this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
3037
- child.proc.kill("SIGKILL");
3038
- }
3039
- }
3040
- }
3041
- } finally {
3042
- if (timer)
3043
- clearTimeout(timer);
3080
+ if (!await DevSupervisor.timesOut(exited, DevSupervisor.shutdownGraceMs))
3081
+ return;
3082
+ for (const child of running) {
3083
+ if (child.proc.killed)
3084
+ continue;
3085
+ this.#note(`${child.status.name} did not exit in time; killing it`, "warn");
3086
+ child.proc.kill("SIGKILL");
3044
3087
  }
3045
3088
  }
3046
3089
  }
3047
3090
 
3048
- // pkgs/@akanjs/cli/application/devUiMode.ts
3049
- var resolveDevUi = (plain, { isTty = !!process.stdout.isTTY, columns = process.stdout.columns ?? 0 } = {}) => {
3050
- if (plain)
3051
- return { mode: "stream", downgraded: false };
3052
- if (!isTty || columns <= 0)
3053
- return { mode: "stream", downgraded: true };
3054
- return { mode: "tui", downgraded: false };
3055
- };
3091
+ // pkgs/@akanjs/cli/application/interruptTeardown.ts
3092
+ import { Logger as Logger5 } from "akanjs/common";
3093
+
3094
+ class InterruptTeardown {
3095
+ #teardowns = [];
3096
+ #exit;
3097
+ #listen;
3098
+ #report;
3099
+ #interrupted = false;
3100
+ #chain = Promise.resolve();
3101
+ ownsExit = true;
3102
+ constructor({ exit, listen, report } = {}) {
3103
+ this.#exit = exit ?? ((code) => process.exit(code));
3104
+ this.#listen = listen ?? ((onSignal) => {
3105
+ process.on("SIGINT", onSignal);
3106
+ });
3107
+ this.#report = report ?? ((message) => Logger5.rawLog(message, undefined, "error"));
3108
+ }
3109
+ add(run, abandoned, exitCode = 0) {
3110
+ this.#teardowns.push({ run, abandoned, exitCode, state: "pending" });
3111
+ if (this.#teardowns.length === 1)
3112
+ this.#listen(() => this.#onSignal());
3113
+ }
3114
+ async runAll() {
3115
+ await this.#runTeardowns();
3116
+ }
3117
+ #onSignal() {
3118
+ if (this.#interrupted) {
3119
+ for (const one of this.#teardowns)
3120
+ if (one.state !== "done")
3121
+ this.#report(one.abandoned);
3122
+ this.#exit(130);
3123
+ return;
3124
+ }
3125
+ this.#interrupted = true;
3126
+ this.#runTeardowns().finally(() => {
3127
+ if (this.ownsExit)
3128
+ this.#exit(Math.max(0, ...this.#teardowns.map((one) => one.exitCode)));
3129
+ });
3130
+ }
3131
+ async#runTeardowns() {
3132
+ this.#chain = this.#chain.then(async () => {
3133
+ for (const one of this.#teardowns.toReversed()) {
3134
+ if (one.state !== "pending")
3135
+ continue;
3136
+ one.state = "running";
3137
+ await one.run().catch(() => {
3138
+ return;
3139
+ });
3140
+ one.state = "done";
3141
+ }
3142
+ });
3143
+ await this.#chain;
3144
+ }
3145
+ }
3056
3146
 
3057
3147
  // pkgs/@akanjs/cli/application/application.script.ts
3058
3148
  class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
3059
3149
  static dbShutdownTimeoutMs = 20000;
3150
+ static devServerReadyTimeoutMs = DevSupervisor.bootTimeoutMs;
3151
+ #interrupt = new InterruptTeardown;
3060
3152
  async confirmDatabaseModeDependencyInstall(databaseMode, installSpecs) {
3061
3153
  return await confirm({
3062
- message: [
3063
- `Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`,
3064
- "Install them now?"
3065
- ].join(" "),
3154
+ message: `Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}. Install them now?`,
3066
3155
  default: true
3067
3156
  });
3068
3157
  }
@@ -3073,67 +3162,22 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3073
3162
  const shouldInstall = await this.confirmDatabaseModeDependencyInstall(databaseMode, installSpecs);
3074
3163
  if (!shouldInstall)
3075
3164
  throw new Error(`Database mode '${databaseMode}' requires missing dependencies: ${installSpecs.join(", ")}.`);
3076
- const spinner = app.workspace.spinning(`Installing database dependencies for ${databaseMode} mode...`);
3077
- try {
3078
- await app.workspace.spawn("bun", ["add", ...installSpecs], {
3079
- stdio: "inherit"
3080
- });
3081
- await app.workspace.getPackageJson({ refresh: true });
3082
- spinner.succeed(`Installed database dependencies for ${databaseMode} mode`);
3083
- } catch (error) {
3084
- spinner.fail(`Failed to install database dependencies for ${databaseMode} mode`);
3085
- throw error;
3086
- }
3165
+ await this.#addDependencies(app, installSpecs, `database dependencies for ${databaseMode} mode`);
3087
3166
  }
3088
- async confirmMobileDependencyInstall(installSpecs) {
3089
- return await confirm({
3090
- message: [`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`, "Install them now?"].join(" "),
3091
- default: true
3092
- });
3093
- }
3094
- async syncMobileDependencies(app, akanConfig) {
3095
- const installSpecs = akanConfig.getMissingMobileDependencySpecs();
3096
- if (installSpecs.length === 0)
3097
- return;
3098
- const shouldInstall = await this.confirmMobileDependencyInstall(installSpecs);
3099
- if (!shouldInstall)
3100
- throw new Error(`Mobile builds require missing dependencies: ${installSpecs.join(", ")}.`);
3101
- const spinner = app.workspace.spinning("Installing mobile dependencies...");
3167
+ async#addDependencies(app, installSpecs, what) {
3168
+ const spinner = app.workspace.spinning(`Installing ${what}...`);
3102
3169
  try {
3103
- await app.workspace.spawn("bun", ["add", ...installSpecs], {
3104
- stdio: "inherit"
3105
- });
3170
+ await app.workspace.spawn("bun", ["add", ...installSpecs], { stdio: "inherit" });
3106
3171
  await app.workspace.getPackageJson({ refresh: true });
3107
- spinner.succeed("Installed mobile dependencies");
3108
- } catch (error) {
3109
- spinner.fail("Failed to install mobile dependencies");
3110
- throw error;
3111
- }
3112
- }
3113
- async syncMobileAppCapacitorPlugins(app, akanConfig) {
3114
- const plugins = akanConfig.getMobileAppCapacitorPlugins();
3115
- if (plugins.length === 0)
3116
- return;
3117
- const packageJson = await app.getPackageJson({ refresh: true });
3118
- const dependencies = packageJson.dependencies ?? {};
3119
- const missing = plugins.filter((plugin) => !dependencies[plugin]);
3120
- if (missing.length === 0)
3121
- return;
3122
- const spinner = app.workspace.spinning(`Adding default Capacitor plugins to ${app.name}...`);
3123
- try {
3124
- packageJson.dependencies = { ...dependencies, ...Object.fromEntries(missing.map((plugin) => [plugin, "*"])) };
3125
- await app.setPackageJson(packageJson);
3126
- await app.workspace.spawn("bun", ["install"], { stdio: "inherit" });
3127
- await app.getPackageJson({ refresh: true });
3128
- spinner.succeed(`Added default Capacitor plugins to ${app.name}: ${missing.join(", ")}`);
3172
+ spinner.succeed(`Installed ${what}`);
3129
3173
  } catch (error) {
3130
- spinner.fail(`Failed to add default Capacitor plugins to ${app.name}`);
3174
+ spinner.fail(`Failed to install ${what}`);
3131
3175
  throw error;
3132
3176
  }
3133
3177
  }
3134
- async createApplication(appName, workspace, { start = false, libs = [] } = {}) {
3178
+ async createApplication(appName, workspace, { start = false, libs = [], sample = false } = {}) {
3135
3179
  const spinner = workspace.spinning("Creating application...");
3136
- const app = await this.applicationRunner.createApplication(appName, workspace, libs);
3180
+ const app = await this.applicationRunner.createApplication(appName, workspace, libs, { sample });
3137
3181
  spinner.succeed(`Application created in apps/${app.name}`);
3138
3182
  await app.scanSync();
3139
3183
  if (start)
@@ -3156,17 +3200,6 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3156
3200
  else
3157
3201
  await this.libraryScript.syncLibrary(sys);
3158
3202
  }
3159
- async compact(sys, { module = null, minLines, interactive = false } = {}) {
3160
- const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
3161
- if (!reports.length) {
3162
- Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
3163
- return;
3164
- }
3165
- for (const report of reports)
3166
- Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
3167
- const compactedNum = reports.filter((report) => report.status === "compacted").length;
3168
- Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
3169
- }
3170
3203
  async script(app, filename) {
3171
3204
  const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
3172
3205
  await app.scanSync();
@@ -3184,10 +3217,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3184
3217
  if (!quiet)
3185
3218
  Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
3186
3219
  try {
3187
- const result = await this.applicationRunner.build(app, {
3188
- fast,
3189
- spinner: !quiet
3190
- });
3220
+ const result = await this.applicationRunner.build(app, { fast, spinner: !quiet });
3191
3221
  Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
3192
3222
  if (!quiet)
3193
3223
  ApplicationBuildReporter.printSummary(result);
@@ -3212,8 +3242,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3212
3242
  async test(exec, { write = true } = {}) {
3213
3243
  if (exec instanceof LibExecutor) {
3214
3244
  await this.libraryScript.syncLibrary(exec);
3215
- const spinner = exec.spinning(`Preparing ${exec.name}...`);
3216
- spinner.succeed(`${exec.name} prepared`);
3245
+ exec.spinning(`Preparing ${exec.name}...`).succeed(`${exec.name} prepared`);
3217
3246
  await this.applicationRunner.test(exec);
3218
3247
  return;
3219
3248
  }
@@ -3230,23 +3259,33 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3230
3259
  }
3231
3260
  await this.applicationRunner.test(exec);
3232
3261
  }
3233
- async start(apps, { open = false, dbup = true, write = true, plain = false, kill = false, concurrency = null } = {}) {
3262
+ async start(apps, {
3263
+ open = false,
3264
+ dbup = true,
3265
+ write = true,
3266
+ plain = false,
3267
+ kill = false,
3268
+ share = false,
3269
+ concurrency = null
3270
+ } = {}) {
3234
3271
  const first = apps[0];
3235
3272
  if (!first)
3236
3273
  throw new Error("No app selected to start");
3237
- const { mode, downgraded } = resolveDevUi(plain);
3274
+ const { mode, downgraded } = DevSupervisor.resolveDevUi(plain);
3238
3275
  if (downgraded)
3239
3276
  first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
3240
3277
  if (kill)
3241
3278
  await this.reclaimDevPorts(apps);
3279
+ const shares = share ? await this.#shareOnInterrupt(apps) : null;
3242
3280
  if (mode === "stream" && apps.length === 1)
3243
3281
  return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
3244
- await this.#startMany(apps, { open, dbup, write, mode, concurrency });
3282
+ this.#interrupt.ownsExit = false;
3283
+ await this.#startMany(apps, { open, dbup, write, mode, concurrency, shares });
3245
3284
  }
3246
3285
  async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
3247
3286
  await app.scanSync({ write });
3248
3287
  const akanConfig = await app.getConfig();
3249
- const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
3288
+ const databaseMode = akanConfig.resolveDatabaseMode();
3250
3289
  await this.syncDatabaseModeDependencies(app, akanConfig, databaseMode);
3251
3290
  if (app.getEnv() === "local" && dbup && databaseMode !== "single") {
3252
3291
  const wasDbAlreadyUp = await this.dbup(app.workspace, databaseMode);
@@ -3254,15 +3293,12 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3254
3293
  this.#stopDatabaseOnInterrupt(app.workspace);
3255
3294
  }
3256
3295
  const spinner = app.spinning("Preparing backend...");
3257
- const akanAppHost = await this.applicationRunner.start(app, {
3296
+ return await this.applicationRunner.start(app, {
3258
3297
  open,
3259
- onStart: () => {
3260
- spinner.succeed(`${app.name} prepared, ready to start`);
3261
- },
3298
+ onStart: () => spinner.succeed(`${app.name} prepared, ready to start`),
3262
3299
  onDevEvent,
3263
3300
  stdio
3264
3301
  });
3265
- return akanAppHost;
3266
3302
  }
3267
3303
  async reclaimDevPorts(apps) {
3268
3304
  const workspace = apps[0]?.workspace;
@@ -3277,13 +3313,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3277
3313
  if (killed.length === 0 && foreign.length === 0)
3278
3314
  workspace.log("dev ports are free; nothing to kill");
3279
3315
  }
3280
- async#startMany(apps, { open, dbup, write, mode, concurrency }) {
3316
+ async#startMany(apps, {
3317
+ open,
3318
+ dbup,
3319
+ write,
3320
+ mode,
3321
+ concurrency,
3322
+ shares
3323
+ }) {
3281
3324
  const workspace = apps[0]?.workspace;
3282
3325
  if (!workspace)
3283
3326
  throw new Error("No app selected to start");
3284
3327
  const startedDatabase = dbup ? await this.#prepareSharedDatabase(apps) : false;
3285
- const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write });
3286
- const view = mode === "tui" ? await (await import("./devTuiView-0hc2emhe.js")).createDevTuiView(supervisor) : new DevStreamView(apps.map((app) => app.name));
3328
+ const supervisor = new DevSupervisor({ apps, mode, concurrency, open, write, shares });
3329
+ const view = mode === "tui" ? new (await import("./devTui-q7k7bakd.js")).DevTui(supervisor) : new DevStreamView(apps.map((app) => app.name));
3287
3330
  await supervisor.run(view);
3288
3331
  if (startedDatabase)
3289
3332
  await this.#stopDatabase(workspace);
@@ -3297,7 +3340,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3297
3340
  if (app.getEnv() !== "local")
3298
3341
  continue;
3299
3342
  const akanConfig = await app.getConfig();
3300
- const mode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
3343
+ const mode = akanConfig.resolveDatabaseMode();
3301
3344
  await this.syncDatabaseModeDependencies(app, akanConfig, mode);
3302
3345
  if (mode !== "single")
3303
3346
  modes.add(mode);
@@ -3308,82 +3351,121 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3308
3351
  started = true;
3309
3352
  return started;
3310
3353
  }
3311
- async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
3354
+ async buildIos(app, { write = true, ...options } = {}) {
3312
3355
  await app.scanSync({ write });
3313
- await this.applicationRunner.buildIos(app, { target, env, regenerate });
3356
+ await this.applicationRunner.buildMobile(app, "ios", options);
3314
3357
  }
3315
- async startIos(app, {
3316
- open = false,
3317
- operation = "local",
3318
- env = "local",
3319
- write = true,
3320
- target,
3321
- device,
3322
- regenerate = false,
3323
- noAllowProvisioningUpdates = false
3324
- } = {}) {
3358
+ async buildAndroid(app, { write = true, ...options } = {}) {
3325
3359
  await app.scanSync({ write });
3326
- const akanConfig = await app.getConfig();
3327
- await this.syncMobileDependencies(app, akanConfig);
3328
- await this.syncMobileAppCapacitorPlugins(app, akanConfig);
3329
- await this.applicationRunner.startIos(app, {
3330
- open,
3331
- operation,
3332
- env,
3333
- target,
3334
- device,
3335
- regenerate,
3336
- noAllowProvisioningUpdates
3360
+ await this.applicationRunner.buildMobile(app, "android", options);
3361
+ }
3362
+ async buildDesktop(app, { write = true, ...options } = {}) {
3363
+ await app.scanSync({ write });
3364
+ await this.applicationRunner.buildDesktop(app, options);
3365
+ }
3366
+ async startIos(app, { write = true, ...options } = {}) {
3367
+ await app.scanSync({ write });
3368
+ await this.applicationRunner.startMobile(app, "ios", options);
3369
+ }
3370
+ async startAndroid(app, { write = true, ...options } = {}) {
3371
+ await app.scanSync({ write });
3372
+ await this.applicationRunner.startMobile(app, "android", options);
3373
+ }
3374
+ async startDesktop(app, { write = true, ...options } = {}) {
3375
+ await app.scanSync({ write });
3376
+ if (options.operation === "release")
3377
+ return await this.applicationRunner.startDesktop(app, options);
3378
+ const mobileTarget = await ApplicationRunner.startTarget(app, undefined, options.target);
3379
+ if (!ApplicationRunner.carriesServer(mobileTarget))
3380
+ return await this.applicationRunner.startDesktop(app, options);
3381
+ DesktopServerStage.assertCarriable(await app.getConfig());
3382
+ const upstream = `http://localhost:${await app.getDevPort()}`;
3383
+ try {
3384
+ if (await ApplicationRunner.answers(upstream, app.name, app.workspace.workspaceRoot))
3385
+ app.log(`The desktop app follows the dev server on ${upstream}.`);
3386
+ else
3387
+ await this.#startDevServerFor(app, upstream);
3388
+ await this.applicationRunner.startDesktop(app, { ...options, interrupt: this.#interrupt });
3389
+ } finally {
3390
+ await this.#interrupt.runAll();
3391
+ }
3392
+ }
3393
+ async#startDevServerFor(app, upstream) {
3394
+ app.log(`No dev server answers on ${upstream}; starting \`akan start ${app.name}\` for the desktop app.`);
3395
+ let settle = () => {};
3396
+ const settled = new Promise((resolve) => settle = resolve);
3397
+ const appHost = await this.startOne(app, {
3398
+ write: false,
3399
+ onDevEvent: (event) => {
3400
+ if ("state" in event && (event.state === "ready" || event.state === "failed"))
3401
+ settle(event.state);
3402
+ }
3337
3403
  });
3404
+ this.#interrupt.add(async () => {
3405
+ await appHost.stop();
3406
+ }, "Abandoning the dev server shutdown; its processes may still be running.", 130);
3407
+ let state;
3408
+ const timedOut = await DevSupervisor.timesOut(settled.then((settledState) => state = settledState), ApplicationScript.devServerReadyTimeoutMs);
3409
+ if (state === "ready")
3410
+ return;
3411
+ await this.#interrupt.runAll();
3412
+ throw new Error(timedOut ? `akan start ${app.name} did not answer within ${ApplicationScript.devServerReadyTimeoutMs / 1000}s; see its log above.` : `akan start ${app.name} gave up restarting its server; see its log above.`);
3338
3413
  }
3339
- async releaseIos(app, { write = true, target, env = "main", regenerate = false, allowLocalRelease = false } = {}) {
3414
+ async releaseIos(app, {
3415
+ write = true,
3416
+ allowLocalRelease = false,
3417
+ ...options
3418
+ } = {}) {
3340
3419
  await app.scanSync({ write });
3341
- if (env === "local" && !allowLocalRelease)
3342
- throw new Error("releaseIos --env local is blocked. Pass allowLocalRelease only for explicit local release testing.");
3343
- await this.applicationRunner.releaseIos(app, { target, env, regenerate });
3420
+ ApplicationScript.#assertReleaseEnv("releaseIos", options.env ?? "main", allowLocalRelease);
3421
+ await this.applicationRunner.releaseIos(app, options);
3344
3422
  }
3345
- async buildAndroid(app, { write = true, target, env = "debug", regenerate = false } = {}) {
3423
+ async releaseAndroid(app, format, {
3424
+ write = true,
3425
+ allowLocalRelease = false,
3426
+ ...options
3427
+ } = {}) {
3346
3428
  await app.scanSync({ write });
3347
- await this.applicationRunner.buildAndroid(app, { target, env, regenerate });
3429
+ ApplicationScript.#assertReleaseEnv("releaseAndroid", options.env ?? "main", allowLocalRelease);
3430
+ await this.applicationRunner.releaseAndroid(app, format, options);
3348
3431
  }
3349
- async startAndroid(app, {
3350
- open = false,
3351
- operation = "local",
3352
- env = "local",
3432
+ async packUpdate(app, platform, {
3353
3433
  write = true,
3354
- target,
3355
- regenerate = false
3434
+ allowLocalRelease = false,
3435
+ ...options
3356
3436
  } = {}) {
3357
3437
  await app.scanSync({ write });
3358
- const akanConfig = await app.getConfig();
3359
- await this.syncMobileDependencies(app, akanConfig);
3360
- await this.syncMobileAppCapacitorPlugins(app, akanConfig);
3361
- await this.applicationRunner.startAndroid(app, {
3362
- open,
3363
- operation,
3364
- env,
3365
- target,
3366
- regenerate
3367
- });
3438
+ ApplicationScript.#assertReleaseEnv("packUpdate", options.env ?? "main", allowLocalRelease);
3439
+ await this.applicationRunner.packUpdate(app, platform, options);
3440
+ }
3441
+ async updateKeygen(app, platform, { target } = {}) {
3442
+ await this.applicationRunner.updateKeygen(app, platform, target);
3368
3443
  }
3369
- async releaseAndroid(app, assembleType, { write = true, target, env = "main", regenerate = false, allowLocalRelease = false } = {}) {
3444
+ async publishUpdate(app, platform, {
3445
+ write = true,
3446
+ allowLocalRelease = false,
3447
+ ...options
3448
+ } = {}) {
3370
3449
  await app.scanSync({ write });
3371
- if (env === "local" && !allowLocalRelease)
3372
- throw new Error("releaseAndroid --env local is blocked. Pass allowLocalRelease only for explicit local release testing.");
3373
- await this.applicationRunner.releaseAndroid(app, assembleType, {
3374
- target,
3375
- env,
3376
- regenerate
3377
- });
3450
+ ApplicationScript.#assertReleaseEnv("publishUpdate", options.env ?? "main", allowLocalRelease);
3451
+ await this.applicationRunner.publishUpdate(app, platform, options);
3378
3452
  }
3379
- async configureApp(app) {
3380
- await this.applicationRunner.configureApp(app);
3453
+ static #assertReleaseEnv(command, env, allowLocalRelease) {
3454
+ if (env === "local" && !allowLocalRelease)
3455
+ throw new Error(`${command} --env local is blocked. Pass allowLocalRelease only for explicit local release testing.`);
3381
3456
  }
3382
- async releaseSource(app, options) {
3383
- await this.applicationRunner.releaseSource(app, options);
3457
+ async transferDatabase(app, direction, dir) {
3458
+ await this.applicationRunner.transferDatabase(app, direction, dir);
3384
3459
  }
3385
- async codepush(app, os) {
3386
- await this.applicationRunner.codepush(app, os);
3460
+ async dbupDeclared(workspace, mode) {
3461
+ const declared = mode ? [mode] : await Promise.all((await workspace.getApps()).map(async (appName) => (await AppExecutor.from(workspace, appName).getConfig()).database.modes)).then((modes) => [...new Set(modes.flat())]);
3462
+ const needing = declared.filter((each) => each !== "single");
3463
+ if (!needing.length) {
3464
+ workspace.log("No app here declares a database mode that runs on local services; single needs none.");
3465
+ return;
3466
+ }
3467
+ for (const each of needing)
3468
+ await this.dbup(workspace, each);
3387
3469
  }
3388
3470
  async dbup(workspace, mode = "multiple") {
3389
3471
  const spinner = workspace.spinning(`Starting local database (${mode})...`);
@@ -3401,34 +3483,36 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
3401
3483
  }
3402
3484
  spinner.succeed("Local database (/local/docker-compose.yaml) is down");
3403
3485
  }
3404
- #stopDatabaseOnInterrupt(workspace) {
3405
- let stopping = false;
3406
- process.on("SIGINT", () => {
3407
- if (stopping) {
3408
- Logger6.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "error");
3409
- process.exit(130);
3410
- }
3411
- stopping = true;
3412
- this.#stopDatabase(workspace).finally(() => process.exit(0));
3486
+ async#shareOnInterrupt(apps) {
3487
+ const shares = [];
3488
+ const urls = new Map;
3489
+ this.#interrupt.add(async () => {
3490
+ await Promise.all(shares.map(async (share) => await share.close()));
3491
+ }, "Abandoning the tunnel release; the shares expire on their own.");
3492
+ await Promise.all(apps.map(async (app) => {
3493
+ const { TunnelShare } = await import("./TunnelShare-cwh9ezsm.js");
3494
+ const share = await TunnelShare.open(app, { workspace: app.workspace });
3495
+ shares.push(share);
3496
+ urls.set(app.name, share.url);
3497
+ Logger6.rawLog(`${app.name} is shared at ${share.url}`);
3498
+ })).catch((error) => {
3499
+ Logger6.rawLog(`Could not open the tunnel: ${error instanceof Error ? error.message : String(error)}`, undefined, "error");
3413
3500
  });
3501
+ return urls;
3502
+ }
3503
+ #stopDatabaseOnInterrupt(workspace) {
3504
+ this.#interrupt.add(async () => {
3505
+ await this.#stopDatabase(workspace);
3506
+ }, "Abandoning the local database teardown; containers are left running.");
3414
3507
  }
3415
3508
  async#stopDatabase(workspace) {
3416
- let timer = null;
3417
- const timeout = new Promise((resolve) => {
3418
- timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
3509
+ const stopped = this.dbdown(workspace).catch(() => {
3510
+ return;
3419
3511
  });
3420
- try {
3421
- const result = await Promise.race([this.dbdown(workspace).catch(() => {
3422
- return;
3423
- }), timeout]);
3424
- if (result !== "timeout")
3425
- return;
3426
- Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
3427
- } finally {
3428
- if (timer)
3429
- clearTimeout(timer);
3430
- }
3512
+ if (!await DevSupervisor.timesOut(stopped, ApplicationScript.dbShutdownTimeoutMs))
3513
+ return;
3514
+ Logger6.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "error");
3431
3515
  }
3432
3516
  }
3433
3517
 
3434
- export { openBrowser, plainTextOf, HOST_SOURCE, DevLogBuffer, ApplicationScript };
3518
+ export { ApplicationScript };