@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.
- package/guidelines/componentRule/componentRule.instruction.md +8 -0
- package/guidelines/cssRule/cssRule.instruction.md +18 -0
- package/guidelines/framework/framework.instruction.md +6 -0
- package/incrementalBuilder.proc.js +86 -38
- package/index.js +92 -35
- package/package.json +2 -2
- package/templates/app/page/styles.css.template +22 -0
- package/templates/workspaceRoot/.gitignore.template +1 -0
|
@@ -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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4807
|
+
this.#logDevPlan(message);
|
|
4808
|
+
if (shouldRestartDevHostByDevPlan(message)) {
|
|
4775
4809
|
try {
|
|
4776
|
-
await this.#
|
|
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 (
|
|
4783
|
-
|
|
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
|
-
#
|
|
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]
|
|
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:
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4805
|
+
this.#logDevPlan(message);
|
|
4806
|
+
if (shouldRestartDevHostByDevPlan(message)) {
|
|
4773
4807
|
try {
|
|
4774
|
-
await this.#
|
|
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 (
|
|
4781
|
-
|
|
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
|
-
#
|
|
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]
|
|
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:
|
|
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
|
-
}).
|
|
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.
|
|
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.
|
|
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
|
}
|