@akanjs/cli 3.0.0-alpha.11 → 3.0.0-alpha.111

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 (140) hide show
  1. package/.build-stamp +1 -1
  2. package/README.ko.md +1 -1
  3. package/README.md +1 -1
  4. package/{abstractCompactor-e5yq60qz.js → abstractCompactor-mjh5ppsq.js} +3 -5
  5. package/{agent.command-a3n15c6t.js → agent.command-d2gfxfck.js} +11 -13
  6. package/{application.command-rdm3et0g.js → application.command-pj2m7584.js} +65 -38
  7. package/{applicationBuildRunner-6m8n71w4.js → applicationBuildRunner-kxwbssgn.js} +27 -23
  8. package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-4yasxwv9.js} +8 -9
  9. package/buildBatch.proc.js +12 -14
  10. package/capacitorApp-36rbr858.js +12 -0
  11. package/{cloud.command-56aeekqn.js → cloud.command-gxqp8xdv.js} +15 -18
  12. package/commandManifest.json +1 -1
  13. package/{context.command-ysxawe7h.js → context.command-ez8a0zyz.js} +19 -23
  14. package/dependencyScanner-shs82ynp.js +8 -0
  15. package/devTui-40ggmd99.js +553 -0
  16. package/devTuiView-c08ccbat.js +9 -0
  17. package/getDirname-dr3jz1xg.js +7 -0
  18. package/{guideline.command-yqf9a372.js → guideline.command-xmqwx8zm.js} +24 -22
  19. package/guidelines/agentRule/agentRule.generate.json +31 -0
  20. package/guidelines/agentRule/agentRule.instruction.md +531 -0
  21. package/guidelines/conventions/conventions.instruction.md +493 -369
  22. package/guidelines/framework/framework.generate.json +3 -2
  23. package/guidelines/framework/framework.instruction.md +0 -9
  24. package/guidelines/mcpRule/mcpRule.generate.json +35 -0
  25. package/guidelines/mcpRule/mcpRule.instruction.md +460 -0
  26. package/guidelines/modelStore/modelStore.instruction.md +12 -0
  27. package/guidelines/modelTemplate/modelTemplate.instruction.md +5 -0
  28. package/guidelines/queryRule/queryRule.generate.json +31 -0
  29. package/guidelines/queryRule/queryRule.instruction.md +178 -0
  30. package/guidelines/runtimeRule/runtimeRule.generate.json +25 -0
  31. package/guidelines/runtimeRule/runtimeRule.instruction.md +220 -0
  32. package/guidelines/ssrRule/ssrRule.instruction.md +51 -5
  33. package/guidelines/transportRule/transportRule.generate.json +31 -0
  34. package/guidelines/transportRule/transportRule.instruction.md +60 -0
  35. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +54 -764
  36. package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +33 -0
  37. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +467 -0
  38. package/incrementalBuilder.proc.js +26 -25
  39. package/{index-8pd02prk.js → index-259dsnz3.js} +58 -25
  40. package/{index-yzsxc30b.js → index-2tv3fdx4.js} +975 -234
  41. package/index-3d9y066c.js +76 -0
  42. package/{index-76rn3g2c.js → index-47pj3aee.js} +4 -4
  43. package/{index-m90fn8xe.js → index-4edzf1gr.js} +32 -32
  44. package/{index-3789fdpg.js → index-600zhnve.js} +94 -61
  45. package/{index-1577bej2.js → index-6wwzs1bc.js} +2 -2
  46. package/{index-cv9cxs5k.js → index-7737gxcp.js} +18 -18
  47. package/{index-a5cs7d8r.js → index-8few9nyh.js} +13 -13
  48. package/{index-mewacxye.js → index-8pm30m73.js} +57 -57
  49. package/{index-wnp7hwq7.js → index-92zztqy1.js} +7 -7
  50. package/{index-fgc8r6dj.js → index-a5vmm6x4.js} +3 -1
  51. package/{index-cav1azma.js → index-a64esmtb.js} +1106 -1369
  52. package/{index-nwqqssnb.js → index-aj7tyh5w.js} +26 -23
  53. package/{index-xys926f2.js → index-by5gpp8x.js} +4 -4
  54. package/{index-yzmqvg23.js → index-fz5q3gd7.js} +8 -8
  55. package/{index-kcsa1zff.js → index-gyd220p2.js} +4 -4
  56. package/{index-j37qq1f2.js → index-h7yw2z9n.js} +7 -7
  57. package/{index-bjpxzr6s.js → index-hwkygn9s.js} +51 -3
  58. package/index-j9yhg404.js +159 -0
  59. package/{index-46tjzh6s.js → index-kcxmv48c.js} +29 -29
  60. package/{index-0cj2zxbm.js → index-kn1h7p1x.js} +12 -11
  61. package/{index-e4b2ae6b.js → index-ktb5njgy.js} +41 -34
  62. package/{index-etwh7sd3.js → index-mak3cd28.js} +100 -32
  63. package/{index-xjd3rqzj.js → index-n68jx79w.js} +6 -15
  64. package/{index-z1s5f4e0.js → index-ne2qy0nk.js} +36 -37
  65. package/{index-65aeh789.js → index-pnqbkf5p.js} +1 -1
  66. package/{index-67546d0j.js → index-q0y2jq9j.js} +14 -2
  67. package/{index-4mgkgba3.js → index-rwg2aa4f.js} +14 -14
  68. package/{index-bzznxgcx.js → index-v0qh106m.js} +1002 -128
  69. package/{index-j9cxndc0.js → index-w8kmx1b6.js} +20 -11
  70. package/{index-h5whaj26.js → index-ws5e0j51.js} +16 -12
  71. package/{index-9va8zayb.js → index-wt7qrtd4.js} +27 -27
  72. package/index.js +22 -25
  73. package/{library.command-x6t29a30.js → library.command-nbsky672.js} +11 -10
  74. package/{localRegistry.command-649g1n7b.js → localRegistry.command-ts6t5e5a.js} +20 -23
  75. package/{module.command-8w8ys5t3.js → module.command-29vqzrh8.js} +13 -16
  76. package/{package.command-zgy7f3fz.js → package.command-axv0abmn.js} +8 -10
  77. package/package.json +5 -7
  78. package/{page.command-yjmnkyk2.js → page.command-vs9s3zrh.js} +7 -9
  79. package/{primitive.command-gfqaagsg.js → primitive.command-cbxmqbtc.js} +13 -16
  80. package/{quality.command-ec8r1dn3.js → quality.command-gmbs7nws.js} +184 -334
  81. package/recipeScanner-fpagtgc1.js +11 -0
  82. package/{repair.command-p9ey6mkm.js → repair.command-bqsjptsj.js} +12 -14
  83. package/{routeSourceValidator-e4jmbfcx.js → routeSourceValidator-apyqw7wk.js} +90 -19
  84. package/{scalar.command-9dhmqhtm.js → scalar.command-6k1vhe9s.js} +11 -14
  85. package/subspace.command-35a3t6vv.js +755 -0
  86. package/templates/app/env/env.client.debug.ts.template +1 -5
  87. package/templates/app/env/env.client.develop.ts.template +1 -5
  88. package/templates/app/env/env.client.local.ts.template +1 -5
  89. package/templates/app/env/env.client.main.ts.template +1 -5
  90. package/templates/app/env/env.client.testing.ts.template +1 -5
  91. package/templates/app/env/env.client.type.ts +2 -6
  92. package/templates/app/env/env.server.debug.ts.template +1 -5
  93. package/templates/app/env/env.server.develop.ts.template +1 -5
  94. package/templates/app/env/env.server.local.ts.template +1 -5
  95. package/templates/app/env/env.server.main.ts.template +1 -5
  96. package/templates/app/main.ts +3 -1
  97. package/templates/app/page/_index.tsx +3 -3
  98. package/templates/app/page/_layout.tsx +31 -32
  99. package/templates/appSample/lib/task/task.signal.ts +11 -6
  100. package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
  101. package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
  102. package/templates/appSample/page/task/_index.tsx +3 -7
  103. package/templates/appSample/page/task/_layout.tsx +4 -15
  104. package/templates/appSample/page/task/new.tsx +4 -10
  105. package/templates/appSample/srvkit/AuthGuard.ts +2 -2
  106. package/templates/crudPages/[__model__Id]/edit/page.tsx +27 -29
  107. package/templates/crudPages/[__model__Id]/page.tsx +24 -32
  108. package/templates/crudPages/new/page.tsx +26 -25
  109. package/templates/crudPages/page.tsx +27 -26
  110. package/templates/crudSinglePage/page.tsx +26 -21
  111. package/templates/env/_env.server.type.ts +7 -5
  112. package/templates/lib/db.ts +1 -1
  113. package/templates/lib/dict.ts +1 -1
  114. package/templates/lib/sig.ts +5 -1
  115. package/templates/lib/srv.ts +6 -2
  116. package/templates/module/__model__.dictionary.ts +4 -1
  117. package/templates/module/__model__.signal.ts +2 -3
  118. package/templates/module/__model__.store.ts +1 -2
  119. package/templates/workspaceRoot/.gitignore.template +0 -1
  120. package/templates/workspaceRoot/.husky/pre-commit.template +16 -0
  121. package/templates/workspaceRoot/.vscode/settings.json.template +1 -1
  122. package/templates/workspaceRoot/AGENTS.md.template +4 -0
  123. package/templates/workspaceRoot/CLAUDE.md.template +19 -0
  124. package/templates/workspaceRoot/biome.json.template +1 -1
  125. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +10 -14
  126. package/templates/workspaceRoot/docs/GENERATED.md.template +12 -10
  127. package/templates/workspaceRoot/package.json.template +2 -4
  128. package/typeChecker-3xgrzc2v.js +7 -0
  129. package/typecheck.proc.js +4 -5
  130. package/{workflow.command-ycwc6zr6.js → workflow.command-nwpcdc8p.js} +22 -25
  131. package/{workspace.command-xhyhajxa.js → workspace.command-aczjxzbf.js} +64 -66
  132. package/capacitorApp-mr81qdxq.js +0 -61
  133. package/dependencyScanner-f6grrep3.js +0 -9
  134. package/getDirname-m9s0cmhq.js +0 -8
  135. package/index-mxvakhsm.js +0 -66
  136. package/index-r24hmh0q.js +0 -4
  137. package/recipeScanner-tghkzrrt.js +0 -12
  138. package/typeChecker-kravn7ns.js +0 -8
  139. /package/{index-0wae5ebk.js → index-meh558jy.js} +0 -0
  140. /package/{index-ss469dec.js → index-nz8prx9g.js} +0 -0
@@ -1,27 +1,123 @@
1
1
  // @bun
2
- import {
3
- LibraryScript
4
- } from "./index-8pd02prk.js";
5
- import {
6
- resolveMobileTargets
7
- } from "./index-76rn3g2c.js";
8
2
  import {
9
3
  runner,
10
4
  script
11
- } from "./index-etwh7sd3.js";
5
+ } from "./index-mak3cd28.js";
6
+ import {
7
+ WorkspaceExecutor,
8
+ AppExecutor,
9
+ LibExecutor,
10
+ PkgExecutor
11
+ } from "./index-2tv3fdx4.js";
12
+ import {
13
+ LibraryScript
14
+ } from "./index-259dsnz3.js";
12
15
  import {
13
16
  HmrWatcher,
14
17
  WatchRootResolver
15
- } from "./index-bjpxzr6s.js";
18
+ } from "./index-hwkygn9s.js";
16
19
  import {
17
- AppExecutor,
18
- LibExecutor,
19
- PkgExecutor,
20
- WorkspaceExecutor
21
- } from "./index-yzsxc30b.js";
20
+ SlicePlanner,
21
+ formatSlicePlan
22
+ } from "./index-j9yhg404.js";
22
23
  import {
23
- __require
24
- } from "./index-r24hmh0q.js";
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
+ }
25
121
 
26
122
  // pkgs/@akanjs/devkit/applicationBuildReporter.ts
27
123
  import { Logger } from "akanjs/common";
@@ -70,11 +166,12 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
70
166
 
71
167
  // pkgs/@akanjs/cli/application/application.script.ts
72
168
  import { confirm } from "@inquirer/prompts";
73
- import { Logger as Logger5 } from "akanjs/common";
169
+ import { Logger as Logger6 } from "akanjs/common";
170
+
171
+ // pkgs/@akanjs/cli/application/application.runner.ts
172
+ import path5 from "path";
74
173
 
75
174
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
76
- import { stat } from "fs/promises";
77
- import path2 from "path";
78
175
  import { Logger as Logger3 } from "akanjs/common";
79
176
 
80
177
  // pkgs/@akanjs/devkit/sshTunnel.ts
@@ -237,6 +334,8 @@ class IncrementalBuilderHost {
237
334
  app;
238
335
  ready = false;
239
336
  #onMessage;
337
+ #stdio;
338
+ #onOutput;
240
339
  #proc = null;
241
340
  #status = "stopped";
242
341
  #restartAttempts = 0;
@@ -247,11 +346,13 @@ class IncrementalBuilderHost {
247
346
  #manualStop = false;
248
347
  #inFlight = new Map;
249
348
  #startOptions = {};
250
- constructor({ app, entry, env, onMessage }) {
349
+ constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }) {
251
350
  this.app = app;
252
351
  this.entry = entry;
253
352
  this.env = env;
353
+ this.#stdio = stdio;
254
354
  this.#onMessage = onMessage;
355
+ this.#onOutput = onOutput ?? null;
255
356
  }
256
357
  get status() {
257
358
  return this.#status;
@@ -277,7 +378,7 @@ class IncrementalBuilderHost {
277
378
  proc = Bun.spawn(["bun", this.entry], {
278
379
  cwd: this.app.cwdPath,
279
380
  env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
280
- stdio: ["ignore", "inherit", "inherit"],
381
+ stdio: ["ignore", this.#stdio, this.#stdio],
281
382
  ipc: (msg) => {
282
383
  if (this.#proc !== proc)
283
384
  return;
@@ -325,8 +426,24 @@ class IncrementalBuilderHost {
325
426
  }
326
427
  });
327
428
  this.#proc = proc;
429
+ if (this.#stdio === "pipe" && this.#onOutput) {
430
+ this.#drain(proc.stdout, "stdout");
431
+ this.#drain(proc.stderr, "stderr");
432
+ }
328
433
  this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
329
434
  }
435
+ async#drain(stream, kind) {
436
+ if (!stream)
437
+ return;
438
+ const decoder = new TextDecoder;
439
+ try {
440
+ for await (const chunk of stream) {
441
+ const text = decoder.decode(chunk, { stream: true });
442
+ if (text)
443
+ this.#onOutput?.(kind, text);
444
+ }
445
+ } catch {}
446
+ }
330
447
  #scheduleRestart() {
331
448
  if (this.#manualStop || this.#restartTimer)
332
449
  return;
@@ -350,7 +467,7 @@ class IncrementalBuilderHost {
350
467
  return false;
351
468
  this.#recycleRequested = true;
352
469
  this.#status = "recycling";
353
- this.logger.info(`recycling builder pid=${proc.pid} (${reason})`);
470
+ this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
354
471
  this.#recycleTimer = setTimeout(() => {
355
472
  this.#recycleTimer = null;
356
473
  if (this.#proc !== proc)
@@ -422,7 +539,10 @@ class IncrementalBuilderHost {
422
539
  this.ready = false;
423
540
  this.#status = "stopped";
424
541
  }
425
- static async create(app, env, onMessage) {
542
+ static async create(app, env, onMessage, {
543
+ stdio = "inherit",
544
+ onOutput
545
+ } = {}) {
426
546
  const candidates = [
427
547
  path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
428
548
  path.join(app.workspace.workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
@@ -431,7 +551,7 @@ class IncrementalBuilderHost {
431
551
  ];
432
552
  for (const c of candidates)
433
553
  if (await Bun.file(c).exists())
434
- return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
554
+ return new IncrementalBuilderHost({ app, entry: c, env, stdio, onMessage, onOutput });
435
555
  throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
436
556
  }
437
557
  }
@@ -470,38 +590,23 @@ class BuilderRequestRouter {
470
590
  }
471
591
  }
472
592
 
473
- // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
474
- var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
475
- var BACKEND_RESTART_DEBOUNCE_MS = 120;
476
- var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
477
- var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
478
- var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
593
+ // pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
594
+ import { stat } from "fs/promises";
595
+ import path3 from "path";
596
+
597
+ // pkgs/@akanjs/devkit/akanApp/devHostPolicy.ts
598
+ import path2 from "path";
479
599
  var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
480
- var BACKEND_STDERR_TAIL_LIMIT = 40;
481
- var BUILDER_READY_TIMEOUT_MS = 150000;
482
- var BUILDER_START_MAX_ATTEMPTS = 3;
483
600
  var HELD_BUILDER_REQUEST_LIMIT = 64;
484
- var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
485
601
  var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
486
602
  var BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
487
- var BUILDER_RSS_SETTLE_MS = 20000;
488
- var PS_RSS_TIMEOUT_MS = 2000;
489
603
  var BUILDER_RSS_HARD_MULTIPLE = 1.5;
490
604
  var DEV_IDLE_SUSPEND_MS = 300000;
491
605
  var DEV_IDLE_MIN_UPTIME_MS = 30000;
492
- var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
493
- var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
494
606
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
495
- 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;
496
607
  var SERVER_SUFFIXES = [".service.ts", ".document.ts"];
497
608
  var SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
498
609
  var RUNTIME_METADATA_BASENAMES = new Set(["dict.ts", "sig.ts", "useClient.ts", "useServer.ts"]);
499
- var GRAPH_IMPORT_KINDS = new Set([
500
- "import-statement",
501
- "require-call",
502
- "require-resolve",
503
- "dynamic-import"
504
- ]);
505
610
  var shouldRestartBackendByDevPlan = (message) => {
506
611
  if (!message.devPlan)
507
612
  return null;
@@ -515,6 +620,15 @@ var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includ
515
620
  var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
516
621
  var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
517
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];
518
632
  var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
519
633
  var generationValue = (generation) => generation ?? -1;
520
634
  var isLegacyBackendFallbackFile = (file, workspaceRoot) => {
@@ -682,6 +796,15 @@ var mergeInvalidateMessages = (current, next) => {
682
796
  var buildStatusReplaySequence = (pendingReplay, latestByPhase) => [...pendingReplay, ...latestByPhase.values()];
683
797
  var filesChangedSince = (before, after) => [...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
684
798
 
799
+ // pkgs/@akanjs/devkit/akanApp/BackendImportGraph.ts
800
+ 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
+ var GRAPH_IMPORT_KINDS = new Set([
802
+ "import-statement",
803
+ "require-call",
804
+ "require-resolve",
805
+ "dynamic-import"
806
+ ]);
807
+
685
808
  class BackendImportGraph {
686
809
  #app;
687
810
  #logger;
@@ -704,7 +827,7 @@ class BackendImportGraph {
704
827
  return this.#lastRefreshSucceeded;
705
828
  }
706
829
  has(file) {
707
- return this.#files.has(path2.resolve(file));
830
+ return this.#files.has(path3.resolve(file));
708
831
  }
709
832
  async fingerprint() {
710
833
  const stamps = await Promise.all([...this.#files].map(async (file) => {
@@ -732,9 +855,9 @@ class BackendImportGraph {
732
855
  const roots = await this.#entrypoints();
733
856
  const files = new Set;
734
857
  const queue = [...roots];
735
- const workspaceRoot = path2.resolve(this.#app.workspace.workspaceRoot);
858
+ const workspaceRoot = path3.resolve(this.#app.workspace.workspaceRoot);
736
859
  while (queue.length > 0) {
737
- const current = path2.resolve(queue.pop());
860
+ const current = path3.resolve(queue.pop());
738
861
  if (files.has(current))
739
862
  continue;
740
863
  if (!this.#isWorkspaceSource(current, workspaceRoot))
@@ -743,7 +866,7 @@ class BackendImportGraph {
743
866
  if (!imports)
744
867
  continue;
745
868
  files.add(current);
746
- const importerDir = path2.dirname(current);
869
+ const importerDir = path3.dirname(current);
747
870
  for (const imp of imports) {
748
871
  if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
749
872
  continue;
@@ -774,7 +897,7 @@ class BackendImportGraph {
774
897
  const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
775
898
  const existing = [];
776
899
  for (const root of roots) {
777
- const abs = path2.resolve(root);
900
+ const abs = path3.resolve(root);
778
901
  if (await Bun.file(abs).exists())
779
902
  existing.push(abs);
780
903
  }
@@ -783,25 +906,25 @@ class BackendImportGraph {
783
906
  #resolve(specifier, importerDir) {
784
907
  try {
785
908
  const resolved = Bun.resolveSync(specifier, importerDir);
786
- if (!path2.isAbsolute(resolved))
909
+ if (!path3.isAbsolute(resolved))
787
910
  return null;
788
- if (!SOURCE_EXTS.has(path2.extname(resolved).toLowerCase()))
911
+ if (!SOURCE_EXTS.has(path3.extname(resolved).toLowerCase()))
789
912
  return null;
790
- return path2.resolve(resolved);
913
+ return path3.resolve(resolved);
791
914
  } catch {
792
915
  return null;
793
916
  }
794
917
  }
795
918
  #isWorkspaceSource(file, workspaceRoot) {
796
- const rel = path2.relative(workspaceRoot, file);
797
- if (rel.startsWith("..") || path2.isAbsolute(rel))
919
+ const rel = path3.relative(workspaceRoot, file);
920
+ if (rel.startsWith("..") || path3.isAbsolute(rel))
798
921
  return false;
799
- if (rel.includes(`${path2.sep}node_modules${path2.sep}`) || rel.includes(`${path2.sep}.akan${path2.sep}`))
922
+ if (rel.includes(`${path3.sep}node_modules${path3.sep}`) || rel.includes(`${path3.sep}.akan${path3.sep}`))
800
923
  return false;
801
- return SOURCE_EXTS.has(path2.extname(file).toLowerCase());
924
+ return SOURCE_EXTS.has(path3.extname(file).toLowerCase());
802
925
  }
803
926
  #scanImports(file, source) {
804
- const ext = path2.extname(file).toLowerCase();
927
+ const ext = path3.extname(file).toLowerCase();
805
928
  if (ext === ".tsx")
806
929
  return this.#tsxTranspiler.scanImports(source);
807
930
  if (ext === ".jsx")
@@ -812,11 +935,28 @@ class BackendImportGraph {
812
935
  }
813
936
  }
814
937
 
938
+ // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
939
+ var backendMsgTypeSet = new Set(["build-route", "build-csr"]);
940
+ var BACKEND_RESTART_DEBOUNCE_MS = 120;
941
+ var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
942
+ var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
943
+ var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
944
+ var BACKEND_STDERR_TAIL_LIMIT = 40;
945
+ var BUILDER_READY_TIMEOUT_MS = 150000;
946
+ var BUILDER_START_MAX_ATTEMPTS = 3;
947
+ var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
948
+ var BUILDER_RSS_SETTLE_MS = 20000;
949
+ var PS_RSS_TIMEOUT_MS = 2000;
950
+ var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
951
+ var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
952
+
815
953
  class AkanAppHost {
816
954
  app;
817
955
  logger = new Logger3("AkanAppHost");
818
- withInk;
956
+ stdio;
819
957
  env;
958
+ #onDevEvent;
959
+ #lastDevState = null;
820
960
  #backend = null;
821
961
  #builder = null;
822
962
  #backendReady = false;
@@ -855,12 +995,23 @@ class AkanAppHost {
855
995
  #builderGapStamp = null;
856
996
  #pendingBuilderMessages = [];
857
997
  #builderRequests = new BuilderRequestRouter;
858
- constructor(app, { env, withInk = false }) {
998
+ constructor(app, {
999
+ env,
1000
+ stdio = "inherit",
1001
+ onDevEvent
1002
+ }) {
859
1003
  this.app = app;
860
1004
  this.env = env;
861
- this.withInk = withInk;
1005
+ this.stdio = stdio;
1006
+ this.#onDevEvent = onDevEvent ?? null;
862
1007
  this.#backendGraph = new BackendImportGraph(app, this.logger);
863
1008
  }
1009
+ #emitDevEvent(state, detail) {
1010
+ if (!this.#onDevEvent || state === this.#lastDevState)
1011
+ return;
1012
+ this.#lastDevState = state;
1013
+ this.#onDevEvent({ app: this.app.name, state, ...detail ? { detail } : {} });
1014
+ }
864
1015
  async start() {
865
1016
  if (this.#backend)
866
1017
  await this.#stopBackend();
@@ -907,6 +1058,7 @@ class AkanAppHost {
907
1058
  }
908
1059
  #startBackend(startStatus = null) {
909
1060
  this.#builderRequests.startGeneration();
1061
+ this.#discardPendingBuilderMessages("the backend restarted while the builder was away");
910
1062
  this.#backendStartStatus = startStatus;
911
1063
  this.#backendGaveUp = false;
912
1064
  this.#setBackendLifecycleState("starting");
@@ -914,7 +1066,7 @@ class AkanAppHost {
914
1066
  this.#backendStderrTail = [];
915
1067
  const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
916
1068
  cwd: this.app.workspace.workspaceRoot,
917
- stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
1069
+ stdio: this.stdio === "pipe" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
918
1070
  env: this.env,
919
1071
  ipc: (msg) => {
920
1072
  if (!msg || typeof msg !== "object")
@@ -955,7 +1107,7 @@ class AkanAppHost {
955
1107
  });
956
1108
  this.#backend = backend;
957
1109
  this.logger.verbose(`backend spawned pid=${backend.pid}`);
958
- if (this.withInk) {
1110
+ if (this.stdio === "pipe") {
959
1111
  this.#forwardBackendStream(backend.stderr, "stderr");
960
1112
  this.#forwardBackendStream(backend.stdout, "stdout");
961
1113
  }
@@ -976,14 +1128,13 @@ class AkanAppHost {
976
1128
  try {
977
1129
  for await (const chunk of stream) {
978
1130
  const text = decoder.decode(chunk, { stream: true });
979
- if (!text.trim())
1131
+ if (!text)
980
1132
  continue;
981
1133
  if (kind === "stderr") {
982
1134
  this.#recordBackendStderr(text);
983
- this.logger.warn(`[backend] ${text.trimEnd()}`);
984
- } else {
985
- this.logger.verbose(`[backend] ${text.trimEnd()}`);
986
- }
1135
+ process.stderr.write(text);
1136
+ } else
1137
+ process.stdout.write(text);
987
1138
  }
988
1139
  } catch {}
989
1140
  }
@@ -1030,6 +1181,7 @@ class AkanAppHost {
1030
1181
  const prev = this.#backendLifecycleState;
1031
1182
  this.#backendLifecycleState = next;
1032
1183
  this.logger.verbose(`[backend-lifecycle] ${prev} -> ${next}${detail ? ` ${detail}` : ""}`);
1184
+ this.#emitDevEvent(devHostStateOf(next, this.#backendGaveUp), detail);
1033
1185
  }
1034
1186
  #sendToBackend(message) {
1035
1187
  if (!this.#backend || !this.#backendReady) {
@@ -1359,6 +1511,7 @@ ${this.#backendStderrTail.join(`
1359
1511
  return;
1360
1512
  }
1361
1513
  this.#suspended = true;
1514
+ this.#emitDevEvent("suspended", `idle ${Math.round(idleMs / 1000)}s`);
1362
1515
  this.#stopBuilder();
1363
1516
  this.#openBuilderGap("idle suspend");
1364
1517
  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`);
@@ -1423,6 +1576,7 @@ ${this.#backendStderrTail.join(`
1423
1576
  this.#wokeAtMono = performance.now();
1424
1577
  this.#flushPendingBuilderMessages();
1425
1578
  this.#armIdleSuspend();
1579
+ this.#emitDevEvent(devHostStateOf(this.#backendLifecycleState, this.#backendGaveUp));
1426
1580
  }
1427
1581
  }
1428
1582
  async#applyIdleWake(batch) {
@@ -1498,6 +1652,12 @@ ${this.#backendStderrTail.join(`
1498
1652
  this.#pendingBuilderMessages.push(message);
1499
1653
  this.logger.verbose(`[builder] holding ${message.type} until the builder is ready (${this.#pendingBuilderMessages.length} waiting)`);
1500
1654
  }
1655
+ #discardPendingBuilderMessages(reason) {
1656
+ const held = this.#pendingBuilderMessages.splice(0);
1657
+ if (held.length === 0)
1658
+ return;
1659
+ this.logger.verbose(`[builder] dropped ${held.length} held builder request(s): ${reason}`);
1660
+ }
1501
1661
  #failPendingBuilderMessages(reason) {
1502
1662
  const held = this.#pendingBuilderMessages.splice(0);
1503
1663
  if (held.length === 0)
@@ -1753,6 +1913,9 @@ ${this.#backendStderrTail.join(`
1753
1913
  for (let attempt = 1;attempt <= BUILDER_START_MAX_ATTEMPTS; attempt++) {
1754
1914
  this.#builder = await IncrementalBuilderHost.create(this.app, this.env, (msg) => {
1755
1915
  this.#enqueueBuilderMessage(msg);
1916
+ }, {
1917
+ stdio: this.stdio,
1918
+ onOutput: (kind, text) => void (kind === "stderr" ? process.stderr : process.stdout).write(text)
1756
1919
  });
1757
1920
  try {
1758
1921
  await this.#waitForBuilderReady(attempt, { announceBootState });
@@ -1858,20 +2021,20 @@ ${this.#backendStderrTail.join(`
1858
2021
  }
1859
2022
  }
1860
2023
  // pkgs/@akanjs/devkit/applicationTestPreload.ts
1861
- import path3 from "path";
2024
+ import path4 from "path";
1862
2025
  var SIGNAL_TEST_PRELOAD_PATH = "test/signalTest.preload.ts";
1863
2026
  async function resolveSignalTestPreloadPath(target) {
1864
2027
  const candidates = [];
1865
2028
  const addResolvedPackageCandidate = (basePath) => {
1866
2029
  try {
1867
- candidates.push(path3.join(path3.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
2030
+ candidates.push(path4.join(path4.dirname(Bun.resolveSync("akanjs/package.json", basePath)), SIGNAL_TEST_PRELOAD_PATH));
1868
2031
  } catch {}
1869
2032
  };
1870
2033
  addResolvedPackageCandidate(target.cwdPath);
1871
2034
  addResolvedPackageCandidate(process.cwd());
1872
- addResolvedPackageCandidate(path3.dirname(Bun.main));
2035
+ addResolvedPackageCandidate(path4.dirname(Bun.main));
1873
2036
  addResolvedPackageCandidate(import.meta.dir);
1874
- candidates.push(path3.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.join(path3.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path3.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
2037
+ 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));
1875
2038
  const uniqueCandidates = [...new Set(candidates)];
1876
2039
  for (const candidate of uniqueCandidates) {
1877
2040
  if (await Bun.file(candidate).exists())
@@ -1886,22 +2049,10 @@ ${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
1886
2049
  // pkgs/@akanjs/cli/application/application.runner.ts
1887
2050
  import { Logger as Logger4 } from "akanjs/common";
1888
2051
  import ora from "ora";
1889
-
1890
- // pkgs/@akanjs/cli/openBrowser.ts
1891
- import { spawn } from "child_process";
1892
- function openBrowser(url) {
1893
- const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
1894
- const child = spawn(command[0], command.slice(1), { detached: true, stdio: "ignore" });
1895
- child.on("error", () => {});
1896
- child.unref();
1897
- return Promise.resolve();
1898
- }
1899
-
1900
- // pkgs/@akanjs/cli/application/application.runner.ts
1901
- var loadBuildRunner = async () => (await import("./applicationBuildRunner-6m8n71w4.js")).ApplicationBuildRunner;
1902
- var loadReleasePackager = async () => (await import("./applicationReleasePackager-gg8ccbeh.js")).ApplicationReleasePackager;
1903
- var loadCapacitorApp = async () => (await import("./capacitorApp-mr81qdxq.js")).CapacitorApp;
1904
- var loadAbstractCompactor = async () => (await import("./abstractCompactor-e5yq60qz.js")).AbstractCompactor;
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-mjh5ppsq.js")).AbstractCompactor;
1905
2056
  var loadPrompts = async () => await import("@inquirer/prompts");
1906
2057
 
1907
2058
  class ApplicationRunner extends runner("application") {
@@ -1914,8 +2065,11 @@ class ApplicationRunner extends runner("application") {
1914
2065
  });
1915
2066
  return AppExecutor.from(workspace, appName);
1916
2067
  }
2068
+ async planSlice(app) {
2069
+ return await new SlicePlanner(app).plan();
2070
+ }
1917
2071
  async removeApplication(app) {
1918
- await app.workspace.exec(`rm -rf apps/${app.name}`);
2072
+ await app.workspace.removeDir(`apps/${app.name}`);
1919
2073
  }
1920
2074
  async getConfig(app) {
1921
2075
  return await app.getConfig();
@@ -1945,10 +2099,69 @@ class ApplicationRunner extends runner("application") {
1945
2099
  stdio: "inherit"
1946
2100
  });
1947
2101
  }
2102
+ #runtimeDirOf(app, override) {
2103
+ return path5.resolve(override ?? process.env.AKAN_RUNTIME_DIR ?? path5.join(app.workspace.workspaceRoot, "local", "apps", app.name, "runtime"));
2104
+ }
2105
+ async runLogs(app, options) {
2106
+ const { LogControlUnavailableError, LogTailClient } = await import("akanjs/server/logging/logTailClient");
2107
+ const { LogQueryMatcher } = await import("akanjs/server/logging/logQuery");
2108
+ const runtimeDir = this.#runtimeDirOf(app, options.runtimeDir);
2109
+ const socketPath = LogTailClient.socketPath(runtimeDir);
2110
+ const { level, grep, endpoint, trace, child, role, origin, since } = options;
2111
+ const query = Object.fromEntries(Object.entries({ level, grep, endpoint, trace, child, role, origin, since }).filter(([, value]) => value !== undefined && value !== null && value !== ""));
2112
+ const print = (record) => process.stdout.write(options.json ? `${JSON.stringify(record)}
2113
+ ` : Logger4.render(record));
2114
+ const note = (text) => process.stderr.write(`[akan logs] ${text}
2115
+ `);
2116
+ let finish = null;
2117
+ const closed = new Promise((resolve) => {
2118
+ finish = resolve;
2119
+ });
2120
+ let client;
2121
+ try {
2122
+ client = await LogTailClient.connect(socketPath, {
2123
+ onRecord: (entry) => print(entry.record),
2124
+ onEvent: (response) => {
2125
+ if (response.type === "dropped")
2126
+ note(`${response.count} records dropped (reader too slow)`);
2127
+ },
2128
+ onClose: () => finish?.()
2129
+ });
2130
+ } catch (error) {
2131
+ if (error instanceof LogControlUnavailableError)
2132
+ throw new Error(`${app.name} is not running (no ${path5.basename(socketPath)} in ${runtimeDir}). Start it with \`akan start ${app.name}\`, or pass --runtime-dir for a built app.`);
2133
+ throw error;
2134
+ }
2135
+ const sinceMs = since ? LogQueryMatcher.parseSince(since) : undefined;
2136
+ const noteCoverage = (coverage) => {
2137
+ if (sinceMs !== undefined && coverage.from !== null && sinceMs < coverage.from)
2138
+ note(`${LogTailClient.describeCoverage(coverage)}; older records are not retained`);
2139
+ };
2140
+ if (endpoint)
2141
+ note("requests served by the primitive query fast path carry no endpoint and are not shown");
2142
+ if (options.follow === false) {
2143
+ const { entries, coverage } = await client.history({
2144
+ ...query,
2145
+ ...options.replay ? { limit: options.replay } : {}
2146
+ });
2147
+ for (const entry of entries)
2148
+ print(entry.record);
2149
+ noteCoverage(coverage);
2150
+ client.close();
2151
+ return;
2152
+ }
2153
+ const subscribed = await client.subscribe(query, { replay: options.replay });
2154
+ noteCoverage(subscribed.coverage);
2155
+ const stop = () => client.close();
2156
+ process.once("SIGINT", stop);
2157
+ process.once("SIGTERM", stop);
2158
+ await closed;
2159
+ }
1948
2160
  async runConsole(app) {
1949
2161
  const serverPath = `${app.cwdPath}/server.ts`;
1950
2162
  if (!await app.exists("server.ts"))
1951
2163
  throw new Error(`Server file not found: apps/${app.name}/server.ts`);
2164
+ const runtimeDir = this.#runtimeDirOf(app);
1952
2165
  const code = `
1953
2166
  const serverModule = await import(${JSON.stringify(serverPath)});
1954
2167
  const { assertAkanConsoleAllowed, startAkanConsole } = await import("akanjs/server");
@@ -1958,6 +2171,7 @@ assertAkanConsoleAllowed(server.env);
1958
2171
  await server.start({ listen: false, web: false });
1959
2172
  try {
1960
2173
  await startAkanConsole(server, {
2174
+ runtimeDir: ${JSON.stringify(runtimeDir)},
1961
2175
  globals: {
1962
2176
  srv: serverModule.srv,
1963
2177
  sig: serverModule.sig,
@@ -2005,9 +2219,14 @@ try {
2005
2219
  } = {}) {
2006
2220
  return new (await loadBuildRunner())(app, { fast, reporter }).build({ spinner });
2007
2221
  }
2008
- async start(app, { open = false, onStart, withInk = false } = {}) {
2222
+ async start(app, {
2223
+ open = false,
2224
+ onStart,
2225
+ onDevEvent,
2226
+ stdio = "inherit"
2227
+ } = {}) {
2009
2228
  const { env } = await app.prepareCommand("start");
2010
- const appHost = await new AkanAppHost(app, { env, withInk }).start();
2229
+ const appHost = await new AkanAppHost(app, { env, stdio, onDevEvent }).start();
2011
2230
  onStart?.();
2012
2231
  if (open)
2013
2232
  setTimeout(() => openBrowser(`http://localhost:${env.AKAN_PUBLIC_CLIENT_PORT ?? env.PORT ?? "8282"}`), 3000);
@@ -2239,6 +2458,602 @@ ${detail}`
2239
2458
  }
2240
2459
  }
2241
2460
 
2461
+ // pkgs/@akanjs/cli/application/devPortReclaimer.ts
2462
+ class DevPortReclaimer {
2463
+ static lsofTimeoutMs = 3000;
2464
+ static releaseTimeoutMs = 8000;
2465
+ static maxAncestorDepth = 8;
2466
+ static #releasePollMs = 100;
2467
+ static wsPortOffset = 1e4;
2468
+ static portsFor(ports) {
2469
+ return [...new Set(ports.flatMap((port) => [port, port + DevPortReclaimer.wsPortOffset]))].sort((left, right) => left - right);
2470
+ }
2471
+ 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");
2473
+ }
2474
+ async reclaim(ports) {
2475
+ const killed = [];
2476
+ const foreign = [];
2477
+ const seen = new Set;
2478
+ for (const port of DevPortReclaimer.portsFor(ports)) {
2479
+ for (const holder of await this.holdersOf(port)) {
2480
+ if (seen.has(holder.pid))
2481
+ continue;
2482
+ seen.add(holder.pid);
2483
+ if (!holder.akan) {
2484
+ foreign.push(holder);
2485
+ continue;
2486
+ }
2487
+ if (await this.#terminate(holder.pid))
2488
+ killed.push(holder);
2489
+ else
2490
+ foreign.push(holder);
2491
+ }
2492
+ }
2493
+ return { killed, foreign };
2494
+ }
2495
+ async holdersOf(port) {
2496
+ const holders = [];
2497
+ for (const listenerPid of await this.#pidsOn(port)) {
2498
+ if (listenerPid === process.pid || listenerPid === process.ppid)
2499
+ continue;
2500
+ const command = await this.#commandOf(listenerPid);
2501
+ if (!command)
2502
+ continue;
2503
+ if (!DevPortReclaimer.isAkanCommand(command)) {
2504
+ holders.push({ pid: listenerPid, command, port, listenerPid, akan: false });
2505
+ continue;
2506
+ }
2507
+ const root = await this.#akanRootOf(listenerPid, command);
2508
+ holders.push({ pid: root.pid, command: root.command, port, listenerPid, akan: true });
2509
+ }
2510
+ return holders;
2511
+ }
2512
+ async#akanRootOf(pid, command) {
2513
+ let root = { pid, command };
2514
+ for (let depth = 0;depth < DevPortReclaimer.maxAncestorDepth; depth += 1) {
2515
+ const parentPid = await this.#ppidOf(root.pid);
2516
+ if (parentPid === null || parentPid <= 1 || parentPid === process.pid || parentPid === process.ppid)
2517
+ break;
2518
+ const parentCommand = await this.#commandOf(parentPid);
2519
+ if (!parentCommand || !DevPortReclaimer.isAkanCommand(parentCommand))
2520
+ break;
2521
+ root = { pid: parentPid, command: parentCommand };
2522
+ }
2523
+ return root;
2524
+ }
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
+ async#terminate(pid) {
2542
+ if (!this.#signal(pid, "SIGTERM"))
2543
+ return !this.#alive(pid);
2544
+ const deadline = Date.now() + DevPortReclaimer.releaseTimeoutMs;
2545
+ while (Date.now() < deadline) {
2546
+ if (!this.#alive(pid))
2547
+ return true;
2548
+ await Bun.sleep(DevPortReclaimer.#releasePollMs);
2549
+ }
2550
+ this.#signal(pid, "SIGKILL");
2551
+ await Bun.sleep(DevPortReclaimer.#releasePollMs);
2552
+ return !this.#alive(pid);
2553
+ }
2554
+ #signal(pid, signal) {
2555
+ try {
2556
+ process.kill(pid, signal);
2557
+ return true;
2558
+ } catch {
2559
+ return false;
2560
+ }
2561
+ }
2562
+ #alive(pid) {
2563
+ try {
2564
+ process.kill(pid, 0);
2565
+ return true;
2566
+ } catch (error) {
2567
+ return error.code === "EPERM";
2568
+ }
2569
+ }
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
+ }
2582
+
2583
+ // pkgs/@akanjs/cli/application/devStreamView.ts
2584
+ import { Logger as Logger5 } from "akanjs/common";
2585
+ import chalk from "chalk";
2586
+ var appColors = ["cyan", "magenta", "yellow", "green", "blue", "red"];
2587
+
2588
+ class DevStreamView {
2589
+ #logger = new Logger5("akan start");
2590
+ #partial = new Map;
2591
+ #lastState = new Map;
2592
+ #color = new Map;
2593
+ #width = 0;
2594
+ constructor(appNames) {
2595
+ this.#width = appNames.reduce((max, name) => Math.max(max, name.length), 0);
2596
+ appNames.forEach((name, idx) => {
2597
+ this.#color.set(name, appColors[idx % appColors.length] ?? "cyan");
2598
+ });
2599
+ }
2600
+ onOutput = (app, kind, text) => {
2601
+ const key = `${app}:${kind}`;
2602
+ const pending = `${this.#partial.get(key) ?? ""}${text}`;
2603
+ const lines = pending.split(`
2604
+ `);
2605
+ this.#partial.set(key, lines.pop() ?? "");
2606
+ if (lines.length === 0)
2607
+ return;
2608
+ const prefixed = lines.map((line) => `${this.#prefix(app)}${line}`).join(`
2609
+ `);
2610
+ (kind === "stderr" ? process.stderr : process.stdout).write(`${prefixed}
2611
+ `);
2612
+ };
2613
+ onStatus = (statuses) => {
2614
+ for (const status of statuses) {
2615
+ if (this.#lastState.get(status.name) === status.state)
2616
+ continue;
2617
+ this.#lastState.set(status.name, status.state);
2618
+ const detail = status.detail ? ` (${status.detail})` : "";
2619
+ const where = status.state === "ready" ? ` \u2014 ${status.url}` : "";
2620
+ const line = `${status.name} ${status.state}${detail}${where}`;
2621
+ if (status.state === "failed")
2622
+ this.#logger.error(line);
2623
+ else if (status.state === "stopped")
2624
+ this.#logger.warn(line);
2625
+ else
2626
+ this.#logger.info(line);
2627
+ }
2628
+ };
2629
+ onNote = (text, level) => {
2630
+ if (level === "warn")
2631
+ this.#logger.warn(text);
2632
+ else
2633
+ this.#logger.info(text);
2634
+ };
2635
+ waitForExit = () => new Promise(() => {
2636
+ return;
2637
+ });
2638
+ close = () => {
2639
+ for (const [key, remainder] of this.#partial) {
2640
+ if (!remainder)
2641
+ continue;
2642
+ const app = key.slice(0, key.lastIndexOf(":"));
2643
+ process.stdout.write(`${this.#prefix(app)}${remainder}
2644
+ `);
2645
+ }
2646
+ this.#partial.clear();
2647
+ };
2648
+ #prefix(app) {
2649
+ const color = this.#color.get(app) ?? "cyan";
2650
+ return `${chalk[color](app.padEnd(this.#width))} ${chalk.dim("\u2502")} `;
2651
+ }
2652
+ }
2653
+
2654
+ // pkgs/@akanjs/cli/application/devBootConcurrency.ts
2655
+ import os from "os";
2656
+ import { MemoryLimit as MemoryLimit2 } from "akanjs/server/memoryLimit";
2657
+
2658
+ class DevBootConcurrency {
2659
+ static perAppBytes = 900 * 1024 * 1024;
2660
+ static memoryShare = 0.5;
2661
+ static coresPerApp = 4;
2662
+ static budget() {
2663
+ const hostBytes = os.totalmem();
2664
+ const declaredBytes = MemoryLimit2.parseBytesEnv("AKAN_MEMORY_LIMIT") ?? MemoryLimit2.readCgroupBytes();
2665
+ return { memoryBytes: Math.min(hostBytes, declaredBytes ?? hostBytes), cores: os.availableParallelism() };
2666
+ }
2667
+ static resolve(appCount, requested, budget = DevBootConcurrency.budget()) {
2668
+ const apps = Math.max(1, Math.trunc(appCount));
2669
+ if (requested !== null && Number.isFinite(requested)) {
2670
+ const asked = Math.max(1, Math.trunc(requested));
2671
+ return { concurrency: Math.min(apps, asked), reason: `--concurrency ${asked}` };
2672
+ }
2673
+ const byMemory = Math.floor(budget.memoryBytes * DevBootConcurrency.memoryShare / DevBootConcurrency.perAppBytes);
2674
+ const byCores = Math.floor(budget.cores / DevBootConcurrency.coresPerApp);
2675
+ return {
2676
+ concurrency: Math.max(1, Math.min(apps, byMemory, byCores)),
2677
+ reason: `${budget.cores} cores, ${DevBootConcurrency.#formatBytes(budget.memoryBytes)} memory`
2678
+ };
2679
+ }
2680
+ static describe(appCount, plan) {
2681
+ if (plan.concurrency >= appCount)
2682
+ return `booting all ${appCount} apps at once (${plan.reason})`;
2683
+ return `booting ${plan.concurrency} of ${appCount} apps at a time (${plan.reason}) \u2014 --concurrency raises it`;
2684
+ }
2685
+ static #formatBytes(bytes) {
2686
+ const gigabytes = bytes / 1024 ** 3;
2687
+ return gigabytes >= 10 ? `${Math.round(gigabytes)}GB` : `${gigabytes.toFixed(1)}GB`;
2688
+ }
2689
+ }
2690
+
2691
+ // pkgs/@akanjs/cli/application/devSessionLog.ts
2692
+ import { mkdir, rename } from "fs/promises";
2693
+ import path6 from "path";
2694
+ import dayjs from "dayjs";
2695
+ class DevSessionLog {
2696
+ static fileName = "dev.log";
2697
+ static previousFileName = "dev.prev.log";
2698
+ static flushMs = 200;
2699
+ static bufferBytes = 64 * 1024;
2700
+ #workspaceRoot;
2701
+ #appNames;
2702
+ #now;
2703
+ #writers = new Map;
2704
+ #partial = new Map;
2705
+ #lastState = new Map;
2706
+ #flushTimer = null;
2707
+ constructor({ workspaceRoot, apps, now = () => new Date }) {
2708
+ this.#workspaceRoot = workspaceRoot;
2709
+ this.#appNames = [...apps];
2710
+ this.#now = now;
2711
+ }
2712
+ get appNames() {
2713
+ return [...this.#appNames];
2714
+ }
2715
+ pathOf(app) {
2716
+ return path6.join(this.#runtimeDirOf(app), DevSessionLog.fileName);
2717
+ }
2718
+ previousPathOf(app) {
2719
+ return path6.join(this.#runtimeDirOf(app), DevSessionLog.previousFileName);
2720
+ }
2721
+ relativePathOf(app) {
2722
+ return path6.relative(this.#workspaceRoot, this.pathOf(app));
2723
+ }
2724
+ async open() {
2725
+ const stamp = dayjs(this.#now()).format("YYYY-MM-DD HH:mm:ss");
2726
+ for (const app of this.#appNames) {
2727
+ const file = this.pathOf(app);
2728
+ await mkdir(path6.dirname(file), { recursive: true });
2729
+ await rename(file, this.previousPathOf(app)).catch(() => {
2730
+ return;
2731
+ });
2732
+ this.#writers.set(app, Bun.file(file).writer({ highWaterMark: DevSessionLog.bufferBytes }));
2733
+ this.#writeLine(app, `\u2500\u2500 akan start \xB7 ${app} \xB7 ${stamp} \u2500\u2500`);
2734
+ }
2735
+ }
2736
+ write(app, kind, chunk) {
2737
+ const key = `${app}:${kind}`;
2738
+ const parts = `${this.#partial.get(key) ?? ""}${chunk}`.split(`
2739
+ `);
2740
+ this.#partial.set(key, parts.pop() ?? "");
2741
+ for (const line of parts)
2742
+ this.#writeLine(app, stripAnsi(line));
2743
+ }
2744
+ note(text) {
2745
+ for (const app of this.#appNames)
2746
+ this.#writeLine(app, `[akan] ${text}`);
2747
+ }
2748
+ status(statuses) {
2749
+ for (const status of statuses) {
2750
+ if (this.#lastState.get(status.name) === status.state)
2751
+ continue;
2752
+ this.#lastState.set(status.name, status.state);
2753
+ const detail = status.detail ? ` (${status.detail})` : "";
2754
+ const where = status.state === "ready" ? ` \u2014 ${status.url}` : "";
2755
+ this.#writeLine(status.name, `[akan] ${status.name} ${status.state}${detail}${where}`);
2756
+ }
2757
+ }
2758
+ async close() {
2759
+ for (const [key, remainder] of this.#partial) {
2760
+ if (!remainder)
2761
+ continue;
2762
+ this.#writeLine(key.slice(0, key.lastIndexOf(":")), stripAnsi(remainder));
2763
+ }
2764
+ this.#partial.clear();
2765
+ if (this.#flushTimer)
2766
+ clearTimeout(this.#flushTimer);
2767
+ this.#flushTimer = null;
2768
+ await Promise.all([...this.#writers.values()].map(async (writer) => await writer.end()));
2769
+ this.#writers.clear();
2770
+ }
2771
+ #runtimeDirOf(app) {
2772
+ return path6.join(this.#workspaceRoot, "local", "apps", app, "runtime");
2773
+ }
2774
+ #writeLine(app, text) {
2775
+ const writer = this.#writers.get(app);
2776
+ if (!writer)
2777
+ return;
2778
+ writer.write(`${text}
2779
+ `);
2780
+ this.#scheduleFlush();
2781
+ }
2782
+ #scheduleFlush() {
2783
+ if (this.#flushTimer)
2784
+ return;
2785
+ this.#flushTimer = setTimeout(() => {
2786
+ this.#flushTimer = null;
2787
+ for (const writer of this.#writers.values())
2788
+ writer.flush();
2789
+ }, DevSessionLog.flushMs);
2790
+ }
2791
+ }
2792
+
2793
+ // pkgs/@akanjs/cli/application/devSupervisor.ts
2794
+ class DevSupervisor {
2795
+ static supervisedEnvKey = "AKAN_DEV_SUPERVISED";
2796
+ static childHooks() {
2797
+ if (process.env[DevSupervisor.supervisedEnvKey] !== "1" || !process.send)
2798
+ return {};
2799
+ const send = process.send.bind(process);
2800
+ return {
2801
+ stdio: "pipe",
2802
+ onDevEvent: (event) => {
2803
+ try {
2804
+ send(event);
2805
+ } catch {}
2806
+ }
2807
+ };
2808
+ }
2809
+ static readyTimeoutMs = 180000;
2810
+ static shutdownGraceMs = 12000;
2811
+ #options;
2812
+ #children = new Map;
2813
+ #sessionLog;
2814
+ #view = null;
2815
+ #stopping = false;
2816
+ constructor(options) {
2817
+ this.#options = options;
2818
+ this.#sessionLog = new DevSessionLog({
2819
+ workspaceRoot: options.apps[0]?.workspace.workspaceRoot ?? process.cwd(),
2820
+ apps: options.apps.map((app) => app.name)
2821
+ });
2822
+ }
2823
+ get statuses() {
2824
+ return [...this.#children.values()].map((child) => child.status);
2825
+ }
2826
+ get sessionLog() {
2827
+ return this.#sessionLog;
2828
+ }
2829
+ async run(view) {
2830
+ this.#view = view;
2831
+ await this.#sessionLog.open();
2832
+ const ports = await this.#assignPorts();
2833
+ for (const app of this.#options.apps)
2834
+ this.#children.set(app.name, this.#makeChild(app, ports.get(app.name) ?? 0));
2835
+ this.#publishStatus();
2836
+ const logPaths = this.#sessionLog.appNames.map((name) => this.#sessionLog.relativePathOf(name));
2837
+ this.#note(`session log: ${logPaths.join(" \xB7 ")}`, "info");
2838
+ const stopped = this.#installSignalHandlers();
2839
+ this.#bootInOrder();
2840
+ await Promise.race([view.waitForExit(), stopped]);
2841
+ await this.stop();
2842
+ view.close?.();
2843
+ await this.#sessionLog.close();
2844
+ }
2845
+ async#assignPorts() {
2846
+ const taken = new Set;
2847
+ const ports = new Map;
2848
+ for (const app of this.#options.apps) {
2849
+ let port = await app.getDevPort();
2850
+ while (taken.has(port))
2851
+ port += 1;
2852
+ taken.add(port);
2853
+ ports.set(app.name, port);
2854
+ }
2855
+ return ports;
2856
+ }
2857
+ #makeChild(app, port) {
2858
+ let markReady;
2859
+ const ready = new Promise((resolve) => {
2860
+ markReady = resolve;
2861
+ });
2862
+ return {
2863
+ status: {
2864
+ app,
2865
+ name: app.name,
2866
+ port,
2867
+ url: `http://localhost:${port}`,
2868
+ state: "starting",
2869
+ detail: "queued",
2870
+ exitCode: null
2871
+ },
2872
+ proc: null,
2873
+ ready,
2874
+ markReady,
2875
+ opened: false
2876
+ };
2877
+ }
2878
+ async#bootInOrder() {
2879
+ const queue = [...this.#children.values()];
2880
+ const plan = DevBootConcurrency.resolve(queue.length, this.#options.concurrency ?? null);
2881
+ const concurrency = plan.concurrency;
2882
+ if (queue.length > 1)
2883
+ this.#note(DevBootConcurrency.describe(queue.length, plan), "info");
2884
+ const waves = [];
2885
+ for (let idx = 0;idx < queue.length; idx += concurrency)
2886
+ waves.push(queue.slice(idx, idx + concurrency));
2887
+ for (const wave of waves) {
2888
+ if (this.#stopping)
2889
+ return;
2890
+ for (const child of wave)
2891
+ this.#spawnChild(child);
2892
+ this.#publishStatus();
2893
+ await Promise.all(wave.map(async (child) => await this.#waitForReady(child)));
2894
+ }
2895
+ }
2896
+ async#waitForReady(child) {
2897
+ let timer = null;
2898
+ const expired = new Promise((resolve) => {
2899
+ timer = setTimeout(() => resolve("expired"), DevSupervisor.readyTimeoutMs);
2900
+ });
2901
+ 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");
2904
+ } finally {
2905
+ if (timer)
2906
+ clearTimeout(timer);
2907
+ }
2908
+ }
2909
+ static childArgs(name, { write = true } = {}) {
2910
+ return [
2911
+ "start",
2912
+ name,
2913
+ "--plain",
2914
+ "--dbup",
2915
+ "false",
2916
+ "--write",
2917
+ String(write)
2918
+ ];
2919
+ }
2920
+ #spawnChild(child) {
2921
+ const { app, name, port } = child.status;
2922
+ const args = DevSupervisor.childArgs(name, { write: this.#options.write ?? true });
2923
+ const proc = Bun.spawn([process.execPath, Bun.main, ...args], {
2924
+ cwd: app.workspace.workspaceRoot,
2925
+ env: {
2926
+ ...process.env,
2927
+ [DevSupervisor.supervisedEnvKey]: "1",
2928
+ AKAN_PUBLIC_APP_NAME: name,
2929
+ AKAN_DEV_PORT: String(port)
2930
+ },
2931
+ stdio: ["ignore", "pipe", "pipe"],
2932
+ ipc: (message) => {
2933
+ if (!message || typeof message !== "object" || typeof message.state !== "string")
2934
+ return;
2935
+ this.#applyChildEvent(child, message);
2936
+ },
2937
+ serialization: "advanced",
2938
+ onExit: (_proc, exitCode, signalCode) => {
2939
+ child.proc = null;
2940
+ child.status.exitCode = exitCode;
2941
+ if (this.#stopping)
2942
+ return;
2943
+ child.status.state = "stopped";
2944
+ child.status.detail = signalCode ? `killed by ${signalCode}` : `exited with ${exitCode}`;
2945
+ child.markReady();
2946
+ this.#publishStatus();
2947
+ }
2948
+ });
2949
+ child.proc = proc;
2950
+ child.status.state = "starting";
2951
+ child.status.detail = `pid ${proc.pid}`;
2952
+ this.#drain(proc.stdout, name, "stdout");
2953
+ this.#drain(proc.stderr, name, "stderr");
2954
+ }
2955
+ #applyChildEvent(child, event) {
2956
+ child.status.state = event.state;
2957
+ child.status.detail = event.detail ?? "";
2958
+ if (event.state === "ready") {
2959
+ child.markReady();
2960
+ if (this.#options.open && !child.opened) {
2961
+ child.opened = true;
2962
+ openBrowser(child.status.url);
2963
+ }
2964
+ }
2965
+ if (event.state === "failed")
2966
+ child.markReady();
2967
+ this.#publishStatus();
2968
+ }
2969
+ async#drain(stream, name, kind) {
2970
+ const decoder = new TextDecoder;
2971
+ try {
2972
+ for await (const chunk of stream) {
2973
+ const text = decoder.decode(chunk, { stream: true });
2974
+ if (!text)
2975
+ continue;
2976
+ this.#view?.onOutput(name, kind, text);
2977
+ this.#sessionLog.write(name, kind, text);
2978
+ }
2979
+ } catch {}
2980
+ }
2981
+ #publishStatus() {
2982
+ this.#view?.onStatus(this.statuses);
2983
+ this.#sessionLog.status(this.statuses);
2984
+ }
2985
+ #note(text, level) {
2986
+ this.#view?.onNote(text, level);
2987
+ this.#sessionLog.note(text);
2988
+ }
2989
+ #installSignalHandlers() {
2990
+ return new Promise((resolve) => {
2991
+ let asked = false;
2992
+ const onSignal = () => {
2993
+ if (asked) {
2994
+ this.#note("abandoning the wait; children may be left running", "warn");
2995
+ process.exit(130);
2996
+ }
2997
+ asked = true;
2998
+ resolve();
2999
+ };
3000
+ process.on("SIGINT", onSignal);
3001
+ process.on("SIGTERM", onSignal);
3002
+ });
3003
+ }
3004
+ async restart(name) {
3005
+ const child = this.#children.get(name);
3006
+ if (!child || this.#stopping)
3007
+ return;
3008
+ const running = child.proc;
3009
+ if (running) {
3010
+ running.kill("SIGTERM");
3011
+ await running.exited;
3012
+ }
3013
+ child.status.state = "starting";
3014
+ child.status.detail = "restarting";
3015
+ child.status.exitCode = null;
3016
+ this.#publishStatus();
3017
+ this.#spawnChild(child);
3018
+ this.#publishStatus();
3019
+ }
3020
+ async stop() {
3021
+ if (this.#stopping)
3022
+ return;
3023
+ this.#stopping = true;
3024
+ const running = [...this.#children.values()].filter((child) => Boolean(child.proc));
3025
+ for (const child of running)
3026
+ child.proc.kill("SIGTERM");
3027
+ 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);
3044
+ }
3045
+ }
3046
+ }
3047
+
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
+ };
3056
+
2242
3057
  // pkgs/@akanjs/cli/application/application.script.ts
2243
3058
  class ApplicationScript extends script("application", [ApplicationRunner, LibraryScript]) {
2244
3059
  static dbShutdownTimeoutMs = 20000;
@@ -2322,13 +3137,19 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2322
3137
  spinner.succeed(`Application created in apps/${app.name}`);
2323
3138
  await app.scanSync();
2324
3139
  if (start)
2325
- await this.start(app, { open: true });
3140
+ await this.startOne(app, { open: true });
2326
3141
  }
2327
3142
  async removeApplication(app) {
2328
3143
  const spinner = app.spinning("Removing application...");
2329
3144
  await this.applicationRunner.removeApplication(app);
2330
3145
  spinner.succeed(`Application ${app.name} (apps/${app.name}) removed`);
2331
3146
  }
3147
+ async planSlice(app, format = "text") {
3148
+ const spinner = app.spinning("Planning distributable slice...");
3149
+ const plan = await this.applicationRunner.planSlice(app);
3150
+ spinner.succeed(`Slice planned (${plan.libs.length} libs, ${plan.warnings.length} warnings)`);
3151
+ Logger6.rawLog(format === "json" ? JSON.stringify(plan, null, 2) : formatSlicePlan(plan));
3152
+ }
2332
3153
  async sync(sys) {
2333
3154
  if (sys.type === "app")
2334
3155
  await sys.scanSync();
@@ -2338,13 +3159,13 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2338
3159
  async compact(sys, { module = null, minLines, interactive = false } = {}) {
2339
3160
  const { scanned, reports } = await this.applicationRunner.compact(sys, { module, minLines, interactive });
2340
3161
  if (!reports.length) {
2341
- Logger5.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
3162
+ Logger6.rawLog(`No abstract file long enough to compact in ${sys.name} (${scanned} scanned)`);
2342
3163
  return;
2343
3164
  }
2344
3165
  for (const report of reports)
2345
- Logger5.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
3166
+ Logger6.rawLog(`${report.status}: ${report.path} (${report.beforeLines} -> ${report.afterLines} lines)`);
2346
3167
  const compactedNum = reports.filter((report) => report.status === "compacted").length;
2347
- Logger5.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
3168
+ Logger6.rawLog(`Compacted ${compactedNum}/${reports.length} abstract files in ${sys.name}`);
2348
3169
  }
2349
3170
  async script(app, filename) {
2350
3171
  const scriptFilename = filename ?? await this.applicationRunner.getScriptFilename(app);
@@ -2355,21 +3176,24 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2355
3176
  await app.scanSync();
2356
3177
  await this.applicationRunner.runConsole(app);
2357
3178
  }
3179
+ async logs(app, options) {
3180
+ await this.applicationRunner.runLogs(app, options);
3181
+ }
2358
3182
  async build(app, { write = true, fast = false, quiet = false } = {}) {
2359
3183
  await app.scanSync({ write });
2360
3184
  if (!quiet)
2361
- Logger5.rawLog(`Creating an optimized production build for ${app.name}...`);
3185
+ Logger6.rawLog(`Creating an optimized production build for ${app.name}...`);
2362
3186
  try {
2363
3187
  const result = await this.applicationRunner.build(app, {
2364
3188
  fast,
2365
3189
  spinner: !quiet
2366
3190
  });
2367
- Logger5.rawLog(`${app.name} built in dist/apps/${app.name}`);
3191
+ Logger6.rawLog(`${app.name} built in dist/apps/${app.name}`);
2368
3192
  if (!quiet)
2369
3193
  ApplicationBuildReporter.printSummary(result);
2370
3194
  } catch (error) {
2371
- Logger5.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
2372
- Logger5.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
3195
+ Logger6.rawLog(`${app.name} build failed in dist/apps/${app.name}`, undefined, "error");
3196
+ Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
2373
3197
  throw error;
2374
3198
  }
2375
3199
  }
@@ -2381,15 +3205,15 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2381
3205
  spinner.succeed(`${app.name} typechecked`);
2382
3206
  } catch (error) {
2383
3207
  spinner.fail(`${app.name} typecheck failed`);
2384
- Logger5.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
3208
+ Logger6.rawLog(ApplicationBuildReporter.formatError(error), undefined, "error");
2385
3209
  throw error;
2386
3210
  }
2387
3211
  }
2388
3212
  async test(exec, { write = true } = {}) {
2389
3213
  if (exec instanceof LibExecutor) {
2390
3214
  await this.libraryScript.syncLibrary(exec);
2391
- const spinner2 = exec.spinning(`Preparing ${exec.name}...`);
2392
- spinner2.succeed(`${exec.name} prepared`);
3215
+ const spinner = exec.spinning(`Preparing ${exec.name}...`);
3216
+ spinner.succeed(`${exec.name} prepared`);
2393
3217
  await this.applicationRunner.test(exec);
2394
3218
  return;
2395
3219
  }
@@ -2406,12 +3230,20 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2406
3230
  }
2407
3231
  await this.applicationRunner.test(exec);
2408
3232
  }
2409
- async start(app, {
2410
- open = false,
2411
- dbup = true,
2412
- withInk = false,
2413
- write = true
2414
- } = {}) {
3233
+ async start(apps, { open = false, dbup = true, write = true, plain = false, kill = false, concurrency = null } = {}) {
3234
+ const first = apps[0];
3235
+ if (!first)
3236
+ throw new Error("No app selected to start");
3237
+ const { mode, downgraded } = resolveDevUi(plain);
3238
+ if (downgraded)
3239
+ first.workspace.log("no sized terminal to draw on; printing prefixed lines instead");
3240
+ if (kill)
3241
+ await this.reclaimDevPorts(apps);
3242
+ if (mode === "stream" && apps.length === 1)
3243
+ return await this.startOne(first, { open, dbup, write, ...DevSupervisor.childHooks() });
3244
+ await this.#startMany(apps, { open, dbup, write, mode, concurrency });
3245
+ }
3246
+ async startOne(app, { open = false, dbup = true, stdio = "inherit", write = true, onDevEvent } = {}) {
2415
3247
  await app.scanSync({ write });
2416
3248
  const akanConfig = await app.getConfig();
2417
3249
  const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
@@ -2427,10 +3259,55 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2427
3259
  onStart: () => {
2428
3260
  spinner.succeed(`${app.name} prepared, ready to start`);
2429
3261
  },
2430
- withInk
3262
+ onDevEvent,
3263
+ stdio
2431
3264
  });
2432
3265
  return akanAppHost;
2433
3266
  }
3267
+ async reclaimDevPorts(apps) {
3268
+ const workspace = apps[0]?.workspace;
3269
+ if (!workspace)
3270
+ return;
3271
+ const ports = await Promise.all(apps.map(async (app) => await app.getDevPort()));
3272
+ const { killed, foreign } = await new DevPortReclaimer().reclaim(ports);
3273
+ for (const holder of killed)
3274
+ workspace.log(`freed port ${holder.port} \u2014 killed pid ${holder.pid} (${holder.command})`);
3275
+ for (const holder of foreign)
3276
+ Logger6.rawLog(`port ${holder.port} is held by pid ${holder.pid} and was left alone: ${holder.command}`, undefined, "error");
3277
+ if (killed.length === 0 && foreign.length === 0)
3278
+ workspace.log("dev ports are free; nothing to kill");
3279
+ }
3280
+ async#startMany(apps, { open, dbup, write, mode, concurrency }) {
3281
+ const workspace = apps[0]?.workspace;
3282
+ if (!workspace)
3283
+ throw new Error("No app selected to start");
3284
+ 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-c08ccbat.js")).createDevTuiView(supervisor) : new DevStreamView(apps.map((app) => app.name));
3287
+ await supervisor.run(view);
3288
+ if (startedDatabase)
3289
+ await this.#stopDatabase(workspace);
3290
+ }
3291
+ async#prepareSharedDatabase(apps) {
3292
+ const workspace = apps[0]?.workspace;
3293
+ if (!workspace)
3294
+ return false;
3295
+ const modes = new Set;
3296
+ for (const app of apps) {
3297
+ if (app.getEnv() !== "local")
3298
+ continue;
3299
+ const akanConfig = await app.getConfig();
3300
+ const mode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
3301
+ await this.syncDatabaseModeDependencies(app, akanConfig, mode);
3302
+ if (mode !== "single")
3303
+ modes.add(mode);
3304
+ }
3305
+ let started = false;
3306
+ for (const mode of modes)
3307
+ if (!await this.dbup(workspace, mode))
3308
+ started = true;
3309
+ return started;
3310
+ }
2434
3311
  async buildIos(app, { write = true, target, env = "debug", regenerate = false } = {}) {
2435
3312
  await app.scanSync({ write });
2436
3313
  await this.applicationRunner.buildIos(app, { target, env, regenerate });
@@ -2528,7 +3405,7 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2528
3405
  let stopping = false;
2529
3406
  process.on("SIGINT", () => {
2530
3407
  if (stopping) {
2531
- Logger5.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "warn");
3408
+ Logger6.rawLog("Abandoning the local database teardown; containers are left running.", undefined, "error");
2532
3409
  process.exit(130);
2533
3410
  }
2534
3411
  stopping = true;
@@ -2536,25 +3413,22 @@ class ApplicationScript extends script("application", [ApplicationRunner, Librar
2536
3413
  });
2537
3414
  }
2538
3415
  async#stopDatabase(workspace) {
2539
- const timeout = new Promise((resolve) => setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs));
2540
- const result = await Promise.race([this.dbdown(workspace).catch(() => {
2541
- return;
2542
- }), timeout]);
2543
- if (result !== "timeout")
2544
- return;
2545
- Logger5.rawLog(`Local database did not stop within ${ApplicationScript.dbShutdownTimeoutMs}ms; run \`akan dbdown\` once Docker responds.`, undefined, "warn");
2546
- }
2547
- async testSys(sys) {
2548
- if (sys.type === "app")
2549
- await this.testApplication(sys);
2550
- else
2551
- await this.libraryScript.testLibrary(sys);
2552
- }
2553
- async testApplication(app) {
2554
- const spinner = app.spinning("Testing application...");
2555
- await this.applicationRunner.test(app);
2556
- spinner.succeed(`Application ${app.name} (apps/${app.name}) test is successful`);
3416
+ let timer = null;
3417
+ const timeout = new Promise((resolve) => {
3418
+ timer = setTimeout(() => resolve("timeout"), ApplicationScript.dbShutdownTimeoutMs);
3419
+ });
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
+ }
2557
3431
  }
2558
3432
  }
2559
3433
 
2560
- export { openBrowser, ApplicationScript };
3434
+ export { openBrowser, plainTextOf, HOST_SOURCE, DevLogBuffer, ApplicationScript };