mason-context 0.14.1 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.2 — 2026-09-07
4
+
5
+ - Remove the repository-wide automation inventory that blocked setup above 100,000 files and directories. Discover module source through Git and Mason exclusions, and search workspace command manifests only when needed.
6
+ - Preserve checks of explicitly documented ignored paths and workspace members. Validate symlinks at the inputs actually read, bound scoped discovery, and identify the operation responsible for a limit failure.
7
+ - Cover setup with 100,001 ignored generated files, retained evidence, and detection of a later source module.
8
+ - Request a new terminal after installation only when the installation directory is absent from the inherited PATH.
9
+
3
10
  ## 0.14.1 — 2026-09-07
4
11
 
5
12
  - Configure PATH automatically during standalone installation for Bash, Zsh, POSIX sh, and Windows. Supported installations finish with a simple instruction to open a new terminal and run setup.
package/README.md CHANGED
@@ -28,7 +28,7 @@ irm https://github.com/adrianczuczka/mason/releases/latest/download/install.ps1
28
28
 
29
29
  No Node or npm required. Git is required. The installer configures PATH for Bash, Zsh, and Windows. [Platform details](docs/distribution.md) · [npm installation](docs/setup.md#unified-project-setup)
30
30
 
31
- Open a new terminal, then run this in your Git repository:
31
+ Run this in your Git repository (open a new terminal if the installer asks):
32
32
 
33
33
  ```bash
34
34
  mason setup --host codex
@@ -93,6 +93,29 @@ async function readBoundedFile(file, maxBytes) {
93
93
  await handle.close();
94
94
  }
95
95
  }
96
+ async function loadProjectConfig(root) {
97
+ try {
98
+ const canonicalRoot = await fs.realpath(root);
99
+ const configPath = await fs.realpath(path2.join(root, ".mason/config.json"));
100
+ if (!isWithinRoot(canonicalRoot, configPath)) throw new Error("Project configuration resolves outside the repository");
101
+ const raw = await readBoundedFile(configPath, 64 * 1024);
102
+ if (raw === null) throw new Error("Project configuration is not a regular file or exceeds 64 KiB");
103
+ const value = JSON.parse(raw);
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
105
+ const config = {};
106
+ for (const key of ["patterns", "alwaysInclude", "ignore"]) {
107
+ if (value[key] === void 0) continue;
108
+ if (!Array.isArray(value[key]) || !value[key].every((s) => typeof s === "string")) {
109
+ throw new Error(`Configuration ${key} must be an array of strings`);
110
+ }
111
+ config[key] = value[key];
112
+ }
113
+ return config;
114
+ } catch (error) {
115
+ if (error.code === "ENOENT") return {};
116
+ throw new Error(`Cannot apply project file policy: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ }
96
119
 
97
120
  // src/utils/storage.ts
98
121
  import fs2 from "fs/promises";
@@ -744,8 +767,126 @@ async function checkDeletedReferences(ctx) {
744
767
  }
745
768
 
746
769
  // src/audit/checks/new-module.ts
747
- import fg2 from "fast-glob";
770
+ import fg3 from "fast-glob";
771
+ import fs7 from "fs/promises";
772
+ import path10 from "path";
773
+
774
+ // src/audit/inputs.ts
775
+ import fs6 from "fs/promises";
776
+ import * as nativeFs from "fs";
748
777
  import path9 from "path";
778
+ import { execFile as execFile6 } from "child_process";
779
+ import { promisify as promisify6 } from "util";
780
+ import fg2 from "fast-glob";
781
+ var execute = promisify6(execFile6);
782
+ var MAX_INPUTS = 1e5;
783
+ async function auditInputPath(root, file) {
784
+ if (file === ".") return fs6.realpath(root);
785
+ try {
786
+ return await storePath(root, file);
787
+ } catch (error) {
788
+ if (error instanceof Error && error.message.startsWith("Symlink")) {
789
+ throw new Error(`Audit input contains a symbolic link: ${file}. Its evidence could not be verified.`, { cause: error });
790
+ }
791
+ throw error;
792
+ }
793
+ }
794
+ async function readAuditInput(root, file) {
795
+ try {
796
+ const value = await readBoundedFile(await auditInputPath(root, file), 10 * 1024 * 1024);
797
+ if (value === null) throw new Error(`Unreadable or oversized audit input: ${file}`);
798
+ return value;
799
+ } catch (error) {
800
+ if (error.code === "ENOENT") return null;
801
+ throw error;
802
+ }
803
+ }
804
+ async function auditGlob(root, patterns, options) {
805
+ if (Array.isArray(patterns) && !patterns.length) return [];
806
+ const directories = /* @__PURE__ */ new Set();
807
+ const readdir2 = ((directory, opts, callback) => {
808
+ const relative = path9.relative(root, directory).split(path9.sep).join("/") || ".";
809
+ directories.add(relative);
810
+ if (directories.size > MAX_INPUTS) {
811
+ callback(new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} directories while inspecting ${relative}. Narrow this check's inputs.`));
812
+ return;
813
+ }
814
+ auditInputPath(root, relative).then(() => {
815
+ nativeFs.readdir(directory, opts, callback);
816
+ }, callback);
817
+ });
818
+ const lstat2 = ((file, callback) => {
819
+ const relative = path9.relative(root, file).split(path9.sep).join("/") || ".";
820
+ auditInputPath(root, relative).then(() => nativeFs.lstat(file, callback), callback);
821
+ });
822
+ const stream = fg2.stream(patterns, {
823
+ cwd: root,
824
+ ignore: options.ignore,
825
+ onlyFiles: false,
826
+ onlyDirectories: options.onlyDirectories,
827
+ objectMode: true,
828
+ followSymbolicLinks: options.followSymbolicLinks ?? true,
829
+ fs: { readdir: readdir2, lstat: lstat2 },
830
+ concurrency: 16
831
+ });
832
+ const matches = [];
833
+ try {
834
+ for await (const value of stream) {
835
+ const entry = value;
836
+ const file = entry.path;
837
+ if (options.select && !options.select(file)) continue;
838
+ await auditInputPath(root, file);
839
+ if (!options.onlyDirectories && !entry.dirent.isFile()) continue;
840
+ matches.push(file);
841
+ if (matches.length > MAX_INPUTS) {
842
+ const largest = [...matches.reduce((counts, item) => {
843
+ const dir = item.split("/")[0];
844
+ counts.set(dir, (counts.get(dir) ?? 0) + 1);
845
+ return counts;
846
+ }, /* @__PURE__ */ new Map())].sort((a, b) => b[1] - a[1]).slice(0, 3);
847
+ throw new Error(`${options.label} exceeds ${MAX_INPUTS.toLocaleString("en-US")} relevant paths. Largest directories: ${largest.map(([dir, count2]) => `${dir} (${count2})`).join(", ")}. Narrow this check's inputs.`);
848
+ }
849
+ }
850
+ } finally {
851
+ stream.destroy();
852
+ }
853
+ return matches.sort();
854
+ }
855
+ async function gitSourcePaths(root) {
856
+ const { stdout } = await execute(
857
+ "git",
858
+ [
859
+ "ls-files",
860
+ "-z",
861
+ "--cached",
862
+ "--others",
863
+ "--exclude-standard",
864
+ "--",
865
+ ...SOURCE_EXTENSIONS.map((extension) => `:(glob)**/*.${extension}`)
866
+ ],
867
+ { cwd: root, maxBuffer: 32 * 1024 * 1024, timeout: 1e4 }
868
+ );
869
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
870
+ }
871
+ async function gitIgnoredPaths(root, files) {
872
+ if (!files.length) return /* @__PURE__ */ new Set();
873
+ const operation = execute(
874
+ "git",
875
+ ["check-ignore", "-z", "--stdin"],
876
+ { cwd: root, maxBuffer: 4 * 1024 * 1024, timeout: 1e4 }
877
+ );
878
+ operation.child.stdin.on("error", () => {
879
+ });
880
+ operation.child.stdin.end(files.join("\0") + "\0");
881
+ try {
882
+ return new Set((await operation).stdout.split("\0").filter(Boolean));
883
+ } catch (error) {
884
+ if (error.code === 1) return /* @__PURE__ */ new Set();
885
+ throw error;
886
+ }
887
+ }
888
+
889
+ // src/audit/checks/new-module.ts
749
890
  var DIR_DENYLIST = /* @__PURE__ */ new Set([
750
891
  "node_modules",
751
892
  "dist",
@@ -777,22 +918,6 @@ function isMentioned(combinedDocs, name) {
777
918
  );
778
919
  return re.test(combinedDocs);
779
920
  }
780
- async function listSubdirs(absDir) {
781
- const dirs = await fg2("*", {
782
- cwd: absDir,
783
- onlyDirectories: true,
784
- suppressErrors: true
785
- });
786
- return dirs.filter((d) => !DIR_DENYLIST.has(d)).sort();
787
- }
788
- async function countSourceFiles(absDir) {
789
- const files = await fg2(SOURCE_GLOB, {
790
- cwd: absDir,
791
- ignore: SOURCE_IGNORE,
792
- suppressErrors: true
793
- });
794
- return files.length;
795
- }
796
921
  async function checkNewModules(ctx) {
797
922
  const result = emptyResult();
798
923
  if (ctx.docs.length === 0) return result;
@@ -820,21 +945,48 @@ async function checkNewModules(ctx) {
820
945
  return result;
821
946
  }
822
947
  async function moduleCandidates(root, combinedDocs) {
948
+ const [config, sourcePaths] = await Promise.all([loadProjectConfig(root), gitSourcePaths(root)]);
949
+ const ignore = [...SOURCE_IGNORE, ...config.ignore ?? []];
950
+ const sources = sourcePaths.filter((file) => !file.split("/").some((part) => part.startsWith(".")));
951
+ const listSubdirs = async (dir) => {
952
+ const entries = await fs7.readdir(await auditInputPath(root, dir), { withFileTypes: true });
953
+ const dirs = entries.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && !entry.name.startsWith(".") && !DIR_DENYLIST.has(entry.name)).map((entry) => path10.posix.join(dir, entry.name));
954
+ const ignored = await gitIgnoredPaths(root, dirs);
955
+ const visible = new Set(dirs.filter((dir2) => !ignored.has(dir2) || sources.some((file) => file.startsWith(dir2 + "/"))));
956
+ return (await auditGlob(root, dir === "." ? "*" : `${fg3.escapePath(dir)}/*`, {
957
+ ignore,
958
+ onlyDirectories: true,
959
+ label: "Module directory discovery",
960
+ select: (file) => visible.has(file)
961
+ })).map((file) => path10.posix.basename(file));
962
+ };
963
+ const countSourceFiles = async (dir) => {
964
+ const files = sources.filter((file) => file.startsWith(dir + "/"));
965
+ let count2 = 0;
966
+ for (let offset = 0; offset < files.length; offset += 256) {
967
+ count2 += (await auditGlob(root, files.slice(offset, offset + 256).map((file) => fg3.escapePath(file)), {
968
+ ignore,
969
+ followSymbolicLinks: false,
970
+ label: `Source discovery in ${dir}`
971
+ })).length;
972
+ if (count2 > 1e5) throw new Error(`Source discovery in ${dir} exceeds 100,000 relevant files. Exclude generated source with .mason/config.json ignore patterns or narrow this audit's checks.`);
973
+ }
974
+ return count2;
975
+ };
823
976
  const candidates = [];
824
- for (const topDir of await listSubdirs(root)) {
825
- const absTop = path9.join(root, topDir);
977
+ for (const topDir of await listSubdirs(".")) {
826
978
  const topMentioned = isMentioned(combinedDocs, topDir);
827
979
  if (!topMentioned) {
828
- const count2 = await countSourceFiles(absTop);
980
+ const count2 = await countSourceFiles(topDir);
829
981
  if (count2 >= 1) candidates.push({ dir: topDir, sourceFileCount: count2 });
830
982
  continue;
831
983
  }
832
- const subdirs = await listSubdirs(absTop);
984
+ const subdirs = await listSubdirs(topDir);
833
985
  const mentioned = subdirs.filter((s) => isMentioned(combinedDocs, s));
834
986
  if (mentioned.length < ENUMERATION_THRESHOLD) continue;
835
987
  for (const sub of subdirs) {
836
988
  if (isMentioned(combinedDocs, sub)) continue;
837
- const count2 = await countSourceFiles(path9.join(absTop, sub));
989
+ const count2 = await countSourceFiles(`${topDir}/${sub}`);
838
990
  if (count2 >= SECOND_LEVEL_MIN_SOURCE_FILES) {
839
991
  candidates.push({ dir: `${topDir}/${sub}`, sourceFileCount: count2 });
840
992
  }
@@ -844,20 +996,11 @@ async function moduleCandidates(root, combinedDocs) {
844
996
  }
845
997
 
846
998
  // src/audit/checks/stale-count.ts
847
- import fs6 from "fs/promises";
848
- import path10 from "path";
849
- import fg3 from "fast-glob";
999
+ import path11 from "path";
850
1000
  var MEMBERS_CAP = 50;
851
- async function readIfExists(absPath) {
852
- try {
853
- return await fs6.readFile(absPath, "utf-8");
854
- } catch {
855
- return null;
856
- }
857
- }
858
1001
  async function countGradleModules(root) {
859
1002
  for (const name of ["settings.gradle.kts", "settings.gradle"]) {
860
- const content = await readIfExists(path10.join(root, name));
1003
+ const content = await readAuditInput(root, name);
861
1004
  if (content === null) continue;
862
1005
  const members = [];
863
1006
  for (const call of content.matchAll(/include\s*\(([^)]*)\)/g)) {
@@ -871,26 +1014,28 @@ async function countGradleModules(root) {
871
1014
  return null;
872
1015
  }
873
1016
  async function countNpmWorkspaces(root) {
874
- const pkgRaw = await readIfExists(path10.join(root, "package.json"));
1017
+ const pkgRaw = await readAuditInput(root, "package.json");
875
1018
  if (pkgRaw !== null) {
1019
+ let globs = [];
876
1020
  try {
877
1021
  const pkg = JSON.parse(pkgRaw);
878
- const globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
879
- if (globs.length > 0) {
880
- const matched = await fg3(
881
- globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
882
- { cwd: root, ignore: ["**/node_modules/**"] }
883
- );
884
- return {
885
- actual: matched.length,
886
- countedFrom: "package.json workspaces",
887
- members: matched.map((m) => path10.dirname(m)).sort()
888
- };
889
- }
1022
+ globs = Array.isArray(pkg.workspaces) ? pkg.workspaces : Array.isArray(pkg.workspaces?.packages) ? pkg.workspaces.packages : [];
890
1023
  } catch {
891
1024
  }
1025
+ if (globs.length > 0) {
1026
+ const matched = await auditGlob(
1027
+ root,
1028
+ globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
1029
+ { ignore: ["**/node_modules/**"], label: "npm workspace discovery" }
1030
+ );
1031
+ return {
1032
+ actual: matched.length,
1033
+ countedFrom: "package.json workspaces",
1034
+ members: matched.map((m) => path11.dirname(m)).sort()
1035
+ };
1036
+ }
892
1037
  }
893
- const pnpmRaw = await readIfExists(path10.join(root, "pnpm-workspace.yaml"));
1038
+ const pnpmRaw = await readAuditInput(root, "pnpm-workspace.yaml");
894
1039
  if (pnpmRaw !== null) {
895
1040
  const globs = [];
896
1041
  let inPackages = false;
@@ -909,21 +1054,22 @@ async function countNpmWorkspaces(root) {
909
1054
  }
910
1055
  }
911
1056
  if (globs.length > 0) {
912
- const matched = await fg3(
1057
+ const matched = await auditGlob(
1058
+ root,
913
1059
  globs.map((g) => `${g.replace(/\/+$/, "")}/package.json`),
914
- { cwd: root, ignore: ["**/node_modules/**"] }
1060
+ { ignore: ["**/node_modules/**"], label: "pnpm workspace discovery" }
915
1061
  );
916
1062
  return {
917
1063
  actual: matched.length,
918
1064
  countedFrom: "pnpm-workspace.yaml",
919
- members: matched.map((m) => path10.dirname(m)).sort()
1065
+ members: matched.map((m) => path11.dirname(m)).sort()
920
1066
  };
921
1067
  }
922
1068
  }
923
1069
  return null;
924
1070
  }
925
1071
  async function countCargoCrates(root) {
926
- const content = await readIfExists(path10.join(root, "Cargo.toml"));
1072
+ const content = await readAuditInput(root, "Cargo.toml");
927
1073
  if (content === null) return null;
928
1074
  const membersBlock = content.match(/members\s*=\s*\[([\s\S]*?)\]/);
929
1075
  if (!membersBlock) return null;
@@ -934,12 +1080,12 @@ async function countCargoCrates(root) {
934
1080
  const members = /* @__PURE__ */ new Set();
935
1081
  for (const entry of entries) {
936
1082
  if (/[*?[\]{}]/.test(entry)) {
937
- const matched = await fg3(`${entry.replace(/\/+$/, "")}/Cargo.toml`, {
938
- cwd: root,
939
- ignore: ["**/target/**"]
1083
+ const matched = await auditGlob(root, `${entry.replace(/\/+$/, "")}/Cargo.toml`, {
1084
+ ignore: ["**/target/**"],
1085
+ label: "Cargo workspace discovery"
940
1086
  });
941
- for (const m of matched) members.add(path10.dirname(m));
942
- } else if (await readIfExists(path10.join(root, entry, "Cargo.toml")) !== null) {
1087
+ for (const m of matched) members.add(path11.dirname(m));
1088
+ } else if (await readAuditInput(root, path11.posix.join(entry, "Cargo.toml")) !== null) {
943
1089
  members.add(entry);
944
1090
  }
945
1091
  }
@@ -991,13 +1137,12 @@ async function checkStaleCounts(ctx) {
991
1137
  }
992
1138
 
993
1139
  // src/audit/checks/dead-command.ts
994
- import fs7 from "fs/promises";
995
- import path11 from "path";
996
- import fg4 from "fast-glob";
997
1140
  var AVAILABLE_SCRIPTS_CAP = 30;
998
- async function scriptsOf(absManifest) {
1141
+ async function scriptsOf(root, manifest) {
1142
+ const raw = await readAuditInput(root, manifest);
1143
+ if (raw === null) return null;
999
1144
  try {
1000
- const pkg = JSON.parse(await fs7.readFile(absManifest, "utf-8"));
1145
+ const pkg = JSON.parse(raw);
1001
1146
  return pkg && typeof pkg.scripts === "object" && pkg.scripts !== null ? Object.keys(pkg.scripts) : [];
1002
1147
  } catch {
1003
1148
  return null;
@@ -1009,7 +1154,7 @@ async function checkDeadCommands(ctx) {
1009
1154
  (doc) => doc.claims.commands.map((claim) => ({ doc, claim }))
1010
1155
  );
1011
1156
  if (commandClaims.length === 0) return result;
1012
- const rootScripts = await scriptsOf(path11.join(ctx.root, "package.json"));
1157
+ const rootScripts = await scriptsOf(ctx.root, "package.json");
1013
1158
  if (rootScripts === null) {
1014
1159
  result.skipped.push({
1015
1160
  check: "dead-command",
@@ -1026,7 +1171,7 @@ async function checkDeadCommands(ctx) {
1026
1171
  const manifests = await commandManifests(ctx.root);
1027
1172
  manifestsChecked = ["package.json", ...manifests.sort()];
1028
1173
  for (const manifest of manifests) {
1029
- const scripts = await scriptsOf(path11.join(ctx.root, manifest));
1174
+ const scripts = await scriptsOf(ctx.root, manifest);
1030
1175
  for (const name of scripts ?? []) workspaceScripts.add(name);
1031
1176
  }
1032
1177
  return workspaceScripts;
@@ -1052,16 +1197,16 @@ async function checkDeadCommands(ctx) {
1052
1197
  return result;
1053
1198
  }
1054
1199
  function commandManifests(root) {
1055
- return fg4("**/package.json", {
1056
- cwd: root,
1057
- ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"]
1200
+ return auditGlob(root, "**/package.json", {
1201
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", ".mason/reports/**", "package.json"],
1202
+ label: "Command manifest discovery"
1058
1203
  });
1059
1204
  }
1060
1205
 
1061
1206
  // src/audit/release-metadata.ts
1062
- import { execFile as execFile6 } from "child_process";
1063
- import { promisify as promisify6 } from "util";
1064
- var exec6 = promisify6(execFile6);
1207
+ import { execFile as execFile7 } from "child_process";
1208
+ import { promisify as promisify7 } from "util";
1209
+ var exec6 = promisify7(execFile7);
1065
1210
  async function git(root, args) {
1066
1211
  return (await exec6("git", args, { cwd: root, timeout: 1e4, maxBuffer: 2 * 1024 * 1024 })).stdout;
1067
1212
  }