@akanjs/devkit 3.0.0-alpha.9 → 3.0.0-alpha.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/DEV_RUNTIME_KNOBS.md +27 -0
  3. package/README.ko.md +1 -1
  4. package/README.md +1 -1
  5. package/agentsIndex.test.ts +10 -0
  6. package/agentsIndex.ts +21 -1
  7. package/aiEditor.ts +1 -1
  8. package/akanApp/BackendImportGraph.test.ts +120 -0
  9. package/akanApp/BackendImportGraph.ts +167 -0
  10. package/akanApp/akanApp.host.test.ts +16 -678
  11. package/akanApp/akanApp.host.ts +113 -578
  12. package/akanApp/devHostPolicy.test.ts +542 -0
  13. package/akanApp/devHostPolicy.ts +442 -0
  14. package/akanApp/index.ts +2 -0
  15. package/akanConfig/akanConfig.test.ts +182 -14
  16. package/akanConfig/akanConfig.ts +143 -47
  17. package/akanConfig/types.ts +9 -0
  18. package/akanContext.ts +30 -14
  19. package/akanMcpContract.ts +25 -4
  20. package/appSelectionMemory.ts +28 -0
  21. package/applicationBuildRunner.test.ts +1 -1
  22. package/applicationBuildRunner.ts +45 -21
  23. package/artifact/implicitRootLayout.test.ts +67 -0
  24. package/artifact/implicitRootLayout.ts +27 -7
  25. package/artifact/routeSeedIndex.test.ts +1 -0
  26. package/biome.base.json +86 -47
  27. package/biomeBase.ts +6 -5
  28. package/capacitorApp.ts +10 -0
  29. package/cloud/globalConfig.ts +9 -2
  30. package/codegenLock.test.ts +137 -0
  31. package/codegenLock.ts +136 -0
  32. package/commandDecorators/argMeta.ts +9 -1
  33. package/commandDecorators/command.ts +94 -7
  34. package/commandDecorators/commandBuilder.ts +20 -5
  35. package/commandDecorators/commandDecorators.test.ts +115 -3
  36. package/commandDecorators/helpFormatter.ts +4 -0
  37. package/dependencyScanner.test.ts +99 -0
  38. package/dependencyScanner.ts +27 -24
  39. package/devkitUtils.test.ts +0 -104
  40. package/executors.test.ts +129 -4
  41. package/executors.ts +105 -24
  42. package/fileEditor.ts +19 -19
  43. package/fileSys.ts +19 -1
  44. package/formSetterScanner.test.ts +80 -0
  45. package/formSetterScanner.ts +92 -0
  46. package/frontendBuild/autoImportSync.test.ts +58 -0
  47. package/frontendBuild/autoImportSync.ts +7 -2
  48. package/frontendBuild/buildRouteClient.test.ts +47 -24
  49. package/frontendBuild/clientBuildTypes.ts +4 -0
  50. package/frontendBuild/clientEntriesBundler.ts +4 -1
  51. package/frontendBuild/clientEntryDiscovery.ts +2 -2
  52. package/frontendBuild/csrArtifactBuilder.ts +116 -84
  53. package/frontendBuild/cssCompiler.ts +123 -12
  54. package/frontendBuild/cssImportResolver.ts +8 -7
  55. package/frontendBuild/devGeneratedIndexSync.ts +3 -2
  56. package/frontendBuild/fontOptimizer.ts +37 -18
  57. package/frontendBuild/fontPruner.test.ts +220 -0
  58. package/frontendBuild/fontPruner.ts +206 -0
  59. package/frontendBuild/frontendBuild.test.ts +178 -10
  60. package/frontendBuild/hmrWatcher.ts +1 -1
  61. package/frontendBuild/index.ts +1 -1
  62. package/frontendBuild/pagesBundleBuilder.ts +3 -3
  63. package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
  64. package/frontendBuild/routeClientBuilder.ts +12 -5
  65. package/frontendBuild/sourceMtimeIndex.ts +1 -1
  66. package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
  67. package/frontendBuild/styleContract.ts +14 -20
  68. package/frontendBuild/themeValidator.ts +22 -17
  69. package/frontendBuild/vendorSpecifiers.ts +1 -0
  70. package/frontendBuild/watchRootResolver.test.ts +67 -0
  71. package/frontendBuild/watchRootResolver.ts +13 -1
  72. package/incrementalBuilder/devWatchBatch.test.ts +18 -20
  73. package/incrementalBuilder/devWatchBatch.ts +6 -2
  74. package/incrementalBuilder/incrementalBuilder.host.ts +51 -7
  75. package/incrementalBuilder/incrementalBuilder.proc.ts +17 -6
  76. package/index.ts +0 -7
  77. package/integration/devStabilityHarness.ts +2 -10
  78. package/libSource.test.ts +112 -0
  79. package/libSource.ts +126 -0
  80. package/lint/__fixtures__/README.md +40 -0
  81. package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
  82. package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
  83. package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
  84. package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
  85. package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
  86. package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
  87. package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
  88. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
  89. package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
  90. package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
  91. package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
  92. package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
  93. package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
  94. package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
  95. package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
  96. package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
  97. package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
  98. package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
  99. package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
  100. package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
  101. package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
  102. package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
  103. package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
  104. package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
  105. package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
  106. package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
  107. package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
  108. package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
  109. package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
  110. package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
  111. package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
  112. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
  113. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
  114. package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
  115. package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
  116. package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
  117. package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
  118. package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
  119. package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
  120. package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
  121. package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
  122. package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
  123. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
  124. package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
  125. package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
  126. package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
  127. package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
  128. package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
  129. package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
  130. package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
  131. package/lint/gritRules.test.ts +178 -0
  132. package/lint/no-arbitrary-color.grit +1 -1
  133. package/lint/no-async-component-in-ui.grit +35 -0
  134. package/lint/no-bang-comment-in-client.grit +23 -10
  135. package/lint/no-daisyui-legacy-class.grit +26 -9
  136. package/lint/no-deprecated-log-level.grit +17 -0
  137. package/lint/no-init-fetch-in-client.grit +47 -0
  138. package/lint/no-inline-color.grit +10 -8
  139. package/lint/no-interpolated-arbitrary-class.grit +3 -3
  140. package/lint/no-model-type-in-util-zone.grit +58 -0
  141. package/lint/no-raw-palette-class.grit +3 -3
  142. package/lint/no-unpublished-form-setter.grit +41 -0
  143. package/lint/non-scalar-props-restricted.grit +16 -7
  144. package/linter.test.ts +80 -0
  145. package/linter.ts +99 -25
  146. package/package.json +5 -8
  147. package/prompter.ts +9 -4
  148. package/qualityScanner.test.ts +116 -0
  149. package/qualityScanner.ts +89 -21
  150. package/recipeScanner.ts +4 -1
  151. package/scanInfo.ts +32 -26
  152. package/semver.test.ts +26 -0
  153. package/semver.ts +31 -0
  154. package/slicePlanner.test.ts +182 -0
  155. package/slicePlanner.ts +235 -0
  156. package/ssrScanner.test.ts +301 -0
  157. package/subspace.test.ts +418 -0
  158. package/subspace.ts +723 -0
  159. package/subspaceConfig.ts +76 -0
  160. package/transforms/asyncDefaultExportDetector.ts +103 -0
  161. package/transforms/barrelImportsPlugin.ts +1 -1
  162. package/transforms/externalizeFrameworkPlugin.ts +0 -1
  163. package/transforms/transforms.test.ts +5 -5
  164. package/transforms/tsconfigPackageResolver.test.ts +230 -0
  165. package/tsconfig.json +6 -2
  166. package/typeChecker.ts +1 -1
  167. package/types.ts +1 -0
  168. package/uploadRelease.ts +2 -2
  169. package/workflow/executor.test.ts +146 -0
  170. package/workspaceLayout.test.ts +56 -4
  171. package/workspaceLayout.ts +48 -2
  172. package/builder.ts +0 -164
  173. package/extractDeps.ts +0 -86
  174. package/frontendBuild/styleGuard.test.ts +0 -165
  175. package/frontendBuild/styleGuard.ts +0 -322
  176. package/getCredentials.ts +0 -19
  177. package/getModelFileData.ts +0 -62
  178. package/src/capacitorApp.ts +0 -282
  179. package/streamAi.ts +0 -45
  180. package/ui/MultiScrollList.tsx +0 -242
  181. package/ui/ScrollList.tsx +0 -107
  182. package/ui/index.ts +0 -2
  183. package/useStdoutDimensions.ts +0 -20
package/linter.test.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { type BiomeReport, parseBiomeReport } from "./linter";
3
+
4
+ const reportOf = (diagnostics: BiomeReport["diagnostics"]): string =>
5
+ JSON.stringify({ summary: { errors: diagnostics?.length ?? 0, warnings: 0 }, diagnostics });
6
+
7
+ const oneDiagnostic: BiomeReport["diagnostics"] = [
8
+ {
9
+ severity: "error",
10
+ message: "Unexpected any",
11
+ category: "lint/suspicious/noExplicitAny",
12
+ location: { path: "apps/demo/ui/Card.tsx", start: { line: 3, column: 10 } },
13
+ },
14
+ ];
15
+
16
+ describe("parseBiomeReport", () => {
17
+ test("reads a report that is the whole output", () => {
18
+ const report = parseBiomeReport(reportOf(oneDiagnostic));
19
+ expect(report.diagnostics).toHaveLength(1);
20
+ expect(report.diagnostics?.[0]?.category).toBe("lint/suspicious/noExplicitAny");
21
+ expect(report.summary?.errors).toBe(1);
22
+ });
23
+
24
+ test("reads a report behind leading text", () => {
25
+ const output = `Skipped 3 files.\n${reportOf(oneDiagnostic)}`;
26
+ expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
27
+ });
28
+
29
+ test("reads a report behind leading text that carries a brace", () => {
30
+ // The whole reason the first-brace-to-last-brace slice was wrong: Biome prints configuration and IO
31
+ // diagnostics onto the same stream as the report, and those messages quote source and config.
32
+ const output = [
33
+ "configuration/deserialize: unknown key `overrides[0].includes { }`",
34
+ " the file `biome.jsonc` cannot be read",
35
+ reportOf(oneDiagnostic),
36
+ ].join("\n");
37
+ expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
38
+ });
39
+
40
+ test("skips a complete JSON object in leading text that is not a report", () => {
41
+ const output = `hint: try {"linter":{"enabled":true}} in your config\n${reportOf(oneDiagnostic)}`;
42
+ const report = parseBiomeReport(output);
43
+ expect(report.diagnostics).toHaveLength(1);
44
+ expect(report.summary?.errors).toBe(1);
45
+ });
46
+
47
+ test("reads a report followed by trailing text", () => {
48
+ const output = `${reportOf(oneDiagnostic)}\nChecked 12 files in 40ms. Found 1 error.`;
49
+ expect(parseBiomeReport(output).diagnostics).toHaveLength(1);
50
+ });
51
+
52
+ test("keeps a diagnostic whose message contains braces and quotes", () => {
53
+ const message = 'Replace {" a "} with { " a " } — the `{}` object syntax is not allowed';
54
+ const report = parseBiomeReport(reportOf([{ severity: "error", message }]));
55
+ expect(report.diagnostics?.[0]?.message).toBe(message);
56
+ });
57
+
58
+ test("keeps a diagnostic whose message ends in an escaped backslash before the closing quote", () => {
59
+ const message = "path separator is \\\\";
60
+ const report = parseBiomeReport(reportOf([{ severity: "warning", message }]));
61
+ expect(report.diagnostics?.[0]?.message).toBe(message);
62
+ expect(report.diagnostics).toHaveLength(1);
63
+ });
64
+
65
+ test("accepts a report carrying only a summary", () => {
66
+ expect(parseBiomeReport('{"summary":{"errors":0,"warnings":0}}').summary?.errors).toBe(0);
67
+ });
68
+
69
+ test("throws the output when it holds no JSON at all", () => {
70
+ expect(() => parseBiomeReport("biome: command not found\n")).toThrow("biome: command not found");
71
+ });
72
+
73
+ test("throws the output when the only brace never closes", () => {
74
+ expect(() => parseBiomeReport('internal error: {"diagnostics":[')).toThrow("internal error");
75
+ });
76
+
77
+ test("throws a named error for empty output", () => {
78
+ expect(() => parseBiomeReport(" \n")).toThrow("No Biome JSON output");
79
+ });
80
+ });
package/linter.ts CHANGED
@@ -3,14 +3,14 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import chalk from "chalk";
5
5
 
6
- type BiomeSeverity = "error" | "warning" | "information" | "hint";
6
+ export type BiomeSeverity = "error" | "warning" | "information" | "hint";
7
7
 
8
- interface BiomePosition {
8
+ export interface BiomePosition {
9
9
  line: number;
10
10
  column: number;
11
11
  }
12
12
 
13
- interface BiomeDiagnostic {
13
+ export interface BiomeDiagnostic {
14
14
  severity: BiomeSeverity;
15
15
  message: string;
16
16
  category?: string;
@@ -21,7 +21,7 @@ interface BiomeDiagnostic {
21
21
  };
22
22
  }
23
23
 
24
- interface BiomeReport {
24
+ export interface BiomeReport {
25
25
  summary?: {
26
26
  changed?: number;
27
27
  errors?: number;
@@ -58,19 +58,81 @@ interface LintResponse {
58
58
  warnings: LintMessage[];
59
59
  }
60
60
 
61
+ /**
62
+ * Extracts the `--reporter=json` report from Biome's output.
63
+ *
64
+ * Biome mixes configuration and IO diagnostics into the same stream as the report, so the output is not
65
+ * always JSON alone. Slicing from the first `{` to the last `}` mis-parses as soon as that leading text
66
+ * carries a brace of its own, which is not hypothetical: a config error printed ahead of the report made
67
+ * one unchanged command alternate between 2 and 10 diagnostics. So every `{` is tried as a start, its
68
+ * match is found by depth while string literals are skipped, and the first candidate that both parses and
69
+ * carries a report field wins — a brace in prose parses as nothing, and `{"a":1}` in prose is not a report.
70
+ */
71
+ export const parseBiomeReport = (output: string): BiomeReport => {
72
+ for (let start = output.indexOf("{"); start !== -1; start = output.indexOf("{", start + 1)) {
73
+ const end = objectEndAt(output, start);
74
+ if (end === -1) break;
75
+ const report = asBiomeReport(output.slice(start, end + 1));
76
+ if (report) return report;
77
+ }
78
+ throw new Error(output.trim() || "No Biome JSON output");
79
+ };
80
+
81
+ const objectEndAt = (output: string, start: number): number => {
82
+ let depth = 0;
83
+ let inString = false;
84
+ let escaped = false;
85
+ for (let idx = start; idx < output.length; idx += 1) {
86
+ const char = output[idx];
87
+ if (inString) {
88
+ if (escaped) escaped = false;
89
+ else if (char === "\\") escaped = true;
90
+ else if (char === '"') inString = false;
91
+ continue;
92
+ }
93
+ if (char === '"') inString = true;
94
+ else if (char === "{") depth += 1;
95
+ else if (char === "}" && --depth === 0) return idx;
96
+ }
97
+ return -1;
98
+ };
99
+
100
+ const asBiomeReport = (candidate: string): BiomeReport | null => {
101
+ try {
102
+ const parsed = JSON.parse(candidate) as unknown;
103
+ if (!parsed || typeof parsed !== "object") return null;
104
+ if (!("diagnostics" in parsed) && !("summary" in parsed)) return null;
105
+ return parsed as BiomeReport;
106
+ } catch {
107
+ return null;
108
+ }
109
+ };
110
+
111
+ /** Biome reads `biome.json` first and `biome.jsonc` second; only the latter may carry comments. */
112
+ const BIOME_CONFIG_FILES = ["biome.json", "biome.jsonc"] as const;
113
+
61
114
  export class Linter {
62
115
  lintRoot: string;
116
+ configPath: string;
63
117
  #biomeBin: string;
64
118
 
65
119
  constructor(cwdPath: string) {
66
120
  this.lintRoot = this.#findBiomeRootPath(cwdPath);
121
+ this.configPath = Linter.#configPathIn(this.lintRoot) ?? path.join(this.lintRoot, "biome.json");
67
122
  const localBiomeBin = path.join(this.lintRoot, "node_modules/.bin/biome");
68
123
  this.#biomeBin = existsSync(localBiomeBin) ? localBiomeBin : "biome";
69
124
  }
70
125
 
126
+ static #configPathIn(dir: string): string | null {
127
+ for (const fileName of BIOME_CONFIG_FILES) {
128
+ const configPath = path.join(dir, fileName);
129
+ if (existsSync(configPath)) return configPath;
130
+ }
131
+ return null;
132
+ }
133
+
71
134
  #findBiomeRootPath(dir: string): string {
72
- const configPath = path.join(dir, "biome.json");
73
- if (existsSync(configPath)) return dir;
135
+ if (Linter.#configPathIn(dir)) return dir;
74
136
  const parentDir = path.dirname(dir);
75
137
  if (parentDir === dir) throw new Error(`biome.json not found from ${dir}`);
76
138
  return this.#findBiomeRootPath(parentDir);
@@ -111,14 +173,6 @@ export class Linter {
111
173
  });
112
174
  }
113
175
 
114
- #parseBiomeReport(output: string): BiomeReport {
115
- const jsonStart = output.indexOf("{");
116
- const jsonEnd = output.lastIndexOf("}");
117
- if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart)
118
- throw new Error(output.trim() || "No Biome JSON output");
119
- return JSON.parse(output.slice(jsonStart, jsonEnd + 1)) as BiomeReport;
120
- }
121
-
122
176
  #diagnosticFilePath(diagnostic: BiomeDiagnostic, fallbackFilePath: string) {
123
177
  const diagnosticPath = diagnostic.location?.path;
124
178
  if (!diagnosticPath) return fallbackFilePath;
@@ -193,10 +247,10 @@ export class Linter {
193
247
  "--max-diagnostics=none",
194
248
  "--no-errors-on-unmatched",
195
249
  "--config-path",
196
- path.join(this.lintRoot, "biome.json"),
250
+ this.configPath,
197
251
  this.#toBiomePath(filePath),
198
252
  ]);
199
- const report = this.#parseBiomeReport(stdout || stderr);
253
+ const report = parseBiomeReport(stdout || stderr);
200
254
  const results = this.#toLintResults(report, filePath);
201
255
  const { errors, warnings } = this.#splitMessages(results);
202
256
  const output = write && existsSync(filePath) ? readFileSync(filePath, "utf8") : undefined;
@@ -390,14 +444,7 @@ export class Linter {
390
444
 
391
445
  const source = readFileSync(resolvedFilePath, "utf8");
392
446
  const { stdout } = await this.#runBiome(
393
- [
394
- "check",
395
- "--write",
396
- "--config-path",
397
- path.join(this.lintRoot, "biome.json"),
398
- "--stdin-file-path",
399
- this.#toBiomePath(resolvedFilePath),
400
- ],
447
+ ["check", "--write", "--config-path", this.configPath, "--stdin-file-path", this.#toBiomePath(resolvedFilePath)],
401
448
  source,
402
449
  );
403
450
  const lintResult = await this.lintFile(resolvedFilePath);
@@ -409,10 +456,37 @@ export class Linter {
409
456
  * @param filePath - Path to the file
410
457
  * @returns Biome configuration object
411
458
  */
459
+ /**
460
+ * Applies Biome's safe fixes to many files in one process.
461
+ *
462
+ * One spawn, not one per file: Biome's startup dominates a small file, so the per-file path turns a
463
+ * fifteen-file scaffold into three seconds of process launches. Paths that do not exist are dropped
464
+ * rather than thrown on, because the caller is usually reporting what a template just wrote and a
465
+ * template is allowed to decline a file.
466
+ */
467
+ async fixFiles(filePaths: string[]): Promise<{ fixed: string[] }> {
468
+ const resolved = filePaths.map((filePath) => this.#resolveFilePath(filePath)).filter(existsSync);
469
+ if (resolved.length === 0) return { fixed: [] };
470
+ const before = new Map(resolved.map((filePath) => [filePath, readFileSync(filePath, "utf8")]));
471
+ await this.#runBiome([
472
+ "check",
473
+ "--write",
474
+ "--reporter=json",
475
+ "--max-diagnostics=none",
476
+ "--no-errors-on-unmatched",
477
+ "--config-path",
478
+ this.configPath,
479
+ ...resolved.map((filePath) => this.#toBiomePath(filePath)),
480
+ ]);
481
+ return {
482
+ fixed: resolved.filter((filePath) => readFileSync(filePath, "utf8") !== before.get(filePath)),
483
+ };
484
+ }
485
+
412
486
  async getConfigForFile(filePath: string): Promise<unknown> {
413
487
  const resolvedFilePath = this.#resolveFilePath(filePath);
414
488
  if (!existsSync(resolvedFilePath)) throw new Error(`File not found: ${filePath}`);
415
- return JSON.parse(readFileSync(path.join(this.lintRoot, "biome.json"), "utf8")) as unknown;
489
+ return JSON.parse(readFileSync(this.configPath, "utf8")) as unknown;
416
490
  }
417
491
 
418
492
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.0-alpha.91",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -14,7 +14,7 @@
14
14
  "directory": "pkgs/@akanjs/devkit"
15
15
  },
16
16
  "engines": {
17
- "bun": ">=1.3.13"
17
+ "bun": ">=1.4.0"
18
18
  },
19
19
  "exports": {
20
20
  ".": {
@@ -33,7 +33,6 @@
33
33
  "./incrementalBuilder": "./incrementalBuilder/index.ts",
34
34
  "./mobile": "./mobile/index.ts",
35
35
  "./transforms": "./transforms/index.ts",
36
- "./ui": "./ui/index.ts",
37
36
  "./workflow": "./workflow/index.ts",
38
37
  "./*.ts": "./*.ts",
39
38
  "./*": "./*.ts"
@@ -45,15 +44,13 @@
45
44
  "@langchain/openai": "^1.4.6",
46
45
  "@tailwindcss/node": "^4.3.0",
47
46
  "@trapezedev/project": "^7.1.4",
48
- "akanjs": "3.0.0-alpha.9",
47
+ "akanjs": "3.0.0-alpha.91",
49
48
  "chalk": "^5.6.2",
50
49
  "commander": "^14.0.3",
51
50
  "dayjs": "^1.11.20",
52
51
  "fontaine": "^0.8.0",
53
52
  "fonteditor-core": "^2.6.3",
54
53
  "ignore": "^7.0.5",
55
- "ink": "^6.8.0",
56
- "js-yaml": "^4.1.1",
57
54
  "ora": "^9.4.0",
58
55
  "ssh2": "^1.17.0",
59
56
  "subset-font": "^2.5.0",
@@ -63,7 +60,7 @@
63
60
  },
64
61
  "peerDependencies": {
65
62
  "@capacitor/cli": "^8.3.4",
66
- "react": "19.2.6"
63
+ "react": "19.2.7"
67
64
  },
68
65
  "peerDependenciesMeta": {
69
66
  "@capacitor/cli": {
@@ -77,4 +74,4 @@
77
74
  "bun": {
78
75
  "platform": "bun"
79
76
  }
80
- }
77
+ }
package/prompter.ts CHANGED
@@ -24,7 +24,6 @@ export class Prompter {
24
24
  }
25
25
 
26
26
  static async selectGuideline() {
27
- const guidelineRoot = await Prompter.#getGuidelineRoot();
28
27
  const guideNames = await Prompter.listGuidelines();
29
28
  return await select({
30
29
  message: "Select a guideline",
@@ -41,11 +40,17 @@ export class Prompter {
41
40
  const guideJson = await fsPromise.readFile(filePath, "utf-8");
42
41
  return JSON.parse(guideJson) as GuideGenerateJson;
43
42
  }
43
+ /**
44
+ * The name is checked against the directory listing rather than interpolated straight into a path:
45
+ * it arrives from an MCP tool argument, and an unknown one has to name the valid set instead of
46
+ * surfacing an ENOENT that carries the host's own paths.
47
+ */
44
48
  static async getInstruction(guideName: string): Promise<string> {
49
+ const guideNames = await Prompter.listGuidelines();
50
+ if (!guideNames.includes(guideName))
51
+ throw new Error(`Unknown guideline "${guideName}". Available: ${guideNames.join(", ")}`);
45
52
  const guidelineRoot = await Prompter.#getGuidelineRoot();
46
- const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
47
- const content = await fsPromise.readFile(filePath, "utf-8");
48
- return content;
53
+ return await fsPromise.readFile(`${guidelineRoot}/${guideName}/${guideName}.instruction.md`, "utf-8");
49
54
  }
50
55
  static async getUpdateRequest(guideName: string) {
51
56
  return await input({
@@ -184,6 +184,15 @@ describe("AkanQualityScanner ssr rules", () => {
184
184
  });
185
185
  });
186
186
 
187
+ const signalOf = (entries: string) =>
188
+ [
189
+ `import { endpoint, slice } from "akanjs/signal";`,
190
+ `export class PostSlice extends slice(srv.post, { guards: {}, mcp: { get: true } }, (init) => ({`,
191
+ entries,
192
+ `})) {}`,
193
+ "",
194
+ ].join("\n");
195
+
187
196
  describe("AkanQualityScanner layout rules", () => {
188
197
  test("flags an unknown app root file but not a facet entrypoint", async () => {
189
198
  const root = await makeWorkspace({
@@ -196,4 +205,111 @@ describe("AkanQualityScanner layout rules", () => {
196
205
  expect(warnings).toHaveLength(1);
197
206
  expect(warnings[0]?.file).toBe("apps/demo/helper.ts");
198
207
  });
208
+
209
+ test("flags an unknown lib root file but not a facet entrypoint", async () => {
210
+ const root = await makeWorkspace({
211
+ "libs/demo/client.ts": "export const client = 1;\n",
212
+ "libs/demo/helper.ts": "export const helper = 1;\n",
213
+ });
214
+
215
+ const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-root-file");
216
+
217
+ expect(warnings).toHaveLength(1);
218
+ expect(warnings[0]?.file).toBe("libs/demo/helper.ts");
219
+ });
220
+
221
+ test("flags a root folder no facet owns, on both sides", async () => {
222
+ const root = await makeWorkspace({
223
+ "apps/demo/base/helper.ts": "export const helper = 1;\n",
224
+ "apps/demo/common/helper.ts": "export const helper = 1;\n",
225
+ "libs/demo/base/helper.ts": "export const helper = 1;\n",
226
+ "libs/demo/common/helper.ts": "export const helper = 1;\n",
227
+ });
228
+
229
+ const result = await new AkanQualityScanner().scan(root);
230
+
231
+ expect(rulesOf(result, "akan.layout.app-root-folder").map((warning) => warning.file)).toEqual([
232
+ "apps/demo/base/helper.ts",
233
+ ]);
234
+ expect(rulesOf(result, "akan.layout.lib-root-folder").map((warning) => warning.file)).toEqual([
235
+ "libs/demo/base/helper.ts",
236
+ ]);
237
+ });
238
+
239
+ test("keeps a root signal test out of the lib facet rule", async () => {
240
+ const root = await makeWorkspace({
241
+ "libs/demo/lib/cnst.ts": "export const cnst = 1;\n",
242
+ "libs/demo/lib/user.signal.test.ts": "export const test = 1;\n",
243
+ "libs/demo/lib/helper.ts": "export const helper = 1;\n",
244
+ });
245
+
246
+ const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.lib-facet-file");
247
+
248
+ expect(warnings).toHaveLength(1);
249
+ expect(warnings[0]?.file).toBe("libs/demo/lib/helper.ts");
250
+ });
251
+ });
252
+
253
+ describe("bang comments in browser-reachable files", () => {
254
+ const bangWarningsIn = async (files: Record<string, string>) => {
255
+ const result = await new AkanQualityScanner().scan(await makeWorkspace(files));
256
+ return rulesOf(result, "akan.file.bang-comment-in-client");
257
+ };
258
+
259
+ test("flags the first and last line, which the grit rule cannot reach", async () => {
260
+ const warnings = await bangWarningsIn({
261
+ "apps/demo/ui/First.tsx": "//! FIXME: broken\nexport const First = () => null;\n",
262
+ "apps/demo/ui/Last.tsx": "export const Last = () => null;\n//! left behind\n",
263
+ });
264
+
265
+ expect(warnings.map((warning) => `${warning.file}:${warning.line}`).sort()).toEqual([
266
+ "apps/demo/ui/First.tsx:1",
267
+ "apps/demo/ui/Last.tsx:2",
268
+ ]);
269
+ expect(warnings[0]?.fix).toContain("// FIXME:");
270
+ });
271
+
272
+ test("flags the block form and a marker trailing code", async () => {
273
+ const warnings = await bangWarningsIn({
274
+ "apps/demo/lib/task/Task.Zone.tsx": ["/*! keep me */", "export const Zone = () => null; //! and me", ""].join(
275
+ "\n",
276
+ ),
277
+ });
278
+
279
+ expect(warnings.map((warning) => warning.line)).toEqual([1, 2]);
280
+ });
281
+
282
+ test("ignores a double slash inside a string literal", async () => {
283
+ expect(
284
+ await bangWarningsIn({ "apps/demo/ui/Url.tsx": 'export const url = "https://host//!path";\n' }),
285
+ ).toHaveLength(0);
286
+ });
287
+
288
+ test("stays out of server files and tests", async () => {
289
+ const body = "//! server-only marker\nexport const value = 1;\n";
290
+ expect(
291
+ await bangWarningsIn({
292
+ "apps/demo/srvkit/storage.ts": body,
293
+ "apps/demo/lib/task/task.service.ts": body,
294
+ "apps/demo/ui/Card.test.tsx": body,
295
+ "apps/demo/lib/task/task.signal.spec.ts": body,
296
+ }),
297
+ ).toHaveLength(0);
298
+ });
299
+
300
+ test("covers every path role the grit rule is scoped to", async () => {
301
+ const body = "//! marker\nexport const value = 1;\n";
302
+ const files = [
303
+ "apps/demo/ui/Card.tsx",
304
+ "apps/demo/webkit/useThing.tsx",
305
+ "apps/demo/common/format.ts",
306
+ "apps/demo/page/task/_index.tsx",
307
+ "apps/demo/lib/task/task.constant.ts",
308
+ "apps/demo/lib/task/task.store.ts",
309
+ "apps/demo/lib/task/Task.Unit.tsx",
310
+ ];
311
+ const warnings = await bangWarningsIn(Object.fromEntries(files.map((file) => [file, body])));
312
+
313
+ expect(warnings.map((warning) => warning.file).sort()).toEqual([...files].sort());
314
+ });
199
315
  });