@massu/core 1.5.0 → 1.5.2
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 +368 -56
- package/dist/hooks/session-start.js +271 -53
- package/package.json +1 -1
- package/src/commands/init.ts +159 -2
- package/src/detect/framework-detector.ts +26 -0
- package/src/detect/manifest-registry.ts +261 -0
- package/src/detect/package-detector.ts +162 -62
- package/src/detect/source-dir-detector.ts +7 -0
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/templates/aspnet/massu.config.yaml +5 -1
|
@@ -15,6 +15,10 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
15
15
|
var __commonJS = (cb, mod) => function __require2() {
|
|
16
16
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
17
17
|
};
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name2 in all)
|
|
20
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
21
|
+
};
|
|
18
22
|
var __copyProps = (to, from, except, desc) => {
|
|
19
23
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
24
|
for (let key of __getOwnPropNames(from))
|
|
@@ -7589,6 +7593,160 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
|
7589
7593
|
return res;
|
|
7590
7594
|
}
|
|
7591
7595
|
|
|
7596
|
+
// src/detect/manifest-registry.ts
|
|
7597
|
+
var manifest_registry_exports = {};
|
|
7598
|
+
__export(manifest_registry_exports, {
|
|
7599
|
+
getManifestPatterns: () => getManifestPatterns,
|
|
7600
|
+
getManifestRegistry: () => getManifestRegistry,
|
|
7601
|
+
matchManifestPattern: () => matchManifestPattern
|
|
7602
|
+
});
|
|
7603
|
+
function matchManifestPattern(name2, pattern) {
|
|
7604
|
+
if (pattern.startsWith("*")) {
|
|
7605
|
+
const suffix = pattern.slice(1);
|
|
7606
|
+
if (suffix.includes("*")) {
|
|
7607
|
+
throw new Error(
|
|
7608
|
+
`[manifest-registry] pattern "${pattern}" has more than one wildcard. Only "*.<ext>" extension-globs are supported.`
|
|
7609
|
+
);
|
|
7610
|
+
}
|
|
7611
|
+
return name2.endsWith(suffix);
|
|
7612
|
+
}
|
|
7613
|
+
return name2 === pattern;
|
|
7614
|
+
}
|
|
7615
|
+
var _registryCache = null;
|
|
7616
|
+
function getManifestRegistry() {
|
|
7617
|
+
if (_registryCache !== null) return _registryCache;
|
|
7618
|
+
_registryCache = [
|
|
7619
|
+
{
|
|
7620
|
+
pattern: "package.json",
|
|
7621
|
+
manifestType: "package.json",
|
|
7622
|
+
language: "typescript",
|
|
7623
|
+
runtime: "node",
|
|
7624
|
+
parse: parsePackageJson,
|
|
7625
|
+
signalKey: "packageJson",
|
|
7626
|
+
signalShape: "json"
|
|
7627
|
+
},
|
|
7628
|
+
{
|
|
7629
|
+
pattern: "pyproject.toml",
|
|
7630
|
+
manifestType: "pyproject.toml",
|
|
7631
|
+
language: "python",
|
|
7632
|
+
runtime: "python3",
|
|
7633
|
+
parse: parsePyproject,
|
|
7634
|
+
signalKey: "pyprojectToml",
|
|
7635
|
+
signalShape: "toml"
|
|
7636
|
+
},
|
|
7637
|
+
{
|
|
7638
|
+
pattern: "requirements.txt",
|
|
7639
|
+
manifestType: "requirements.txt",
|
|
7640
|
+
language: "python",
|
|
7641
|
+
runtime: "python3",
|
|
7642
|
+
parse: parseRequirementsTxt,
|
|
7643
|
+
// Captured via pyprojectToml sibling already; no separate signal.
|
|
7644
|
+
signalKey: null,
|
|
7645
|
+
signalShape: "string"
|
|
7646
|
+
},
|
|
7647
|
+
{
|
|
7648
|
+
pattern: "Pipfile",
|
|
7649
|
+
manifestType: "Pipfile",
|
|
7650
|
+
language: "python",
|
|
7651
|
+
runtime: "python3",
|
|
7652
|
+
parse: parsePipfile,
|
|
7653
|
+
// Captured via pyprojectToml sibling already; no separate signal.
|
|
7654
|
+
signalKey: null,
|
|
7655
|
+
signalShape: "string"
|
|
7656
|
+
},
|
|
7657
|
+
{
|
|
7658
|
+
pattern: "Cargo.toml",
|
|
7659
|
+
manifestType: "Cargo.toml",
|
|
7660
|
+
language: "rust",
|
|
7661
|
+
runtime: "cargo",
|
|
7662
|
+
parse: parseCargoToml,
|
|
7663
|
+
signalKey: "cargoToml",
|
|
7664
|
+
signalShape: "toml"
|
|
7665
|
+
},
|
|
7666
|
+
{
|
|
7667
|
+
pattern: "Package.swift",
|
|
7668
|
+
manifestType: "Package.swift",
|
|
7669
|
+
language: "swift",
|
|
7670
|
+
runtime: "xcode",
|
|
7671
|
+
parse: parsePackageSwift,
|
|
7672
|
+
// No AST adapter consumer yet (swift-swiftui doesn't need it).
|
|
7673
|
+
signalKey: null,
|
|
7674
|
+
signalShape: "string"
|
|
7675
|
+
},
|
|
7676
|
+
{
|
|
7677
|
+
pattern: "go.mod",
|
|
7678
|
+
manifestType: "go.mod",
|
|
7679
|
+
language: "go",
|
|
7680
|
+
runtime: "go",
|
|
7681
|
+
parse: parseGoMod,
|
|
7682
|
+
signalKey: "goMod",
|
|
7683
|
+
signalShape: "string"
|
|
7684
|
+
},
|
|
7685
|
+
{
|
|
7686
|
+
pattern: "pom.xml",
|
|
7687
|
+
manifestType: "pom.xml",
|
|
7688
|
+
language: "java",
|
|
7689
|
+
runtime: "jvm",
|
|
7690
|
+
parse: parsePomXml,
|
|
7691
|
+
signalKey: "pomXml",
|
|
7692
|
+
signalShape: "string"
|
|
7693
|
+
},
|
|
7694
|
+
{
|
|
7695
|
+
pattern: "build.gradle",
|
|
7696
|
+
manifestType: "build.gradle",
|
|
7697
|
+
language: "java",
|
|
7698
|
+
runtime: "jvm",
|
|
7699
|
+
parse: parseBuildGradle,
|
|
7700
|
+
signalKey: "gradleBuild",
|
|
7701
|
+
signalShape: "string"
|
|
7702
|
+
},
|
|
7703
|
+
{
|
|
7704
|
+
pattern: "build.gradle.kts",
|
|
7705
|
+
manifestType: "build.gradle",
|
|
7706
|
+
language: "java",
|
|
7707
|
+
runtime: "jvm",
|
|
7708
|
+
parse: parseBuildGradle,
|
|
7709
|
+
signalKey: "gradleBuild",
|
|
7710
|
+
signalShape: "string"
|
|
7711
|
+
},
|
|
7712
|
+
{
|
|
7713
|
+
pattern: "Gemfile",
|
|
7714
|
+
manifestType: "Gemfile",
|
|
7715
|
+
language: "ruby",
|
|
7716
|
+
runtime: "ruby",
|
|
7717
|
+
parse: parseGemfile,
|
|
7718
|
+
signalKey: "gemfile",
|
|
7719
|
+
signalShape: "string"
|
|
7720
|
+
},
|
|
7721
|
+
// Plan 1.5.1 — closes CR-39 violation (1.5.0 init failed for Phoenix
|
|
7722
|
+
// + ASP.NET fixtures). Both rely on AST adapters that already work
|
|
7723
|
+
// in introspect; the gap was solely package-detector unaware of the
|
|
7724
|
+
// manifest filenames.
|
|
7725
|
+
{
|
|
7726
|
+
pattern: "mix.exs",
|
|
7727
|
+
manifestType: "mix.exs",
|
|
7728
|
+
language: "elixir",
|
|
7729
|
+
runtime: "beam",
|
|
7730
|
+
parse: parseMixExs,
|
|
7731
|
+
signalKey: "mixExs",
|
|
7732
|
+
signalShape: "string"
|
|
7733
|
+
},
|
|
7734
|
+
{
|
|
7735
|
+
pattern: "*.csproj",
|
|
7736
|
+
manifestType: "*.csproj",
|
|
7737
|
+
language: "csharp",
|
|
7738
|
+
runtime: "dotnet",
|
|
7739
|
+
parse: parseCsproj,
|
|
7740
|
+
signalKey: "csproj",
|
|
7741
|
+
signalShape: "string"
|
|
7742
|
+
}
|
|
7743
|
+
];
|
|
7744
|
+
return _registryCache;
|
|
7745
|
+
}
|
|
7746
|
+
function getManifestPatterns() {
|
|
7747
|
+
return getManifestRegistry().map((e) => e.pattern);
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7592
7750
|
// src/detect/package-detector.ts
|
|
7593
7751
|
var WORKSPACE_DIRS = ["apps", "packages", "services", "libs", "modules"];
|
|
7594
7752
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -7612,19 +7770,6 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
7612
7770
|
"DerivedData",
|
|
7613
7771
|
"Pods"
|
|
7614
7772
|
]);
|
|
7615
|
-
var MANIFEST_FILES = [
|
|
7616
|
-
"package.json",
|
|
7617
|
-
"pyproject.toml",
|
|
7618
|
-
"requirements.txt",
|
|
7619
|
-
"Pipfile",
|
|
7620
|
-
"Cargo.toml",
|
|
7621
|
-
"Package.swift",
|
|
7622
|
-
"go.mod",
|
|
7623
|
-
"pom.xml",
|
|
7624
|
-
"build.gradle",
|
|
7625
|
-
"build.gradle.kts",
|
|
7626
|
-
"Gemfile"
|
|
7627
|
-
];
|
|
7628
7773
|
function safeRead(path) {
|
|
7629
7774
|
try {
|
|
7630
7775
|
if (!existsSync3(path)) return null;
|
|
@@ -8029,46 +8174,86 @@ function parseGemfile(path, directory, root, _warnings) {
|
|
|
8029
8174
|
manifestType: "Gemfile"
|
|
8030
8175
|
};
|
|
8031
8176
|
}
|
|
8177
|
+
function parseMixExs(path, directory, root, _warnings) {
|
|
8178
|
+
const raw = safeRead(path);
|
|
8179
|
+
if (raw === null) return null;
|
|
8180
|
+
const deps = [];
|
|
8181
|
+
const depPattern = /\{\s*:([a-z][a-z0-9_]*)\s*,/g;
|
|
8182
|
+
let m;
|
|
8183
|
+
while ((m = depPattern.exec(raw)) !== null) {
|
|
8184
|
+
if (!deps.includes(m[1])) deps.push(m[1]);
|
|
8185
|
+
}
|
|
8186
|
+
const appMatch = /\bapp\s*:\s*:([a-z][a-z0-9_]*)/.exec(raw);
|
|
8187
|
+
const name2 = appMatch ? appMatch[1] : null;
|
|
8188
|
+
return {
|
|
8189
|
+
path,
|
|
8190
|
+
relativePath: normalizeRelative(root, path),
|
|
8191
|
+
directory,
|
|
8192
|
+
language: "elixir",
|
|
8193
|
+
runtime: "beam",
|
|
8194
|
+
name: name2,
|
|
8195
|
+
version: null,
|
|
8196
|
+
dependencies: deps,
|
|
8197
|
+
devDependencies: [],
|
|
8198
|
+
scripts: [],
|
|
8199
|
+
manifestType: "mix.exs"
|
|
8200
|
+
};
|
|
8201
|
+
}
|
|
8202
|
+
function parseCsproj(path, directory, root, _warnings) {
|
|
8203
|
+
const raw = safeRead(path);
|
|
8204
|
+
if (raw === null) return null;
|
|
8205
|
+
const deps = [];
|
|
8206
|
+
const pkgRefPattern = /<PackageReference\s+[^>]*Include\s*=\s*"([^"]+)"/gi;
|
|
8207
|
+
let m;
|
|
8208
|
+
while ((m = pkgRefPattern.exec(raw)) !== null) {
|
|
8209
|
+
if (!deps.includes(m[1])) deps.push(m[1]);
|
|
8210
|
+
}
|
|
8211
|
+
const sdkMatch = /<Project\s+[^>]*Sdk\s*=\s*"([^"]+)"/i.exec(raw);
|
|
8212
|
+
if (sdkMatch && !deps.includes(sdkMatch[1])) {
|
|
8213
|
+
deps.push(sdkMatch[1]);
|
|
8214
|
+
}
|
|
8215
|
+
const fname = path.split(/[/\\]/).pop() ?? "";
|
|
8216
|
+
const name2 = fname.endsWith(".csproj") ? fname.slice(0, -".csproj".length) : null;
|
|
8217
|
+
return {
|
|
8218
|
+
path,
|
|
8219
|
+
relativePath: normalizeRelative(root, path),
|
|
8220
|
+
directory,
|
|
8221
|
+
language: "csharp",
|
|
8222
|
+
runtime: "dotnet",
|
|
8223
|
+
name: name2,
|
|
8224
|
+
version: null,
|
|
8225
|
+
dependencies: deps,
|
|
8226
|
+
devDependencies: [],
|
|
8227
|
+
scripts: [],
|
|
8228
|
+
manifestType: "*.csproj"
|
|
8229
|
+
};
|
|
8230
|
+
}
|
|
8032
8231
|
function detectManifestsInDir(dir, root, warnings) {
|
|
8232
|
+
const { getManifestRegistry: getManifestRegistry2, matchManifestPattern: matchManifestPattern2 } = manifest_registry_exports;
|
|
8033
8233
|
const out2 = [];
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
if (!
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
break;
|
|
8057
|
-
case "go.mod":
|
|
8058
|
-
m = parseGoMod(path, dir, root, warnings);
|
|
8059
|
-
break;
|
|
8060
|
-
case "pom.xml":
|
|
8061
|
-
m = parsePomXml(path, dir, root, warnings);
|
|
8062
|
-
break;
|
|
8063
|
-
case "build.gradle":
|
|
8064
|
-
case "build.gradle.kts":
|
|
8065
|
-
m = parseBuildGradle(path, dir, root, warnings);
|
|
8066
|
-
break;
|
|
8067
|
-
case "Gemfile":
|
|
8068
|
-
m = parseGemfile(path, dir, root, warnings);
|
|
8069
|
-
break;
|
|
8234
|
+
let dirEntries = null;
|
|
8235
|
+
for (const entry of getManifestRegistry2()) {
|
|
8236
|
+
if (!entry.pattern.startsWith("*")) {
|
|
8237
|
+
const path = join(dir, entry.pattern);
|
|
8238
|
+
if (!existsSync3(path)) continue;
|
|
8239
|
+
const m = entry.parse(path, dir, root, warnings);
|
|
8240
|
+
if (m !== null) out2.push(m);
|
|
8241
|
+
} else {
|
|
8242
|
+
if (dirEntries === null) {
|
|
8243
|
+
try {
|
|
8244
|
+
dirEntries = readdirSync(dir);
|
|
8245
|
+
} catch {
|
|
8246
|
+
dirEntries = [];
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
for (const fname of dirEntries) {
|
|
8250
|
+
if (!matchManifestPattern2(fname, entry.pattern)) continue;
|
|
8251
|
+
const path = join(dir, fname);
|
|
8252
|
+
if (!existsSync3(path)) continue;
|
|
8253
|
+
const m = entry.parse(path, dir, root, warnings);
|
|
8254
|
+
if (m !== null) out2.push(m);
|
|
8255
|
+
}
|
|
8070
8256
|
}
|
|
8071
|
-
if (m !== null) out2.push(m);
|
|
8072
8257
|
}
|
|
8073
8258
|
return out2;
|
|
8074
8259
|
}
|
|
@@ -8181,6 +8366,13 @@ var DETECTION_RULES = [
|
|
|
8181
8366
|
{ language: "go", kind: "framework", keyword: "github.com/labstack/echo", value: "echo", priority: 10 },
|
|
8182
8367
|
{ language: "go", kind: "framework", keyword: "github.com/gofiber/fiber", value: "fiber", priority: 10 },
|
|
8183
8368
|
{ language: "go", kind: "framework", keyword: "github.com/go-chi/chi", value: "chi", priority: 9 },
|
|
8369
|
+
// chi versioned import paths (Go convention: github.com/<org>/<name>/v<N>).
|
|
8370
|
+
// matchRule does exact case-insensitive set lookup, so the unversioned and
|
|
8371
|
+
// each major-version path each need their own rule.
|
|
8372
|
+
{ language: "go", kind: "framework", keyword: "github.com/go-chi/chi/v2", value: "chi", priority: 9 },
|
|
8373
|
+
{ language: "go", kind: "framework", keyword: "github.com/go-chi/chi/v3", value: "chi", priority: 9 },
|
|
8374
|
+
{ language: "go", kind: "framework", keyword: "github.com/go-chi/chi/v4", value: "chi", priority: 9 },
|
|
8375
|
+
{ language: "go", kind: "framework", keyword: "github.com/go-chi/chi/v5", value: "chi", priority: 9 },
|
|
8184
8376
|
{ language: "go", kind: "test_framework", keyword: "github.com/stretchr/testify", value: "testify", priority: 8 },
|
|
8185
8377
|
{ language: "go", kind: "orm", keyword: "gorm.io/gorm", value: "gorm", priority: 10 },
|
|
8186
8378
|
// Swift (SPM dependency names, best-effort)
|
|
@@ -8196,7 +8388,26 @@ var DETECTION_RULES = [
|
|
|
8196
8388
|
{ language: "ruby", kind: "framework", keyword: "rails", value: "rails", priority: 10 },
|
|
8197
8389
|
{ language: "ruby", kind: "framework", keyword: "sinatra", value: "sinatra", priority: 9 },
|
|
8198
8390
|
{ language: "ruby", kind: "test_framework", keyword: "rspec", value: "rspec", priority: 10 },
|
|
8199
|
-
{ language: "ruby", kind: "orm", keyword: "activerecord", value: "activerecord", priority: 10 }
|
|
8391
|
+
{ language: "ruby", kind: "orm", keyword: "activerecord", value: "activerecord", priority: 10 },
|
|
8392
|
+
// Plan 1.5.1: elixir + csharp framework rules. Closes the CR-39 gap
|
|
8393
|
+
// where Phoenix + ASP.NET projects produced `framework.languages.<lang>`
|
|
8394
|
+
// entries WITHOUT a `framework:` value, which prevented variant
|
|
8395
|
+
// templates from being looked up.
|
|
8396
|
+
{ language: "elixir", kind: "framework", keyword: "phoenix", value: "phoenix", priority: 10 },
|
|
8397
|
+
{ language: "elixir", kind: "test_framework", keyword: "ex_unit", value: "ex-unit", priority: 10 },
|
|
8398
|
+
{ language: "elixir", kind: "orm", keyword: "ecto", value: "ecto", priority: 10 },
|
|
8399
|
+
// ASP.NET Core surfaces via several PackageReference names; the canonical
|
|
8400
|
+
// ones in modern .NET projects are .App and .Mvc. matchRule does exact
|
|
8401
|
+
// (case-insensitive) lookup against the deps set parseCsproj extracts.
|
|
8402
|
+
{ language: "csharp", kind: "framework", keyword: "Microsoft.AspNetCore.App", value: "aspnet-core", priority: 10 },
|
|
8403
|
+
{ language: "csharp", kind: "framework", keyword: "Microsoft.AspNetCore.Mvc", value: "aspnet-core", priority: 10 },
|
|
8404
|
+
{ language: "csharp", kind: "framework", keyword: "Microsoft.AspNetCore", value: "aspnet-core", priority: 9 },
|
|
8405
|
+
// SDK-style projects: `<Project Sdk="Microsoft.NET.Sdk.Web">` is the
|
|
8406
|
+
// canonical ASP.NET Core declaration in modern .NET. parseCsproj
|
|
8407
|
+
// surfaces the Sdk attribute as a dep so this rule can match.
|
|
8408
|
+
{ language: "csharp", kind: "framework", keyword: "Microsoft.NET.Sdk.Web", value: "aspnet-core", priority: 10 },
|
|
8409
|
+
{ language: "csharp", kind: "test_framework", keyword: "xunit", value: "xunit", priority: 10 },
|
|
8410
|
+
{ language: "csharp", kind: "orm", keyword: "EntityFrameworkCore", value: "ef-core", priority: 10 }
|
|
8200
8411
|
];
|
|
8201
8412
|
function matchRule(rules, language, kind, deps) {
|
|
8202
8413
|
let best = null;
|
|
@@ -8312,7 +8523,10 @@ var EXTENSIONS = {
|
|
|
8312
8523
|
swift: ["swift"],
|
|
8313
8524
|
go: ["go"],
|
|
8314
8525
|
java: ["java", "kt"],
|
|
8315
|
-
ruby: ["rb"]
|
|
8526
|
+
ruby: ["rb"],
|
|
8527
|
+
// Plan 1.5.1 — closing CR-39 init gap for Phoenix + ASP.NET projects.
|
|
8528
|
+
elixir: ["ex", "exs"],
|
|
8529
|
+
csharp: ["cs"]
|
|
8316
8530
|
};
|
|
8317
8531
|
var TEST_FILE_PATTERNS = {
|
|
8318
8532
|
python: [/_test\.py$/, /test_[^/]*\.py$/],
|
|
@@ -8322,7 +8536,11 @@ var TEST_FILE_PATTERNS = {
|
|
|
8322
8536
|
swift: [/Tests\//],
|
|
8323
8537
|
go: [/_test\.go$/],
|
|
8324
8538
|
java: [/Test[^/]*\.(java|kt)$/, /[^/]*Test\.(java|kt)$/],
|
|
8325
|
-
ruby: [/_spec\.rb$/, /_test\.rb$/]
|
|
8539
|
+
ruby: [/_spec\.rb$/, /_test\.rb$/],
|
|
8540
|
+
// Phoenix/ExUnit canonical: `test/**_test.exs`. ASP.NET / xUnit
|
|
8541
|
+
// canonical: `*Tests.cs` or `*.Tests/...`.
|
|
8542
|
+
elixir: [/_test\.exs$/, /\/test\//],
|
|
8543
|
+
csharp: [/Tests?\.cs$/, /\.Tests?\//]
|
|
8326
8544
|
};
|
|
8327
8545
|
var TEST_DIR_KEYWORDS = ["tests", "test", "__tests__", "spec", "specs"];
|
|
8328
8546
|
function extsFor(language) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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
|
@@ -543,6 +543,145 @@ export function buildConfigFromDetection(
|
|
|
543
543
|
return config;
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Plan 1.5.1 §3 — variant template merge.
|
|
548
|
+
*
|
|
549
|
+
* Map from detected `framework.languages.<lang>.framework` value → variant
|
|
550
|
+
* template directory under `packages/core/templates/`. Most detected
|
|
551
|
+
* frameworks map 1:1 to a template dir of the same name, but a few have
|
|
552
|
+
* naming divergence (e.g., detection emits `spring-boot` but the template
|
|
553
|
+
* dir is `spring`; detection emits `chi` but the template dir is `go-chi`).
|
|
554
|
+
*
|
|
555
|
+
* The mapping is intentionally tight — only frameworks with an actual
|
|
556
|
+
* variant template under `templates/` are listed. Adding a new framework
|
|
557
|
+
* = one entry here + one templates/<id>/massu.config.yaml file. The
|
|
558
|
+
* `manifest-registry-drift.test.ts` already gates the manifest side; the
|
|
559
|
+
* `init-end-to-end.test.ts` gates this map.
|
|
560
|
+
*/
|
|
561
|
+
const FRAMEWORK_TO_TEMPLATE_ID: Record<string, string> = {
|
|
562
|
+
rails: 'rails',
|
|
563
|
+
phoenix: 'phoenix',
|
|
564
|
+
'aspnet-core': 'aspnet',
|
|
565
|
+
'spring-boot': 'spring',
|
|
566
|
+
chi: 'go-chi',
|
|
567
|
+
flask: 'python-flask',
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* After `buildConfigFromDetection` produces a baseline config, look up the
|
|
572
|
+
* variant template for the detected framework (if any) and selectively
|
|
573
|
+
* merge fields. The variant wins on a small allowlist:
|
|
574
|
+
* - `framework.router`
|
|
575
|
+
* - `framework.orm`
|
|
576
|
+
* - `framework.ui`
|
|
577
|
+
* - `paths.source`
|
|
578
|
+
* - `verification.<lang>.{lint,syntax,test,type,build}` — variant lint
|
|
579
|
+
* is the canonical project-style command (rubocop, credo, etc.) and
|
|
580
|
+
* should not be overridden by the generic detection default.
|
|
581
|
+
*
|
|
582
|
+
* The variant template's `framework.type`, `framework.languages`,
|
|
583
|
+
* `project.name`, `domains`, and `rules` are NOT merged — those come from
|
|
584
|
+
* detection and reflect the actual repo state. Allowlist keeps the merge
|
|
585
|
+
* precise; future fields require an explicit decision.
|
|
586
|
+
*/
|
|
587
|
+
export function applyVariantTemplate(
|
|
588
|
+
config: Record<string, unknown>,
|
|
589
|
+
templatesDir: string | null,
|
|
590
|
+
): Record<string, unknown> {
|
|
591
|
+
if (!templatesDir) return config;
|
|
592
|
+
const fw = config.framework as Record<string, unknown> | undefined;
|
|
593
|
+
if (!fw) return config;
|
|
594
|
+
const langs = fw.languages as Record<string, unknown> | undefined;
|
|
595
|
+
if (!langs || typeof langs !== 'object') return config;
|
|
596
|
+
|
|
597
|
+
// Find the first language that has a `framework` value with a known
|
|
598
|
+
// variant template. Most projects have ONE primary language; in
|
|
599
|
+
// monorepos the detection-driven primary is what we honor.
|
|
600
|
+
let templateId: string | null = null;
|
|
601
|
+
for (const langEntry of Object.values(langs)) {
|
|
602
|
+
if (langEntry && typeof langEntry === 'object') {
|
|
603
|
+
const fwName = (langEntry as Record<string, unknown>).framework;
|
|
604
|
+
if (typeof fwName === 'string' && FRAMEWORK_TO_TEMPLATE_ID[fwName]) {
|
|
605
|
+
templateId = FRAMEWORK_TO_TEMPLATE_ID[fwName];
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (templateId === null) return config;
|
|
611
|
+
|
|
612
|
+
const templatePath = resolve(templatesDir, templateId, 'massu.config.yaml');
|
|
613
|
+
if (!existsSync(templatePath)) return config;
|
|
614
|
+
|
|
615
|
+
let template: Record<string, unknown>;
|
|
616
|
+
try {
|
|
617
|
+
// pattern-scanner-allow: yaml-parse — reason: this loads a per-framework
|
|
618
|
+
// variant config-template shipped inside @massu/core. getConfig() reads
|
|
619
|
+
// the project's massu.config.yaml from cwd; this is a SEPARATE file
|
|
620
|
+
// (the template) that doesn't pass through that cache and isn't a Zod-
|
|
621
|
+
// validated config — it's a partial override map.
|
|
622
|
+
template = yamlParse(readFileSync(templatePath, 'utf-8')) as Record<string, unknown>;
|
|
623
|
+
} catch {
|
|
624
|
+
return config;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const out = { ...config };
|
|
628
|
+
const tplFw = template.framework as Record<string, unknown> | undefined;
|
|
629
|
+
const outFw = (out.framework as Record<string, unknown>) ?? {};
|
|
630
|
+
if (tplFw) {
|
|
631
|
+
if (typeof tplFw.router === 'string' && (!outFw.router || outFw.router === 'none')) {
|
|
632
|
+
outFw.router = tplFw.router;
|
|
633
|
+
}
|
|
634
|
+
if (typeof tplFw.orm === 'string' && (!outFw.orm || outFw.orm === 'none')) {
|
|
635
|
+
outFw.orm = tplFw.orm;
|
|
636
|
+
}
|
|
637
|
+
if (typeof tplFw.ui === 'string' && (!outFw.ui || outFw.ui === 'none')) {
|
|
638
|
+
outFw.ui = tplFw.ui;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
out.framework = outFw;
|
|
642
|
+
|
|
643
|
+
const tplPaths = template.paths as Record<string, unknown> | undefined;
|
|
644
|
+
const outPaths = (out.paths as Record<string, unknown>) ?? {};
|
|
645
|
+
if (tplPaths && typeof tplPaths.source === 'string' && tplPaths.source) {
|
|
646
|
+
outPaths.source = tplPaths.source;
|
|
647
|
+
}
|
|
648
|
+
out.paths = outPaths;
|
|
649
|
+
|
|
650
|
+
const tplVerify = template.verification as Record<string, Record<string, unknown>> | undefined;
|
|
651
|
+
const outVerify = (out.verification as Record<string, Record<string, unknown>>) ?? {};
|
|
652
|
+
if (tplVerify) {
|
|
653
|
+
for (const [lang, tplLangVerify] of Object.entries(tplVerify)) {
|
|
654
|
+
if (!tplLangVerify || typeof tplLangVerify !== 'object') continue;
|
|
655
|
+
const outLangVerify = outVerify[lang] ?? {};
|
|
656
|
+
// Variant wins on lint + syntax (canonical project commands like
|
|
657
|
+
// rubocop, credo, golangci-lint that the generic detection layer
|
|
658
|
+
// doesn't know to suggest). For test/type/build, prefer detection-
|
|
659
|
+
// derived values when present (e.g., monorepo `cd packages/foo`
|
|
660
|
+
// prefixing) and fall back to variant template otherwise.
|
|
661
|
+
for (const key of ['lint', 'syntax']) {
|
|
662
|
+
if (typeof tplLangVerify[key] === 'string' && tplLangVerify[key]) {
|
|
663
|
+
outLangVerify[key] = tplLangVerify[key];
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
for (const key of ['test', 'type', 'build']) {
|
|
667
|
+
if (
|
|
668
|
+
typeof tplLangVerify[key] === 'string' &&
|
|
669
|
+
tplLangVerify[key] &&
|
|
670
|
+
!outLangVerify[key]
|
|
671
|
+
) {
|
|
672
|
+
outLangVerify[key] = tplLangVerify[key];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
outVerify[lang] = outLangVerify;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (Object.keys(outVerify).length > 0) {
|
|
679
|
+
out.verification = outVerify;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
546
685
|
/**
|
|
547
686
|
* Serialize a built config object into YAML with a header comment.
|
|
548
687
|
* Safe for `writeConfigAtomic` and for `fs.writeFileSync` directly.
|
|
@@ -761,7 +900,17 @@ export function listTemplates(): readonly string[] {
|
|
|
761
900
|
export function resolveTemplatesDir(): string | null {
|
|
762
901
|
const cwd = process.cwd();
|
|
763
902
|
const candidates = [
|
|
903
|
+
// Project-local install: `<project>/node_modules/@massu/core/templates`.
|
|
764
904
|
resolve(cwd, 'node_modules/@massu/core/templates'),
|
|
905
|
+
// Bundled cli.js layout: cli.js sits at `<package>/dist/cli.js`, so
|
|
906
|
+
// templates live one level up at `<package>/templates`. (Plan 1.5.1
|
|
907
|
+
// bug discovery: pre-existing layout assumed `dist/commands/init.js`
|
|
908
|
+
// depth which never matched the bundled cli, so resolveTemplatesDir
|
|
909
|
+
// returned null in production for both `--template` mode AND the
|
|
910
|
+
// applyVariantTemplate path.)
|
|
911
|
+
resolve(__dirname, '../templates'),
|
|
912
|
+
// Legacy nested layouts retained as fallbacks (in case a future
|
|
913
|
+
// build moves cli.js back into a subdirectory).
|
|
765
914
|
resolve(__dirname, '../../templates'),
|
|
766
915
|
resolve(__dirname, '../../../templates'),
|
|
767
916
|
];
|
|
@@ -1228,8 +1377,16 @@ export async function runInit(argv?: string[], overrides?: InitOptions): Promise
|
|
|
1228
1377
|
}
|
|
1229
1378
|
}
|
|
1230
1379
|
|
|
1231
|
-
// Build config + write atomically.
|
|
1232
|
-
|
|
1380
|
+
// Build config + apply variant template + write atomically. Plan 1.5.1
|
|
1381
|
+
// §3: the framework-specific variant template under
|
|
1382
|
+
// packages/core/templates/<id>/massu.config.yaml supplies router,
|
|
1383
|
+
// paths.source, and verification.<lang>.lint that the generic
|
|
1384
|
+
// detection-derived baseline doesn't know to set. Pre-1.5.1 init
|
|
1385
|
+
// emitted configs with `router: none` even for clear-Rails / clear-
|
|
1386
|
+
// Phoenix / clear-Spring projects (CR-39 violation per the Plan 1.5.1
|
|
1387
|
+
// 5-fixture verification).
|
|
1388
|
+
const baseConfig = buildConfigFromDetection({ projectRoot, detection });
|
|
1389
|
+
const config = applyVariantTemplate(baseConfig, resolveTemplatesDir());
|
|
1233
1390
|
const content = renderConfigYaml(config);
|
|
1234
1391
|
const writeRes = writeConfigAtomic(configPath, content);
|
|
1235
1392
|
if (!writeRes.validated) {
|
|
@@ -138,6 +138,13 @@ export const DETECTION_RULES: DetectionRule[] = [
|
|
|
138
138
|
{ language: 'go', kind: 'framework', keyword: 'github.com/labstack/echo', value: 'echo', priority: 10 },
|
|
139
139
|
{ language: 'go', kind: 'framework', keyword: 'github.com/gofiber/fiber', value: 'fiber', priority: 10 },
|
|
140
140
|
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi', value: 'chi', priority: 9 },
|
|
141
|
+
// chi versioned import paths (Go convention: github.com/<org>/<name>/v<N>).
|
|
142
|
+
// matchRule does exact case-insensitive set lookup, so the unversioned and
|
|
143
|
+
// each major-version path each need their own rule.
|
|
144
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v2', value: 'chi', priority: 9 },
|
|
145
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v3', value: 'chi', priority: 9 },
|
|
146
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v4', value: 'chi', priority: 9 },
|
|
147
|
+
{ language: 'go', kind: 'framework', keyword: 'github.com/go-chi/chi/v5', value: 'chi', priority: 9 },
|
|
141
148
|
{ language: 'go', kind: 'test_framework', keyword: 'github.com/stretchr/testify', value: 'testify', priority: 8 },
|
|
142
149
|
{ language: 'go', kind: 'orm', keyword: 'gorm.io/gorm', value: 'gorm', priority: 10 },
|
|
143
150
|
|
|
@@ -157,6 +164,25 @@ export const DETECTION_RULES: DetectionRule[] = [
|
|
|
157
164
|
{ language: 'ruby', kind: 'framework', keyword: 'sinatra', value: 'sinatra', priority: 9 },
|
|
158
165
|
{ language: 'ruby', kind: 'test_framework', keyword: 'rspec', value: 'rspec', priority: 10 },
|
|
159
166
|
{ language: 'ruby', kind: 'orm', keyword: 'activerecord', value: 'activerecord', priority: 10 },
|
|
167
|
+
// Plan 1.5.1: elixir + csharp framework rules. Closes the CR-39 gap
|
|
168
|
+
// where Phoenix + ASP.NET projects produced `framework.languages.<lang>`
|
|
169
|
+
// entries WITHOUT a `framework:` value, which prevented variant
|
|
170
|
+
// templates from being looked up.
|
|
171
|
+
{ language: 'elixir', kind: 'framework', keyword: 'phoenix', value: 'phoenix', priority: 10 },
|
|
172
|
+
{ language: 'elixir', kind: 'test_framework', keyword: 'ex_unit', value: 'ex-unit', priority: 10 },
|
|
173
|
+
{ language: 'elixir', kind: 'orm', keyword: 'ecto', value: 'ecto', priority: 10 },
|
|
174
|
+
// ASP.NET Core surfaces via several PackageReference names; the canonical
|
|
175
|
+
// ones in modern .NET projects are .App and .Mvc. matchRule does exact
|
|
176
|
+
// (case-insensitive) lookup against the deps set parseCsproj extracts.
|
|
177
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.App', value: 'aspnet-core', priority: 10 },
|
|
178
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore.Mvc', value: 'aspnet-core', priority: 10 },
|
|
179
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.AspNetCore', value: 'aspnet-core', priority: 9 },
|
|
180
|
+
// SDK-style projects: `<Project Sdk="Microsoft.NET.Sdk.Web">` is the
|
|
181
|
+
// canonical ASP.NET Core declaration in modern .NET. parseCsproj
|
|
182
|
+
// surfaces the Sdk attribute as a dep so this rule can match.
|
|
183
|
+
{ language: 'csharp', kind: 'framework', keyword: 'Microsoft.NET.Sdk.Web', value: 'aspnet-core', priority: 10 },
|
|
184
|
+
{ language: 'csharp', kind: 'test_framework', keyword: 'xunit', value: 'xunit', priority: 10 },
|
|
185
|
+
{ language: 'csharp', kind: 'orm', keyword: 'EntityFrameworkCore', value: 'ef-core', priority: 10 },
|
|
160
186
|
];
|
|
161
187
|
|
|
162
188
|
/**
|