@outfitter/tooling 0.2.1 → 0.2.3

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.
Files changed (53) hide show
  1. package/.markdownlint-cli2.jsonc +55 -55
  2. package/README.md +30 -3
  3. package/biome.json +79 -72
  4. package/dist/cli/check-boundary-invocations.d.ts +34 -0
  5. package/dist/cli/check-boundary-invocations.js +14 -0
  6. package/dist/cli/check-bunup-registry.d.ts +36 -0
  7. package/dist/cli/check-bunup-registry.js +12 -0
  8. package/dist/cli/check-changeset.d.ts +64 -0
  9. package/dist/cli/check-changeset.js +14 -0
  10. package/dist/cli/check-clean-tree.d.ts +36 -0
  11. package/dist/cli/check-clean-tree.js +14 -0
  12. package/dist/cli/check-exports.d.ts +2 -0
  13. package/dist/cli/check-exports.js +12 -0
  14. package/dist/cli/check-readme-imports.d.ts +60 -0
  15. package/dist/cli/check-readme-imports.js +194 -0
  16. package/dist/cli/check.js +2 -1
  17. package/dist/cli/fix.js +2 -1
  18. package/dist/cli/index.js +1106 -17
  19. package/dist/cli/init.js +2 -1
  20. package/dist/cli/pre-push.d.ts +34 -1
  21. package/dist/cli/pre-push.js +15 -2
  22. package/dist/cli/upgrade-bun.js +2 -1
  23. package/dist/index.d.ts +110 -35
  24. package/dist/index.js +23 -8
  25. package/dist/registry/build.d.ts +6 -0
  26. package/dist/registry/build.js +32 -13
  27. package/dist/registry/index.js +1 -0
  28. package/dist/registry/schema.js +1 -0
  29. package/dist/shared/@outfitter/{tooling-xx1146e3.js → tooling-0x5q15ec.js} +2 -1
  30. package/dist/shared/@outfitter/tooling-1y8w5ahg.js +70 -0
  31. package/dist/shared/@outfitter/tooling-3w8vr2w3.js +94 -0
  32. package/dist/shared/@outfitter/tooling-8sd32ts6.js +277 -0
  33. package/dist/shared/@outfitter/{tooling-s4eqq91d.js → tooling-9errkcvk.js} +2 -1
  34. package/dist/shared/@outfitter/tooling-9vs606gq.d.ts +3 -0
  35. package/dist/shared/@outfitter/{tooling-75j500dv.js → tooling-9yzd08v1.js} +10 -6
  36. package/dist/shared/@outfitter/tooling-ctmgnap5.js +19 -0
  37. package/dist/shared/@outfitter/tooling-dvwh9qve.js +4 -0
  38. package/dist/shared/@outfitter/{tooling-xaxdr9da.js → tooling-mxwc1n8w.js} +13 -3
  39. package/dist/shared/@outfitter/tooling-q0d60xb3.d.ts +58 -0
  40. package/dist/shared/@outfitter/tooling-r9976n43.js +100 -0
  41. package/dist/shared/@outfitter/tooling-t17gnh9b.js +78 -0
  42. package/dist/shared/@outfitter/tooling-tf22zt9p.js +226 -0
  43. package/dist/shared/chunk-3s189drz.js +4 -0
  44. package/dist/shared/chunk-6a7tjcgm.js +193 -0
  45. package/dist/shared/chunk-8aenrm6f.js +18 -0
  46. package/dist/version.d.ts +2 -0
  47. package/dist/version.js +8 -0
  48. package/lefthook.yml +5 -7
  49. package/package.json +122 -121
  50. package/registry/registry.json +78 -76
  51. package/tsconfig.preset.bun.json +5 -5
  52. package/tsconfig.preset.json +33 -33
  53. package/dist/shared/@outfitter/tooling-qm7jeg0d.js +0 -99
@@ -0,0 +1,277 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/pre-push.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ var COLORS = {
6
+ reset: "\x1B[0m",
7
+ red: "\x1B[31m",
8
+ green: "\x1B[32m",
9
+ yellow: "\x1B[33m",
10
+ blue: "\x1B[34m"
11
+ };
12
+ function log(msg) {
13
+ process.stdout.write(`${msg}
14
+ `);
15
+ }
16
+ function getCurrentBranch() {
17
+ const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
18
+ return result.stdout.toString().trim();
19
+ }
20
+ function runGit(args) {
21
+ try {
22
+ const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
23
+ if (result.exitCode !== 0) {
24
+ return { ok: false, lines: [] };
25
+ }
26
+ return {
27
+ ok: true,
28
+ lines: result.stdout.toString().split(`
29
+ `).map((line) => line.trim()).filter(Boolean)
30
+ };
31
+ } catch {
32
+ return { ok: false, lines: [] };
33
+ }
34
+ }
35
+ function isRedPhaseBranch(branch) {
36
+ return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
37
+ }
38
+ function isScaffoldBranch(branch) {
39
+ return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
40
+ }
41
+ var TEST_PATH_PATTERNS = [
42
+ /(^|\/)__tests__\//,
43
+ /(^|\/)__snapshots__\//,
44
+ /\.(test|spec)\.[cm]?[jt]sx?$/,
45
+ /\.snap$/,
46
+ /(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
47
+ /(^|\/)tsconfig\.test\.json$/,
48
+ /(^|\/)\.env\.test(\.|$)/
49
+ ];
50
+ function isTestOnlyPath(path) {
51
+ const normalized = path.replaceAll("\\", "/");
52
+ return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
53
+ }
54
+ function areFilesTestOnly(paths) {
55
+ return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
56
+ }
57
+ function canBypassRedPhaseByChangedFiles(changedFiles) {
58
+ return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
59
+ }
60
+ function resolveBaseRef() {
61
+ const candidates = [
62
+ "origin/main",
63
+ "main",
64
+ "origin/trunk",
65
+ "trunk",
66
+ "origin/master",
67
+ "master"
68
+ ];
69
+ for (const candidate of candidates) {
70
+ const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
71
+ if (resolved.ok) {
72
+ return candidate;
73
+ }
74
+ }
75
+ return;
76
+ }
77
+ function changedFilesFromRange(range) {
78
+ const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
79
+ return {
80
+ ok: result.ok,
81
+ files: result.lines
82
+ };
83
+ }
84
+ function getChangedFilesForPush() {
85
+ const upstream = runGit([
86
+ "rev-parse",
87
+ "--abbrev-ref",
88
+ "--symbolic-full-name",
89
+ "@{upstream}"
90
+ ]);
91
+ if (upstream.ok && upstream.lines[0]) {
92
+ const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
93
+ if (rangeResult.ok) {
94
+ return {
95
+ files: rangeResult.files,
96
+ deterministic: true,
97
+ source: "upstream"
98
+ };
99
+ }
100
+ }
101
+ const baseRef = resolveBaseRef();
102
+ if (baseRef) {
103
+ const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
104
+ if (rangeResult.ok) {
105
+ return {
106
+ files: rangeResult.files,
107
+ deterministic: true,
108
+ source: "baseRef"
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ files: [],
114
+ deterministic: false,
115
+ source: "undetermined"
116
+ };
117
+ }
118
+ function maybeSkipForRedPhase(reason, branch) {
119
+ const changedFiles = getChangedFilesForPush();
120
+ if (!changedFiles.deterministic) {
121
+ log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
122
+ log("Running strict verification.");
123
+ log("");
124
+ return false;
125
+ }
126
+ if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
127
+ log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
128
+ if (changedFiles.files.length > 0) {
129
+ log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
130
+ } else {
131
+ log(`No changed files detected in ${changedFiles.source} range. Running strict verification.`);
132
+ }
133
+ log("");
134
+ return false;
135
+ }
136
+ if (reason === "branch") {
137
+ log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
138
+ } else {
139
+ log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
140
+ }
141
+ log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
142
+ log(`Diff source: ${changedFiles.source}`);
143
+ log("");
144
+ log("Remember: GREEN phase (implementation) must make these tests pass!");
145
+ return true;
146
+ }
147
+ function hasRedPhaseBranchInContext(currentBranch) {
148
+ let branches = [];
149
+ try {
150
+ const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
151
+ if (gtResult.exitCode === 0) {
152
+ branches = gtResult.stdout.toString().split(`
153
+ `).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
154
+ }
155
+ } catch {}
156
+ if (branches.length === 0) {
157
+ const gitResult = Bun.spawnSync([
158
+ "git",
159
+ "branch",
160
+ "--list",
161
+ "cli/*",
162
+ "types/*",
163
+ "contracts/*"
164
+ ]);
165
+ branches = gitResult.stdout.toString().split(`
166
+ `).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
167
+ }
168
+ for (const branch of branches) {
169
+ if (branch === currentBranch)
170
+ continue;
171
+ if (isRedPhaseBranch(branch))
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ function createVerificationPlan(scripts) {
177
+ if (scripts["verify:ci"]) {
178
+ return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
179
+ }
180
+ const requiredScripts = ["typecheck", "build", "test"];
181
+ const missingRequired = requiredScripts.filter((name) => !scripts[name]);
182
+ const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
183
+ if (!checkOrLint || missingRequired.length > 0) {
184
+ const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
185
+ return {
186
+ ok: false,
187
+ error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
188
+ };
189
+ }
190
+ return {
191
+ ok: true,
192
+ scripts: ["typecheck", checkOrLint, "build", "test"],
193
+ source: "fallback"
194
+ };
195
+ }
196
+ function readPackageScripts(cwd = process.cwd()) {
197
+ const packageJsonPath = join(cwd, "package.json");
198
+ if (!existsSync(packageJsonPath)) {
199
+ return {};
200
+ }
201
+ try {
202
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
203
+ const scripts = parsed.scripts ?? {};
204
+ const normalized = {};
205
+ for (const [name, value] of Object.entries(scripts)) {
206
+ if (typeof value === "string") {
207
+ normalized[name] = value;
208
+ }
209
+ }
210
+ return normalized;
211
+ } catch {
212
+ return {};
213
+ }
214
+ }
215
+ function runScript(scriptName) {
216
+ log("");
217
+ log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
218
+ const result = Bun.spawnSync(["bun", "run", scriptName], {
219
+ stdio: ["inherit", "inherit", "inherit"]
220
+ });
221
+ return result.exitCode === 0;
222
+ }
223
+ async function runPrePush(options = {}) {
224
+ log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
225
+ log("");
226
+ const branch = getCurrentBranch();
227
+ if (isRedPhaseBranch(branch)) {
228
+ if (maybeSkipForRedPhase("branch", branch)) {
229
+ process.exit(0);
230
+ }
231
+ }
232
+ if (isScaffoldBranch(branch)) {
233
+ if (hasRedPhaseBranchInContext(branch)) {
234
+ if (maybeSkipForRedPhase("context", branch)) {
235
+ process.exit(0);
236
+ }
237
+ }
238
+ }
239
+ if (options.force) {
240
+ log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
241
+ process.exit(0);
242
+ }
243
+ const plan = createVerificationPlan(readPackageScripts());
244
+ if (!plan.ok) {
245
+ log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
246
+ log(plan.error);
247
+ log("");
248
+ log("Add one of:");
249
+ log(" - verify:ci");
250
+ log(" - typecheck + (check or lint) + build + test");
251
+ process.exit(1);
252
+ }
253
+ log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
254
+ if (plan.source === "verify:ci") {
255
+ log("Using `verify:ci` script.");
256
+ } else {
257
+ log(`Using fallback scripts: ${plan.scripts.join(" -> ")}`);
258
+ }
259
+ for (const scriptName of plan.scripts) {
260
+ if (runScript(scriptName)) {
261
+ continue;
262
+ }
263
+ log("");
264
+ log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
265
+ log("");
266
+ log("If this is intentional TDD RED phase work, name your branch:");
267
+ log(" - feature-tests");
268
+ log(" - feature/tests");
269
+ log(" - feature_tests");
270
+ process.exit(1);
271
+ }
272
+ log("");
273
+ log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
274
+ process.exit(0);
275
+ }
276
+
277
+ export { isRedPhaseBranch, isScaffoldBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, createVerificationPlan, runPrePush };
@@ -9,7 +9,8 @@ function buildFixCommand(options) {
9
9
  }
10
10
  async function runFix(paths = []) {
11
11
  const cmd = buildFixCommand({ paths });
12
- console.log(`Running: bun x ${cmd.join(" ")}`);
12
+ process.stdout.write(`Running: bun x ${cmd.join(" ")}
13
+ `);
13
14
  const proc = Bun.spawn(["bun", "x", ...cmd], {
14
15
  stdio: ["inherit", "inherit", "inherit"]
15
16
  });
@@ -0,0 +1,3 @@
1
+ /** Package version, read from package.json at load time. */
2
+ declare const VERSION: string;
3
+ export { VERSION };
@@ -10,16 +10,20 @@ var COLORS = {
10
10
  blue: "\x1B[34m"
11
11
  };
12
12
  function log(msg) {
13
- console.log(msg);
13
+ process.stdout.write(`${msg}
14
+ `);
14
15
  }
15
16
  function info(msg) {
16
- console.log(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}`);
17
+ process.stdout.write(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}
18
+ `);
17
19
  }
18
20
  function success(msg) {
19
- console.log(`${COLORS.green}\u2713${COLORS.reset} ${msg}`);
21
+ process.stdout.write(`${COLORS.green}\u2713${COLORS.reset} ${msg}
22
+ `);
20
23
  }
21
24
  function warn(msg) {
22
- console.log(`${COLORS.yellow}!${COLORS.reset} ${msg}`);
25
+ process.stdout.write(`${COLORS.yellow}!${COLORS.reset} ${msg}
26
+ `);
23
27
  }
24
28
  async function fetchLatestVersion() {
25
29
  const response = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest");
@@ -91,13 +95,13 @@ async function runUpgradeBun(targetVersion, options = {}) {
91
95
  info("Updating engines.bun...");
92
96
  for (const file of packageFiles) {
93
97
  if (updateEnginesBun(file, version)) {
94
- log(` ${file.replace(cwd + "/", "")}`);
98
+ log(` ${file.replace(`${cwd}/`, "")}`);
95
99
  }
96
100
  }
97
101
  info("Updating @types/bun...");
98
102
  for (const file of packageFiles) {
99
103
  if (updateTypesBun(file, version)) {
100
- log(` ${file.replace(cwd + "/", "")}`);
104
+ log(` ${file.replace(`${cwd}/`, "")}`);
101
105
  }
102
106
  }
103
107
  if (options.install !== false) {
@@ -0,0 +1,19 @@
1
+ // @bun
2
+ // packages/tooling/src/version.ts
3
+ import { readFileSync } from "fs";
4
+ import { createRequire } from "module";
5
+ var DEFAULT_VERSION = "0.0.0";
6
+ function readPackageVersion() {
7
+ try {
8
+ const require2 = createRequire(import.meta.url);
9
+ const pkgPath = require2.resolve("@outfitter/tooling/package.json");
10
+ const packageJson = JSON.parse(readFileSync(pkgPath, "utf8"));
11
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
12
+ return packageJson.version;
13
+ }
14
+ } catch {}
15
+ return DEFAULT_VERSION;
16
+ }
17
+ var VERSION = readPackageVersion();
18
+
19
+ export { VERSION };
@@ -0,0 +1,4 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ export { __require };
@@ -29,7 +29,15 @@ function detectFrameworks(pkg) {
29
29
  return ["--frameworks", ...detected];
30
30
  }
31
31
  function buildUltraciteCommand(options) {
32
- const cmd = ["ultracite", "init", "--linter", "biome", "--pm", "bun", "--quiet"];
32
+ const cmd = [
33
+ "ultracite",
34
+ "init",
35
+ "--linter",
36
+ "biome",
37
+ "--pm",
38
+ "bun",
39
+ "--quiet"
40
+ ];
33
41
  if (options.frameworks && options.frameworks.length > 0) {
34
42
  cmd.push("--frameworks", ...options.frameworks);
35
43
  }
@@ -39,14 +47,16 @@ async function runInit(cwd = process.cwd()) {
39
47
  const pkgPath = `${cwd}/package.json`;
40
48
  const pkgFile = Bun.file(pkgPath);
41
49
  if (!await pkgFile.exists()) {
42
- console.error("No package.json found in current directory");
50
+ process.stderr.write(`No package.json found in current directory
51
+ `);
43
52
  process.exit(1);
44
53
  }
45
54
  const pkg = await pkgFile.json();
46
55
  const frameworkFlags = detectFrameworks(pkg);
47
56
  const frameworks = frameworkFlags.length > 0 ? frameworkFlags.slice(1) : [];
48
57
  const cmd = buildUltraciteCommand({ frameworks });
49
- console.log(`Running: bun x ${cmd.join(" ")}`);
58
+ process.stdout.write(`Running: bun x ${cmd.join(" ")}
59
+ `);
50
60
  const proc = Bun.spawn(["bun", "x", ...cmd], {
51
61
  cwd,
52
62
  stdio: ["inherit", "inherit", "inherit"]
@@ -0,0 +1,58 @@
1
+ /** A package.json map: keys are subpaths, values are conditions or strings */
2
+ type ExportMap = Record<string, unknown>;
3
+ /** Describes drift between expected and actual exports for a single package */
4
+ interface ExportDrift {
5
+ readonly package: string;
6
+ readonly path: string;
7
+ readonly added: string[];
8
+ readonly removed: string[];
9
+ readonly changed: Array<{
10
+ readonly key: string;
11
+ readonly expected: unknown;
12
+ readonly actual: unknown;
13
+ }>;
14
+ }
15
+ /** Per-package comparison result */
16
+ interface PackageResult {
17
+ readonly name: string;
18
+ readonly status: "ok" | "drift";
19
+ readonly drift?: ExportDrift;
20
+ }
21
+ /** Aggregated result across all checked packages */
22
+ interface CheckResult {
23
+ readonly ok: boolean;
24
+ readonly packages: PackageResult[];
25
+ }
26
+ /** Input for comparing a single package's exports */
27
+ interface CompareInput {
28
+ readonly name: string;
29
+ readonly actual: ExportMap;
30
+ readonly expected: ExportMap;
31
+ readonly path?: string;
32
+ }
33
+ /**
34
+ * Convert a source entry file path to its subpath.
35
+ *
36
+ * @example
37
+ * entryToSubpath("src/index.ts") // "."
38
+ * entryToSubpath("src/branded.ts") // "./branded"
39
+ * entryToSubpath("src/cli/index.ts") // "./cli"
40
+ * entryToSubpath("src/cli/check.ts") // "./cli/check"
41
+ */
42
+ declare function entryToSubpath(entry: string): string;
43
+ /**
44
+ * Compare actual vs expected exports for a single package.
45
+ *
46
+ * Returns a PackageResult with status "ok" or "drift" and detailed diff.
47
+ */
48
+ declare function compareExports(input: CompareInput): PackageResult;
49
+ interface CheckExportsOptions {
50
+ readonly json?: boolean;
51
+ }
52
+ /**
53
+ * Run check-exports across all workspace packages.
54
+ *
55
+ * Reads the bunup workspace config to discover packages and their * settings, then compares expected vs actual exports in each package.json.
56
+ */
57
+ declare function runCheckExports(options?: CheckExportsOptions): Promise<void>;
58
+ export { ExportMap, ExportDrift, PackageResult, CheckResult, CompareInput, entryToSubpath, compareExports, CheckExportsOptions, runCheckExports };
@@ -0,0 +1,100 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-boundary-invocations.ts
3
+ import { relative, resolve } from "path";
4
+ var ROOT_RUNS_PACKAGE_SRC = /\bbun(?:x)?\s+(?:run\s+)?(?:\.\.\/|\.\/)?packages\/[^/\s]+\/src\/\S+/;
5
+ var CD_PACKAGE_THEN_RUNS_SRC = /\bcd\s+(?:\.\.\/|\.\/)?packages\/[^/\s]+\s*&&\s*bun(?:x)?\s+(?:run\s+)?(?:\.\.\/|\.\/)?src\/\S+/;
6
+ function detectBoundaryViolation(location) {
7
+ if (ROOT_RUNS_PACKAGE_SRC.test(location.command)) {
8
+ return { ...location, rule: "root-runs-package-src" };
9
+ }
10
+ if (CD_PACKAGE_THEN_RUNS_SRC.test(location.command)) {
11
+ return { ...location, rule: "cd-package-then-runs-src" };
12
+ }
13
+ return null;
14
+ }
15
+ function findBoundaryViolations(entries) {
16
+ const violations = [];
17
+ for (const entry of entries) {
18
+ for (const [scriptName, command] of Object.entries(entry.scripts)) {
19
+ const violation = detectBoundaryViolation({
20
+ file: entry.file,
21
+ scriptName,
22
+ command
23
+ });
24
+ if (violation) {
25
+ violations.push(violation);
26
+ }
27
+ }
28
+ }
29
+ return violations.sort((a, b) => {
30
+ const fileCompare = a.file.localeCompare(b.file);
31
+ if (fileCompare !== 0) {
32
+ return fileCompare;
33
+ }
34
+ return a.scriptName.localeCompare(b.scriptName);
35
+ });
36
+ }
37
+ async function readScriptEntries(cwd, options = {}) {
38
+ const entries = [];
39
+ const rootPackagePath = resolve(cwd, "package.json");
40
+ const candidatePaths = [rootPackagePath];
41
+ if (options.appManifestRelativePaths) {
42
+ for (const file of options.appManifestRelativePaths) {
43
+ candidatePaths.push(resolve(cwd, file));
44
+ }
45
+ } else {
46
+ const appPackageGlob = new Bun.Glob("apps/*/package.json");
47
+ for (const match of appPackageGlob.scanSync({ cwd })) {
48
+ candidatePaths.push(resolve(cwd, match));
49
+ }
50
+ }
51
+ const readPackageJson = options.readPackageJson ?? (async (filePath) => await Bun.file(filePath).json());
52
+ for (const filePath of candidatePaths) {
53
+ const isRootManifest = filePath === rootPackagePath;
54
+ try {
55
+ const pkg = await readPackageJson(filePath);
56
+ if (!pkg.scripts) {
57
+ continue;
58
+ }
59
+ entries.push({
60
+ file: relative(cwd, filePath),
61
+ scripts: pkg.scripts
62
+ });
63
+ } catch (error) {
64
+ if (isRootManifest) {
65
+ const message = error instanceof Error ? error.message : "unknown parse error";
66
+ throw new Error(`Failed to read root package manifest (${filePath}): ${message}`);
67
+ }
68
+ }
69
+ }
70
+ return entries;
71
+ }
72
+ async function runCheckBoundaryInvocations() {
73
+ const cwd = process.cwd();
74
+ let entries;
75
+ try {
76
+ entries = await readScriptEntries(cwd);
77
+ } catch (error) {
78
+ const message = error instanceof Error ? error.message : "unknown read failure";
79
+ process.stderr.write(`Boundary invocation check failed before evaluation: ${message}
80
+ `);
81
+ process.exit(1);
82
+ }
83
+ const violations = findBoundaryViolations(entries);
84
+ if (violations.length === 0) {
85
+ process.stdout.write(`No boundary invocation violations detected in root/apps scripts.
86
+ `);
87
+ process.exit(0);
88
+ }
89
+ process.stderr.write(`Boundary invocation violations detected:
90
+
91
+ `);
92
+ for (const violation of violations) {
93
+ process.stderr.write(`- ${violation.file}#${violation.scriptName}: ${violation.command}
94
+ `);
95
+ }
96
+ process.stderr.write("\nUse canonical command surfaces (e.g. `outfitter repo ...` or package bins) instead of executing packages/*/src directly.\n");
97
+ process.exit(1);
98
+ }
99
+
100
+ export { detectBoundaryViolation, findBoundaryViolations, readScriptEntries, runCheckBoundaryInvocations };
@@ -0,0 +1,78 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-bunup-registry.ts
3
+ import { resolve } from "path";
4
+ function extractBunupFilterName(script) {
5
+ const match = script.match(/bunup\s+--filter\s+(\S+)/);
6
+ return match?.[1] ?? null;
7
+ }
8
+ function findUnregisteredPackages(packagesWithFilter, registeredNames) {
9
+ const registered = new Set(registeredNames);
10
+ const missing = packagesWithFilter.filter((name) => !registered.has(name)).sort();
11
+ return {
12
+ ok: missing.length === 0,
13
+ missing
14
+ };
15
+ }
16
+ var COLORS = {
17
+ reset: "\x1B[0m",
18
+ red: "\x1B[31m",
19
+ green: "\x1B[32m",
20
+ yellow: "\x1B[33m",
21
+ blue: "\x1B[34m",
22
+ dim: "\x1B[2m"
23
+ };
24
+ async function runCheckBunupRegistry() {
25
+ const cwd = process.cwd();
26
+ const configPath = resolve(cwd, "bunup.config.ts");
27
+ let registeredNames;
28
+ try {
29
+ const configModule = await import(configPath);
30
+ const rawConfig = configModule.default;
31
+ if (!Array.isArray(rawConfig)) {
32
+ process.stderr.write(`bunup.config.ts must export a workspace array
33
+ `);
34
+ process.exit(1);
35
+ }
36
+ registeredNames = rawConfig.map((entry) => entry.name);
37
+ } catch {
38
+ process.stderr.write(`Could not load bunup.config.ts from ${cwd}
39
+ `);
40
+ process.exit(1);
41
+ }
42
+ const packagesWithFilter = [];
43
+ const glob = new Bun.Glob("{packages,apps}/*/package.json");
44
+ for (const match of glob.scanSync({ cwd })) {
45
+ const pkgPath = resolve(cwd, match);
46
+ try {
47
+ const pkg = await Bun.file(pkgPath).json();
48
+ const buildScript = pkg.scripts?.["build"];
49
+ if (!buildScript)
50
+ continue;
51
+ const filterName = extractBunupFilterName(buildScript);
52
+ if (filterName) {
53
+ packagesWithFilter.push(filterName);
54
+ }
55
+ } catch {}
56
+ }
57
+ const result = findUnregisteredPackages(packagesWithFilter, registeredNames);
58
+ if (result.ok) {
59
+ process.stdout.write(`${COLORS.green}All ${packagesWithFilter.length} packages with bunup --filter are registered in bunup.config.ts.${COLORS.reset}
60
+ `);
61
+ process.exit(0);
62
+ }
63
+ process.stderr.write(`${COLORS.red}${result.missing.length} package(s) have bunup --filter build scripts but are not registered in bunup.config.ts:${COLORS.reset}
64
+
65
+ `);
66
+ for (const name of result.missing) {
67
+ process.stderr.write(` ${COLORS.yellow}${name}${COLORS.reset} ${COLORS.dim}(missing from workspace array)${COLORS.reset}
68
+ `);
69
+ }
70
+ process.stderr.write(`
71
+ Add the missing entries to ${COLORS.blue}bunup.config.ts${COLORS.reset} defineWorkspace array.
72
+ `);
73
+ process.stderr.write(`Without registration, ${COLORS.dim}bunup --filter <name>${COLORS.reset} silently exits with no output.
74
+ `);
75
+ process.exit(1);
76
+ }
77
+
78
+ export { extractBunupFilterName, findUnregisteredPackages, runCheckBunupRegistry };