@massu/core 1.2.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 +80 -8
- 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/init.ts +89 -4
- package/src/config.ts +5 -0
- package/src/detect/index.ts +10 -1
- package/src/detect/migrate.ts +52 -1
- 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);
|
|
@@ -22958,13 +23008,21 @@ function migrateV1ToV2(v1Config, detection) {
|
|
|
22958
23008
|
let pathsSource = typeof v1Paths.source === "string" ? v1Paths.source : "src";
|
|
22959
23009
|
if (pathsSource === "src" && primary) {
|
|
22960
23010
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
22961
|
-
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
|
+
}
|
|
22962
23016
|
}
|
|
22963
23017
|
const aliases = v1Paths.aliases && typeof v1Paths.aliases === "object" ? v1Paths.aliases : { "@": pathsSource };
|
|
22964
23018
|
const paths = {
|
|
22965
23019
|
source: pathsSource,
|
|
22966
23020
|
aliases
|
|
22967
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
|
+
}
|
|
22968
23026
|
for (const k3 of ["routers", "routerRoot", "pages", "middleware", "schema", "components", "hooks"]) {
|
|
22969
23027
|
if (typeof v1Paths[k3] === "string") paths[k3] = v1Paths[k3];
|
|
22970
23028
|
}
|
|
@@ -23035,6 +23093,20 @@ function migrateV1ToV2(v1Config, detection) {
|
|
|
23035
23093
|
}
|
|
23036
23094
|
return v22;
|
|
23037
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
|
+
}
|
|
23038
23110
|
var PRESERVED_FIELDS2;
|
|
23039
23111
|
var init_migrate = __esm({
|
|
23040
23112
|
"src/detect/migrate.ts"() {
|
|
@@ -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.2.
|
|
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",
|
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
|
@@ -196,10 +196,21 @@ export function migrateV1ToV2(
|
|
|
196
196
|
preserveNestedSubkeys(v1Framework, framework);
|
|
197
197
|
|
|
198
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.
|
|
199
202
|
let pathsSource: string = typeof v1Paths.source === 'string' ? v1Paths.source : 'src';
|
|
200
203
|
if (pathsSource === 'src' && primary) {
|
|
201
204
|
const primaryDirs = detection.sourceDirs[primary]?.source_dirs ?? [];
|
|
202
|
-
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
|
+
}
|
|
203
214
|
}
|
|
204
215
|
const aliases = v1Paths.aliases && typeof v1Paths.aliases === 'object'
|
|
205
216
|
? (v1Paths.aliases as Record<string, string>)
|
|
@@ -208,6 +219,17 @@ export function migrateV1ToV2(
|
|
|
208
219
|
source: pathsSource,
|
|
209
220
|
aliases,
|
|
210
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
|
+
}
|
|
211
233
|
for (const k of ['routers', 'routerRoot', 'pages', 'middleware', 'schema', 'components', 'hooks']) {
|
|
212
234
|
if (typeof v1Paths[k] === 'string') paths[k] = v1Paths[k];
|
|
213
235
|
}
|
|
@@ -309,3 +331,32 @@ export function migrateV1ToV2(
|
|
|
309
331
|
|
|
310
332
|
return v2;
|
|
311
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
|
+
}
|
|
@@ -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[];
|