@massu/core 1.1.0 → 1.2.1
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/dist/cli.js +207 -13
- package/dist/hooks/auto-learning-pipeline.js +1 -0
- package/dist/hooks/classify-failure.js +1 -0
- package/dist/hooks/cost-tracker.js +1 -0
- package/dist/hooks/fix-detector.js +1 -0
- package/dist/hooks/incident-pipeline.js +1 -0
- package/dist/hooks/post-edit-context.js +1 -0
- package/dist/hooks/post-tool-use.js +1 -0
- package/dist/hooks/pre-compact.js +1 -0
- package/dist/hooks/pre-delete-check.js +1 -0
- package/dist/hooks/quality-event.js +1 -0
- package/dist/hooks/rule-enforcement-pipeline.js +1 -0
- package/dist/hooks/session-end.js +1 -0
- package/dist/hooks/session-start.js +13 -3
- package/dist/hooks/user-prompt.js +1 -0
- package/package.json +1 -1
- package/src/commands/config-refresh.ts +103 -0
- package/src/commands/init.ts +89 -4
- package/src/config.ts +5 -0
- package/src/detect/index.ts +10 -1
- package/src/detect/migrate.ts +89 -5
- package/src/detect/passthrough.ts +108 -0
- package/src/detect/source-dir-detector.ts +28 -2
package/dist/cli.js
CHANGED
|
@@ -414,6 +414,7 @@ var init_config = __esm({
|
|
|
414
414
|
PathsConfigSchema = z.object({
|
|
415
415
|
source: z.string().default("src"),
|
|
416
416
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
417
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
417
418
|
routers: z.string().optional(),
|
|
418
419
|
routerRoot: z.string().optional(),
|
|
419
420
|
pages: z.string().optional(),
|
|
@@ -9022,6 +9023,13 @@ import { resolve as resolve4 } from "path";
|
|
|
9022
9023
|
function extsFor(language) {
|
|
9023
9024
|
return EXTENSIONS[language] ?? [];
|
|
9024
9025
|
}
|
|
9026
|
+
function extsWithFallback(language, fallbackTsForJs) {
|
|
9027
|
+
const base = extsFor(language);
|
|
9028
|
+
if (language === "javascript" && fallbackTsForJs) {
|
|
9029
|
+
return [...base, "ts", "tsx"];
|
|
9030
|
+
}
|
|
9031
|
+
return base;
|
|
9032
|
+
}
|
|
9025
9033
|
function isTestPath(language, path) {
|
|
9026
9034
|
const segments = path.split("/");
|
|
9027
9035
|
for (const seg of segments) {
|
|
@@ -9043,10 +9051,11 @@ function isInsideRoot(root, candidate) {
|
|
|
9043
9051
|
return false;
|
|
9044
9052
|
}
|
|
9045
9053
|
}
|
|
9046
|
-
function detectSourceDirs(projectRoot, languages) {
|
|
9054
|
+
function detectSourceDirs(projectRoot, languages, opts) {
|
|
9055
|
+
const fallbackTsForJs = opts?.fallbackTsForJs ?? false;
|
|
9047
9056
|
const out = {};
|
|
9048
9057
|
for (const lang of languages) {
|
|
9049
|
-
const exts =
|
|
9058
|
+
const exts = extsWithFallback(lang, fallbackTsForJs);
|
|
9050
9059
|
if (exts.length === 0) continue;
|
|
9051
9060
|
const patterns = exts.map((e2) => `**/*.${e2}`);
|
|
9052
9061
|
let files;
|
|
@@ -9644,8 +9653,9 @@ async function runDetection(projectRoot, overrides) {
|
|
|
9644
9653
|
const languages = Array.from(
|
|
9645
9654
|
new Set(pkg.manifests.map((m3) => m3.language))
|
|
9646
9655
|
);
|
|
9656
|
+
const fallbackTsForJs = languages.includes("javascript") && !languages.includes("typescript");
|
|
9647
9657
|
const [sourceDirs, monorepo] = await Promise.all([
|
|
9648
|
-
Promise.resolve(detectSourceDirs(projectRoot, languages)),
|
|
9658
|
+
Promise.resolve(detectSourceDirs(projectRoot, languages, { fallbackTsForJs })),
|
|
9649
9659
|
Promise.resolve(detectMonorepo(projectRoot))
|
|
9650
9660
|
]);
|
|
9651
9661
|
const domains = inferDomains(projectRoot, monorepo, sourceDirs);
|
|
@@ -10978,6 +10988,9 @@ function detectPython(projectRoot) {
|
|
|
10978
10988
|
return result;
|
|
10979
10989
|
}
|
|
10980
10990
|
function generateConfig(projectRoot, framework) {
|
|
10991
|
+
console.warn(
|
|
10992
|
+
"[@massu/core] generateConfig() is deprecated since 1.2.1 \u2014 use buildConfigFromDetection instead. It cannot produce valid configs for monorepos."
|
|
10993
|
+
);
|
|
10981
10994
|
const configPath = resolve5(projectRoot, "massu.config.yaml");
|
|
10982
10995
|
if (existsSync8(configPath)) {
|
|
10983
10996
|
return false;
|
|
@@ -11028,6 +11041,20 @@ ${yamlStringify(config)}`;
|
|
|
11028
11041
|
writeFileSync2(configPath, yamlContent, "utf-8");
|
|
11029
11042
|
return true;
|
|
11030
11043
|
}
|
|
11044
|
+
function monorepoCommonRoot(packages) {
|
|
11045
|
+
const roots = monorepoDistinctRoots(packages);
|
|
11046
|
+
return roots.length === 1 ? roots[0] : ".";
|
|
11047
|
+
}
|
|
11048
|
+
function monorepoDistinctRoots(packages) {
|
|
11049
|
+
const set = /* @__PURE__ */ new Set();
|
|
11050
|
+
for (const p19 of packages) {
|
|
11051
|
+
const parts = p19.path.split("/");
|
|
11052
|
+
if (parts.length > 1 && parts[0] !== "" && parts[0] !== ".") {
|
|
11053
|
+
set.add(parts[0]);
|
|
11054
|
+
}
|
|
11055
|
+
}
|
|
11056
|
+
return [...set].sort();
|
|
11057
|
+
}
|
|
11031
11058
|
function buildConfigFromDetection(opts) {
|
|
11032
11059
|
const { projectRoot, detection } = opts;
|
|
11033
11060
|
if (!detection) {
|
|
@@ -11072,6 +11099,16 @@ function buildConfigFromDetection(opts) {
|
|
|
11072
11099
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
11073
11100
|
if (primaryDirs.length > 0) {
|
|
11074
11101
|
pathsSource = primaryDirs[0];
|
|
11102
|
+
} else if (detection.monorepo.type !== "single" && detection.monorepo.packages.length > 0) {
|
|
11103
|
+
pathsSource = monorepoCommonRoot(detection.monorepo.packages);
|
|
11104
|
+
}
|
|
11105
|
+
}
|
|
11106
|
+
let monorepoRoots;
|
|
11107
|
+
if (detection.monorepo.type !== "single") {
|
|
11108
|
+
if (detection.monorepo.packages.length > 0) {
|
|
11109
|
+
monorepoRoots = monorepoDistinctRoots(detection.monorepo.packages);
|
|
11110
|
+
} else if (pathsSource !== "src" && pathsSource !== ".") {
|
|
11111
|
+
monorepoRoots = [pathsSource];
|
|
11075
11112
|
}
|
|
11076
11113
|
}
|
|
11077
11114
|
const verification = {};
|
|
@@ -11108,6 +11145,13 @@ function buildConfigFromDetection(opts) {
|
|
|
11108
11145
|
if (Object.keys(languageEntries).length > 0) {
|
|
11109
11146
|
frameworkBlock.languages = languageEntries;
|
|
11110
11147
|
}
|
|
11148
|
+
const pathsBlock = {
|
|
11149
|
+
source: pathsSource,
|
|
11150
|
+
aliases: { "@": pathsSource }
|
|
11151
|
+
};
|
|
11152
|
+
if (monorepoRoots && monorepoRoots.length > 0) {
|
|
11153
|
+
pathsBlock.monorepo_roots = monorepoRoots;
|
|
11154
|
+
}
|
|
11111
11155
|
const config = {
|
|
11112
11156
|
schema_version: 2,
|
|
11113
11157
|
project: {
|
|
@@ -11115,10 +11159,7 @@ function buildConfigFromDetection(opts) {
|
|
|
11115
11159
|
root: "auto"
|
|
11116
11160
|
},
|
|
11117
11161
|
framework: frameworkBlock,
|
|
11118
|
-
paths:
|
|
11119
|
-
source: pathsSource,
|
|
11120
|
-
aliases: { "@": pathsSource }
|
|
11121
|
-
},
|
|
11162
|
+
paths: pathsBlock,
|
|
11122
11163
|
toolPrefix: "massu",
|
|
11123
11164
|
domains,
|
|
11124
11165
|
rules: []
|
|
@@ -11225,6 +11266,15 @@ function validateWrittenConfig(configPath, projectRoot, checkPaths = true) {
|
|
|
11225
11266
|
}
|
|
11226
11267
|
}
|
|
11227
11268
|
}
|
|
11269
|
+
const mRoots = cfg.paths.monorepo_roots;
|
|
11270
|
+
if (Array.isArray(mRoots)) {
|
|
11271
|
+
for (const r2 of mRoots) {
|
|
11272
|
+
if (typeof r2 !== "string" || r2 === ".") continue;
|
|
11273
|
+
if (!existsSync8(resolve5(projectRoot, r2))) {
|
|
11274
|
+
return `paths.monorepo_roots '${r2}' does not exist on disk`;
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
}
|
|
11228
11278
|
}
|
|
11229
11279
|
} catch (err) {
|
|
11230
11280
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -22570,6 +22620,48 @@ var init_server = __esm({
|
|
|
22570
22620
|
}
|
|
22571
22621
|
});
|
|
22572
22622
|
|
|
22623
|
+
// src/detect/passthrough.ts
|
|
22624
|
+
function copyUnknownKeys(source, target, handledKeys) {
|
|
22625
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
22626
|
+
return;
|
|
22627
|
+
}
|
|
22628
|
+
for (const k3 of Object.keys(source)) {
|
|
22629
|
+
if (UNSAFE_KEYS.has(k3)) continue;
|
|
22630
|
+
if (source[k3] === void 0) continue;
|
|
22631
|
+
if (handledKeys.has(k3)) continue;
|
|
22632
|
+
if (Object.prototype.hasOwnProperty.call(target, k3)) continue;
|
|
22633
|
+
target[k3] = safeClone(source[k3]);
|
|
22634
|
+
}
|
|
22635
|
+
}
|
|
22636
|
+
function preserveNestedSubkeys(sourceBlock, targetBlock) {
|
|
22637
|
+
if (sourceBlock === null || sourceBlock === void 0 || typeof sourceBlock !== "object" || Array.isArray(sourceBlock)) {
|
|
22638
|
+
return;
|
|
22639
|
+
}
|
|
22640
|
+
const src = sourceBlock;
|
|
22641
|
+
for (const k3 of Object.keys(src)) {
|
|
22642
|
+
if (UNSAFE_KEYS.has(k3)) continue;
|
|
22643
|
+
if (src[k3] === void 0) continue;
|
|
22644
|
+
if (Object.prototype.hasOwnProperty.call(targetBlock, k3)) continue;
|
|
22645
|
+
targetBlock[k3] = safeClone(src[k3]);
|
|
22646
|
+
}
|
|
22647
|
+
}
|
|
22648
|
+
function safeClone(v3) {
|
|
22649
|
+
if (typeof structuredClone === "function") {
|
|
22650
|
+
try {
|
|
22651
|
+
return structuredClone(v3);
|
|
22652
|
+
} catch {
|
|
22653
|
+
}
|
|
22654
|
+
}
|
|
22655
|
+
return v3;
|
|
22656
|
+
}
|
|
22657
|
+
var UNSAFE_KEYS;
|
|
22658
|
+
var init_passthrough = __esm({
|
|
22659
|
+
"src/detect/passthrough.ts"() {
|
|
22660
|
+
"use strict";
|
|
22661
|
+
UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
22662
|
+
}
|
|
22663
|
+
});
|
|
22664
|
+
|
|
22573
22665
|
// src/commands/config-refresh.ts
|
|
22574
22666
|
var config_refresh_exports = {};
|
|
22575
22667
|
__export(config_refresh_exports, {
|
|
@@ -22629,6 +22721,67 @@ function mergeRefresh(existing, refreshed) {
|
|
|
22629
22721
|
out[field] = existing[field];
|
|
22630
22722
|
}
|
|
22631
22723
|
}
|
|
22724
|
+
if (typeof existing.toolPrefix === "string" && existing.toolPrefix !== "") {
|
|
22725
|
+
out.toolPrefix = existing.toolPrefix;
|
|
22726
|
+
}
|
|
22727
|
+
for (const block of ["framework", "paths", "project"]) {
|
|
22728
|
+
const existingBlock = existing[block];
|
|
22729
|
+
const outBlock = out[block];
|
|
22730
|
+
if (existingBlock && typeof existingBlock === "object" && !Array.isArray(existingBlock) && outBlock && typeof outBlock === "object" && !Array.isArray(outBlock)) {
|
|
22731
|
+
preserveNestedSubkeys(existingBlock, outBlock);
|
|
22732
|
+
}
|
|
22733
|
+
}
|
|
22734
|
+
const existingProject = existing.project;
|
|
22735
|
+
const outProject = out.project;
|
|
22736
|
+
if (existingProject && typeof existingProject === "object" && !Array.isArray(existingProject) && outProject && typeof outProject === "object" && !Array.isArray(outProject)) {
|
|
22737
|
+
const userRoot = existingProject.root;
|
|
22738
|
+
if (typeof userRoot === "string" && userRoot !== "") {
|
|
22739
|
+
outProject.root = userRoot;
|
|
22740
|
+
}
|
|
22741
|
+
}
|
|
22742
|
+
const existingPaths = existing.paths;
|
|
22743
|
+
const outPaths = out.paths;
|
|
22744
|
+
if (existingPaths && typeof existingPaths === "object" && !Array.isArray(existingPaths) && outPaths && typeof outPaths === "object" && !Array.isArray(outPaths)) {
|
|
22745
|
+
const existingAliases = existingPaths.aliases;
|
|
22746
|
+
const outAliases = outPaths.aliases;
|
|
22747
|
+
if (existingAliases && typeof existingAliases === "object" && !Array.isArray(existingAliases) && outAliases && typeof outAliases === "object" && !Array.isArray(outAliases)) {
|
|
22748
|
+
outPaths.aliases = {
|
|
22749
|
+
...outAliases,
|
|
22750
|
+
...existingAliases
|
|
22751
|
+
};
|
|
22752
|
+
} else if (existingAliases && typeof existingAliases === "object" && !Array.isArray(existingAliases)) {
|
|
22753
|
+
outPaths.aliases = existingAliases;
|
|
22754
|
+
}
|
|
22755
|
+
}
|
|
22756
|
+
const existingVer = existing.verification;
|
|
22757
|
+
const outVer = out.verification;
|
|
22758
|
+
if (existingVer && typeof existingVer === "object" && !Array.isArray(existingVer) && outVer && typeof outVer === "object" && !Array.isArray(outVer)) {
|
|
22759
|
+
const eVer = existingVer;
|
|
22760
|
+
const oVer = outVer;
|
|
22761
|
+
for (const lang of Object.keys(eVer)) {
|
|
22762
|
+
const userLang = eVer[lang];
|
|
22763
|
+
if (userLang === void 0) continue;
|
|
22764
|
+
if (!(lang in oVer)) {
|
|
22765
|
+
oVer[lang] = userLang;
|
|
22766
|
+
} else if (userLang && typeof userLang === "object" && !Array.isArray(userLang) && oVer[lang] && typeof oVer[lang] === "object" && !Array.isArray(oVer[lang])) {
|
|
22767
|
+
oVer[lang] = {
|
|
22768
|
+
...oVer[lang],
|
|
22769
|
+
...userLang
|
|
22770
|
+
};
|
|
22771
|
+
}
|
|
22772
|
+
}
|
|
22773
|
+
}
|
|
22774
|
+
const handledTopLevel = /* @__PURE__ */ new Set([
|
|
22775
|
+
"schema_version",
|
|
22776
|
+
"project",
|
|
22777
|
+
"framework",
|
|
22778
|
+
"paths",
|
|
22779
|
+
"toolPrefix",
|
|
22780
|
+
"verification",
|
|
22781
|
+
"detection",
|
|
22782
|
+
...PRESERVED_FIELDS
|
|
22783
|
+
]);
|
|
22784
|
+
copyUnknownKeys(existing, out, handledTopLevel);
|
|
22632
22785
|
return out;
|
|
22633
22786
|
}
|
|
22634
22787
|
function renderDiff(diff) {
|
|
@@ -22721,6 +22874,7 @@ var init_config_refresh = __esm({
|
|
|
22721
22874
|
"use strict";
|
|
22722
22875
|
init_detect();
|
|
22723
22876
|
init_drift();
|
|
22877
|
+
init_passthrough();
|
|
22724
22878
|
init_init();
|
|
22725
22879
|
PRESERVED_FIELDS = [
|
|
22726
22880
|
"rules",
|
|
@@ -22738,7 +22892,8 @@ var init_config_refresh = __esm({
|
|
|
22738
22892
|
"cloud",
|
|
22739
22893
|
"conventions",
|
|
22740
22894
|
"autoLearning",
|
|
22741
|
-
"python"
|
|
22895
|
+
"python",
|
|
22896
|
+
"toolPrefix"
|
|
22742
22897
|
];
|
|
22743
22898
|
}
|
|
22744
22899
|
});
|
|
@@ -22849,26 +23004,38 @@ function migrateV1ToV2(v1Config, detection) {
|
|
|
22849
23004
|
if (Object.keys(languageEntries).length > 0) {
|
|
22850
23005
|
framework.languages = languageEntries;
|
|
22851
23006
|
}
|
|
23007
|
+
preserveNestedSubkeys(v1Framework, framework);
|
|
22852
23008
|
let pathsSource = typeof v1Paths.source === "string" ? v1Paths.source : "src";
|
|
22853
23009
|
if (pathsSource === "src" && primary) {
|
|
22854
23010
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
22855
|
-
if (primaryDirs.length > 0)
|
|
23011
|
+
if (primaryDirs.length > 0) {
|
|
23012
|
+
pathsSource = primaryDirs[0];
|
|
23013
|
+
} else if (detection.monorepo?.type !== void 0 && detection.monorepo.type !== "single" && detection.monorepo.packages.length > 0) {
|
|
23014
|
+
pathsSource = monorepoCommonRootMigrate(detection.monorepo.packages);
|
|
23015
|
+
}
|
|
22856
23016
|
}
|
|
22857
23017
|
const aliases = v1Paths.aliases && typeof v1Paths.aliases === "object" ? v1Paths.aliases : { "@": pathsSource };
|
|
22858
23018
|
const paths = {
|
|
22859
23019
|
source: pathsSource,
|
|
22860
23020
|
aliases
|
|
22861
23021
|
};
|
|
23022
|
+
if (detection.monorepo?.type !== void 0 && detection.monorepo.type !== "single" && detection.monorepo.packages.length > 0 && !("monorepo_roots" in v1Paths)) {
|
|
23023
|
+
const roots = monorepoDistinctRootsMigrate(detection.monorepo.packages);
|
|
23024
|
+
if (roots.length > 0) paths.monorepo_roots = roots;
|
|
23025
|
+
}
|
|
22862
23026
|
for (const k3 of ["routers", "routerRoot", "pages", "middleware", "schema", "components", "hooks"]) {
|
|
22863
23027
|
if (typeof v1Paths[k3] === "string") paths[k3] = v1Paths[k3];
|
|
22864
23028
|
}
|
|
23029
|
+
preserveNestedSubkeys(v1Paths, paths);
|
|
22865
23030
|
const verification = buildVerificationBlock(detection, v1Verification);
|
|
23031
|
+
const project = {
|
|
23032
|
+
name: typeof v1Project.name === "string" ? v1Project.name : "my-project",
|
|
23033
|
+
root: typeof v1Project.root === "string" ? v1Project.root : "auto"
|
|
23034
|
+
};
|
|
23035
|
+
preserveNestedSubkeys(v1Project, project);
|
|
22866
23036
|
const v22 = {
|
|
22867
23037
|
schema_version: 2,
|
|
22868
|
-
project
|
|
22869
|
-
name: typeof v1Project.name === "string" ? v1Project.name : "my-project",
|
|
22870
|
-
root: typeof v1Project.root === "string" ? v1Project.root : "auto"
|
|
22871
|
-
},
|
|
23038
|
+
project,
|
|
22872
23039
|
framework,
|
|
22873
23040
|
paths,
|
|
22874
23041
|
toolPrefix: typeof v1.toolPrefix === "string" ? v1.toolPrefix : "massu"
|
|
@@ -22878,6 +23045,17 @@ function migrateV1ToV2(v1Config, detection) {
|
|
|
22878
23045
|
v22[field] = v1[field];
|
|
22879
23046
|
}
|
|
22880
23047
|
}
|
|
23048
|
+
const handledTopLevel = /* @__PURE__ */ new Set([
|
|
23049
|
+
"schema_version",
|
|
23050
|
+
"project",
|
|
23051
|
+
"framework",
|
|
23052
|
+
"paths",
|
|
23053
|
+
"toolPrefix",
|
|
23054
|
+
"verification",
|
|
23055
|
+
"python",
|
|
23056
|
+
...PRESERVED_FIELDS2
|
|
23057
|
+
]);
|
|
23058
|
+
copyUnknownKeys(v1, v22, handledTopLevel);
|
|
22881
23059
|
if (!Array.isArray(v22.domains)) {
|
|
22882
23060
|
v22.domains = [];
|
|
22883
23061
|
}
|
|
@@ -22908,16 +23086,32 @@ function migrateV1ToV2(v1Config, detection) {
|
|
|
22908
23086
|
} else if (existing.orm !== void 0) {
|
|
22909
23087
|
pythonBlock.orm = existing.orm;
|
|
22910
23088
|
}
|
|
23089
|
+
preserveNestedSubkeys(v1.python, pythonBlock);
|
|
22911
23090
|
v22.python = pythonBlock;
|
|
22912
23091
|
} else if (v1.python !== void 0) {
|
|
22913
23092
|
v22.python = v1.python;
|
|
22914
23093
|
}
|
|
22915
23094
|
return v22;
|
|
22916
23095
|
}
|
|
23096
|
+
function monorepoCommonRootMigrate(packages) {
|
|
23097
|
+
const roots = monorepoDistinctRootsMigrate(packages);
|
|
23098
|
+
return roots.length === 1 ? roots[0] : ".";
|
|
23099
|
+
}
|
|
23100
|
+
function monorepoDistinctRootsMigrate(packages) {
|
|
23101
|
+
const set = /* @__PURE__ */ new Set();
|
|
23102
|
+
for (const p19 of packages) {
|
|
23103
|
+
const parts = p19.path.split("/");
|
|
23104
|
+
if (parts.length > 1 && parts[0] !== "" && parts[0] !== ".") {
|
|
23105
|
+
set.add(parts[0]);
|
|
23106
|
+
}
|
|
23107
|
+
}
|
|
23108
|
+
return [...set].sort();
|
|
23109
|
+
}
|
|
22917
23110
|
var PRESERVED_FIELDS2;
|
|
22918
23111
|
var init_migrate = __esm({
|
|
22919
23112
|
"src/detect/migrate.ts"() {
|
|
22920
23113
|
"use strict";
|
|
23114
|
+
init_passthrough();
|
|
22921
23115
|
PRESERVED_FIELDS2 = [
|
|
22922
23116
|
"rules",
|
|
22923
23117
|
"domains",
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -225,6 +225,7 @@ var PythonConfigSchema = z.object({
|
|
|
225
225
|
var PathsConfigSchema = z.object({
|
|
226
226
|
source: z.string().default("src"),
|
|
227
227
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
228
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
228
229
|
routers: z.string().optional(),
|
|
229
230
|
routerRoot: z.string().optional(),
|
|
230
231
|
pages: z.string().optional(),
|
|
@@ -224,6 +224,7 @@ var PythonConfigSchema = z.object({
|
|
|
224
224
|
var PathsConfigSchema = z.object({
|
|
225
225
|
source: z.string().default("src"),
|
|
226
226
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
227
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
227
228
|
routers: z.string().optional(),
|
|
228
229
|
routerRoot: z.string().optional(),
|
|
229
230
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -225,6 +225,7 @@ var PythonConfigSchema = z.object({
|
|
|
225
225
|
var PathsConfigSchema = z.object({
|
|
226
226
|
source: z.string().default("src"),
|
|
227
227
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
228
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
228
229
|
routers: z.string().optional(),
|
|
229
230
|
routerRoot: z.string().optional(),
|
|
230
231
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -224,6 +224,7 @@ var PythonConfigSchema = z.object({
|
|
|
224
224
|
var PathsConfigSchema = z.object({
|
|
225
225
|
source: z.string().default("src"),
|
|
226
226
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
227
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
227
228
|
routers: z.string().optional(),
|
|
228
229
|
routerRoot: z.string().optional(),
|
|
229
230
|
pages: z.string().optional(),
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
|
@@ -6002,6 +6002,7 @@ var PythonConfigSchema = z.object({
|
|
|
6002
6002
|
var PathsConfigSchema = z.object({
|
|
6003
6003
|
source: z.string().default("src"),
|
|
6004
6004
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
6005
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
6005
6006
|
routers: z.string().optional(),
|
|
6006
6007
|
routerRoot: z.string().optional(),
|
|
6007
6008
|
pages: z.string().optional(),
|
|
@@ -8263,6 +8264,13 @@ var TEST_DIR_KEYWORDS = ["tests", "test", "__tests__", "spec", "specs"];
|
|
|
8263
8264
|
function extsFor(language) {
|
|
8264
8265
|
return EXTENSIONS[language] ?? [];
|
|
8265
8266
|
}
|
|
8267
|
+
function extsWithFallback(language, fallbackTsForJs) {
|
|
8268
|
+
const base = extsFor(language);
|
|
8269
|
+
if (language === "javascript" && fallbackTsForJs) {
|
|
8270
|
+
return [...base, "ts", "tsx"];
|
|
8271
|
+
}
|
|
8272
|
+
return base;
|
|
8273
|
+
}
|
|
8266
8274
|
function isTestPath(language, path) {
|
|
8267
8275
|
const segments = path.split("/");
|
|
8268
8276
|
for (const seg of segments) {
|
|
@@ -8284,10 +8292,11 @@ function isInsideRoot(root, candidate) {
|
|
|
8284
8292
|
return false;
|
|
8285
8293
|
}
|
|
8286
8294
|
}
|
|
8287
|
-
function detectSourceDirs(projectRoot, languages) {
|
|
8295
|
+
function detectSourceDirs(projectRoot, languages, opts) {
|
|
8296
|
+
const fallbackTsForJs = opts?.fallbackTsForJs ?? false;
|
|
8288
8297
|
const out = {};
|
|
8289
8298
|
for (const lang of languages) {
|
|
8290
|
-
const exts =
|
|
8299
|
+
const exts = extsWithFallback(lang, fallbackTsForJs);
|
|
8291
8300
|
if (exts.length === 0) continue;
|
|
8292
8301
|
const patterns = exts.map((e) => `**/*.${e}`);
|
|
8293
8302
|
let files;
|
|
@@ -8808,8 +8817,9 @@ async function runDetection(projectRoot, overrides) {
|
|
|
8808
8817
|
const languages = Array.from(
|
|
8809
8818
|
new Set(pkg.manifests.map((m) => m.language))
|
|
8810
8819
|
);
|
|
8820
|
+
const fallbackTsForJs = languages.includes("javascript") && !languages.includes("typescript");
|
|
8811
8821
|
const [sourceDirs, monorepo] = await Promise.all([
|
|
8812
|
-
Promise.resolve(detectSourceDirs(projectRoot, languages)),
|
|
8822
|
+
Promise.resolve(detectSourceDirs(projectRoot, languages, { fallbackTsForJs })),
|
|
8813
8823
|
Promise.resolve(detectMonorepo(projectRoot))
|
|
8814
8824
|
]);
|
|
8815
8825
|
const domains = inferDomains(projectRoot, monorepo, sourceDirs);
|
|
@@ -226,6 +226,7 @@ var PythonConfigSchema = z.object({
|
|
|
226
226
|
var PathsConfigSchema = z.object({
|
|
227
227
|
source: z.string().default("src"),
|
|
228
228
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
229
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
229
230
|
routers: z.string().optional(),
|
|
230
231
|
routerRoot: z.string().optional(),
|
|
231
232
|
pages: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -29,6 +29,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
29
29
|
import { runDetection } from '../detect/index.ts';
|
|
30
30
|
import { computeFingerprint } from '../detect/drift.ts';
|
|
31
31
|
import type { AnyConfig } from '../detect/migrate.ts';
|
|
32
|
+
import { copyUnknownKeys, preserveNestedSubkeys } from '../detect/passthrough.ts';
|
|
32
33
|
import { buildConfigFromDetection, renderConfigYaml, writeConfigAtomic } from './init.ts';
|
|
33
34
|
|
|
34
35
|
const PRESERVED_FIELDS = [
|
|
@@ -48,6 +49,7 @@ const PRESERVED_FIELDS = [
|
|
|
48
49
|
'conventions',
|
|
49
50
|
'autoLearning',
|
|
50
51
|
'python',
|
|
52
|
+
'toolPrefix',
|
|
51
53
|
] as const;
|
|
52
54
|
|
|
53
55
|
export interface ConfigRefreshOptions {
|
|
@@ -116,12 +118,113 @@ export function computeDiff(before: AnyConfig, after: AnyConfig): DiffLine[] {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
export function mergeRefresh(existing: AnyConfig, refreshed: AnyConfig): AnyConfig {
|
|
121
|
+
// P1-008: Start from detector output (fresh framework, paths.source, verification, detection).
|
|
119
122
|
const out: AnyConfig = { ...refreshed };
|
|
123
|
+
|
|
124
|
+
// Restore user-authored top-level sections verbatim.
|
|
120
125
|
for (const field of PRESERVED_FIELDS) {
|
|
121
126
|
if (existing[field] !== undefined) {
|
|
122
127
|
out[field] = existing[field];
|
|
123
128
|
}
|
|
124
129
|
}
|
|
130
|
+
|
|
131
|
+
// Restore toolPrefix from existing (never let detector-defaulted 'massu' overwrite a custom prefix).
|
|
132
|
+
if (typeof existing.toolPrefix === 'string' && existing.toolPrefix !== '') {
|
|
133
|
+
out.toolPrefix = existing.toolPrefix;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// For detector-owned blocks (framework, paths, project), preserve any user subkey the detector didn't emit.
|
|
137
|
+
for (const block of ['framework', 'paths', 'project'] as const) {
|
|
138
|
+
const existingBlock = existing[block];
|
|
139
|
+
const outBlock = out[block];
|
|
140
|
+
if (
|
|
141
|
+
existingBlock && typeof existingBlock === 'object' && !Array.isArray(existingBlock) &&
|
|
142
|
+
outBlock && typeof outBlock === 'object' && !Array.isArray(outBlock)
|
|
143
|
+
) {
|
|
144
|
+
preserveNestedSubkeys(existingBlock, outBlock as Record<string, unknown>);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Restore user-set project.root (detector at init.ts:418 always writes 'auto'; user value wins).
|
|
149
|
+
// Separated from the block loop above for readability (A-004 architecture-review follow-up).
|
|
150
|
+
const existingProject = existing.project;
|
|
151
|
+
const outProject = out.project;
|
|
152
|
+
if (
|
|
153
|
+
existingProject && typeof existingProject === 'object' && !Array.isArray(existingProject) &&
|
|
154
|
+
outProject && typeof outProject === 'object' && !Array.isArray(outProject)
|
|
155
|
+
) {
|
|
156
|
+
const userRoot = (existingProject as Record<string, unknown>).root;
|
|
157
|
+
if (typeof userRoot === 'string' && userRoot !== '') {
|
|
158
|
+
(outProject as Record<string, unknown>).root = userRoot;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// paths.aliases is a 2-level-nested user block. Detector always writes
|
|
163
|
+
// { '@': <source-dir> }; user-authored alias map must survive. Spread user
|
|
164
|
+
// over detector so user keys win for any overlap AND user-only keys survive.
|
|
165
|
+
// (P5-002 discovery — hedge's paths.aliases['@'] was being overwritten.)
|
|
166
|
+
const existingPaths = existing.paths;
|
|
167
|
+
const outPaths = out.paths;
|
|
168
|
+
if (
|
|
169
|
+
existingPaths && typeof existingPaths === 'object' && !Array.isArray(existingPaths) &&
|
|
170
|
+
outPaths && typeof outPaths === 'object' && !Array.isArray(outPaths)
|
|
171
|
+
) {
|
|
172
|
+
const existingAliases = (existingPaths as Record<string, unknown>).aliases;
|
|
173
|
+
const outAliases = (outPaths as Record<string, unknown>).aliases;
|
|
174
|
+
if (
|
|
175
|
+
existingAliases && typeof existingAliases === 'object' && !Array.isArray(existingAliases) &&
|
|
176
|
+
outAliases && typeof outAliases === 'object' && !Array.isArray(outAliases)
|
|
177
|
+
) {
|
|
178
|
+
(outPaths as Record<string, unknown>).aliases = {
|
|
179
|
+
...(outAliases as Record<string, unknown>),
|
|
180
|
+
...(existingAliases as Record<string, unknown>),
|
|
181
|
+
};
|
|
182
|
+
} else if (existingAliases && typeof existingAliases === 'object' && !Array.isArray(existingAliases)) {
|
|
183
|
+
(outPaths as Record<string, unknown>).aliases = existingAliases;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// verification is the other 2-level-nested detector-owned block. Semantics
|
|
188
|
+
// mirror migrate.ts:132-138 buildVerificationBlock: user's custom language
|
|
189
|
+
// sections (e.g., hedge's `gateway`, `ios`, `runtime`, `web`) survive
|
|
190
|
+
// wholesale; user's command overrides on shared languages (e.g., `python`)
|
|
191
|
+
// win over detector defaults. (P5-002 discovery — hedge was losing 15
|
|
192
|
+
// verification command entries across 4 custom language sections plus
|
|
193
|
+
// having 4 python commands overwritten with detector defaults.)
|
|
194
|
+
const existingVer = existing.verification;
|
|
195
|
+
const outVer = out.verification;
|
|
196
|
+
if (
|
|
197
|
+
existingVer && typeof existingVer === 'object' && !Array.isArray(existingVer) &&
|
|
198
|
+
outVer && typeof outVer === 'object' && !Array.isArray(outVer)
|
|
199
|
+
) {
|
|
200
|
+
const eVer = existingVer as Record<string, unknown>;
|
|
201
|
+
const oVer = outVer as Record<string, unknown>;
|
|
202
|
+
for (const lang of Object.keys(eVer)) {
|
|
203
|
+
const userLang = eVer[lang];
|
|
204
|
+
if (userLang === undefined) continue;
|
|
205
|
+
if (!(lang in oVer)) {
|
|
206
|
+
// User-custom language (no detector counterpart) → preserve wholesale.
|
|
207
|
+
oVer[lang] = userLang;
|
|
208
|
+
} else if (
|
|
209
|
+
userLang && typeof userLang === 'object' && !Array.isArray(userLang) &&
|
|
210
|
+
oVer[lang] && typeof oVer[lang] === 'object' && !Array.isArray(oVer[lang])
|
|
211
|
+
) {
|
|
212
|
+
// Shared language → user commands win over detector defaults (spread).
|
|
213
|
+
oVer[lang] = {
|
|
214
|
+
...(oVer[lang] as Record<string, unknown>),
|
|
215
|
+
...(userLang as Record<string, unknown>),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Preserve top-level user keys not handled above (mirrors P1-001 passthrough for upgrade).
|
|
222
|
+
const handledTopLevel = new Set<string>([
|
|
223
|
+
'schema_version', 'project', 'framework', 'paths', 'toolPrefix', 'verification', 'detection',
|
|
224
|
+
...PRESERVED_FIELDS,
|
|
225
|
+
]);
|
|
226
|
+
copyUnknownKeys(existing, out, handledTopLevel);
|
|
227
|
+
|
|
125
228
|
return out;
|
|
126
229
|
}
|
|
127
230
|
|
package/src/commands/init.ts
CHANGED
|
@@ -231,7 +231,18 @@ export function detectPython(projectRoot: string): PythonDetection {
|
|
|
231
231
|
// Legacy Config File Generation (preserved for cli.test.ts)
|
|
232
232
|
// ============================================================
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* @deprecated Since @massu/core@1.2.1. Use {@link buildConfigFromDetection}
|
|
236
|
+
* with {@link runDetection} for monorepo-aware path resolution and
|
|
237
|
+
* schema_version=2 output. This path hardcodes `paths.source = 'src'` and
|
|
238
|
+
* cannot emit `paths.monorepo_roots`, so it would roll back on every
|
|
239
|
+
* non-`src/` layout. Kept only for the legacy `cli.test.ts` smoke tests;
|
|
240
|
+
* new callers must use the v2 builder.
|
|
241
|
+
*/
|
|
234
242
|
export function generateConfig(projectRoot: string, framework: FrameworkDetection): boolean {
|
|
243
|
+
console.warn(
|
|
244
|
+
'[@massu/core] generateConfig() is deprecated since 1.2.1 — use buildConfigFromDetection instead. It cannot produce valid configs for monorepos.'
|
|
245
|
+
);
|
|
235
246
|
const configPath = resolve(projectRoot, 'massu.config.yaml');
|
|
236
247
|
|
|
237
248
|
if (existsSync(configPath)) {
|
|
@@ -294,6 +305,37 @@ ${yamlStringify(config)}`;
|
|
|
294
305
|
// V2 Config Builder (detection-driven)
|
|
295
306
|
// ============================================================
|
|
296
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Return the common top-level parent directory across every workspace
|
|
310
|
+
* package. Returns `'.'` when packages span multiple parents (e.g. a repo
|
|
311
|
+
* with both `apps/*` and `packages/*`) — the project root is always a valid
|
|
312
|
+
* paths.source value (see validateWrittenConfig at init.ts:572).
|
|
313
|
+
*/
|
|
314
|
+
function monorepoCommonRoot(
|
|
315
|
+
packages: ReadonlyArray<{ path: string }>
|
|
316
|
+
): string {
|
|
317
|
+
const roots = monorepoDistinctRoots(packages);
|
|
318
|
+
return roots.length === 1 ? roots[0] : '.';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Return the distinct top-level parent directories of every workspace
|
|
323
|
+
* package (e.g. `['apps', 'packages']` when both are present). Sorted for
|
|
324
|
+
* determinism. Excludes root-level ('.') workspaces.
|
|
325
|
+
*/
|
|
326
|
+
function monorepoDistinctRoots(
|
|
327
|
+
packages: ReadonlyArray<{ path: string }>
|
|
328
|
+
): string[] {
|
|
329
|
+
const set = new Set<string>();
|
|
330
|
+
for (const p of packages) {
|
|
331
|
+
const parts = p.path.split('/');
|
|
332
|
+
if (parts.length > 1 && parts[0] !== '' && parts[0] !== '.') {
|
|
333
|
+
set.add(parts[0]);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return [...set].sort();
|
|
337
|
+
}
|
|
338
|
+
|
|
297
339
|
/**
|
|
298
340
|
* Build a schema_version=2 config object from a DetectionResult.
|
|
299
341
|
*
|
|
@@ -364,11 +406,38 @@ export function buildConfigFromDetection(
|
|
|
364
406
|
const legacyUi = (primaryEntry?.ui as string | undefined) ?? 'none';
|
|
365
407
|
|
|
366
408
|
// Determine paths.source from primary language's dominant source dir.
|
|
409
|
+
// P1-003: when the primary language has no detectable source dir AND the
|
|
410
|
+
// repo is a monorepo, fall back to the common parent of workspace packages
|
|
411
|
+
// (e.g. 'apps' for turbo + apps/*, 'packages' for pnpm + packages/*). This
|
|
412
|
+
// prevents the validator from rejecting a nonexistent top-level 'src/' on
|
|
413
|
+
// monorepo shapes where code actually lives under apps/ or packages/.
|
|
367
414
|
let pathsSource = 'src';
|
|
368
415
|
if (primary) {
|
|
369
416
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
370
417
|
if (primaryDirs.length > 0) {
|
|
371
418
|
pathsSource = primaryDirs[0];
|
|
419
|
+
} else if (
|
|
420
|
+
detection.monorepo.type !== 'single' &&
|
|
421
|
+
detection.monorepo.packages.length > 0
|
|
422
|
+
) {
|
|
423
|
+
pathsSource = monorepoCommonRoot(detection.monorepo.packages);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// P1-005: emit `paths.monorepo_roots` as the distinct parent directories of
|
|
428
|
+
// every workspace package when this is a monorepo. Optional + additive;
|
|
429
|
+
// v1 consumers ignore it. When detection identified a monorepo type
|
|
430
|
+
// (turbo/nx/pnpm/etc) but no manifested workspace packages were found
|
|
431
|
+
// (e.g. fresh-install fixtures with apps/*/main.py that haven't declared
|
|
432
|
+
// sub-manifests yet), fall back to deriving roots from the resolved
|
|
433
|
+
// paths.source so the field is still accurate for monorepo-aware tools.
|
|
434
|
+
let monorepoRoots: string[] | undefined;
|
|
435
|
+
if (detection.monorepo.type !== 'single') {
|
|
436
|
+
if (detection.monorepo.packages.length > 0) {
|
|
437
|
+
monorepoRoots = monorepoDistinctRoots(detection.monorepo.packages);
|
|
438
|
+
} else if (pathsSource !== 'src' && pathsSource !== '.') {
|
|
439
|
+
// Derive from paths.source when no workspace manifests exist.
|
|
440
|
+
monorepoRoots = [pathsSource];
|
|
372
441
|
}
|
|
373
442
|
}
|
|
374
443
|
|
|
@@ -411,6 +480,14 @@ export function buildConfigFromDetection(
|
|
|
411
480
|
frameworkBlock.languages = languageEntries;
|
|
412
481
|
}
|
|
413
482
|
|
|
483
|
+
const pathsBlock: Record<string, unknown> = {
|
|
484
|
+
source: pathsSource,
|
|
485
|
+
aliases: { '@': pathsSource },
|
|
486
|
+
};
|
|
487
|
+
if (monorepoRoots && monorepoRoots.length > 0) {
|
|
488
|
+
pathsBlock.monorepo_roots = monorepoRoots;
|
|
489
|
+
}
|
|
490
|
+
|
|
414
491
|
const config: Record<string, unknown> = {
|
|
415
492
|
schema_version: 2,
|
|
416
493
|
project: {
|
|
@@ -418,10 +495,7 @@ export function buildConfigFromDetection(
|
|
|
418
495
|
root: 'auto',
|
|
419
496
|
},
|
|
420
497
|
framework: frameworkBlock,
|
|
421
|
-
paths:
|
|
422
|
-
source: pathsSource,
|
|
423
|
-
aliases: { '@': pathsSource },
|
|
424
|
-
},
|
|
498
|
+
paths: pathsBlock,
|
|
425
499
|
toolPrefix: 'massu',
|
|
426
500
|
domains,
|
|
427
501
|
rules: [],
|
|
@@ -588,6 +662,17 @@ export function validateWrittenConfig(
|
|
|
588
662
|
}
|
|
589
663
|
}
|
|
590
664
|
}
|
|
665
|
+
// P2-001: verify paths.monorepo_roots entries exist on disk (parity
|
|
666
|
+
// with paths.source existence check at line 624-631 above).
|
|
667
|
+
const mRoots = (cfg.paths as Record<string, unknown>).monorepo_roots;
|
|
668
|
+
if (Array.isArray(mRoots)) {
|
|
669
|
+
for (const r of mRoots) {
|
|
670
|
+
if (typeof r !== 'string' || r === '.') continue;
|
|
671
|
+
if (!existsSync(resolve(projectRoot, r))) {
|
|
672
|
+
return `paths.monorepo_roots '${r}' does not exist on disk`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
591
676
|
}
|
|
592
677
|
} catch (err) {
|
|
593
678
|
return err instanceof Error ? err.message : String(err);
|
package/src/config.ts
CHANGED
|
@@ -254,9 +254,14 @@ const PythonConfigSchema = z.object({
|
|
|
254
254
|
export type PythonConfig = z.infer<typeof PythonConfigSchema>;
|
|
255
255
|
|
|
256
256
|
// --- Paths Config ---
|
|
257
|
+
// `monorepo_roots` (P1-005): optional, additive. Emitted by `init --ci` when
|
|
258
|
+
// `monorepo.type !== 'single'` and at least one workspace package exists.
|
|
259
|
+
// Downstream tools may consume it for monorepo-aware scanning. Existing v1
|
|
260
|
+
// configs omit it — `.optional()` preserves full back-compat.
|
|
257
261
|
const PathsConfigSchema = z.object({
|
|
258
262
|
source: z.string().default('src'),
|
|
259
263
|
aliases: z.record(z.string(), z.string()).default({ '@': 'src' }),
|
|
264
|
+
monorepo_roots: z.array(z.string()).optional(),
|
|
260
265
|
routers: z.string().optional(),
|
|
261
266
|
routerRoot: z.string().optional(),
|
|
262
267
|
pages: z.string().optional(),
|
package/src/detect/index.ts
CHANGED
|
@@ -135,9 +135,18 @@ export async function runDetection(
|
|
|
135
135
|
new Set(pkg.manifests.map((m) => m.language))
|
|
136
136
|
) as SupportedLanguage[];
|
|
137
137
|
|
|
138
|
+
// P1-002: when the repo has a `javascript` manifest but NO `typescript`
|
|
139
|
+
// manifest, still glob `.ts`/`.tsx` for the javascript slot. This fixes
|
|
140
|
+
// plain-JS monorepos (e.g. turbo + next in a package.json with no
|
|
141
|
+
// `typescript` dep and no `tsconfig.json`) that contain `.tsx` files
|
|
142
|
+
// under `apps/*/`. Without this, `init --ci` falls back to the nonexistent
|
|
143
|
+
// `src/` and rolls back with a validation error.
|
|
144
|
+
const fallbackTsForJs =
|
|
145
|
+
languages.includes('javascript') && !languages.includes('typescript');
|
|
146
|
+
|
|
138
147
|
// 3b. run source-dir + monorepo detection in parallel (both pure fs).
|
|
139
148
|
const [sourceDirs, monorepo] = await Promise.all([
|
|
140
|
-
Promise.resolve(detectSourceDirs(projectRoot, languages)),
|
|
149
|
+
Promise.resolve(detectSourceDirs(projectRoot, languages, { fallbackTsForJs })),
|
|
141
150
|
Promise.resolve(detectMonorepo(projectRoot)),
|
|
142
151
|
]);
|
|
143
152
|
|
package/src/detect/migrate.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { DetectionResult, SupportedLanguage, VRCommandSet } from './index.ts';
|
|
25
|
+
import { copyUnknownKeys, preserveNestedSubkeys } from './passthrough.ts';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Shape accepted for input. We intentionally use `Record<string, unknown>`
|
|
@@ -190,12 +191,26 @@ export function migrateV1ToV2(
|
|
|
190
191
|
if (Object.keys(languageEntries).length > 0) {
|
|
191
192
|
framework.languages = languageEntries;
|
|
192
193
|
}
|
|
194
|
+
// P1-004: preserve any v1Framework subkey the explicit rebuild didn't emit
|
|
195
|
+
// (e.g., hedge's `framework.{python, rust, swift, typescript}` language sub-blocks).
|
|
196
|
+
preserveNestedSubkeys(v1Framework, framework);
|
|
193
197
|
|
|
194
198
|
// Paths: preserve user-set fields; fill `source` from detection if user had 'src' default.
|
|
199
|
+
// P1-003 (mirror of init.ts:367-390): when primary language has no source dir
|
|
200
|
+
// AND this is a monorepo, fall back to the common parent of workspace packages
|
|
201
|
+
// instead of leaving the nonexistent default 'src' in place.
|
|
195
202
|
let pathsSource: string = typeof v1Paths.source === 'string' ? v1Paths.source : 'src';
|
|
196
203
|
if (pathsSource === 'src' && primary) {
|
|
197
204
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
198
|
-
if (primaryDirs.length > 0)
|
|
205
|
+
if (primaryDirs.length > 0) {
|
|
206
|
+
pathsSource = primaryDirs[0];
|
|
207
|
+
} else if (
|
|
208
|
+
detection.monorepo?.type !== undefined &&
|
|
209
|
+
detection.monorepo.type !== 'single' &&
|
|
210
|
+
detection.monorepo.packages.length > 0
|
|
211
|
+
) {
|
|
212
|
+
pathsSource = monorepoCommonRootMigrate(detection.monorepo.packages);
|
|
213
|
+
}
|
|
199
214
|
}
|
|
200
215
|
const aliases = v1Paths.aliases && typeof v1Paths.aliases === 'object'
|
|
201
216
|
? (v1Paths.aliases as Record<string, string>)
|
|
@@ -204,19 +219,38 @@ export function migrateV1ToV2(
|
|
|
204
219
|
source: pathsSource,
|
|
205
220
|
aliases,
|
|
206
221
|
};
|
|
222
|
+
// P1-005 mirror: emit monorepo_roots for upgraded v2 configs (additive,
|
|
223
|
+
// only when monorepo + not already user-specified).
|
|
224
|
+
if (
|
|
225
|
+
detection.monorepo?.type !== undefined &&
|
|
226
|
+
detection.monorepo.type !== 'single' &&
|
|
227
|
+
detection.monorepo.packages.length > 0 &&
|
|
228
|
+
!('monorepo_roots' in v1Paths)
|
|
229
|
+
) {
|
|
230
|
+
const roots = monorepoDistinctRootsMigrate(detection.monorepo.packages);
|
|
231
|
+
if (roots.length > 0) paths.monorepo_roots = roots;
|
|
232
|
+
}
|
|
207
233
|
for (const k of ['routers', 'routerRoot', 'pages', 'middleware', 'schema', 'components', 'hooks']) {
|
|
208
234
|
if (typeof v1Paths[k] === 'string') paths[k] = v1Paths[k];
|
|
209
235
|
}
|
|
236
|
+
// P1-005: preserve any v1Paths subkey the explicit rebuild didn't emit
|
|
237
|
+
// (e.g., hedge's 19 custom `paths.*` entries like adr, plans, monorepo_root).
|
|
238
|
+
preserveNestedSubkeys(v1Paths, paths);
|
|
210
239
|
|
|
211
240
|
const verification = buildVerificationBlock(detection, v1Verification);
|
|
212
241
|
|
|
242
|
+
// P1-006: build project block with nested passthrough so custom subkeys
|
|
243
|
+
// (e.g., hedge's `project.description`) survive the migration.
|
|
244
|
+
const project: Record<string, unknown> = {
|
|
245
|
+
name: typeof v1Project.name === 'string' ? v1Project.name : 'my-project',
|
|
246
|
+
root: typeof v1Project.root === 'string' ? v1Project.root : 'auto',
|
|
247
|
+
};
|
|
248
|
+
preserveNestedSubkeys(v1Project, project);
|
|
249
|
+
|
|
213
250
|
// Construct v2 output.
|
|
214
251
|
const v2: AnyConfig = {
|
|
215
252
|
schema_version: 2,
|
|
216
|
-
project
|
|
217
|
-
name: typeof v1Project.name === 'string' ? v1Project.name : 'my-project',
|
|
218
|
-
root: typeof v1Project.root === 'string' ? v1Project.root : 'auto',
|
|
219
|
-
},
|
|
253
|
+
project,
|
|
220
254
|
framework,
|
|
221
255
|
paths,
|
|
222
256
|
toolPrefix: typeof v1.toolPrefix === 'string' ? v1.toolPrefix : 'massu',
|
|
@@ -229,6 +263,24 @@ export function migrateV1ToV2(
|
|
|
229
263
|
}
|
|
230
264
|
}
|
|
231
265
|
|
|
266
|
+
// P1-001: preserve any v1 top-level key not already handled by the explicit
|
|
267
|
+
// migrator. This is the generalization of PRESERVED_FIELDS — custom sections
|
|
268
|
+
// like `services`, `workflow`, `north_stars` (hedge) now pass through.
|
|
269
|
+
//
|
|
270
|
+
// `detection` is intentionally NOT in handledTopLevel: when a v2 config is
|
|
271
|
+
// fed back in (idempotence check at migrate.ts:16), the existing `detection`
|
|
272
|
+
// block round-trips via this passthrough path. It gets re-stamped with a
|
|
273
|
+
// fresh fingerprint by the caller at config-upgrade.ts:96-99 right after
|
|
274
|
+
// migrateV1ToV2 returns. Any future v2-only top-level key added here must
|
|
275
|
+
// either appear in this list (with explicit handling above) or round-trip
|
|
276
|
+
// through this passthrough — never add a v2-only key that does neither.
|
|
277
|
+
// (A-006 architecture-review follow-up.)
|
|
278
|
+
const handledTopLevel = new Set<string>([
|
|
279
|
+
'schema_version', 'project', 'framework', 'paths', 'toolPrefix',
|
|
280
|
+
'verification', 'python', ...PRESERVED_FIELDS,
|
|
281
|
+
]);
|
|
282
|
+
copyUnknownKeys(v1, v2, handledTopLevel);
|
|
283
|
+
|
|
232
284
|
// Ensure domains / rules exist as arrays (v2 requires them).
|
|
233
285
|
if (!Array.isArray(v2.domains)) {
|
|
234
286
|
v2.domains = [];
|
|
@@ -268,6 +320,9 @@ export function migrateV1ToV2(
|
|
|
268
320
|
} else if (existing.orm !== undefined) {
|
|
269
321
|
pythonBlock.orm = existing.orm;
|
|
270
322
|
}
|
|
323
|
+
// P1-007: preserve any v1 python subkey not already handled above
|
|
324
|
+
// (e.g., `python.test_framework`, `python.database`).
|
|
325
|
+
preserveNestedSubkeys(v1.python, pythonBlock);
|
|
271
326
|
v2.python = pythonBlock;
|
|
272
327
|
} else if (v1.python !== undefined) {
|
|
273
328
|
// Preserve even if detection didn't find python (e.g. non-monorepo-with-python).
|
|
@@ -276,3 +331,32 @@ export function migrateV1ToV2(
|
|
|
276
331
|
|
|
277
332
|
return v2;
|
|
278
333
|
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Return the common top-level parent directory across every workspace
|
|
337
|
+
* package (mirror of init.ts:monorepoCommonRoot). Returns `'.'` when
|
|
338
|
+
* packages span multiple parents.
|
|
339
|
+
*/
|
|
340
|
+
function monorepoCommonRootMigrate(
|
|
341
|
+
packages: ReadonlyArray<{ path: string }>
|
|
342
|
+
): string {
|
|
343
|
+
const roots = monorepoDistinctRootsMigrate(packages);
|
|
344
|
+
return roots.length === 1 ? roots[0] : '.';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Return the distinct top-level parent directories of every workspace
|
|
349
|
+
* package (mirror of init.ts:monorepoDistinctRoots). Sorted for determinism.
|
|
350
|
+
*/
|
|
351
|
+
function monorepoDistinctRootsMigrate(
|
|
352
|
+
packages: ReadonlyArray<{ path: string }>
|
|
353
|
+
): string[] {
|
|
354
|
+
const set = new Set<string>();
|
|
355
|
+
for (const p of packages) {
|
|
356
|
+
const parts = p.path.split('/');
|
|
357
|
+
if (parts.length > 1 && parts[0] !== '' && parts[0] !== '.') {
|
|
358
|
+
set.add(parts[0]);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return [...set].sort();
|
|
362
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared passthrough helpers for config migration + refresh.
|
|
6
|
+
*
|
|
7
|
+
* These helpers exist to prevent the class of bug fixed in @massu/core@1.2.0
|
|
8
|
+
* (incident 2026-04-19-config-upgrade-data-loss): a hand-maintained allow-list
|
|
9
|
+
* in `migrate.ts` silently dropped any v1 top-level key not on the list, and
|
|
10
|
+
* the parallel rebuild blocks inside `framework` / `paths` / `project` /
|
|
11
|
+
* `python` did the same thing at the nested level.
|
|
12
|
+
*
|
|
13
|
+
* Both helpers are TARGET-WINS: the migrator writes the keys it actively owns,
|
|
14
|
+
* then the helper fills in everything else the user had. A user-authored value
|
|
15
|
+
* in `target` is NEVER overwritten by the source.
|
|
16
|
+
*
|
|
17
|
+
* Why two exports instead of one:
|
|
18
|
+
* - `copyUnknownKeys` takes an explicit `handledKeys` set — used for TOP-LEVEL
|
|
19
|
+
* passthrough where the caller enumerates the keys it migrated explicitly
|
|
20
|
+
* (e.g., schema_version, project, framework, paths, toolPrefix, …).
|
|
21
|
+
* - `preserveNestedSubkeys` takes no handled-set — used for NESTED passthrough
|
|
22
|
+
* where the target block was just rebuilt, so `k in target` already skips
|
|
23
|
+
* any key the rebuild populated. Splitting the two makes callsites
|
|
24
|
+
* self-documenting without a verbose `new Set()` argument at every nested
|
|
25
|
+
* call (A-002 architecture-review follow-up).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Keys that would mutate Object.prototype if copied as own properties. Explicit
|
|
29
|
+
* denylist defense-in-depth on top of the existing `k in target` guard and the
|
|
30
|
+
* `yaml@2.8` parser's non-polluting behavior (S-001 security-review follow-up). */
|
|
31
|
+
const UNSAFE_KEYS: ReadonlySet<string> = new Set(['__proto__', 'constructor', 'prototype']);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copy any key from `source` into `target` that target doesn't already have set,
|
|
35
|
+
* skipping keys listed in `handledKeys`. Target values ALWAYS win — this function
|
|
36
|
+
* never overwrites an existing target key.
|
|
37
|
+
*
|
|
38
|
+
* - If source[k] is undefined → skip (undefined is not a preservable value).
|
|
39
|
+
* - If k is an UNSAFE_KEYS entry → skip (prototype-pollution defense).
|
|
40
|
+
* - If handledKeys.has(k) → skip (caller has its own handling).
|
|
41
|
+
* - If target already owns k → skip (target wins).
|
|
42
|
+
* - Otherwise → target[k] = deepClone(source[k]).
|
|
43
|
+
*
|
|
44
|
+
* Values are DEEP-CLONED via structuredClone so that mutating the output v2
|
|
45
|
+
* object never reaches back into the v1 input (S-002 security-review follow-up).
|
|
46
|
+
* Preserves the migrator's "pure data in, pure data out" contract.
|
|
47
|
+
*/
|
|
48
|
+
export function copyUnknownKeys(
|
|
49
|
+
source: Record<string, unknown>,
|
|
50
|
+
target: Record<string, unknown>,
|
|
51
|
+
handledKeys: ReadonlySet<string>
|
|
52
|
+
): void {
|
|
53
|
+
if (source === null || typeof source !== 'object' || Array.isArray(source)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const k of Object.keys(source)) {
|
|
57
|
+
if (UNSAFE_KEYS.has(k)) continue;
|
|
58
|
+
if (source[k] === undefined) continue;
|
|
59
|
+
if (handledKeys.has(k)) continue;
|
|
60
|
+
if (Object.prototype.hasOwnProperty.call(target, k)) continue;
|
|
61
|
+
target[k] = safeClone(source[k]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Preserve every subkey from sourceBlock into targetBlock that targetBlock
|
|
67
|
+
* doesn't already have. Target values ALWAYS win.
|
|
68
|
+
*
|
|
69
|
+
* If sourceBlock is not a plain object (string, array, null, undefined),
|
|
70
|
+
* return early — there are no subkeys to preserve. This matches the coercion
|
|
71
|
+
* semantics of `getRecord` in migrate.ts and prevents throws on loose v1 inputs
|
|
72
|
+
* like `framework: "typescript"`.
|
|
73
|
+
*
|
|
74
|
+
* Values are deep-cloned (see copyUnknownKeys); UNSAFE_KEYS are skipped.
|
|
75
|
+
*/
|
|
76
|
+
export function preserveNestedSubkeys(
|
|
77
|
+
sourceBlock: unknown,
|
|
78
|
+
targetBlock: Record<string, unknown>
|
|
79
|
+
): void {
|
|
80
|
+
if (
|
|
81
|
+
sourceBlock === null ||
|
|
82
|
+
sourceBlock === undefined ||
|
|
83
|
+
typeof sourceBlock !== 'object' ||
|
|
84
|
+
Array.isArray(sourceBlock)
|
|
85
|
+
) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const src = sourceBlock as Record<string, unknown>;
|
|
89
|
+
for (const k of Object.keys(src)) {
|
|
90
|
+
if (UNSAFE_KEYS.has(k)) continue;
|
|
91
|
+
if (src[k] === undefined) continue;
|
|
92
|
+
if (Object.prototype.hasOwnProperty.call(targetBlock, k)) continue;
|
|
93
|
+
targetBlock[k] = safeClone(src[k]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** structuredClone with a fallback for environments without it (Node <17). */
|
|
98
|
+
function safeClone<T>(v: T): T {
|
|
99
|
+
if (typeof structuredClone === 'function') {
|
|
100
|
+
try {
|
|
101
|
+
return structuredClone(v);
|
|
102
|
+
} catch {
|
|
103
|
+
// structuredClone throws on functions, DOM nodes, etc. — YAML never produces
|
|
104
|
+
// those, but if a caller passes something exotic, fall through to shallow.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return v;
|
|
108
|
+
}
|
|
@@ -109,6 +109,25 @@ function extsFor(language: SupportedLanguage): string[] {
|
|
|
109
109
|
return EXTENSIONS[language] ?? [];
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Extensions for a language, with optional fallback for javascript-only repos
|
|
114
|
+
* that still contain `.ts`/`.tsx` files (no `typescript` manifest, no
|
|
115
|
+
* `tsconfig.json`). Fixes the bug where `apps/web/page.tsx` in a plain-JS
|
|
116
|
+
* monorepo is invisible to the javascript glob, causing `init --ci` to fall
|
|
117
|
+
* back to the nonexistent `src/`. See plan item P1-001 and incident
|
|
118
|
+
* `2026-04-20-massu-core-monorepo-paths-source.md`.
|
|
119
|
+
*/
|
|
120
|
+
function extsWithFallback(
|
|
121
|
+
language: SupportedLanguage,
|
|
122
|
+
fallbackTsForJs: boolean
|
|
123
|
+
): string[] {
|
|
124
|
+
const base = extsFor(language);
|
|
125
|
+
if (language === 'javascript' && fallbackTsForJs) {
|
|
126
|
+
return [...base, 'ts', 'tsx'];
|
|
127
|
+
}
|
|
128
|
+
return base;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
function isTestPath(language: SupportedLanguage, path: string): boolean {
|
|
113
132
|
// Any dedicated test-dir keyword in the path segments
|
|
114
133
|
const segments = path.split('/');
|
|
@@ -144,14 +163,21 @@ function isInsideRoot(root: string, candidate: string): boolean {
|
|
|
144
163
|
*
|
|
145
164
|
* @param projectRoot absolute path to repo root
|
|
146
165
|
* @param languages list of languages to probe (derived from P1-001 manifests)
|
|
166
|
+
* @param opts optional flags:
|
|
167
|
+
* - `fallbackTsForJs`: when true AND the language is `javascript` AND no
|
|
168
|
+
* `typescript` manifest was discovered, also glob `.ts`/`.tsx`. This is
|
|
169
|
+
* the P1-001 fix for plain-JS monorepos (e.g. turbo with `next` in a
|
|
170
|
+
* package.json that lacks a typescript dep) that still use `.tsx`.
|
|
147
171
|
*/
|
|
148
172
|
export function detectSourceDirs(
|
|
149
173
|
projectRoot: string,
|
|
150
|
-
languages: SupportedLanguage[]
|
|
174
|
+
languages: SupportedLanguage[],
|
|
175
|
+
opts?: { fallbackTsForJs?: boolean }
|
|
151
176
|
): SourceDirMap {
|
|
177
|
+
const fallbackTsForJs = opts?.fallbackTsForJs ?? false;
|
|
152
178
|
const out: SourceDirMap = {};
|
|
153
179
|
for (const lang of languages) {
|
|
154
|
-
const exts =
|
|
180
|
+
const exts = extsWithFallback(lang, fallbackTsForJs);
|
|
155
181
|
if (exts.length === 0) continue;
|
|
156
182
|
const patterns = exts.map((e) => `**/*.${e}`);
|
|
157
183
|
let files: string[];
|