karajan-code 3.12.3 → 3.13.0
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/package.json +1 -1
- package/src/harden/advisory.js +23 -4
- package/src/harden/alternatives.js +50 -0
- package/src/harden/check.js +11 -0
- package/src/harden/config-engine.js +19 -3
package/package.json
CHANGED
package/src/harden/advisory.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
|
|
14
14
|
import { findManagedBlock } from "../utils/managed-markers.js";
|
|
15
|
+
import { findAlternative } from "./alternatives.js";
|
|
15
16
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
16
17
|
import { detectStackRoots } from "./stack-roots.js";
|
|
17
18
|
|
|
@@ -98,6 +99,13 @@ function readArtifactContent(searchDir, foundAt) {
|
|
|
98
99
|
|
|
99
100
|
/** Map a classified state to a recommendation + human rationale. */
|
|
100
101
|
function recommend(state) {
|
|
102
|
+
if (state.status === "SATISFIED_BY_ALTERNATIVE") {
|
|
103
|
+
return {
|
|
104
|
+
recommendation: "keep",
|
|
105
|
+
rationale: `covered by ${state.foundAt} (${state.alternativeTool}) — kj won't add a second linter/formatter`,
|
|
106
|
+
mergeable: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
101
109
|
if (state.status === "MISSING") return { recommendation: "install", rationale: `kj would add ${state.file}`, mergeable: true };
|
|
102
110
|
if (state.status === "USER_OWNED") {
|
|
103
111
|
const n = state.improvements?.length ?? 0;
|
|
@@ -109,11 +117,20 @@ function recommend(state) {
|
|
|
109
117
|
return { recommendation: "update", rationale: `kj v${state.currentVersion} available (you have v${state.managedVersion})`, mergeable: true };
|
|
110
118
|
}
|
|
111
119
|
|
|
112
|
-
/**
|
|
113
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Classify a single artifact found (or not) under `searchDir`. `altDirs`
|
|
122
|
+
* widens the cross-tool alternative lookup (a root-level biome.json covers a
|
|
123
|
+
* monorepo's language roots); the user's OWN config of the same tool still
|
|
124
|
+
* wins over an alternative.
|
|
125
|
+
*/
|
|
126
|
+
export function classifyArtifact(searchDir, artifact, altDirs = [searchDir]) {
|
|
114
127
|
const foundAt = findArtifactFile(searchDir, artifact);
|
|
115
128
|
const base = { foundAt, managedVersion: null, upToDate: null, improvements: [] };
|
|
116
129
|
let state = { ...base, status: "MISSING" };
|
|
130
|
+
if (!foundAt) {
|
|
131
|
+
const alt = findAlternative(altDirs, artifact.id);
|
|
132
|
+
if (alt) state = { ...base, status: "SATISFIED_BY_ALTERNATIVE", foundAt: alt.foundAt, alternativeTool: alt.tool };
|
|
133
|
+
}
|
|
117
134
|
if (foundAt) {
|
|
118
135
|
const content = readArtifactContent(searchDir, foundAt);
|
|
119
136
|
if (artifact.blockId && content.includes(`kj:managed:${artifact.blockId}`)) {
|
|
@@ -139,8 +156,10 @@ export function compareHarden({ projectDir = process.cwd(), profile = "standard"
|
|
|
139
156
|
for (const { dir, language } of detectStackRoots(projectDir, { only, exclude })) {
|
|
140
157
|
const searchDir = dir === "." ? projectDir : join(projectDir, dir);
|
|
141
158
|
for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
|
|
142
|
-
const res = classifyArtifact(searchDir, toArtifact(cfg));
|
|
143
|
-
|
|
159
|
+
const res = classifyArtifact(searchDir, toArtifact(cfg), [searchDir, projectDir]);
|
|
160
|
+
// Alternative matches may live at the repo root — keep their path unprefixed.
|
|
161
|
+
const prefix = res.foundAt && dir !== "." && res.status !== "SATISFIED_BY_ALTERNATIVE";
|
|
162
|
+
artifacts.push({ dir, ...res, foundAt: prefix ? join(dir, res.foundAt) : res.foundAt });
|
|
144
163
|
}
|
|
145
164
|
}
|
|
146
165
|
return { artifacts };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-tool alternatives for `kj harden` (KJC-TSK-0614).
|
|
3
|
+
*
|
|
4
|
+
* A user's Biome config replaces BOTH eslint and prettier — seeding kj's
|
|
5
|
+
* configs next to biome.json installs a second, conflicting linter/formatter
|
|
6
|
+
* that fights the user's own hooks. The advisory's EQUIVALENTS map only
|
|
7
|
+
* covers same-tool filename variants; this map covers whole-tool
|
|
8
|
+
* substitutions, and the three surfaces (advisory, config-engine, check)
|
|
9
|
+
* consult it before treating an artifact as missing.
|
|
10
|
+
*
|
|
11
|
+
* Detection is config-file based on purpose: deterministic, and a false
|
|
12
|
+
* negative just means kj proposes its default (the pre-0614 behaviour),
|
|
13
|
+
* never that it overwrites the user's tool.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
const ALTERNATIVES = {
|
|
20
|
+
eslint: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
|
|
21
|
+
prettier: [{ tool: "biome", files: ["biome.json", "biome.jsonc"] }],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* First alternative tool that satisfies `artifactId`, searching each dir in
|
|
26
|
+
* order (a language root first, then the repo root — a root-level biome.json
|
|
27
|
+
* covers the whole monorepo). Null when none applies.
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} searchDirs
|
|
30
|
+
* @param {string} artifactId - "eslint" | "prettier" | ...
|
|
31
|
+
* @returns {{ tool: string, foundAt: string }|null}
|
|
32
|
+
*/
|
|
33
|
+
export function findAlternative(searchDirs, artifactId) {
|
|
34
|
+
for (const alt of ALTERNATIVES[artifactId] ?? []) {
|
|
35
|
+
for (const dir of searchDirs) {
|
|
36
|
+
for (const rel of alt.files) {
|
|
37
|
+
if (existsSync(join(dir, rel))) return { tool: alt.tool, foundAt: rel };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Artifact id for a config-template entry — same convention as the
|
|
46
|
+
* advisory's toArtifact: prettier is the only marker-less JSON config.
|
|
47
|
+
*/
|
|
48
|
+
export function artifactIdForConfig(cfg) {
|
|
49
|
+
return cfg.blockId ?? "prettier";
|
|
50
|
+
}
|
package/src/harden/check.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
|
|
13
13
|
import { runCommand } from "../utils/process.js";
|
|
14
|
+
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
14
15
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
15
16
|
import { PROFILE_HOOKS } from "./hook-templates.js";
|
|
16
17
|
import { detectStackRoots } from "./stack-roots.js";
|
|
@@ -53,8 +54,18 @@ export async function checkHarden({ projectDir = process.cwd(), profile = "stand
|
|
|
53
54
|
checks.push(presence(projectDir, cfg.file, `config:${cfg.file}`, "missing — run kj harden"));
|
|
54
55
|
}
|
|
55
56
|
for (const { dir, language } of roots) {
|
|
57
|
+
const rootDir = dir === "." ? projectDir : join(projectDir, dir);
|
|
56
58
|
for (const cfg of CONFIGS_BY_LANGUAGE[language] ?? []) {
|
|
57
59
|
const rel = dir === "." ? cfg.file : join(dir, cfg.file);
|
|
60
|
+
// A config covered by the user's own tool (biome.json ⇒ eslint+prettier)
|
|
61
|
+
// is not drift — kj must not demand a second linter/formatter (KJC-TSK-0614).
|
|
62
|
+
if (!existsSync(join(projectDir, rel))) {
|
|
63
|
+
const alt = findAlternative([rootDir, projectDir], artifactIdForConfig(cfg));
|
|
64
|
+
if (alt) {
|
|
65
|
+
checks.push({ id: `config:${rel}`, ok: true, detail: `covered by ${alt.foundAt} (${alt.tool})` });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
checks.push(presence(projectDir, rel, `config:${rel}`, `missing for ${language} — run kj harden`));
|
|
59
70
|
}
|
|
60
71
|
}
|
|
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
import { upsertManagedBlock } from "../utils/managed-markers.js";
|
|
12
|
+
import { artifactIdForConfig, findAlternative } from "./alternatives.js";
|
|
12
13
|
import { CONFIGS_BY_LANGUAGE, UNIVERSAL_CONFIGS } from "./config-templates.js";
|
|
13
14
|
|
|
14
15
|
const BLOCK_VERSION = 1;
|
|
@@ -18,13 +19,26 @@ function writeFile(target, content) {
|
|
|
18
19
|
writeFileSync(target, content);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Seed a list of config entries into `targetDir`, labelling each by `prefix`.
|
|
24
|
+
* `altDirs` widens the cross-tool alternative lookup (KJC-TSK-0614): a config
|
|
25
|
+
* covered by the user's own tool (biome.json ⇒ eslint+prettier) is never
|
|
26
|
+
* seeded — kj must not install a second, conflicting linter/formatter.
|
|
27
|
+
*/
|
|
28
|
+
function seedInto(targetDir, configs, dryRun, prefix, results, altDirs = [targetDir]) {
|
|
23
29
|
for (const cfg of configs) {
|
|
24
30
|
const target = join(targetDir, cfg.file);
|
|
25
31
|
const exists = existsSync(target);
|
|
26
32
|
const file = prefix === "." ? cfg.file : join(prefix, cfg.file);
|
|
27
33
|
|
|
34
|
+
if (!exists) {
|
|
35
|
+
const alt = findAlternative(altDirs, artifactIdForConfig(cfg));
|
|
36
|
+
if (alt) {
|
|
37
|
+
results.push({ file, action: "covered", by: alt.foundAt });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
if (cfg.json) {
|
|
29
43
|
const action = exists ? "skipped" : "inserted";
|
|
30
44
|
if (!dryRun && !exists) writeFile(target, `${cfg.body}\n`);
|
|
@@ -69,7 +83,9 @@ export function installConfigsForRoots({ projectDir = process.cwd(), roots = [],
|
|
|
69
83
|
const results = [];
|
|
70
84
|
seedInto(projectDir, UNIVERSAL_CONFIGS, dryRun, ".", results);
|
|
71
85
|
for (const { dir, language } of roots) {
|
|
72
|
-
|
|
86
|
+
const rootDir = join(projectDir, dir);
|
|
87
|
+
// A root-level biome.json covers every language root of the monorepo.
|
|
88
|
+
seedInto(rootDir, CONFIGS_BY_LANGUAGE[language] ?? [], dryRun, dir, results, [rootDir, projectDir]);
|
|
73
89
|
}
|
|
74
90
|
return { dryRun, configs: results };
|
|
75
91
|
}
|