@akanjs/devkit 3.0.0-alpha.8 → 3.0.0-alpha.81
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/CHANGELOG.md +15 -0
- 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/akanApp/BackendImportGraph.test.ts +120 -0
- package/akanApp/BackendImportGraph.ts +167 -0
- package/akanApp/akanApp.host.test.ts +16 -678
- package/akanApp/akanApp.host.ts +65 -562
- package/akanApp/devHostPolicy.test.ts +542 -0
- package/akanApp/devHostPolicy.ts +412 -0
- package/akanApp/index.ts +2 -0
- package/akanConfig/akanConfig.test.ts +182 -14
- package/akanConfig/akanConfig.ts +132 -47
- package/akanConfig/types.ts +8 -0
- package/akanContext.ts +70 -14
- package/akanMcpContract.ts +25 -4
- package/applicationBuildRunner.test.ts +1 -1
- package/applicationBuildRunner.ts +45 -21
- package/artifact/implicitRootLayout.test.ts +67 -0
- package/artifact/implicitRootLayout.ts +27 -7
- package/artifact/routeSeedIndex.test.ts +1 -0
- package/biome.base.json +340 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.ts +10 -0
- package/cloud/globalConfig.ts +9 -2
- package/commandDecorators/command.ts +30 -6
- package/commandDecorators/commandBuilder.ts +20 -5
- package/commandDecorators/commandDecorators.test.ts +115 -3
- package/dependencyScanner.test.ts +99 -0
- package/dependencyScanner.ts +27 -24
- package/devkitUtils.test.ts +0 -104
- package/executors.test.ts +132 -5
- package/executors.ts +89 -14
- package/fileEditor.ts +19 -19
- package/formSetterScanner.test.ts +80 -0
- package/formSetterScanner.ts +92 -0
- package/frontendBuild/autoImportSync.test.ts +58 -0
- package/frontendBuild/autoImportSync.ts +4 -0
- package/frontendBuild/buildRouteClient.test.ts +47 -24
- package/frontendBuild/clientBuildTypes.ts +4 -0
- package/frontendBuild/clientEntriesBundler.ts +4 -1
- package/frontendBuild/clientEntryDiscovery.ts +2 -2
- package/frontendBuild/csrArtifactBuilder.ts +116 -84
- package/frontendBuild/cssCompiler.ts +123 -12
- package/frontendBuild/cssImportResolver.ts +8 -7
- package/frontendBuild/fontOptimizer.ts +37 -18
- package/frontendBuild/fontPruner.test.ts +220 -0
- package/frontendBuild/fontPruner.ts +206 -0
- package/frontendBuild/frontendBuild.test.ts +178 -10
- package/frontendBuild/hmrWatcher.ts +1 -1
- package/frontendBuild/index.ts +1 -1
- package/frontendBuild/pagesBundleBuilder.ts +3 -3
- package/frontendBuild/pagesEntrySourceGenerator.ts +11 -88
- package/frontendBuild/routeClientBuilder.ts +12 -5
- package/frontendBuild/ssrBaseArtifactBuilder.ts +21 -4
- package/frontendBuild/styleContract.ts +14 -20
- package/frontendBuild/themeValidator.ts +22 -17
- package/frontendBuild/vendorSpecifiers.ts +1 -0
- package/incrementalBuilder/devWatchBatch.test.ts +18 -20
- package/incrementalBuilder/devWatchBatch.ts +6 -2
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.proc.ts +8 -4
- package/index.ts +0 -5
- package/integration/devStabilityHarness.ts +2 -10
- package/libSource.test.ts +109 -0
- package/libSource.ts +126 -0
- package/lint/__fixtures__/README.md +40 -0
- package/lint/__fixtures__/no-arbitrary-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-arbitrary-color/good.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/bad.tsx +4 -0
- package/lint/__fixtures__/no-async-component-in-ui/good.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-bang-comment-in-client/fixture.json +1 -0
- package/lint/__fixtures__/no-bang-comment-in-client/good.tsx +3 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +7 -0
- package/lint/__fixtures__/no-daisyui-legacy-class/good.tsx +5 -0
- package/lint/__fixtures__/no-deep-internal-import/bad.tsx +3 -0
- package/lint/__fixtures__/no-deep-internal-import/fixture.json +1 -0
- package/lint/__fixtures__/no-deep-internal-import/good.tsx +3 -0
- package/lint/__fixtures__/no-deprecated-log-level/bad.tsx +4 -0
- package/lint/__fixtures__/no-deprecated-log-level/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-functions/bad.tsx +2 -0
- package/lint/__fixtures__/no-import-client-functions/good.tsx +4 -0
- package/lint/__fixtures__/no-import-client-in-server/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-client-in-server/good.tsx +5 -0
- package/lint/__fixtures__/no-import-external-library/bad.tsx +4 -0
- package/lint/__fixtures__/no-import-external-library/good.tsx +5 -0
- package/lint/__fixtures__/no-import-server-in-client/bad.tsx +6 -0
- package/lint/__fixtures__/no-import-server-in-client/good.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/bad.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/fixture.json +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/store-file/good.ts +1 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/bad.tsx +4 -0
- package/lint/__fixtures__/no-init-fetch-in-client/use-client/good.tsx +5 -0
- package/lint/__fixtures__/no-inline-color/bad.tsx +3 -0
- package/lint/__fixtures__/no-inline-color/good.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/bad.tsx +3 -0
- package/lint/__fixtures__/no-interpolated-arbitrary-class/good.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/bad.tsx +3 -0
- package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/bad.tsx +3 -0
- package/lint/__fixtures__/no-model-type-in-util-zone/good.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/bad.tsx +6 -0
- package/lint/__fixtures__/no-raw-palette-class/good.tsx +8 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/bad.ts +10 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/fixture.json +1 -0
- package/lint/__fixtures__/no-redeclare-predefined-endpoint/good.ts +6 -0
- package/lint/__fixtures__/no-return-in-store-action/bad.tsx +2 -0
- package/lint/__fixtures__/no-return-in-store-action/good.tsx +6 -0
- package/lint/__fixtures__/no-throw-raw-error/bad.tsx +4 -0
- package/lint/__fixtures__/no-throw-raw-error/good.tsx +4 -0
- package/lint/__fixtures__/no-unpublished-form-setter/bad.tsx +3 -0
- package/lint/__fixtures__/no-unpublished-form-setter/good.tsx +5 -0
- package/lint/__fixtures__/no-use-client-in-server/bad.tsx +3 -0
- package/lint/__fixtures__/no-use-client-in-server/good.tsx +2 -0
- package/lint/__fixtures__/non-scalar-props-restricted/bad.tsx +5 -0
- package/lint/__fixtures__/non-scalar-props-restricted/good.tsx +4 -0
- package/lint/gritRules.test.ts +178 -0
- package/lint/no-arbitrary-color.grit +1 -1
- package/lint/no-async-component-in-ui.grit +35 -0
- package/lint/no-bang-comment-in-client.grit +23 -10
- package/lint/no-daisyui-legacy-class.grit +26 -9
- package/lint/no-deprecated-log-level.grit +17 -0
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-init-fetch-in-client.grit +47 -0
- package/lint/no-inline-color.grit +10 -8
- package/lint/no-interpolated-arbitrary-class.grit +3 -3
- package/lint/no-model-type-in-util-zone.grit +58 -0
- package/lint/no-raw-palette-class.grit +3 -3
- package/lint/no-unpublished-form-setter.grit +41 -0
- package/lint/non-scalar-props-restricted.grit +16 -7
- package/linter.test.ts +80 -0
- package/linter.ts +99 -25
- package/package.json +6 -6
- package/prompter.ts +9 -4
- package/qualityScanner.test.ts +116 -0
- package/qualityScanner.ts +89 -21
- package/recipeScanner.ts +4 -1
- package/repoIdentity.ts +42 -0
- package/scanInfo.ts +32 -26
- package/semver.test.ts +26 -0
- package/semver.ts +31 -0
- package/slicePlanner.test.ts +182 -0
- package/slicePlanner.ts +235 -0
- package/ssrScanner.test.ts +301 -0
- package/subspace.test.ts +418 -0
- package/subspace.ts +723 -0
- package/subspaceConfig.ts +76 -0
- package/transforms/asyncDefaultExportDetector.ts +103 -0
- package/transforms/barrelImportsPlugin.ts +1 -1
- package/transforms/externalizeFrameworkPlugin.ts +0 -1
- package/transforms/transforms.test.ts +5 -5
- package/transforms/tsconfigPackageResolver.test.ts +230 -0
- package/tsconfig.json +6 -2
- package/typeChecker.ts +1 -1
- package/types.ts +1 -0
- package/ui/ScrollList.tsx +6 -8
- package/uploadRelease.ts +2 -2
- package/workflow/executor.test.ts +146 -0
- package/workspaceLayout.test.ts +56 -4
- package/workspaceLayout.ts +49 -4
- package/builder.ts +0 -164
- package/extractDeps.ts +0 -86
- package/frontendBuild/styleGuard.test.ts +0 -165
- package/frontendBuild/styleGuard.ts +0 -322
- package/getCredentials.ts +0 -19
- package/getModelFileData.ts +0 -62
- package/src/capacitorApp.ts +0 -282
- package/streamAi.ts +0 -45
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
|
-
|
|
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
|
-
|
|
250
|
+
this.configPath,
|
|
197
251
|
this.#toBiomePath(filePath),
|
|
198
252
|
]);
|
|
199
|
-
const report =
|
|
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(
|
|
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.
|
|
3
|
+
"version": "3.0.0-alpha.81",
|
|
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.
|
|
17
|
+
"bun": ">=1.4.0"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"default": "./index.ts"
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json",
|
|
26
|
+
"./biome.base.json": "./biome.base.json",
|
|
26
27
|
"./akanApp": "./akanApp/index.ts",
|
|
27
28
|
"./akanConfig": "./akanConfig/index.ts",
|
|
28
29
|
"./artifact": "./artifact/index.ts",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@langchain/openai": "^1.4.6",
|
|
45
46
|
"@tailwindcss/node": "^4.3.0",
|
|
46
47
|
"@trapezedev/project": "^7.1.4",
|
|
47
|
-
"akanjs": "3.0.0-alpha.
|
|
48
|
+
"akanjs": "3.0.0-alpha.81",
|
|
48
49
|
"chalk": "^5.6.2",
|
|
49
50
|
"commander": "^14.0.3",
|
|
50
51
|
"dayjs": "^1.11.20",
|
|
@@ -52,7 +53,6 @@
|
|
|
52
53
|
"fonteditor-core": "^2.6.3",
|
|
53
54
|
"ignore": "^7.0.5",
|
|
54
55
|
"ink": "^6.8.0",
|
|
55
|
-
"js-yaml": "^4.1.1",
|
|
56
56
|
"ora": "^9.4.0",
|
|
57
57
|
"ssh2": "^1.17.0",
|
|
58
58
|
"subset-font": "^2.5.0",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@capacitor/cli": "^8.3.4",
|
|
65
|
-
"react": "19.2.
|
|
65
|
+
"react": "19.2.7"
|
|
66
66
|
},
|
|
67
67
|
"peerDependenciesMeta": {
|
|
68
68
|
"@capacitor/cli": {
|
|
@@ -76,4 +76,4 @@
|
|
|
76
76
|
"bun": {
|
|
77
77
|
"platform": "bun"
|
|
78
78
|
}
|
|
79
|
-
}
|
|
79
|
+
}
|
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
|
-
|
|
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({
|
package/qualityScanner.test.ts
CHANGED
|
@@ -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
|
});
|
package/qualityScanner.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
|
5
5
|
import ignore from "ignore";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
8
|
+
import { FormSetterScanner } from "./formSetterScanner";
|
|
8
9
|
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
9
|
-
import {
|
|
10
|
+
import { isAllowedLibFacetRootFile, rootAllowedDirs, rootAllowedFiles } from "./workspaceLayout";
|
|
10
11
|
|
|
11
12
|
type QualitySeverity = "warning";
|
|
12
|
-
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr";
|
|
13
|
+
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr" | "agent";
|
|
13
14
|
|
|
14
15
|
export interface QualityWarning {
|
|
15
16
|
rule: string;
|
|
@@ -88,7 +89,8 @@ const SUGGESTED_RULES = [
|
|
|
88
89
|
"Keep apps/*/lib and libs/*/lib root files limited to generated support facets such as cnst.ts, db.ts, dict.ts, sig.ts, srv.ts, st.ts, useClient.ts, and useServer.ts.",
|
|
89
90
|
"Use domain module folders consistently: lib/<model> for database modules, lib/_<service> for service modules, and lib/__scalar/<scalar> for scalar modules.",
|
|
90
91
|
"Keep module UI filenames predictable: database modules use <Model>.Template/Unit/Util/View/Zone.tsx, service modules use <Service>.Util/Zone.tsx, and scalar modules use <Scalar>.Template/Unit.tsx.",
|
|
91
|
-
"
|
|
92
|
+
"Hand a form control its store setter by reference so the field publishes an agent tool; put normalization in the control's transform prop and a multi-write in a _postSet<Field> hook on the store.",
|
|
93
|
+
"Move shared app and lib utilities to common/ instead of creating apps/*/base or libs/*/base.",
|
|
92
94
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline.",
|
|
93
95
|
];
|
|
94
96
|
|
|
@@ -115,6 +117,8 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
115
117
|
"Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
|
|
116
118
|
"akan.file.placeholder-export":
|
|
117
119
|
"Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
120
|
+
"akan.file.bang-comment-in-client":
|
|
121
|
+
"Rewrite the marker as `// FIXME:` or `// TODO:`. Keep the bang form for server, srvkit, and CLI files.",
|
|
118
122
|
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
119
123
|
"akan.file.global-declaration":
|
|
120
124
|
"Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
|
|
@@ -122,14 +126,22 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
122
126
|
"Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
123
127
|
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
124
128
|
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
129
|
+
"akan.agent.unpublished-form-setter":
|
|
130
|
+
"Pass the setter by reference where the wrapper only forwards (onChange={st.do.setTitleOnTask}); move normalization into the control's own transform prop; move a multi-write into a _postSet<Field> hook on the store, keeping the generated setter on the control.",
|
|
125
131
|
"akan.file.component-internal-declaration":
|
|
126
132
|
"Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
127
133
|
"akan.file.component-export":
|
|
128
134
|
"Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix — only PascalCase components and their `<Component>Props` interface belong here.",
|
|
129
135
|
"akan.layout.app-root-file":
|
|
130
136
|
"Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
137
|
+
"akan.layout.app-root-folder":
|
|
138
|
+
"Move the folder's contents into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit) and delete it.",
|
|
131
139
|
"akan.layout.lib-root-file":
|
|
132
|
-
"Move the file into a
|
|
140
|
+
"Move the file into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit).",
|
|
141
|
+
"akan.layout.lib-root-folder":
|
|
142
|
+
"Move the folder's contents into a conventional lib folder (common, env, lib, page, private, public, srvkit, ui, or webkit) and delete it.",
|
|
143
|
+
"akan.layout.lib-facet-file":
|
|
144
|
+
"Move the file into a domain module folder under lib/; keep the lib facet root limited to generated support facets.",
|
|
133
145
|
"akan.layout.module-ui-file":
|
|
134
146
|
"Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
|
|
135
147
|
"akan.ssr.unnecessary-use-client":
|
|
@@ -173,6 +185,7 @@ export class AkanQualityScanner {
|
|
|
173
185
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
174
186
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
175
187
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
188
|
+
...new FormSetterScanner().scan(sourceFiles),
|
|
176
189
|
...ssr.warnings,
|
|
177
190
|
];
|
|
178
191
|
|
|
@@ -303,6 +316,7 @@ export class AkanQualityScanner {
|
|
|
303
316
|
});
|
|
304
317
|
}
|
|
305
318
|
|
|
319
|
+
warnings.push(...getBangCommentWarnings(sourceFile));
|
|
306
320
|
warnings.push(...getPlaceholderExportWarnings(sourceFile));
|
|
307
321
|
warnings.push(...getDictionaryTextWarnings(sourceFile));
|
|
308
322
|
warnings.push(...getGlobalMutationWarnings(sourceFile));
|
|
@@ -408,26 +422,31 @@ export class AkanQualityScanner {
|
|
|
408
422
|
}
|
|
409
423
|
|
|
410
424
|
#scanLayoutQuality(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
411
|
-
const segments = sourceFile.file.split("/");
|
|
412
425
|
const warnings: QualityWarning[] = [];
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
const rootEntry = getSysRootEntry(sourceFile.file);
|
|
427
|
+
if (rootEntry) {
|
|
428
|
+
const { type, name, isDir } = rootEntry;
|
|
429
|
+
const allowed = isDir ? rootAllowedDirs[type].has(name) : rootAllowedFiles[type].has(name);
|
|
430
|
+
const kind = isDir ? "folder" : "file";
|
|
431
|
+
if (!allowed) {
|
|
432
|
+
warnings.push({
|
|
433
|
+
rule: `akan.layout.${type}-root-${kind}`,
|
|
434
|
+
scope: "layout",
|
|
435
|
+
severity: "warning",
|
|
436
|
+
file: sourceFile.file,
|
|
437
|
+
message: `Unexpected ${type} root ${kind} "${name}". Keep ${type} code in conventional ${type} folders.`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
421
440
|
}
|
|
422
441
|
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
442
|
+
const libFacetFile = getLibFacetRootFile(sourceFile.file);
|
|
443
|
+
if (libFacetFile && !isAllowedLibFacetRootFile(libFacetFile)) {
|
|
425
444
|
warnings.push({
|
|
426
|
-
rule: "akan.layout.lib-
|
|
445
|
+
rule: "akan.layout.lib-facet-file",
|
|
427
446
|
scope: "layout",
|
|
428
447
|
severity: "warning",
|
|
429
448
|
file: sourceFile.file,
|
|
430
|
-
message: `Unexpected lib root file "${
|
|
449
|
+
message: `Unexpected lib facet root file "${libFacetFile}". Keep direct lib/ root files limited to generated support facets.`,
|
|
431
450
|
});
|
|
432
451
|
}
|
|
433
452
|
|
|
@@ -580,9 +599,10 @@ function isEnumClassStatement(sourceFile: ts.SourceFile, statement: ts.Statement
|
|
|
580
599
|
|
|
581
600
|
function getExportedClassNames(sourceFile: ts.SourceFile) {
|
|
582
601
|
return sourceFile.statements
|
|
583
|
-
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement)
|
|
602
|
+
.filter((statement): statement is ts.ClassDeclaration => ts.isClassDeclaration(statement))
|
|
584
603
|
.filter((statement) => isExported(statement))
|
|
585
|
-
.map((statement) => statement.name
|
|
604
|
+
.map((statement) => statement.name?.text)
|
|
605
|
+
.filter((name): name is string => !!name);
|
|
586
606
|
}
|
|
587
607
|
|
|
588
608
|
function isComponentDeclarationFile(file: string) {
|
|
@@ -662,7 +682,43 @@ function isPascalCaseName(name: string) {
|
|
|
662
682
|
|
|
663
683
|
function isDefaultExportStatement(statement: ts.Statement) {
|
|
664
684
|
if (ts.isExportAssignment(statement)) return !statement.isExportEquals;
|
|
665
|
-
|
|
685
|
+
if (!ts.canHaveModifiers(statement)) return false;
|
|
686
|
+
return !!ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Bun's bundler keeps `//!` and `/*!` through minification (it reads them as the `@license` class), so a
|
|
691
|
+
* bang marker in browser-reachable code ships verbatim to every visitor. `no-bang-comment-in-client.grit`
|
|
692
|
+
* is the build gate; this exists because that rule cannot see the whole file.
|
|
693
|
+
*
|
|
694
|
+
* A comment is trivia, which Biome's GritQL exposes to no node pattern, so the rule matches through
|
|
695
|
+
* `file($name, $body)` — and `$body` is the module's *token* span. A marker above the first statement or
|
|
696
|
+
* below the last one sits in leading or trailing trivia and is unreachable from GritQL at all. Reading the
|
|
697
|
+
* raw text here covers both, and re-reporting the markers grit already catches costs nothing: the two run
|
|
698
|
+
* from different commands, and a file with one is failing lint either way.
|
|
699
|
+
*/
|
|
700
|
+
function getBangCommentWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
701
|
+
if (!isClientReachableFile(sourceFile.file)) return [];
|
|
702
|
+
// Anchored to line start or to whitespace after code, so a literal like `"https://host//!path"` is not a hit.
|
|
703
|
+
return findPatternLines(sourceFile.content, /(?:^|[^\s/][ \t]+)\/[*/]!/).map((line) => ({
|
|
704
|
+
rule: "akan.file.bang-comment-in-client",
|
|
705
|
+
scope: "file" as const,
|
|
706
|
+
severity: "warning" as const,
|
|
707
|
+
file: sourceFile.file,
|
|
708
|
+
line,
|
|
709
|
+
message: "A `//!` or `/*!` marker survives minification and ships to the browser.",
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Mirrors the path scope of `no-bang-comment-in-client.grit` in `biome.base.json`. */
|
|
714
|
+
function isClientReachableFile(file: string) {
|
|
715
|
+
if (/\.(test|spec)\.tsx?$/.test(file)) return false;
|
|
716
|
+
if (/\.(constant|store)\.ts$/.test(file)) return true;
|
|
717
|
+
if (/\.(Template|Unit|Util|View|Zone)\.tsx$/.test(file)) return true;
|
|
718
|
+
const segments = file.split("/");
|
|
719
|
+
if (segments.includes("ui") || segments.includes("webkit")) return /\.tsx?$/.test(file);
|
|
720
|
+
if (segments.includes("common")) return file.endsWith(".ts");
|
|
721
|
+
return segments.includes("page") && file.endsWith(".tsx");
|
|
666
722
|
}
|
|
667
723
|
|
|
668
724
|
function getPlaceholderExportWarnings(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
@@ -821,7 +877,19 @@ function getConventionDescription(suffix: (typeof CONVENTION_SUFFIXES)[number],
|
|
|
821
877
|
return `${modelName}Store`;
|
|
822
878
|
}
|
|
823
879
|
|
|
824
|
-
|
|
880
|
+
const sysRootTypes = { apps: "app", libs: "lib" } as const;
|
|
881
|
+
|
|
882
|
+
function getSysRootEntry(file: string) {
|
|
883
|
+
const segments = file.split("/");
|
|
884
|
+
const root = segments[0];
|
|
885
|
+
const name = segments[2];
|
|
886
|
+
if (!root || !name || segments.length < 3) return null;
|
|
887
|
+
const type = sysRootTypes[root as keyof typeof sysRootTypes];
|
|
888
|
+
if (!type) return null;
|
|
889
|
+
return { type, name, isDir: segments.length > 3 };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getLibFacetRootFile(file: string) {
|
|
825
893
|
const segments = file.split("/");
|
|
826
894
|
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
827
895
|
return segments[3];
|
package/recipeScanner.ts
CHANGED
|
@@ -118,7 +118,10 @@ const extractVariants = (config: ts.ObjectLiteralExpression) => {
|
|
|
118
118
|
if (!ts.isPropertyAssignment(variant) || !isNamed(variant.name)) continue;
|
|
119
119
|
if (!ts.isObjectLiteralExpression(variant.initializer)) continue;
|
|
120
120
|
variants[propName(variant.name)] = variant.initializer.properties
|
|
121
|
-
.filter(
|
|
121
|
+
.filter(
|
|
122
|
+
(option): option is ts.PropertyAssignment & { name: ts.Identifier | ts.StringLiteral } =>
|
|
123
|
+
ts.isPropertyAssignment(option) && isNamed(option.name),
|
|
124
|
+
)
|
|
122
125
|
.map((option) => propName(option.name));
|
|
123
126
|
}
|
|
124
127
|
} else if (key === "defaultVariants" && ts.isObjectLiteralExpression(property.initializer)) {
|
package/repoIdentity.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Resolved once per root: every CLI command builds a WorkspaceExecutor, and this would otherwise fork git on each.
|
|
5
|
+
const resolved = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
const readRemoteName = (workspaceRoot: string): string | null => {
|
|
8
|
+
try {
|
|
9
|
+
const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
10
|
+
cwd: workspaceRoot,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
}).trim();
|
|
14
|
+
// Both remote spellings end in the repository: git@host:owner/name.git and https://host/owner/name.git.
|
|
15
|
+
return (
|
|
16
|
+
url
|
|
17
|
+
.replace(/\.git$/, "")
|
|
18
|
+
.split(/[/:]/)
|
|
19
|
+
.pop() || null
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
// No git, no origin, or no git binary — a fresh `akan workspace` before its first commit lands here.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The repository's own name.
|
|
29
|
+
*
|
|
30
|
+
* Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
|
|
31
|
+
* `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
|
|
32
|
+
* a different guide per developer and the diff never settled. The origin remote is the one identity every clone
|
|
33
|
+
* shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
|
|
34
|
+
* cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
|
|
35
|
+
*/
|
|
36
|
+
export const resolveRepoName = (workspaceRoot: string): string => {
|
|
37
|
+
const cached = resolved.get(workspaceRoot);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
const repoName = readRemoteName(workspaceRoot) ?? path.basename(workspaceRoot);
|
|
40
|
+
resolved.set(workspaceRoot, repoName);
|
|
41
|
+
return repoName;
|
|
42
|
+
};
|