@akanjs/cli 2.4.0-rc.0 → 2.4.0-rc.2

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.
@@ -15,6 +15,14 @@ Use this for shared UI rules across module, scalar, app UI, and docs components.
15
15
  - Client components can use store hooks and event handlers where interaction is required.
16
16
  - Use generated model types from app client or module constants; do not invent duplicate UI-only model shapes.
17
17
 
18
+ ## Customizing Framework Components (Slot Overrides)
19
+ - When a default `akanjs/ui` component (Button, Select, Modal, Table, Input, Radio, DatePicker, Loading, …) is too restrictive for the design, re-skin it per route with a slot override instead of forking, wrapping every call site, or fighting it with `!important`.
20
+ - Write the drop-in replacement in `apps/<app>/ui/`, typed against the slot contract — `AkanModalComponent`, or `AkanUiOverrides["<Slot>"]` for any other slot — so it is compile-checked as a real substitute. Compose the framework's headless parts (e.g. `Dialog`) rather than re-implementing focus trapping, portals, or scroll-lock.
21
+ - Bind it in a logic-free `page/**/_overrides.tsx` manifest: imports plus a single `export default override({ Slot: BrandComponent })` (from `akanjs/ui`), no `"use client"`.
22
+ - Place the manifest at `page/` for an app-wide skin, or inside a route group/segment to scope it; nested manifests merge over ancestors slot-by-slot (closest ancestor wins, unlisted slots keep inheriting).
23
+ - Compound components expose one slot per leaf named `<Base><Sub>` (e.g. `InputPassword`, `RadioItem`, `LoadingSpin`); override only the leaves you need.
24
+ - See the `references/ui/customize` docs page for the full slot list and examples.
25
+
18
26
  ## Codegen Rules
19
27
  - Do not put business workflow decisions in render code.
20
28
  - Do not use undocumented UI components or props.
@@ -15,6 +15,24 @@ Use TailwindCSS and DaisyUI in a theme-safe, composable way for Akan UI and docs
15
15
  - Use consistent density and spacing within one component family.
16
16
  - Keep custom CSS files rare and scoped to cases utilities cannot express cleanly.
17
17
 
18
+ ## Theme Customization (`apps/<app>/page/styles.css`)
19
+ - The app theme is defined in `apps/<app>/page/styles.css` with daisyUI v5 `@plugin "daisyui/theme"` blocks (typically `light` and a `default: true` `dark`).
20
+ - Brand-level customization means tuning the whole theme block, not only the color tokens. A theme that only changes `--color-*` still looks like the default framework skin.
21
+ - Color tokens: `--color-primary`, `--color-secondary`, `--color-accent`, `--color-neutral`, `--color-info`, `--color-success`, `--color-warning`, `--color-error`, `--color-base-100/200/300`, and their `*-content` pairs.
22
+ - Shape and feel tokens (set these too):
23
+ - `--radius-selector` — rounding for checkbox, toggle, badge.
24
+ - `--radius-field` — rounding for button, input, select, tab.
25
+ - `--radius-box` — rounding for card, modal, alert.
26
+ - `--size-selector`, `--size-field` — base scale (density) of selector and field controls.
27
+ - `--border` — component border width (e.g. `1px`, `2px`).
28
+ - `--depth` — `0` flat, `1` adds a subtle 3D lift to components.
29
+ - `--noise` — `0` off, `1` adds a grain texture to surfaces.
30
+ - Keep shape/feel tokens consistent across the light and dark theme blocks unless the design deliberately differs by mode.
31
+
32
+ ## When Utilities Are Not Enough
33
+ - Prefer theme tokens and DaisyUI component classes over one-off utility stacks that re-implement a component's look.
34
+ - When a framework `akanjs/ui` component is structurally too restrictive, re-skin it with a `page/**/_overrides.tsx` slot override (see the componentRule guideline), not with `!important` utilities or a fork.
35
+
18
36
  ## Codegen Rules
19
37
  - Do not hardcode hex colors or one-off brand colors unless the existing file already defines that design system.
20
38
  - Do not use important flags to fight component composition.
@@ -22,6 +22,12 @@ Use this as the compact framework context for AI codegen. It should explain how
22
22
  - Keep business decisions in constant, document, or service; keep API exposure in signal; keep client coordination in store; keep rendering in UI files.
23
23
  - Use direct module imports where scanner rules expect them, and avoid inventing new top-level app folders.
24
24
 
25
+ ## Theming And UI Customization
26
+ When a request implies a distinct look and feel, do not stop at colors — customize both the theme and, when needed, the components.
27
+
28
+ - **Theme (`apps/<app>/page/styles.css`).** The app theme is one or more daisyUI v5 `@plugin "daisyui/theme"` blocks. Match the brand by tuning the whole block, not only the `--color-*` tokens: set corner rounding (`--radius-selector`, `--radius-field`, `--radius-box`), control density (`--size-selector`, `--size-field`), outline weight (`--border`), and surface treatment (`--depth`, `--noise`). New workspaces ship these knobs at neutral defaults so they are visible to tune. Fetch `get_guideline` with `cssRule` for the full variable reference before a deep theme pass.
29
+ - **Components (`page/**/_overrides.tsx`).** When a default `akanjs/ui` component (Button, Modal, Table, Input, Select, …) is too restrictive for the design, re-skin it per route instead of forking, wrapping, or fighting it with utility classes. Write a drop-in replacement in `apps/<app>/ui/` typed against the slot contract (`AkanModalComponent`, or `AkanUiOverrides["<Slot>"]`), composing the framework's headless parts, then bind it in a `page/**/_overrides.tsx` manifest with a single `export default override({ Slot: BrandComponent })`. Overrides cascade down the route tree like layouts (closest ancestor wins). Fetch `get_guideline` with `componentRule` and read the `references/ui/customize` docs page for the slot list and patterns.
30
+
25
31
  ## Review Checklist
26
32
  - The instruction points to current docs pages, not removed docs routes.
27
33
  - Generated examples use current Akan builder APIs and scanner-friendly filenames.
@@ -986,9 +986,15 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
986
986
  command
987
987
  };
988
988
  }
989
- static async from(app) {
989
+ static #importGeneration = 0;
990
+ static async importConfigModule(cwdPath, { bustImportCache = false } = {}) {
991
+ const configPath2 = `${cwdPath}/akan.config.ts`;
992
+ const importPath = bustImportCache ? `${configPath2}?akanConfigGeneration=${++AkanAppConfig.#importGeneration}` : configPath2;
993
+ return await import(importPath).then((mod) => mod.default);
994
+ }
995
+ static async from(app, { bustImportCache = false } = {}) {
990
996
  const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
991
- import(`${app.cwdPath}/akan.config.ts`).then((mod) => mod.default),
997
+ AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
992
998
  WorkspaceExecutor.getBaseDevEnv(path.join(app.workspace.workspaceRoot, ".env")),
993
999
  app.workspace.getLibs(),
994
1000
  app.workspace.getPackageJson()
@@ -1408,8 +1414,8 @@ ${errorText}, ${warningText} found`];
1408
1414
  }
1409
1415
 
1410
1416
  // pkgs/@akanjs/devkit/scanInfo.ts
1411
- import path5 from "path";
1412
1417
  import { rm } from "fs/promises";
1418
+ import path5 from "path";
1413
1419
 
1414
1420
  // pkgs/@akanjs/devkit/dependencyScanner.ts
1415
1421
  import { builtinModules } from "module";
@@ -1746,7 +1752,8 @@ var appRootAllowedDirs = new Set([
1746
1752
  "ui",
1747
1753
  "srvkit",
1748
1754
  "webkit",
1749
- "common"
1755
+ "common",
1756
+ "secrets"
1750
1757
  ]);
1751
1758
  var libRootAllowedFiles = new Set([
1752
1759
  "cnst.ts",
@@ -3660,7 +3667,7 @@ class AppExecutor extends SysExecutor {
3660
3667
  async getConfig({ refresh } = {}) {
3661
3668
  if (this.#akanConfig && !refresh)
3662
3669
  return this.#akanConfig;
3663
- this.#akanConfig = await AkanAppConfig.from(this);
3670
+ this.#akanConfig = await AkanAppConfig.from(this, { bustImportCache: refresh });
3664
3671
  return this.#akanConfig;
3665
3672
  }
3666
3673
  #pageKeys = null;
@@ -4287,9 +4294,10 @@ class IncrementalBuilderHost {
4287
4294
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
4288
4295
  var backendMsgTypeSet = new Set(["build-route"]);
4289
4296
  var BACKEND_RESTART_DEBOUNCE_MS = 120;
4290
- var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
4297
+ var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
4291
4298
  var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
4292
4299
  var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
4300
+ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4293
4301
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4294
4302
  var BUILDER_READY_TIMEOUT_MS = 150000;
4295
4303
  var BUILDER_START_MAX_ATTEMPTS = 3;
@@ -4314,6 +4322,8 @@ var shouldRestartBackendByDevPlan = (message) => {
4314
4322
  return message.devPlan.actions.includes("restart-backend");
4315
4323
  };
4316
4324
  var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includes("restart-builder") ?? false;
4325
+ var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
4326
+ var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
4317
4327
  var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
4318
4328
  var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
4319
4329
  var generationValue = (generation) => generation ?? -1;
@@ -4562,6 +4572,16 @@ class AkanAppHost {
4562
4572
  this.#replayBuilderState();
4563
4573
  return;
4564
4574
  }
4575
+ if (msg.type === "build-status") {
4576
+ const status = this.#recordBackendBuildStatus({
4577
+ generation: normalizeBackendReportedGeneration(msg.data.generation),
4578
+ ok: msg.data.ok,
4579
+ files: msg.data.files,
4580
+ message: msg.data.message
4581
+ });
4582
+ this.#sendOrQueueBuildStatus(status);
4583
+ return;
4584
+ }
4565
4585
  if (backendMsgTypeSet.has(msg.type))
4566
4586
  this.#sendToBuilder(msg);
4567
4587
  },
@@ -4723,6 +4743,19 @@ class AkanAppHost {
4723
4743
  #scheduleBackendRecovery(reason) {
4724
4744
  if (this.#backendRecoveryTimer || this.#backend)
4725
4745
  return;
4746
+ if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4747
+ const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for a server-side edit to retry.`;
4748
+ this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4749
+ this.logger.error(`[backend-recovery] ${message}`);
4750
+ if (this.#backendStderrTail.length > 0) {
4751
+ this.logger.error(`[backend-recovery] recent backend stderr:
4752
+ ${this.#backendStderrTail.join(`
4753
+ `)}`);
4754
+ }
4755
+ const abandonedStatus = this.#recordBackendBuildStatus({ ok: false, files: [], message });
4756
+ this.#sendOrQueueBuildStatus(abandonedStatus);
4757
+ return;
4758
+ }
4726
4759
  this.#setBackendLifecycleState("recovering", reason);
4727
4760
  const attempt = this.#backendRecoveryAttempts;
4728
4761
  const delay = Math.min(BACKEND_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BACKEND_RECOVERY_MAX_DELAY_MS);
@@ -4771,16 +4804,21 @@ ${this.#backendStderrTail.join(`
4771
4804
  this.#sendToBackend(message);
4772
4805
  }
4773
4806
  async#handleInvalidate(message) {
4774
- if (shouldRestartBuilderByDevPlan(message)) {
4807
+ this.#logDevPlan(message);
4808
+ if (shouldRestartDevHostByDevPlan(message)) {
4775
4809
  try {
4776
- await this.#restartDevChildren(message);
4810
+ await this.#restartDevHost(message);
4777
4811
  } catch (err) {
4778
- this.#recordDevHostRestartFailure(message, err);
4812
+ this.#recordDevHostRestartFailure(message, err, "Config");
4779
4813
  }
4780
4814
  return;
4781
4815
  }
4782
- if (shouldRestartDevHostByDevPlan(message)) {
4783
- this.#recordDevHostRestartRequired(message);
4816
+ if (shouldRestartBuilderByDevPlan(message)) {
4817
+ try {
4818
+ await this.#restartDevChildren(message);
4819
+ } catch (err) {
4820
+ this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4821
+ }
4784
4822
  return;
4785
4823
  }
4786
4824
  if (await this.#shouldRestartBackend(message)) {
@@ -4792,6 +4830,15 @@ ${this.#backendStderrTail.join(`
4792
4830
  async#restartDevChildren(message) {
4793
4831
  const generation = message.devPlan?.generation ?? message.generation;
4794
4832
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
4833
+ await this.#recycleDevChildren(message);
4834
+ }
4835
+ async#restartDevHost(message) {
4836
+ const generation = message.devPlan?.generation ?? message.generation;
4837
+ this.logger.warn(`[dev-host] config change detected; restarting dev host generation=${generation ?? "(unknown)"} files=${message.files.length}`);
4838
+ await this.#recycleDevChildren(message, { refreshConfig: true });
4839
+ }
4840
+ async#recycleDevChildren(message, { refreshConfig = false } = {}) {
4841
+ const generation = message.devPlan?.generation ?? message.generation;
4795
4842
  if (this.#restartTimer) {
4796
4843
  clearTimeout(this.#restartTimer);
4797
4844
  this.#restartTimer = null;
@@ -4806,6 +4853,11 @@ ${this.#backendStderrTail.join(`
4806
4853
  this.#pendingBuildStatusReplay = [];
4807
4854
  await this.#stopBackend();
4808
4855
  this.#stopBuilder();
4856
+ if (refreshConfig) {
4857
+ await this.app.getConfig({ refresh: true });
4858
+ const { env } = await this.app.prepareCommand("start");
4859
+ Object.assign(this.env, env);
4860
+ }
4809
4861
  await this.#backendGraph.refresh();
4810
4862
  await this.#startBuilder();
4811
4863
  this.#startBackend({ generation, files: message.files });
@@ -4823,32 +4875,16 @@ ${this.#backendStderrTail.join(`
4823
4875
  this.#lastGoodFrontend.css = message;
4824
4876
  this.logger.verbose(`[last-good] css generation=${message.data.generation ?? "(unknown)"} assets=${Object.keys(message.data.cssAssets).length}`);
4825
4877
  }
4826
- #recordDevHostRestartRequired(message) {
4827
- const generation = message.devPlan?.generation ?? message.generation;
4828
- const detail = `generation=${generation ?? "(unknown)"} files=${message.files.length}`;
4829
- this.logger.warn(`[dev-host] config change requires a manual restart until controlled dev-host restart is implemented (${detail})`);
4830
- if (typeof generation === "number") {
4831
- const status = {
4832
- generation,
4833
- phase: "scan",
4834
- ok: false,
4835
- files: message.files,
4836
- message: "Config change requires restarting `akan start` to apply."
4837
- };
4838
- this.#recordBuildStatus(status);
4839
- this.#sendOrQueueBuildStatus(status);
4840
- }
4841
- }
4842
- #recordDevHostRestartFailure(message, err) {
4878
+ #recordDevHostRestartFailure(message, err, kind) {
4843
4879
  const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
4844
4880
  const detail = err instanceof Error ? err.message : String(err);
4845
- this.logger.warn(`[dev-host] runtime metadata restart failed generation=${generation}: ${detail}`);
4881
+ this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart failed generation=${generation}: ${detail}`);
4846
4882
  const status = {
4847
4883
  generation,
4848
4884
  phase: "scan",
4849
4885
  ok: false,
4850
4886
  files: message.files,
4851
- message: `Runtime metadata change requires restarting \`akan start\` to apply: ${detail}`
4887
+ message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
4852
4888
  };
4853
4889
  this.#recordBuildStatus(status);
4854
4890
  this.#sendOrQueueBuildStatus(status);
@@ -4882,12 +4918,16 @@ ${this.#backendStderrTail.join(`
4882
4918
  this.#sendToBackend({ type: "build-status", data: status });
4883
4919
  }
4884
4920
  }
4921
+ #logDevPlan(message) {
4922
+ if (!message.devPlan)
4923
+ return;
4924
+ const { generation, roles, actions, reasonByFile } = message.devPlan;
4925
+ this.logger.verbose(`[dev-plan] generation=${generation} roles=${roles.join(",") || "(none)"} actions=${actions.join(",") || "(none)"} reasons=${Object.keys(reasonByFile).length}`);
4926
+ }
4885
4927
  async#shouldRestartBackend(message) {
4886
4928
  if (message.kinds.length === 1 && message.kinds[0] === "css")
4887
4929
  return false;
4888
4930
  if (message.devPlan) {
4889
- const { generation, roles, actions, reasonByFile } = message.devPlan;
4890
- this.logger.verbose(`[dev-plan] generation=${generation} roles=${roles.join(",") || "(none)"} actions=${actions.join(",") || "(none)"} reasons=${Object.keys(reasonByFile).length}`);
4891
4931
  const shouldRestart = shouldRestartBackendByDevPlan(message) ?? false;
4892
4932
  if (shouldRestart && message.kinds.includes("code"))
4893
4933
  await this.#backendGraph.refresh();
@@ -17269,14 +17309,18 @@ ${cssText}`).toString(36);
17269
17309
  }
17270
17310
  if (indexSync.changedFiles.length > 0)
17271
17311
  this.#sendBuildStatus("barrel", { generation, ok: true, files });
17272
- if (kinds.includes("code") && await this.batchMayChangePageKeys(appDir, expandedBatch)) {
17312
+ const rebuildClient = devPlan.actions.includes("rebuild-client");
17313
+ if (kinds.includes("code") && !rebuildClient) {
17314
+ this.#logger.verbose(`client rebuild skipped; devPlan actions=${devPlan.actions.join(",") || "(none)"}`);
17315
+ }
17316
+ if (kinds.includes("code") && rebuildClient && await this.batchMayChangePageKeys(appDir, expandedBatch)) {
17273
17317
  const started = Date.now();
17274
17318
  await this.#app.getPageKeys({ refresh: true });
17275
17319
  this.#logger.verbose(`pageKeys updated, app pageKeys are refreshed (${Date.now() - started}ms)`);
17276
- } else if (kinds.includes("code") && this.batchTouchesPagesTree(appDir, expandedBatch)) {
17320
+ } else if (kinds.includes("code") && rebuildClient && this.batchTouchesPagesTree(appDir, expandedBatch)) {
17277
17321
  this.#logger.verbose("pageKeys refresh skipped; changed page source cannot add/remove a route key");
17278
17322
  }
17279
- if (kinds.includes("code") && this.#shouldRebuildCsr()) {
17323
+ if (kinds.includes("code") && rebuildClient && this.#shouldRebuildCsr()) {
17280
17324
  try {
17281
17325
  const started = Date.now();
17282
17326
  await new CsrArtifactBuilder(this.#app).build();
@@ -17287,11 +17331,11 @@ ${cssText}`).toString(36);
17287
17331
  this.#logger.error(`csr-rebundle failed: ${message}`);
17288
17332
  this.#sendBuildStatus("csr", { generation, ok: false, files, message });
17289
17333
  }
17290
- } else if (kinds.includes("code")) {
17334
+ } else if (kinds.includes("code") && rebuildClient) {
17291
17335
  this.#logger.verbose(`csr-rebundle skipped; set AKAN_DEV_CSR_REBUILD=1 to enable per-save CSR rebuilds`);
17292
17336
  }
17293
17337
  process.send?.(event);
17294
- if (kinds.includes("code")) {
17338
+ if (kinds.includes("code") && rebuildClient) {
17295
17339
  try {
17296
17340
  const started = Date.now();
17297
17341
  const next = await new PagesBundleBuilder(this.#app).build();
@@ -17307,7 +17351,7 @@ ${cssText}`).toString(36);
17307
17351
  this.#sendBuildStatus("pages", { generation, ok: false, files, message });
17308
17352
  }
17309
17353
  }
17310
- if (kinds.includes("code") || kinds.includes("css")) {
17354
+ if (kinds.includes("css") || kinds.includes("code") && rebuildClient) {
17311
17355
  this.scheduleCssRebuild(artifactDir, { refresh: true, generation, changedFiles: files });
17312
17356
  this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
17313
17357
  }
@@ -17326,6 +17370,10 @@ ${cssText}`).toString(36);
17326
17370
  return;
17327
17371
  }
17328
17372
  });
17373
+ process.on("disconnect", () => {
17374
+ this.#logger.warn("host IPC channel closed; exiting builder");
17375
+ process.exit(0);
17376
+ });
17329
17377
  if (this.#watch)
17330
17378
  await this.installWatcher();
17331
17379
  process.send?.({ type: "builder-ready" });
package/index.js CHANGED
@@ -984,9 +984,15 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
984
984
  command
985
985
  };
986
986
  }
987
- static async from(app) {
987
+ static #importGeneration = 0;
988
+ static async importConfigModule(cwdPath, { bustImportCache = false } = {}) {
989
+ const configPath2 = `${cwdPath}/akan.config.ts`;
990
+ const importPath = bustImportCache ? `${configPath2}?akanConfigGeneration=${++AkanAppConfig.#importGeneration}` : configPath2;
991
+ return await import(importPath).then((mod) => mod.default);
992
+ }
993
+ static async from(app, { bustImportCache = false } = {}) {
988
994
  const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
989
- import(`${app.cwdPath}/akan.config.ts`).then((mod) => mod.default),
995
+ AkanAppConfig.importConfigModule(app.cwdPath, { bustImportCache }),
990
996
  WorkspaceExecutor.getBaseDevEnv(path.join(app.workspace.workspaceRoot, ".env")),
991
997
  app.workspace.getLibs(),
992
998
  app.workspace.getPackageJson()
@@ -1406,8 +1412,8 @@ ${errorText}, ${warningText} found`];
1406
1412
  }
1407
1413
 
1408
1414
  // pkgs/@akanjs/devkit/scanInfo.ts
1409
- import path5 from "path";
1410
1415
  import { rm } from "fs/promises";
1416
+ import path5 from "path";
1411
1417
 
1412
1418
  // pkgs/@akanjs/devkit/dependencyScanner.ts
1413
1419
  import { builtinModules } from "module";
@@ -1744,7 +1750,8 @@ var appRootAllowedDirs = new Set([
1744
1750
  "ui",
1745
1751
  "srvkit",
1746
1752
  "webkit",
1747
- "common"
1753
+ "common",
1754
+ "secrets"
1748
1755
  ]);
1749
1756
  var libRootAllowedFiles = new Set([
1750
1757
  "cnst.ts",
@@ -3658,7 +3665,7 @@ class AppExecutor extends SysExecutor {
3658
3665
  async getConfig({ refresh } = {}) {
3659
3666
  if (this.#akanConfig && !refresh)
3660
3667
  return this.#akanConfig;
3661
- this.#akanConfig = await AkanAppConfig.from(this);
3668
+ this.#akanConfig = await AkanAppConfig.from(this, { bustImportCache: refresh });
3662
3669
  return this.#akanConfig;
3663
3670
  }
3664
3671
  #pageKeys = null;
@@ -4285,9 +4292,10 @@ class IncrementalBuilderHost {
4285
4292
  // pkgs/@akanjs/devkit/akanApp/akanApp.host.ts
4286
4293
  var backendMsgTypeSet = new Set(["build-route"]);
4287
4294
  var BACKEND_RESTART_DEBOUNCE_MS = 120;
4288
- var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
4295
+ var BACKEND_GRACEFUL_TIMEOUT_MS = 8000;
4289
4296
  var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
4290
4297
  var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
4298
+ var BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
4291
4299
  var BACKEND_STDERR_TAIL_LIMIT = 40;
4292
4300
  var BUILDER_READY_TIMEOUT_MS = 150000;
4293
4301
  var BUILDER_START_MAX_ATTEMPTS = 3;
@@ -4312,6 +4320,8 @@ var shouldRestartBackendByDevPlan = (message) => {
4312
4320
  return message.devPlan.actions.includes("restart-backend");
4313
4321
  };
4314
4322
  var shouldRestartBuilderByDevPlan = (message) => message.devPlan?.actions.includes("restart-builder") ?? false;
4323
+ var shouldAbandonBackendRecovery = (attempts, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS) => attempts >= maxAttempts;
4324
+ var normalizeBackendReportedGeneration = (generation) => generation >= 0 ? generation : undefined;
4315
4325
  var shouldRestartDevHostByDevPlan = (message) => message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
4316
4326
  var RESTART_ROLE_ORDER = ["server", "shared", "barrel", "config"];
4317
4327
  var generationValue = (generation) => generation ?? -1;
@@ -4560,6 +4570,16 @@ class AkanAppHost {
4560
4570
  this.#replayBuilderState();
4561
4571
  return;
4562
4572
  }
4573
+ if (msg.type === "build-status") {
4574
+ const status = this.#recordBackendBuildStatus({
4575
+ generation: normalizeBackendReportedGeneration(msg.data.generation),
4576
+ ok: msg.data.ok,
4577
+ files: msg.data.files,
4578
+ message: msg.data.message
4579
+ });
4580
+ this.#sendOrQueueBuildStatus(status);
4581
+ return;
4582
+ }
4563
4583
  if (backendMsgTypeSet.has(msg.type))
4564
4584
  this.#sendToBuilder(msg);
4565
4585
  },
@@ -4721,6 +4741,19 @@ class AkanAppHost {
4721
4741
  #scheduleBackendRecovery(reason) {
4722
4742
  if (this.#backendRecoveryTimer || this.#backend)
4723
4743
  return;
4744
+ if (shouldAbandonBackendRecovery(this.#backendRecoveryAttempts)) {
4745
+ const message = `Backend exited ${this.#backendRecoveryAttempts} times in a row (${reason}); waiting for a server-side edit to retry.`;
4746
+ this.#setBackendLifecycleState("stopped", `gave up after ${this.#backendRecoveryAttempts} recovery attempts`);
4747
+ this.logger.error(`[backend-recovery] ${message}`);
4748
+ if (this.#backendStderrTail.length > 0) {
4749
+ this.logger.error(`[backend-recovery] recent backend stderr:
4750
+ ${this.#backendStderrTail.join(`
4751
+ `)}`);
4752
+ }
4753
+ const abandonedStatus = this.#recordBackendBuildStatus({ ok: false, files: [], message });
4754
+ this.#sendOrQueueBuildStatus(abandonedStatus);
4755
+ return;
4756
+ }
4724
4757
  this.#setBackendLifecycleState("recovering", reason);
4725
4758
  const attempt = this.#backendRecoveryAttempts;
4726
4759
  const delay = Math.min(BACKEND_RECOVERY_BASE_DELAY_MS * 2 ** attempt, BACKEND_RECOVERY_MAX_DELAY_MS);
@@ -4769,16 +4802,21 @@ ${this.#backendStderrTail.join(`
4769
4802
  this.#sendToBackend(message);
4770
4803
  }
4771
4804
  async#handleInvalidate(message) {
4772
- if (shouldRestartBuilderByDevPlan(message)) {
4805
+ this.#logDevPlan(message);
4806
+ if (shouldRestartDevHostByDevPlan(message)) {
4773
4807
  try {
4774
- await this.#restartDevChildren(message);
4808
+ await this.#restartDevHost(message);
4775
4809
  } catch (err) {
4776
- this.#recordDevHostRestartFailure(message, err);
4810
+ this.#recordDevHostRestartFailure(message, err, "Config");
4777
4811
  }
4778
4812
  return;
4779
4813
  }
4780
- if (shouldRestartDevHostByDevPlan(message)) {
4781
- this.#recordDevHostRestartRequired(message);
4814
+ if (shouldRestartBuilderByDevPlan(message)) {
4815
+ try {
4816
+ await this.#restartDevChildren(message);
4817
+ } catch (err) {
4818
+ this.#recordDevHostRestartFailure(message, err, "Runtime metadata");
4819
+ }
4782
4820
  return;
4783
4821
  }
4784
4822
  if (await this.#shouldRestartBackend(message)) {
@@ -4790,6 +4828,15 @@ ${this.#backendStderrTail.join(`
4790
4828
  async#restartDevChildren(message) {
4791
4829
  const generation = message.devPlan?.generation ?? message.generation;
4792
4830
  this.logger.warn(`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`);
4831
+ await this.#recycleDevChildren(message);
4832
+ }
4833
+ async#restartDevHost(message) {
4834
+ const generation = message.devPlan?.generation ?? message.generation;
4835
+ this.logger.warn(`[dev-host] config change detected; restarting dev host generation=${generation ?? "(unknown)"} files=${message.files.length}`);
4836
+ await this.#recycleDevChildren(message, { refreshConfig: true });
4837
+ }
4838
+ async#recycleDevChildren(message, { refreshConfig = false } = {}) {
4839
+ const generation = message.devPlan?.generation ?? message.generation;
4793
4840
  if (this.#restartTimer) {
4794
4841
  clearTimeout(this.#restartTimer);
4795
4842
  this.#restartTimer = null;
@@ -4804,6 +4851,11 @@ ${this.#backendStderrTail.join(`
4804
4851
  this.#pendingBuildStatusReplay = [];
4805
4852
  await this.#stopBackend();
4806
4853
  this.#stopBuilder();
4854
+ if (refreshConfig) {
4855
+ await this.app.getConfig({ refresh: true });
4856
+ const { env } = await this.app.prepareCommand("start");
4857
+ Object.assign(this.env, env);
4858
+ }
4807
4859
  await this.#backendGraph.refresh();
4808
4860
  await this.#startBuilder();
4809
4861
  this.#startBackend({ generation, files: message.files });
@@ -4821,32 +4873,16 @@ ${this.#backendStderrTail.join(`
4821
4873
  this.#lastGoodFrontend.css = message;
4822
4874
  this.logger.verbose(`[last-good] css generation=${message.data.generation ?? "(unknown)"} assets=${Object.keys(message.data.cssAssets).length}`);
4823
4875
  }
4824
- #recordDevHostRestartRequired(message) {
4825
- const generation = message.devPlan?.generation ?? message.generation;
4826
- const detail = `generation=${generation ?? "(unknown)"} files=${message.files.length}`;
4827
- this.logger.warn(`[dev-host] config change requires a manual restart until controlled dev-host restart is implemented (${detail})`);
4828
- if (typeof generation === "number") {
4829
- const status = {
4830
- generation,
4831
- phase: "scan",
4832
- ok: false,
4833
- files: message.files,
4834
- message: "Config change requires restarting `akan start` to apply."
4835
- };
4836
- this.#recordBuildStatus(status);
4837
- this.#sendOrQueueBuildStatus(status);
4838
- }
4839
- }
4840
- #recordDevHostRestartFailure(message, err) {
4876
+ #recordDevHostRestartFailure(message, err, kind) {
4841
4877
  const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
4842
4878
  const detail = err instanceof Error ? err.message : String(err);
4843
- this.logger.warn(`[dev-host] runtime metadata restart failed generation=${generation}: ${detail}`);
4879
+ this.logger.warn(`[dev-host] ${kind.toLowerCase()} restart failed generation=${generation}: ${detail}`);
4844
4880
  const status = {
4845
4881
  generation,
4846
4882
  phase: "scan",
4847
4883
  ok: false,
4848
4884
  files: message.files,
4849
- message: `Runtime metadata change requires restarting \`akan start\` to apply: ${detail}`
4885
+ message: `${kind} change requires restarting \`akan start\` to apply: ${detail}`
4850
4886
  };
4851
4887
  this.#recordBuildStatus(status);
4852
4888
  this.#sendOrQueueBuildStatus(status);
@@ -4880,12 +4916,16 @@ ${this.#backendStderrTail.join(`
4880
4916
  this.#sendToBackend({ type: "build-status", data: status });
4881
4917
  }
4882
4918
  }
4919
+ #logDevPlan(message) {
4920
+ if (!message.devPlan)
4921
+ return;
4922
+ const { generation, roles, actions, reasonByFile } = message.devPlan;
4923
+ this.logger.verbose(`[dev-plan] generation=${generation} roles=${roles.join(",") || "(none)"} actions=${actions.join(",") || "(none)"} reasons=${Object.keys(reasonByFile).length}`);
4924
+ }
4883
4925
  async#shouldRestartBackend(message) {
4884
4926
  if (message.kinds.length === 1 && message.kinds[0] === "css")
4885
4927
  return false;
4886
4928
  if (message.devPlan) {
4887
- const { generation, roles, actions, reasonByFile } = message.devPlan;
4888
- this.logger.verbose(`[dev-plan] generation=${generation} roles=${roles.join(",") || "(none)"} actions=${actions.join(",") || "(none)"} reasons=${Object.keys(reasonByFile).length}`);
4889
4929
  const shouldRestart = shouldRestartBackendByDevPlan(message) ?? false;
4890
4930
  if (shouldRestart && message.kinds.includes("code"))
4891
4931
  await this.#backendGraph.refresh();
@@ -22756,14 +22796,18 @@ class WorkspaceScript extends script("workspace", [
22756
22796
  ApplicationScript,
22757
22797
  LibraryScript,
22758
22798
  PackageScript,
22759
- CloudScript
22799
+ CloudScript,
22800
+ ContextScript,
22801
+ AgentScript
22760
22802
  ]) {
22761
22803
  async createWorkspace(repoName, appName, {
22762
22804
  dirname: dirname2 = ".",
22763
22805
  installLibs = false,
22764
22806
  init = true,
22765
22807
  registryUrl,
22766
- owner
22808
+ owner,
22809
+ mcpInstall = true,
22810
+ agentInstall = true
22767
22811
  }) {
22768
22812
  const akanVersion = await this.packageScript.version(null, { log: false });
22769
22813
  const workspace = await this.workspaceRunner.createWorkspace(repoName, appName, {
@@ -22784,6 +22828,10 @@ class WorkspaceScript extends script("workspace", [
22784
22828
  dict: { appName },
22785
22829
  options: { libs: installLibs ? ["util", "shared"] : [] }
22786
22830
  });
22831
+ if (agentInstall)
22832
+ await this.agentScript.agent(workspace, "install", "all", { force: true });
22833
+ if (mcpInstall)
22834
+ await this.contextScript.mcpInstall(workspace, "all", { force: true });
22787
22835
  const gitSpinner = workspace.spinning("Initializing git repository and commit...");
22788
22836
  try {
22789
22837
  await workspace.commit("Initial commit", { init: true });
@@ -22882,13 +22930,22 @@ class WorkspaceCommand extends command("workspace", [WorkspaceScript], ({ public
22882
22930
  desc: "owner of the workspace",
22883
22931
  default: process.env.GITHUB_OWNER,
22884
22932
  nullable: true
22885
- }).exec(async function(workspaceName, app, dir, libs, init, registry, owner) {
22933
+ }).option("mcpInstall", Boolean, {
22934
+ desc: "Install the Akan MCP server config for Cursor, Claude Code, and Codex? (Recommended)",
22935
+ default: true
22936
+ }).option("agentInstall", Boolean, {
22937
+ flag: "A",
22938
+ desc: "Install Akan agent rules (AGENTS.md, CLAUDE.md, Cursor)? (Recommended)",
22939
+ default: true
22940
+ }).exec(async function(workspaceName, app, dir, libs, init, registry, owner, mcpInstall, agentInstall) {
22886
22941
  const appName = app || "app";
22887
22942
  await this.workspaceScript.createWorkspace(workspaceName.toLowerCase().replace(/ /g, "-"), appName.toLowerCase().replace(/ /g, "-"), {
22888
22943
  dirname: dir,
22889
22944
  installLibs: libs,
22890
22945
  init,
22891
22946
  owner,
22947
+ mcpInstall,
22948
+ agentInstall,
22892
22949
  ...registry ? { registryUrl: registry } : {}
22893
22950
  });
22894
22951
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "2.4.0-rc.0",
3
+ "version": "2.4.0-rc.2",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "@langchain/openai": "^1.4.6",
35
35
  "@tailwindcss/node": "^4.3.0",
36
36
  "@trapezedev/project": "^7.1.4",
37
- "akanjs": "2.4.0-rc.0",
37
+ "akanjs": "2.4.0-rc.2",
38
38
  "chalk": "^5.6.2",
39
39
  "commander": "^14.0.3",
40
40
  "daisyui": "5.5.23",
@@ -28,6 +28,18 @@
28
28
  --color-error-content: #fafafa;
29
29
  --color-open: #27ae60;
30
30
  --color-open-content: #fafafa;
31
+
32
+ /* Shape & feel — customize these too, not just colors, to make the theme yours.
33
+ These are daisyUI v5 theme variables; the values below are the neutral defaults.
34
+ See `akan guideline show cssRule` and https://daisyui.com/docs/themes/ for the full set. */
35
+ --radius-selector: 1rem; /* checkbox, toggle, badge corner rounding */
36
+ --radius-field: 0.5rem; /* button, input, select, tab corner rounding */
37
+ --radius-box: 1rem; /* card, modal, alert corner rounding */
38
+ --size-selector: 0.25rem; /* base scale (density) for selector controls */
39
+ --size-field: 0.25rem; /* base scale (density) for field controls */
40
+ --border: 1px; /* component border width */
41
+ --depth: 0; /* 1 = subtle 3D depth on components */
42
+ --noise: 0; /* 1 = grain texture on surfaces */
31
43
  }
32
44
 
33
45
  @plugin "daisyui/theme" {
@@ -51,4 +63,14 @@
51
63
  --color-error: #f02020;
52
64
  --color-error-content: #ffffff;
53
65
  --color-open: #10fc00;
66
+
67
+ /* Keep shape & feel consistent with the light theme (usually the same values across modes). */
68
+ --radius-selector: 1rem;
69
+ --radius-field: 0.5rem;
70
+ --radius-box: 1rem;
71
+ --size-selector: 0.25rem;
72
+ --size-field: 0.25rem;
73
+ --border: 1px;
74
+ --depth: 0;
75
+ --noise: 0;
54
76
  }
@@ -102,6 +102,7 @@ apps/*/common/index.ts
102
102
  apps/*/plugin/index.ts
103
103
  apps/*/client.ts
104
104
  apps/*/server.ts
105
+ apps/*/capacitor.config.json
105
106
  libs/*/lib/cnst.ts
106
107
  libs/*/lib/dict.ts
107
108
  libs/*/lib/db.ts