@akanjs/cli 2.4.1-rc.3 → 2.4.1-rc.4

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 (44) hide show
  1. package/.build-stamp +1 -0
  2. package/{agent.command-h4afc69n.js → agent.command-t4346579.js} +6 -6
  3. package/{application.command-jkmbsd1x.js → application.command-zkmeemdt.js} +5 -4
  4. package/{applicationBuildRunner-ny84tjhc.js → applicationBuildRunner-5w8j9yp7.js} +2 -1
  5. package/buildBatch.proc.js +3 -2
  6. package/{capacitorApp-y0h6cgft.js → capacitorApp-wnxzrppx.js} +2 -2
  7. package/{cloud.command-53j76grx.js → cloud.command-5ztqxppm.js} +8 -7
  8. package/{context.command-ds3mka9p.js → context.command-smwxwgwf.js} +13 -13
  9. package/{guideline.command-ya0dh44f.js → guideline.command-1r0esdjs.js} +3 -3
  10. package/incrementalBuilder.proc.js +36 -13
  11. package/index-0fn1r7gg.js +466 -0
  12. package/{index-1fpjvqhs.js → index-0jwvs8vp.js} +11 -11
  13. package/{index-a6sbyy0b.js → index-0t7pwff2.js} +3 -0
  14. package/{index-h6ca6qg0.js → index-2v10yr8g.js} +1 -1
  15. package/{index-m9pca6jv.js → index-2v6a4wd5.js} +4 -4
  16. package/{index-45aj5ry0.js → index-3sd2vape.js} +3 -3
  17. package/{index-gb26e7z0.js → index-ahpcr9ss.js} +3 -3
  18. package/{index-8rc0bm04.js → index-eay4t1te.js} +2 -2
  19. package/{index-qaq13qk3.js → index-eqxmxan6.js} +1 -1
  20. package/{index-pmm9e2jf.js → index-fh990y67.js} +2 -2
  21. package/{index-dynknvzd.js → index-gr4cjv99.js} +3 -3
  22. package/{index-1xdrsbry.js → index-hk58kr1m.js} +1 -1
  23. package/{index-85msc0wg.js → index-jbtn8h1y.js} +3 -3
  24. package/{index-q7zcac6n.js → index-khzzttv1.js} +108 -191
  25. package/{index-rgfxj6qc.js → index-n5y2gbf2.js} +307 -15
  26. package/{index-n6h3482q.js → index-nv7et4t3.js} +2 -2
  27. package/{index-hwzpw9c1.js → index-pya1h7wx.js} +3 -3
  28. package/{index-hdqztm58.js → index-qpz4csbs.js} +1 -1
  29. package/{index-0hjg9qs5.js → index-s9yajzkj.js} +5 -5
  30. package/{index-fmky5k3p.js → index-wh201a76.js} +5 -5
  31. package/{index-swf4bmbg.js → index-y4kg682s.js} +1 -1
  32. package/index.js +18 -18
  33. package/{library.command-r15zdqvp.js → library.command-jrew04m6.js} +3 -3
  34. package/{localRegistry.command-25sv5hmt.js → localRegistry.command-m08dkm1b.js} +7 -6
  35. package/{module.command-tnj2bzst.js → module.command-p98t7522.js} +6 -6
  36. package/{package.command-5x5m0ej1.js → package.command-57dyrfmn.js} +3 -3
  37. package/package.json +2 -2
  38. package/{page.command-c6xdx0xm.js → page.command-tas6f3na.js} +3 -3
  39. package/{primitive.command-q1ycj5vr.js → primitive.command-bjrbsakw.js} +7 -7
  40. package/{quality.command-es67wvdp.js → quality.command-zavaafbw.js} +2 -2
  41. package/{repair.command-677675vw.js → repair.command-je57wx67.js} +5 -5
  42. package/{scalar.command-bmrmp8n4.js → scalar.command-gqqyy1kr.js} +5 -5
  43. package/{workflow.command-64550hka.js → workflow.command-d6mrc47s.js} +10 -10
  44. package/{workspace.command-pmfxxfew.js → workspace.command-bbg96z8k.js} +20 -19
@@ -4,17 +4,21 @@ import {
4
4
  } from "./index-76rn3g2c.js";
5
5
  import {
6
6
  LibraryScript
7
- } from "./index-pmm9e2jf.js";
7
+ } from "./index-fh990y67.js";
8
8
  import {
9
9
  runner,
10
10
  script
11
- } from "./index-hdqztm58.js";
11
+ } from "./index-qpz4csbs.js";
12
12
  import {
13
13
  AppExecutor,
14
14
  LibExecutor,
15
15
  PkgExecutor,
16
16
  WorkspaceExecutor
17
- } from "./index-a6sbyy0b.js";
17
+ } from "./index-0t7pwff2.js";
18
+ import {
19
+ HmrWatcher,
20
+ WatchRootResolver
21
+ } from "./index-0fn1r7gg.js";
18
22
  import {
19
23
  __require
20
24
  } from "./index-r24hmh0q.js";
@@ -69,6 +73,7 @@ import { confirm } from "@inquirer/prompts";
69
73
  import { Logger as Logger5 } from "akanjs/common";
70
74
 
71
75
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
76
+ import { stat } from "fs/promises";
72
77
  import path2 from "path";
73
78
  import { Logger as Logger3 } from "akanjs/common";
74
79
 
@@ -240,6 +245,7 @@ class IncrementalBuilderHost {
240
245
  #recycleRequested = false;
241
246
  #spawnAfterRecycle = false;
242
247
  #manualStop = false;
248
+ #inFlight = new Map;
243
249
  #startOptions = {};
244
250
  constructor({ app, entry, env, onMessage }) {
245
251
  this.app = app;
@@ -250,11 +256,15 @@ class IncrementalBuilderHost {
250
256
  get status() {
251
257
  return this.#status;
252
258
  }
259
+ get pid() {
260
+ return this.#proc?.pid ?? null;
261
+ }
253
262
  start(options = {}) {
254
263
  if (this.#proc)
255
264
  this.stop();
256
265
  this.#manualStop = false;
257
266
  this.#startOptions = options;
267
+ this.#spawnAfterRecycle = options.announceBootState ?? false;
258
268
  this.#spawn(false);
259
269
  return this;
260
270
  }
@@ -266,13 +276,15 @@ class IncrementalBuilderHost {
266
276
  let proc;
267
277
  proc = Bun.spawn(["bun", this.entry], {
268
278
  cwd: this.app.cwdPath,
269
- env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_RECYCLED: "1" } : {} },
279
+ env: { ...this.env, AKAN_WATCH: "1", ...afterRecycle ? { AKAN_BUILDER_ANNOUNCE_BOOT: "1" } : {} },
270
280
  stdio: ["ignore", "inherit", "inherit"],
271
281
  ipc: (msg) => {
272
282
  if (this.#proc !== proc)
273
283
  return;
274
284
  if (!msg || typeof msg !== "object")
275
285
  return;
286
+ if (msg.type === "build-route-res" || msg.type === "build-csr-res")
287
+ this.#inFlight.delete(msg.id);
276
288
  if (builderMsgTypeSet.has(msg.type))
277
289
  this.#onMessage(msg);
278
290
  if (msg.type === "builder-ready" && !this.ready) {
@@ -294,6 +306,7 @@ class IncrementalBuilderHost {
294
306
  const wasRecycle = this.#recycleRequested;
295
307
  this.#clearRecycle();
296
308
  this.ready = false;
309
+ this.#failInFlight(wasRecycle ? "builder exited to release bundler memory before answering; reload to retry" : "builder exited unexpectedly before answering; reload once it is back");
297
310
  if (this.#manualStop || this.#status === "stopped")
298
311
  return;
299
312
  if (!wasReady) {
@@ -369,15 +382,31 @@ class IncrementalBuilderHost {
369
382
  }
370
383
  try {
371
384
  this.#proc.send(message);
385
+ if (message.type === "build-route" || message.type === "build-csr")
386
+ this.#inFlight.set(message.id, message.type);
372
387
  return true;
373
388
  } catch (error) {
374
389
  this.logger.warn(`failed to send ${message.type} to builder: ${error instanceof Error ? error.message : String(error)}`);
375
390
  return false;
376
391
  }
377
392
  }
393
+ #failInFlight(reason) {
394
+ if (!this.#inFlight.size)
395
+ return;
396
+ const lost = [...this.#inFlight];
397
+ this.#inFlight.clear();
398
+ this.logger.warn(`failing ${lost.length} unanswered builder request(s): ${reason}`);
399
+ for (const [id, type] of lost) {
400
+ if (type === "build-route")
401
+ this.#onMessage({ type: "build-route-res", id, ok: false, error: reason });
402
+ else
403
+ this.#onMessage({ type: "build-csr-res", id, ok: false, error: reason });
404
+ }
405
+ }
378
406
  stop() {
379
407
  this.#manualStop = true;
380
408
  this.#clearRecycle();
409
+ this.#failInFlight("builder was stopped before answering");
381
410
  if (this.#restartTimer) {
382
411
  clearTimeout(this.#restartTimer);
383
412
  this.#restartTimer = null;
@@ -414,6 +443,10 @@ var BUILDER_START_MAX_ATTEMPTS = 3;
414
443
  var BUILDER_RSS_RECYCLE_QUIET_MS = 750;
415
444
  var BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30000;
416
445
  var BUILDER_INEFFECTIVE_RSS_RECYCLE_LIMIT = 2;
446
+ var BUILDER_RSS_SETTLE_MS = 20000;
447
+ var BUILDER_RSS_HARD_MULTIPLE = 1.5;
448
+ var DEV_IDLE_SUSPEND_MS = 300000;
449
+ var DEV_IDLE_MIN_UPTIME_MS = 30000;
417
450
  var BUILDER_RECOVERY_BASE_DELAY_MS = 2000;
418
451
  var BUILDER_RECOVERY_MAX_DELAY_MS = 60000;
419
452
  var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -518,6 +551,47 @@ var decideBuilderRssRecycle = ({
518
551
  return "too-soon";
519
552
  return "recycle";
520
553
  };
554
+ var decideBuilderRssSettle = ({
555
+ rssBytes,
556
+ ceilingBytes,
557
+ hardMultiple = BUILDER_RSS_HARD_MULTIPLE
558
+ }) => rssBytes >= ceilingBytes * hardMultiple ? "recycle-now" : "wait-and-recheck";
559
+ var decideIdleSuspend = ({
560
+ enabled,
561
+ suspended,
562
+ builderReady,
563
+ backendReady,
564
+ buildFailed,
565
+ restartPending,
566
+ msSinceWake,
567
+ minUptimeMs = DEV_IDLE_MIN_UPTIME_MS
568
+ }) => {
569
+ if (!enabled)
570
+ return "disabled";
571
+ if (suspended)
572
+ return "already-suspended";
573
+ if (!builderReady)
574
+ return "builder-not-ready";
575
+ if (!backendReady)
576
+ return "backend-not-ready";
577
+ if (buildFailed)
578
+ return "build-failed";
579
+ if (restartPending)
580
+ return "restart-pending";
581
+ if (msSinceWake !== null && msSinceWake < minUptimeMs)
582
+ return "too-soon";
583
+ return "suspend";
584
+ };
585
+ var resolveIdleSuspendMs = (raw) => {
586
+ if (raw === undefined || raw === "")
587
+ return DEV_IDLE_SUSPEND_MS;
588
+ const parsed = Number(raw);
589
+ if (!Number.isFinite(parsed) || parsed <= 0)
590
+ return null;
591
+ return Math.round(parsed);
592
+ };
593
+ var hasAnyBuildFailure = (statusByPhase) => [...statusByPhase.values()].some((status) => !status.ok);
594
+ var shouldRefreshConfigOnIdleWake = (batch) => !!batch && batch.kinds.has("config");
521
595
  var shouldAbandonBuilderRssCeiling = (ineffectiveRecycles, limit = BUILDER_INEFFECTIVE_RSS_RECYCLE_LIMIT) => ineffectiveRecycles >= limit;
522
596
  var shouldRelayRecycledFrontendState = (current, next) => {
523
597
  if (!current || current.type !== next.type)
@@ -566,6 +640,7 @@ class BackendImportGraph {
566
640
  #jsTranspiler = new Bun.Transpiler({ loader: "js" });
567
641
  #jsxTranspiler = new Bun.Transpiler({ loader: "jsx" });
568
642
  #files = new Set;
643
+ #scanCache = new Map;
569
644
  #ready = false;
570
645
  #lastRefreshSucceeded = false;
571
646
  constructor(app, logger) {
@@ -607,11 +682,10 @@ class BackendImportGraph {
607
682
  continue;
608
683
  if (!this.#isWorkspaceSource(current, workspaceRoot))
609
684
  continue;
610
- if (!await Bun.file(current).exists())
685
+ const imports = await this.#importsOf(current);
686
+ if (!imports)
611
687
  continue;
612
688
  files.add(current);
613
- const source = await Bun.file(current).text();
614
- const imports = this.#scanImports(current, source);
615
689
  const importerDir = path2.dirname(current);
616
690
  for (const imp of imports) {
617
691
  if (!GRAPH_IMPORT_KINDS.has(imp.kind) || !imp.path || NON_SOURCE_EXT_RE.test(imp.path))
@@ -622,8 +696,23 @@ class BackendImportGraph {
622
696
  queue.push(resolved);
623
697
  }
624
698
  }
699
+ for (const cached of this.#scanCache.keys())
700
+ if (!files.has(cached))
701
+ this.#scanCache.delete(cached);
625
702
  return files;
626
703
  }
704
+ async#importsOf(file) {
705
+ const stats = await stat(file).catch(() => null);
706
+ if (!stats?.isFile())
707
+ return null;
708
+ const mtimeMs = Math.round(stats.mtimeMs);
709
+ const cached = this.#scanCache.get(file);
710
+ if (cached && cached.mtimeMs === mtimeMs && cached.size === stats.size)
711
+ return cached.specifiers;
712
+ const specifiers = this.#scanImports(file, await Bun.file(file).text());
713
+ this.#scanCache.set(file, { mtimeMs, size: stats.size, specifiers });
714
+ return specifiers;
715
+ }
627
716
  async#entrypoints() {
628
717
  const roots = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
629
718
  const existing = [];
@@ -692,11 +781,20 @@ class AkanAppHost {
692
781
  #rssRecycleReason = null;
693
782
  #lastRssRecycleAtMono = null;
694
783
  #rssCeilingIneffective = 0;
784
+ #rssSettleToken = 0;
785
+ #rssRecycleOver = null;
695
786
  #rssCeilingAbandoned = false;
696
787
  #buildStatusByPhase = new Map;
697
788
  #pendingBuildStatusReplay = [];
698
789
  #builderMessageQueue = Promise.resolve();
699
790
  #backendGraph;
791
+ #idleSuspendTimer = null;
792
+ #suspended = false;
793
+ #waking = false;
794
+ #wokeAtMono = null;
795
+ #idleWatcher = null;
796
+ #suspendedChanges = null;
797
+ #pendingBuilderMessages = [];
700
798
  constructor(app, { env, withInk = false }) {
701
799
  this.app = app;
702
800
  this.env = env;
@@ -715,9 +813,12 @@ class AkanAppHost {
715
813
  ]);
716
814
  Object.assign(this.env, { REDIS_HOST: redisHost });
717
815
  this.#startBackend();
816
+ this.#armIdleSuspend();
718
817
  return this;
719
818
  }
720
819
  async stop() {
820
+ this.#cancelIdleSuspend();
821
+ this.#stopIdleWatcher();
721
822
  if (this.#restartTimer) {
722
823
  clearTimeout(this.#restartTimer);
723
824
  this.#restartTimer = null;
@@ -982,6 +1083,7 @@ ${this.#backendStderrTail.join(`
982
1083
  });
983
1084
  }
984
1085
  async#handleBuilderMessage(message) {
1086
+ this.#markDevActivity();
985
1087
  if (message.type === "build-status") {
986
1088
  this.#recordBuildStatus(message.data);
987
1089
  this.#sendOrQueueBuildStatus(message.data);
@@ -1042,29 +1144,210 @@ ${this.#backendStderrTail.join(`
1042
1144
  this.logger.error(`[builder-recycle] the builder is still at ${asMib(metrics.rssBytes)}MiB right after being recycled, so the ${asMib(ceilingBytes ?? 0)}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.`);
1043
1145
  return;
1044
1146
  }
1045
- this.#armRssRecycle(`rss=${asMib(metrics.rssBytes)}MiB>=${asMib(ceilingBytes ?? 0)}MiB after ${metrics.workCount} build(s)`);
1147
+ this.#armRssRecycle(`rss=${asMib(metrics.rssBytes)}MiB>=${asMib(ceilingBytes ?? 0)}MiB after ${metrics.workCount} build(s)`, { rssBytes: metrics.rssBytes, ceilingBytes: ceilingBytes ?? 0 });
1046
1148
  }
1047
- #armRssRecycle(reason) {
1149
+ #armRssRecycle(reason, over) {
1048
1150
  if (this.#rssRecycleReason !== reason)
1049
1151
  this.logger.verbose(`[builder-recycle] armed (${reason}); replacing the builder once it stays quiet`);
1050
1152
  this.#rssRecycleReason = reason;
1153
+ if (over)
1154
+ this.#rssRecycleOver = over;
1051
1155
  if (this.#rssRecycleTimer)
1052
1156
  clearTimeout(this.#rssRecycleTimer);
1053
1157
  this.#rssRecycleTimer = setTimeout(() => {
1054
1158
  this.#rssRecycleTimer = null;
1055
1159
  const pendingReason = this.#rssRecycleReason;
1160
+ const pendingOver = this.#rssRecycleOver;
1056
1161
  this.#rssRecycleReason = null;
1057
- if (pendingReason)
1162
+ this.#rssRecycleOver = null;
1163
+ if (!pendingReason)
1164
+ return;
1165
+ if (!pendingOver) {
1058
1166
  this.#recycleBuilderForRss(pendingReason);
1167
+ return;
1168
+ }
1169
+ this.#recycleBuilderForRssWhenStillOver(pendingReason, pendingOver);
1059
1170
  }, BUILDER_RSS_RECYCLE_QUIET_MS);
1060
1171
  }
1172
+ async#recycleBuilderForRssWhenStillOver(reason, { rssBytes, ceilingBytes }) {
1173
+ const asMib = (bytes) => Math.round(bytes / 1024 / 1024);
1174
+ if (decideBuilderRssSettle({ rssBytes, ceilingBytes }) === "recycle-now") {
1175
+ this.#recycleBuilderForRss(reason);
1176
+ return;
1177
+ }
1178
+ const pid = this.#builder?.pid;
1179
+ if (!pid) {
1180
+ this.#recycleBuilderForRss(reason);
1181
+ return;
1182
+ }
1183
+ this.#rssSettleToken += 1;
1184
+ const token = this.#rssSettleToken;
1185
+ this.logger.verbose(`[builder-recycle] holding ${Math.round(BUILDER_RSS_SETTLE_MS / 1000)}s to see whether the allocator returns it (${reason})`);
1186
+ await Bun.sleep(BUILDER_RSS_SETTLE_MS);
1187
+ if (token !== this.#rssSettleToken || this.#builder?.pid !== pid || this.#suspended || this.#waking) {
1188
+ this.logger.verbose("[builder-recycle] settle check abandoned; the builder moved on");
1189
+ return;
1190
+ }
1191
+ const settledBytes = await AkanAppHost.readProcessRssBytes(pid);
1192
+ if (settledBytes !== null && settledBytes < ceilingBytes) {
1193
+ this.logger.info(`[builder-recycle] skipped: the builder fell to ${asMib(settledBytes)}MiB (ceiling ${asMib(ceilingBytes)}MiB) on its own, so a recycle would have cost a boot build for nothing`);
1194
+ return;
1195
+ }
1196
+ this.#recycleBuilderForRss(settledBytes === null ? reason : `${reason}; still ${asMib(settledBytes)}MiB after settling`);
1197
+ }
1198
+ static async readProcessRssBytes(pid) {
1199
+ const status = await Bun.file(`/proc/${pid}/status`).text().catch(() => null);
1200
+ const vmRssKb = status === null ? null : /VmRSS:\s+(\d+) kB/.exec(status)?.[1];
1201
+ if (vmRssKb)
1202
+ return Number(vmRssKb) * 1024;
1203
+ const psOutput = await Bun.$`ps -o rss= -p ${pid}`.quiet().text().catch(() => "");
1204
+ const rssKb = Number(psOutput.trim());
1205
+ return Number.isFinite(rssKb) && rssKb > 0 ? rssKb * 1024 : null;
1206
+ }
1061
1207
  #cancelRssRecycle() {
1062
1208
  this.#rssRecycleReason = null;
1209
+ this.#rssRecycleOver = null;
1210
+ this.#rssSettleToken += 1;
1063
1211
  if (!this.#rssRecycleTimer)
1064
1212
  return;
1065
1213
  clearTimeout(this.#rssRecycleTimer);
1066
1214
  this.#rssRecycleTimer = null;
1067
1215
  }
1216
+ static idleSuspendMs() {
1217
+ return resolveIdleSuspendMs(process.env.AKAN_DEV_IDLE_SUSPEND_MS);
1218
+ }
1219
+ #markDevActivity() {
1220
+ if (this.#suspended || this.#waking)
1221
+ return;
1222
+ this.#armIdleSuspend();
1223
+ }
1224
+ #armIdleSuspend() {
1225
+ const idleMs = AkanAppHost.idleSuspendMs();
1226
+ this.#cancelIdleSuspend();
1227
+ if (idleMs === null)
1228
+ return;
1229
+ this.#idleSuspendTimer = setTimeout(() => {
1230
+ this.#idleSuspendTimer = null;
1231
+ this.#suspendWhenIdle(idleMs);
1232
+ }, idleMs);
1233
+ }
1234
+ #cancelIdleSuspend() {
1235
+ if (!this.#idleSuspendTimer)
1236
+ return;
1237
+ clearTimeout(this.#idleSuspendTimer);
1238
+ this.#idleSuspendTimer = null;
1239
+ }
1240
+ async#suspendWhenIdle(idleMs) {
1241
+ const decision = decideIdleSuspend({
1242
+ enabled: true,
1243
+ suspended: this.#suspended,
1244
+ builderReady: this.#builder?.status === "ready",
1245
+ backendReady: this.#backendReady,
1246
+ buildFailed: hasAnyBuildFailure(this.#buildStatusByPhase),
1247
+ restartPending: this.#restartPending,
1248
+ msSinceWake: this.#wokeAtMono === null ? null : performance.now() - this.#wokeAtMono
1249
+ });
1250
+ if (decision !== "suspend") {
1251
+ this.logger.verbose(`[idle-suspend] skipped (${decision}); re-arming`);
1252
+ this.#armIdleSuspend();
1253
+ return;
1254
+ }
1255
+ if (!await this.#startIdleWatcher()) {
1256
+ this.#armIdleSuspend();
1257
+ return;
1258
+ }
1259
+ this.#suspended = true;
1260
+ this.#stopBuilder();
1261
+ 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`);
1262
+ }
1263
+ get #restartPending() {
1264
+ return !!(this.#pendingRecycle || this.#restartTimer || this.#backendRecoveryTimer || this.#builderRecoveryTimer || this.#rssRecycleReason);
1265
+ }
1266
+ async#startIdleWatcher() {
1267
+ try {
1268
+ const roots = await new WatchRootResolver(this.app).resolve();
1269
+ const watcher = new HmrWatcher({
1270
+ roots,
1271
+ logger: this.logger,
1272
+ onBatch: (batch) => {
1273
+ this.#recordSuspendedChange(batch);
1274
+ this.#wakeFromIdle(`${batch.files.length} file(s) changed`);
1275
+ }
1276
+ });
1277
+ await watcher.start();
1278
+ this.#idleWatcher = watcher;
1279
+ return true;
1280
+ } catch (err) {
1281
+ this.logger.warn(`[idle-suspend] could not install the idle watcher; staying awake: ${err instanceof Error ? err.message : String(err)}`);
1282
+ this.#stopIdleWatcher();
1283
+ return false;
1284
+ }
1285
+ }
1286
+ #stopIdleWatcher() {
1287
+ this.#idleWatcher?.stop();
1288
+ this.#idleWatcher = null;
1289
+ }
1290
+ #recordSuspendedChange(batch) {
1291
+ const current = this.#suspendedChanges;
1292
+ if (!current) {
1293
+ this.#suspendedChanges = { files: [...batch.files], kinds: new Set(batch.kinds) };
1294
+ return;
1295
+ }
1296
+ this.#suspendedChanges = {
1297
+ files: [...new Set([...current.files, ...batch.files])],
1298
+ kinds: new Set([...current.kinds, ...batch.kinds])
1299
+ };
1300
+ }
1301
+ async#wakeFromIdle(reason) {
1302
+ if (!this.#suspended || this.#waking)
1303
+ return;
1304
+ this.#waking = true;
1305
+ this.#cancelIdleSuspend();
1306
+ this.#stopIdleWatcher();
1307
+ const batch = this.#suspendedChanges;
1308
+ this.#suspendedChanges = null;
1309
+ const startedAtMono = performance.now();
1310
+ this.logger.info(`[idle-suspend] waking (${reason})`);
1311
+ try {
1312
+ await this.#applyIdleWake(batch);
1313
+ this.logger.info(`[idle-suspend] awake in ${Math.round(performance.now() - startedAtMono)}ms`);
1314
+ } catch (err) {
1315
+ this.logger.error(`[idle-suspend] wake failed; recovering the builder: ${err instanceof Error ? err.message : String(err)}`);
1316
+ this.#scheduleBuilderRecovery({ files: batch?.files ?? [] });
1317
+ } finally {
1318
+ this.#suspended = false;
1319
+ this.#waking = false;
1320
+ this.#wokeAtMono = performance.now();
1321
+ this.#flushPendingBuilderMessages();
1322
+ this.#armIdleSuspend();
1323
+ }
1324
+ }
1325
+ async#applyIdleWake(batch) {
1326
+ const files = batch?.files ?? [];
1327
+ if (shouldRefreshConfigOnIdleWake(batch)) {
1328
+ this.logger.verbose("[idle-suspend] config changed while suspended; restarting the dev host");
1329
+ await this.#recycleDevChildren({ type: "invalidate", kinds: [...batch?.kinds ?? []], files }, {
1330
+ refreshConfig: true
1331
+ });
1332
+ return;
1333
+ }
1334
+ if (files.length > 0)
1335
+ await this.#backendGraph.refresh();
1336
+ await this.#startBuilder({ announceBootState: true });
1337
+ const backendFiles = files.filter((file) => this.#isBackendFile(file));
1338
+ if (backendFiles.length === 0)
1339
+ return;
1340
+ this.logger.verbose(`[idle-suspend] ${backendFiles.length} backend file(s) changed while suspended`);
1341
+ this.#scheduleBackendRestart({ files: backendFiles, roles: [] });
1342
+ }
1343
+ #flushPendingBuilderMessages() {
1344
+ const pending = this.#pendingBuilderMessages.splice(0);
1345
+ if (pending.length === 0)
1346
+ return;
1347
+ this.logger.verbose(`[idle-suspend] replaying ${pending.length} request(s) held during the wake`);
1348
+ for (const message of pending)
1349
+ this.#sendToBuilder(message);
1350
+ }
1068
1351
  #recycleBuilderForRss(reason) {
1069
1352
  if (this.#pendingRecycle || this.#restartTimer) {
1070
1353
  this.logger.verbose(`[builder-recycle] skipped (${reason}); a dev restart is already pending`);
@@ -1297,7 +1580,9 @@ ${this.#backendStderrTail.join(`
1297
1580
  #isBackendFile(file) {
1298
1581
  return this.#backendGraph.has(file);
1299
1582
  }
1300
- async#startBuilder() {
1583
+ async#startBuilder({
1584
+ announceBootState = false
1585
+ } = {}) {
1301
1586
  const startTime = Date.now();
1302
1587
  this.app.verbose(`[cli] waiting for builder to complete initial base build\u2026`);
1303
1588
  let lastError;
@@ -1306,7 +1591,7 @@ ${this.#backendStderrTail.join(`
1306
1591
  this.#enqueueBuilderMessage(msg);
1307
1592
  });
1308
1593
  try {
1309
- await this.#waitForBuilderReady(attempt);
1594
+ await this.#waitForBuilderReady(attempt, { announceBootState });
1310
1595
  this.app.verbose(`[cli] base build ready in ${Date.now() - startTime}ms \u2014 starting backend`);
1311
1596
  return this.#builder;
1312
1597
  } catch (err) {
@@ -1319,7 +1604,7 @@ ${this.#backendStderrTail.join(`
1319
1604
  }
1320
1605
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
1321
1606
  }
1322
- #waitForBuilderReady(attempt) {
1607
+ #waitForBuilderReady(attempt, { announceBootState = false } = {}) {
1323
1608
  return new Promise((resolve, reject) => {
1324
1609
  if (!this.#builder)
1325
1610
  throw new Error("Builder Not Found");
@@ -1335,6 +1620,7 @@ ${this.#backendStderrTail.join(`
1335
1620
  settle(() => reject(new Error("[cli] builder timed out before emitting builder-ready")));
1336
1621
  }, BUILDER_READY_TIMEOUT_MS);
1337
1622
  this.#builder.start({
1623
+ announceBootState,
1338
1624
  onExit: () => {
1339
1625
  settle(() => reject(new Error(`[cli] builder exited before emitting builder-ready (attempt ${attempt})`)));
1340
1626
  },
@@ -1349,6 +1635,12 @@ ${this.#backendStderrTail.join(`
1349
1635
  });
1350
1636
  }
1351
1637
  #sendToBuilder(message) {
1638
+ this.#markDevActivity();
1639
+ if (this.#suspended || this.#waking) {
1640
+ this.#pendingBuilderMessages.push(message);
1641
+ this.#wakeFromIdle(`${message.type} arrived while suspended`);
1642
+ return;
1643
+ }
1352
1644
  if (message.type === "build-csr" && this.env.AKAN_DEV_CSR_REBUILD !== "1") {
1353
1645
  Object.assign(this.env, { AKAN_DEV_CSR_REBUILD: "1" });
1354
1646
  this.logger.verbose(`[csr] armed dev CSR rebuilds (${message.reason})`);
@@ -1425,9 +1717,9 @@ function openBrowser(url) {
1425
1717
  }
1426
1718
 
1427
1719
  // pkgs/@akanjs/cli/application/application.runner.ts
1428
- var loadBuildRunner = async () => (await import("./applicationBuildRunner-ny84tjhc.js")).ApplicationBuildRunner;
1720
+ var loadBuildRunner = async () => (await import("./applicationBuildRunner-5w8j9yp7.js")).ApplicationBuildRunner;
1429
1721
  var loadReleasePackager = async () => (await import("./applicationReleasePackager-brvth6rs.js")).ApplicationReleasePackager;
1430
- var loadCapacitorApp = async () => (await import("./capacitorApp-y0h6cgft.js")).CapacitorApp;
1722
+ var loadCapacitorApp = async () => (await import("./capacitorApp-wnxzrppx.js")).CapacitorApp;
1431
1723
  var loadPrompts = async () => await import("@inquirer/prompts");
1432
1724
 
1433
1725
  class ApplicationRunner extends runner("application") {
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-hdqztm58.js";
5
+ } from "./index-qpz4csbs.js";
6
6
  import {
7
7
  PkgExecutor
8
- } from "./index-a6sbyy0b.js";
8
+ } from "./index-0t7pwff2.js";
9
9
  import {
10
10
  TypeScriptDependencyScanner
11
11
  } from "./index-x53a5nya.js";
@@ -1,14 +1,14 @@
1
1
  // @bun
2
2
  import {
3
3
  AkanContextAnalyzer
4
- } from "./index-8rc0bm04.js";
4
+ } from "./index-eay4t1te.js";
5
5
  import {
6
6
  Prompter
7
- } from "./index-qaq13qk3.js";
7
+ } from "./index-eqxmxan6.js";
8
8
  import {
9
9
  runner,
10
10
  script
11
- } from "./index-hdqztm58.js";
11
+ } from "./index-qpz4csbs.js";
12
12
 
13
13
  // pkgs/@akanjs/cli/agent/agent.script.ts
14
14
  import { Logger } from "akanjs/common";
@@ -7,7 +7,7 @@ import {
7
7
  PkgExecutor,
8
8
  WorkspaceExecutor,
9
9
  getDirname
10
- } from "./index-a6sbyy0b.js";
10
+ } from "./index-0t7pwff2.js";
11
11
  import {
12
12
  FileSys
13
13
  } from "./index-61keag0s.js";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  CloudRunner
4
- } from "./index-dynknvzd.js";
4
+ } from "./index-gr4cjv99.js";
5
5
  import {
6
6
  AiSession,
7
7
  CloudApi,
@@ -9,16 +9,16 @@ import {
9
9
  } from "./index-5vvwc0cz.js";
10
10
  import {
11
11
  ApplicationScript
12
- } from "./index-rgfxj6qc.js";
12
+ } from "./index-n5y2gbf2.js";
13
13
  import {
14
14
  PackageScript
15
- } from "./index-n6h3482q.js";
15
+ } from "./index-nv7et4t3.js";
16
16
  import {
17
17
  script
18
- } from "./index-hdqztm58.js";
18
+ } from "./index-qpz4csbs.js";
19
19
  import {
20
20
  PkgExecutor
21
- } from "./index-a6sbyy0b.js";
21
+ } from "./index-0t7pwff2.js";
22
22
 
23
23
  // pkgs/@akanjs/cli/cloud/cloud.script.ts
24
24
  import { Logger } from "akanjs/common";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  PageScript
4
- } from "./index-swf4bmbg.js";
4
+ } from "./index-y4kg682s.js";
5
5
  import {
6
6
  pluralizeName
7
7
  } from "./index-ss469dec.js";
@@ -12,20 +12,20 @@ import {
12
12
  generatedFilesForSync,
13
13
  moduleSourcePaths,
14
14
  sourceFile
15
- } from "./index-h6ca6qg0.js";
15
+ } from "./index-2v10yr8g.js";
16
16
  import {
17
17
  Prompter
18
- } from "./index-qaq13qk3.js";
18
+ } from "./index-eqxmxan6.js";
19
19
  import {
20
20
  AiSession
21
21
  } from "./index-5vvwc0cz.js";
22
22
  import {
23
23
  runner,
24
24
  script
25
- } from "./index-hdqztm58.js";
25
+ } from "./index-qpz4csbs.js";
26
26
  import {
27
27
  ModuleExecutor
28
- } from "./index-a6sbyy0b.js";
28
+ } from "./index-0t7pwff2.js";
29
29
  import {
30
30
  FileSys
31
31
  } from "./index-61keag0s.js";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  runner,
4
4
  script
5
- } from "./index-hdqztm58.js";
5
+ } from "./index-qpz4csbs.js";
6
6
 
7
7
  // pkgs/@akanjs/cli/page/page.runner.ts
8
8
  class PageRunner extends runner("page") {