@akanjs/devkit 3.0.0-alpha.5 → 3.0.0-alpha.51
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/agentsIndex.test.ts +10 -0
- package/agentsIndex.ts +47 -1
- package/aiEditor.ts +1 -1
- package/akanConfig/akanConfig.test.ts +47 -10
- package/akanConfig/akanConfig.ts +23 -16
- package/akanContext.ts +53 -11
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +0 -1
- package/biome.base.json +325 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.test.ts +8 -0
- package/capacitorApp.ts +112 -18
- package/executors.test.ts +29 -5
- package/executors.ts +23 -5
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/buildRouteClient.test.ts +28 -2
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/cssCandidateCache.test.ts +38 -0
- package/frontendBuild/cssCandidateCache.ts +15 -3
- package/frontendBuild/cssCompiler.ts +122 -11
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/frontendBuild.test.ts +88 -1
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +20 -4
- package/frontendBuild/styleGuard.test.ts +15 -0
- package/frontendBuild/styleGuard.ts +17 -0
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/getCredentials.ts +1 -3
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +2 -2
- package/integration/devStabilityHarness.ts +1 -5
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-return-in-store-action.grit +35 -0
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/linter.ts +17 -12
- package/package.json +4 -4
- package/qualityScanner.test.ts +52 -0
- package/qualityScanner.ts +48 -37
- package/repoIdentity.ts +42 -0
- package/routeSourceValidator.test.ts +41 -0
- package/routeSourceValidator.ts +2 -44
- package/scanInfo.ts +21 -20
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
package/README.ko.md
CHANGED
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ defaults, the shares they derive from `AKAN_MEMORY_LIMIT`, and what a small cont
|
|
|
60
60
|
|
|
61
61
|
## Requirements
|
|
62
62
|
|
|
63
|
-
- [Bun](https://bun.sh) `>=1.
|
|
63
|
+
- [Bun](https://bun.sh) `>=1.4.0`
|
|
64
64
|
- TypeScript
|
|
65
65
|
- Optional peers are only needed for the features that use them, such as Capacitor integration.
|
|
66
66
|
|
package/agentsIndex.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderRecipeEntries,
|
|
11
11
|
renderScopeAgentBlock,
|
|
12
12
|
renderScopeAgentsMd,
|
|
13
|
+
renderScopeClaudeMd,
|
|
13
14
|
upsertAgentBlock,
|
|
14
15
|
} from "./agentsIndex";
|
|
15
16
|
import type { RecipeInfo } from "./recipeScanner";
|
|
@@ -48,6 +49,15 @@ describe("renderScopeAgentBlock", () => {
|
|
|
48
49
|
});
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
describe("renderScopeClaudeMd", () => {
|
|
53
|
+
test("points at AGENTS.md and restates only the comment rule", () => {
|
|
54
|
+
const claude = renderScopeClaudeMd({ type: "app", name: "minimal" });
|
|
55
|
+
expect(claude).toContain("@AGENTS.md");
|
|
56
|
+
expect(claude).toContain("## Comments — Overrides Your Default");
|
|
57
|
+
expect(claude).not.toContain("## Recipes In Scope");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
51
61
|
describe("upsertAgentBlock / extractAgentBlock", () => {
|
|
52
62
|
test("round-trips: fresh file → replace block → extract equals block", () => {
|
|
53
63
|
const fresh = renderScopeAgentsMd({ type: "app", name: "minimal" }, "OLD");
|
package/agentsIndex.ts
CHANGED
|
@@ -19,6 +19,32 @@ const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner")
|
|
|
19
19
|
export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
20
20
|
export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
21
21
|
|
|
22
|
+
const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
|
|
26
|
+
* without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
|
|
27
|
+
* `akan doctor` compares this against the installed devkit and says so.
|
|
28
|
+
*/
|
|
29
|
+
export const stampBlockVersion = (block: string, version: string): string =>
|
|
30
|
+
`${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
|
|
31
|
+
|
|
32
|
+
/** The stamped version of a committed AGENTS.md, or null when it predates stamping or carries no block. */
|
|
33
|
+
export const extractBlockVersion = (content: string): string | null =>
|
|
34
|
+
content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
|
|
35
|
+
|
|
36
|
+
/** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
|
|
37
|
+
export const readDevkitVersion = async (): Promise<string | null> => {
|
|
38
|
+
const { readFile } = await import("node:fs/promises");
|
|
39
|
+
const { getDirname } = await import("./getDirname");
|
|
40
|
+
try {
|
|
41
|
+
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
42
|
+
return (JSON.parse(raw) as { version?: string }).version ?? null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
22
48
|
export interface AgentsIndexScope {
|
|
23
49
|
type: "app" | "lib";
|
|
24
50
|
name: string;
|
|
@@ -139,8 +165,28 @@ ${block}
|
|
|
139
165
|
${AGENT_BLOCK_END}
|
|
140
166
|
`;
|
|
141
167
|
|
|
142
|
-
|
|
168
|
+
export const CLAUDE_COMMENT_RULE = `## Comments — Overrides Your Default
|
|
169
|
+
|
|
170
|
+
Write **no comments** unless the comment passes the test below. This is the rule agents break most often here, so it
|
|
171
|
+
is repeated outside the guide: a diff that adds a comment the test rejects is a diff to redo.
|
|
172
|
+
|
|
173
|
+
Before typing \`//\`, \`/*\`, or a doc block, ask — **does this sentence carry a fact that is nowhere in the code?**
|
|
174
|
+
|
|
175
|
+
- Restates the identifier, the signature, or the line under it → delete it.
|
|
176
|
+
- Labels a section (\`// helpers\`, \`// state\`) or narrates a step (\`// fetch the user\`, \`// then save\`) → delete it.
|
|
177
|
+
- JSDoc on an ordinary function, or a why/how preamble on ordinary logic → delete it.
|
|
178
|
+
- Explains the edit you just made, for whoever reads the diff → say it in your reply, not in the file.
|
|
179
|
+
- Names a vendor or protocol quirk, an infrastructure constraint, a library gotcha, security reasoning, a math
|
|
180
|
+
derivation, a domain field's business meaning, a state transition, or why an obvious alternative was rejected →
|
|
181
|
+
keep it, one line.
|
|
182
|
+
|
|
183
|
+
That keep-list is exact — \`Comments\` in the guide is the full version. "It aids readability" and "this logic is
|
|
184
|
+
subtle" are not on it: rename or split the code instead. When you edit an existing file, match its density; if the
|
|
185
|
+
surrounding code carries none, your diff carries none.`;
|
|
186
|
+
|
|
143
187
|
export const renderScopeClaudeMd = (scope: AgentsIndexScope): string => `# ${scope.name} — Claude Code Guide
|
|
144
188
|
|
|
145
189
|
@AGENTS.md
|
|
190
|
+
|
|
191
|
+
${CLAUDE_COMMENT_RULE}
|
|
146
192
|
`;
|
package/aiEditor.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { FileContent } from "./types";
|
|
|
19
19
|
|
|
20
20
|
const MAX_ASK_TRY = 300;
|
|
21
21
|
|
|
22
|
-
const deepSeekLlmModels = ["deepseek-
|
|
22
|
+
const deepSeekLlmModels = ["deepseek-v4-flash", "deepseek-v4-pro"] as const;
|
|
23
23
|
|
|
24
24
|
const openAiLlmModels = ["gpt-5.5"] as const;
|
|
25
25
|
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AppExecutor, WorkspaceExecutor } from "../executors";
|
|
6
7
|
import type { PackageJson } from "../types";
|
|
7
8
|
import { AkanAppConfig, AkanLibConfig, deriveDefaultAppId } from "./akanConfig";
|
|
8
9
|
import type { DeepPartial, LibConfigResult } from "./types";
|
|
@@ -19,7 +20,6 @@ const packageJson: PackageJson = {
|
|
|
19
20
|
react: "19.0.0",
|
|
20
21
|
"react-dom": "19.0.0",
|
|
21
22
|
"react-server-dom-webpack": "19.0.0",
|
|
22
|
-
sharp: "1.0.0",
|
|
23
23
|
"@external/runtime": "2.0.0",
|
|
24
24
|
},
|
|
25
25
|
};
|
|
@@ -144,8 +144,6 @@ describe("AkanAppConfig", () => {
|
|
|
144
144
|
react: "19.0.0",
|
|
145
145
|
"react-dom": "19.0.0",
|
|
146
146
|
"react-server-dom-webpack": "19.0.0",
|
|
147
|
-
croner: akanPackageJson.peerDependencies?.croner,
|
|
148
|
-
sharp: "1.0.0",
|
|
149
147
|
"@external/runtime": "2.0.0",
|
|
150
148
|
},
|
|
151
149
|
});
|
|
@@ -184,8 +182,6 @@ describe("AkanAppConfig", () => {
|
|
|
184
182
|
react: runtimeDependencies.react,
|
|
185
183
|
"react-dom": runtimeDependencies["react-dom"],
|
|
186
184
|
"react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
|
|
187
|
-
croner: runtimeDependencies.croner,
|
|
188
|
-
sharp: runtimeDependencies.sharp,
|
|
189
185
|
});
|
|
190
186
|
});
|
|
191
187
|
|
|
@@ -198,9 +194,6 @@ describe("AkanAppConfig", () => {
|
|
|
198
194
|
const multipleConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "multiple" }, baseDevEnv);
|
|
199
195
|
const clusterConfig = new AkanAppConfig(app, [], packageJson, { defaultDatabaseMode: "cluster" }, baseDevEnv);
|
|
200
196
|
|
|
201
|
-
expect(singleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
202
|
-
croner: runtimeDependencies.croner,
|
|
203
|
-
});
|
|
204
197
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("ioredis");
|
|
205
198
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("bullmq");
|
|
206
199
|
expect(singleConfig.getProductionPackageJson().dependencies).not.toHaveProperty("@libsql/client");
|
|
@@ -210,7 +203,6 @@ describe("AkanAppConfig", () => {
|
|
|
210
203
|
expect(multipleConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
211
204
|
"@libsql/client": runtimeDependencies["@libsql/client"],
|
|
212
205
|
bullmq: runtimeDependencies.bullmq,
|
|
213
|
-
croner: runtimeDependencies.croner,
|
|
214
206
|
ioredis: runtimeDependencies.ioredis,
|
|
215
207
|
protobufjs: runtimeDependencies.protobufjs,
|
|
216
208
|
});
|
|
@@ -218,7 +210,6 @@ describe("AkanAppConfig", () => {
|
|
|
218
210
|
|
|
219
211
|
expect(clusterConfig.getProductionPackageJson().dependencies).toMatchObject({
|
|
220
212
|
bullmq: runtimeDependencies.bullmq,
|
|
221
|
-
croner: runtimeDependencies.croner,
|
|
222
213
|
ioredis: runtimeDependencies.ioredis,
|
|
223
214
|
postgres: runtimeDependencies.postgres,
|
|
224
215
|
protobufjs: runtimeDependencies.protobufjs,
|
|
@@ -392,6 +383,52 @@ describe("deriveDefaultAppId", () => {
|
|
|
392
383
|
});
|
|
393
384
|
});
|
|
394
385
|
|
|
386
|
+
describe("AkanAppConfig lib externalLibs", () => {
|
|
387
|
+
test("merges lib-declared external libs into the app's own, deduped", () => {
|
|
388
|
+
const libAwarePackageJson: PackageJson = {
|
|
389
|
+
...packageJson,
|
|
390
|
+
dependencies: { ...packageJson.dependencies, puppeteer: "24.0.0" },
|
|
391
|
+
};
|
|
392
|
+
const config = new AkanAppConfig(
|
|
393
|
+
app,
|
|
394
|
+
["shared"],
|
|
395
|
+
libAwarePackageJson,
|
|
396
|
+
{ externalLibs: ["@external/runtime"] },
|
|
397
|
+
baseDevEnv,
|
|
398
|
+
[],
|
|
399
|
+
["@external/runtime", "puppeteer"],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(config.externalLibs).toEqual(["@external/runtime", "puppeteer"]);
|
|
403
|
+
expect(config.getProductionPackageJson().dependencies).toMatchObject({
|
|
404
|
+
"@external/runtime": "2.0.0",
|
|
405
|
+
puppeteer: "24.0.0",
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("reads them off every workspace lib config on load", async () => {
|
|
410
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "akan-config-libext-"));
|
|
411
|
+
try {
|
|
412
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
413
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
414
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), "export default { externalLibs: ['shiki'] };\n");
|
|
415
|
+
fs.writeFileSync(
|
|
416
|
+
path.join(root, "libs/extlib/akan.config.ts"),
|
|
417
|
+
"export default { externalLibs: ['puppeteer'] };\n",
|
|
418
|
+
);
|
|
419
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
420
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
421
|
+
|
|
422
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
423
|
+
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
424
|
+
|
|
425
|
+
expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
|
|
426
|
+
} finally {
|
|
427
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
395
432
|
describe("AkanLibConfig", () => {
|
|
396
433
|
test("uses empty external libs by default and preserves explicit libs", () => {
|
|
397
434
|
const lib = { name: "shared" } as never;
|
package/akanConfig/akanConfig.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { AkanPlugin } from "akanjs";
|
|
|
5
5
|
import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
|
|
6
6
|
import type { AkanImageConfig } from "akanjs/server";
|
|
7
7
|
import type { App, Lib } from "../commandDecorators";
|
|
8
|
-
import { WorkspaceExecutor } from "../executors";
|
|
8
|
+
import { LibExecutor, WorkspaceExecutor } from "../executors";
|
|
9
9
|
import type { BaseDevEnv, PackageJson } from "../types";
|
|
10
10
|
import {
|
|
11
11
|
type AkanMobileConfig,
|
|
@@ -50,8 +50,6 @@ const DEFAULT_OPTIMIZE_IMPORTS = [
|
|
|
50
50
|
];
|
|
51
51
|
const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
|
|
52
52
|
const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
|
|
53
|
-
const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
|
|
54
|
-
const DEFAULT_BACKEND_RUNTIME_PACKAGES = ["croner"] as const;
|
|
55
53
|
// The firebase client (push tokens) and the Capacitor toolchain — `@capacitor/cli` (`npx cap`),
|
|
56
54
|
// `@capacitor/assets` (`npx @capacitor/assets`) plus the `@capacitor/core`/`ios`/`android` runtime
|
|
57
55
|
// and native-platform packages that `npx cap add`/`sync` resolve from the workspace node_modules.
|
|
@@ -92,8 +90,6 @@ const DATABASE_MODE_RUNTIME_PACKAGES = {
|
|
|
92
90
|
} satisfies Record<DatabaseMode, readonly string[]>;
|
|
93
91
|
const AKAN_RUNTIME_PACKAGES = new Set<string>([
|
|
94
92
|
...SSR_RUNTIME_PACKAGES,
|
|
95
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
96
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
97
93
|
...MOBILE_RUNTIME_PACKAGES,
|
|
98
94
|
...Object.values(DATABASE_MODE_RUNTIME_PACKAGES).flat(),
|
|
99
95
|
]);
|
|
@@ -199,6 +195,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
199
195
|
config: DeepPartial<AppConfigResult>,
|
|
200
196
|
baseDevEnv: BaseDevEnv,
|
|
201
197
|
plugins: AkanPlugin[] = [],
|
|
198
|
+
libExternalLibs: readonly string[] = [],
|
|
202
199
|
) {
|
|
203
200
|
this.app = app;
|
|
204
201
|
this.rootPackageJson = rootPackageJson;
|
|
@@ -207,7 +204,7 @@ export class AkanAppConfig implements AppConfigResult {
|
|
|
207
204
|
this.plugins = plugins;
|
|
208
205
|
this.#applyRoutes(config?.routes);
|
|
209
206
|
this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
|
|
210
|
-
this.externalLibs = config?.externalLibs ?? [];
|
|
207
|
+
this.externalLibs = [...new Set([...(config?.externalLibs ?? []), ...libExternalLibs])];
|
|
211
208
|
this.barrelImports = [
|
|
212
209
|
...DEFAULT_BARREL_IMPORTS,
|
|
213
210
|
...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
|
|
@@ -416,7 +413,23 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
416
413
|
]);
|
|
417
414
|
const resolved = typeof configImp === "function" ? configImp(app) : configImp;
|
|
418
415
|
const { plugins, ...config } = (resolved ?? {}) as DeepPartial<AppConfigResult> & { plugins?: AkanPlugin[] };
|
|
419
|
-
|
|
416
|
+
const libExternalLibs = await AkanAppConfig.#collectLibExternalLibs(app, libs, bustImportCache);
|
|
417
|
+
return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv, plugins ?? [], libExternalLibs);
|
|
418
|
+
}
|
|
419
|
+
//* Every workspace lib is read, not just this app's lib deps: narrowing the set needs the dependency
|
|
420
|
+
//* scan, and the incremental page rebundle re-reads this config on every file change.
|
|
421
|
+
static async #collectLibExternalLibs(app: App, libs: string[], bustImportCache: boolean): Promise<string[]> {
|
|
422
|
+
const libConfigs = await Promise.all(
|
|
423
|
+
libs.map(async (libName) =>
|
|
424
|
+
LibExecutor.from(app, libName)
|
|
425
|
+
.getConfig({ refresh: bustImportCache })
|
|
426
|
+
.catch((error: unknown) => {
|
|
427
|
+
app.logger.warn(`Skipped libs/${libName}/akan.config.ts externalLibs: ${String(error)}`);
|
|
428
|
+
return null;
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
return libConfigs.flatMap((libConfig) => libConfig?.externalLibs ?? []);
|
|
420
433
|
}
|
|
421
434
|
#resolveProductionDependencyVersion(lib: string) {
|
|
422
435
|
const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
|
|
@@ -428,13 +441,7 @@ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
|
|
|
428
441
|
return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
|
|
429
442
|
}
|
|
430
443
|
#getProductionRuntimePackages() {
|
|
431
|
-
return [
|
|
432
|
-
...this.externalLibs,
|
|
433
|
-
...SSR_RUNTIME_PACKAGES,
|
|
434
|
-
...NATIVE_RUNTIME_PACKAGES,
|
|
435
|
-
...DEFAULT_BACKEND_RUNTIME_PACKAGES,
|
|
436
|
-
...this.getDatabaseModeRuntimePackages(),
|
|
437
|
-
];
|
|
444
|
+
return [...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...this.getDatabaseModeRuntimePackages()];
|
|
438
445
|
}
|
|
439
446
|
getDatabaseModeRuntimePackages(databaseMode: DatabaseMode = this.defaultDatabaseMode) {
|
|
440
447
|
return [...DATABASE_MODE_RUNTIME_PACKAGES[databaseMode]];
|
|
@@ -539,8 +546,8 @@ export class AkanLibConfig implements LibConfigResult {
|
|
|
539
546
|
this.externalLibs = config?.externalLibs ?? [];
|
|
540
547
|
this.plugins = plugins;
|
|
541
548
|
}
|
|
542
|
-
static async from(lib: Lib) {
|
|
543
|
-
const
|
|
549
|
+
static async from(lib: Lib, { bustImportCache = false }: { bustImportCache?: boolean } = {}) {
|
|
550
|
+
const configImp = await AkanAppConfig.importConfigModule(lib.cwdPath, { bustImportCache });
|
|
544
551
|
const resolved = typeof configImp === "function" ? configImp(lib) : configImp;
|
|
545
552
|
const { plugins, ...config } = (resolved ?? {}) as DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
|
|
546
553
|
return new AkanLibConfig(lib, config, plugins ?? []);
|
package/akanContext.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { capitalize } from "akanjs/common";
|
|
4
|
+
import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
|
|
4
5
|
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
5
6
|
import { FileSys } from "./fileSys";
|
|
6
7
|
import { collectRecipeSources, findInlineRecipeDuplicates, scanRecipes } from "./recipeScanner";
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
workflowRunArtifactPath,
|
|
15
16
|
workflowSyncDir,
|
|
16
17
|
} from "./workflow";
|
|
17
|
-
import {
|
|
18
|
+
import { isScannedRootEntry, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
18
19
|
|
|
19
20
|
export type AkanContextFormat = "json" | "markdown";
|
|
20
21
|
export type AkanModuleKind = "domain" | "service" | "scalar";
|
|
@@ -276,6 +277,8 @@ const validationCommands = [
|
|
|
276
277
|
"akan test <app-or-lib-or-pkg>",
|
|
277
278
|
"akan build <app-name>",
|
|
278
279
|
"akan doctor --strict --format json",
|
|
280
|
+
"akan quality scan [--format json]",
|
|
281
|
+
"akan quality ssr [--format json]",
|
|
279
282
|
];
|
|
280
283
|
|
|
281
284
|
const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
|
|
@@ -618,6 +621,30 @@ export class AkanContextAnalyzer {
|
|
|
618
621
|
};
|
|
619
622
|
}
|
|
620
623
|
|
|
624
|
+
// The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
|
|
625
|
+
// keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
|
|
626
|
+
// running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
|
|
627
|
+
static async #agentGuideDrift(workspace: WorkspaceExecutor) {
|
|
628
|
+
const installed = await readDevkitVersion();
|
|
629
|
+
if (!installed) return null;
|
|
630
|
+
const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
|
|
631
|
+
if (content === null) return null;
|
|
632
|
+
const stamped = extractBlockVersion(content);
|
|
633
|
+
if (stamped === installed) return null;
|
|
634
|
+
const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
|
|
635
|
+
if (!stamped)
|
|
636
|
+
return {
|
|
637
|
+
code: "agent-guide-unstamped",
|
|
638
|
+
message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
|
|
639
|
+
hint,
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
code: "agent-guide-stale",
|
|
643
|
+
message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
|
|
644
|
+
hint,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
621
648
|
static async doctor(
|
|
622
649
|
workspace: WorkspaceExecutor,
|
|
623
650
|
{
|
|
@@ -639,23 +666,25 @@ export class AkanContextAnalyzer {
|
|
|
639
666
|
),
|
|
640
667
|
];
|
|
641
668
|
|
|
642
|
-
for (const
|
|
643
|
-
const
|
|
644
|
-
for (const entry of await safeReadDir(
|
|
645
|
-
if (!
|
|
646
|
-
const allowed = entry.isDirectory()
|
|
669
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
670
|
+
const sysPath = path.join(workspace.workspaceRoot, sys.path);
|
|
671
|
+
for (const entry of await safeReadDir(sysPath)) {
|
|
672
|
+
if (!isScannedRootEntry(sys.type, entry.name)) continue;
|
|
673
|
+
const allowed = entry.isDirectory()
|
|
674
|
+
? rootAllowedDirs[sys.type].has(entry.name)
|
|
675
|
+
: rootAllowedFiles[sys.type].has(entry.name);
|
|
647
676
|
if (!allowed) {
|
|
648
677
|
const action = repairAction(
|
|
649
678
|
"module-shape",
|
|
650
|
-
`akan repair module-shape --app ${
|
|
651
|
-
|
|
679
|
+
`akan repair module-shape --app ${sys.name}`,
|
|
680
|
+
`Review ${sys.type} root shape and remove or move the unknown entry.`,
|
|
652
681
|
false,
|
|
653
682
|
);
|
|
654
683
|
diagnostics.push({
|
|
655
684
|
severity: "error",
|
|
656
|
-
code:
|
|
657
|
-
path: `${
|
|
658
|
-
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in
|
|
685
|
+
code: `${sys.type}-root-unknown-entry`,
|
|
686
|
+
path: `${sys.path}/${entry.name}`,
|
|
687
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in ${sys.type} root: ${sys.path}/${entry.name}`,
|
|
659
688
|
repairActions: [action],
|
|
660
689
|
});
|
|
661
690
|
repairActions.push(action);
|
|
@@ -663,6 +692,19 @@ export class AkanContextAnalyzer {
|
|
|
663
692
|
}
|
|
664
693
|
}
|
|
665
694
|
|
|
695
|
+
const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
|
|
696
|
+
if (agentDrift) {
|
|
697
|
+
const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
|
|
698
|
+
diagnostics.push({
|
|
699
|
+
severity: "warning",
|
|
700
|
+
code: agentDrift.code,
|
|
701
|
+
path: "AGENTS.md",
|
|
702
|
+
message: agentDrift.message,
|
|
703
|
+
repairActions: [action],
|
|
704
|
+
});
|
|
705
|
+
repairActions.push(action);
|
|
706
|
+
}
|
|
707
|
+
|
|
666
708
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
667
709
|
for (const module of sys.modules) {
|
|
668
710
|
if (!module.abstract.exists) {
|
|
@@ -4,7 +4,7 @@ import { AKAN_OPTIONAL_BACKEND_EXTERNALS } from "./applicationBuildRunner";
|
|
|
4
4
|
describe("ApplicationBuildRunner", () => {
|
|
5
5
|
test("externalizes Akan optional backend dependencies", () => {
|
|
6
6
|
expect(AKAN_OPTIONAL_BACKEND_EXTERNALS).toEqual(
|
|
7
|
-
expect.arrayContaining(["@libsql/client", "bullmq", "
|
|
7
|
+
expect.arrayContaining(["@libsql/client", "bullmq", "ioredis", "postgres", "protobufjs"]),
|
|
8
8
|
);
|
|
9
9
|
});
|
|
10
10
|
});
|