@outfitter/tooling 0.2.2 → 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 (40) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/check-boundary-invocations.d.ts +34 -0
  3. package/dist/cli/check-boundary-invocations.js +14 -0
  4. package/dist/cli/check-bunup-registry.d.ts +36 -0
  5. package/dist/cli/check-bunup-registry.js +12 -0
  6. package/dist/cli/check-changeset.d.ts +64 -0
  7. package/dist/cli/check-changeset.js +14 -0
  8. package/dist/cli/check-clean-tree.d.ts +36 -0
  9. package/dist/cli/check-clean-tree.js +14 -0
  10. package/dist/cli/check-exports.d.ts +2 -0
  11. package/dist/cli/check-exports.js +12 -0
  12. package/dist/cli/check-readme-imports.d.ts +60 -0
  13. package/dist/cli/check-readme-imports.js +194 -0
  14. package/dist/cli/check.js +1 -0
  15. package/dist/cli/fix.js +1 -0
  16. package/dist/cli/index.js +598 -18
  17. package/dist/cli/init.js +1 -0
  18. package/dist/cli/pre-push.js +1 -0
  19. package/dist/cli/upgrade-bun.js +1 -0
  20. package/dist/index.d.ts +2 -33
  21. package/dist/index.js +5 -2
  22. package/dist/registry/build.js +1 -0
  23. package/dist/registry/index.js +1 -0
  24. package/dist/registry/schema.js +1 -0
  25. package/dist/shared/@outfitter/tooling-1y8w5ahg.js +70 -0
  26. package/dist/shared/@outfitter/tooling-3w8vr2w3.js +94 -0
  27. package/dist/shared/@outfitter/tooling-9vs606gq.d.ts +3 -0
  28. package/dist/shared/@outfitter/tooling-ctmgnap5.js +19 -0
  29. package/dist/shared/@outfitter/tooling-dvwh9qve.js +4 -0
  30. package/dist/shared/@outfitter/tooling-q0d60xb3.d.ts +58 -0
  31. package/dist/shared/@outfitter/tooling-r9976n43.js +100 -0
  32. package/dist/shared/@outfitter/tooling-t17gnh9b.js +78 -0
  33. package/dist/shared/@outfitter/tooling-tf22zt9p.js +226 -0
  34. package/dist/shared/chunk-3s189drz.js +4 -0
  35. package/dist/shared/chunk-6a7tjcgm.js +193 -0
  36. package/dist/shared/chunk-8aenrm6f.js +18 -0
  37. package/dist/version.d.ts +2 -0
  38. package/dist/version.js +8 -0
  39. package/package.json +22 -21
  40. package/registry/registry.json +1 -1
package/dist/cli/index.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ import {
3
+ VERSION
4
+ } from "../shared/chunk-8aenrm6f.js";
5
+ import {
6
+ __require
7
+ } from "../shared/chunk-3s189drz.js";
2
8
 
3
9
  // src/cli/index.ts
4
10
  import { Command } from "commander";
@@ -22,6 +28,561 @@ async function runCheck(paths = []) {
22
28
  process.exit(exitCode);
23
29
  }
24
30
 
31
+ // src/cli/check-boundary-invocations.ts
32
+ import { relative, resolve } from "node:path";
33
+ var ROOT_RUNS_PACKAGE_SRC = /\bbun(?:x)?\s+(?:run\s+)?(?:\.\.\/|\.\/)?packages\/[^/\s]+\/src\/\S+/;
34
+ var CD_PACKAGE_THEN_RUNS_SRC = /\bcd\s+(?:\.\.\/|\.\/)?packages\/[^/\s]+\s*&&\s*bun(?:x)?\s+(?:run\s+)?(?:\.\.\/|\.\/)?src\/\S+/;
35
+ function detectBoundaryViolation(location) {
36
+ if (ROOT_RUNS_PACKAGE_SRC.test(location.command)) {
37
+ return { ...location, rule: "root-runs-package-src" };
38
+ }
39
+ if (CD_PACKAGE_THEN_RUNS_SRC.test(location.command)) {
40
+ return { ...location, rule: "cd-package-then-runs-src" };
41
+ }
42
+ return null;
43
+ }
44
+ function findBoundaryViolations(entries) {
45
+ const violations = [];
46
+ for (const entry of entries) {
47
+ for (const [scriptName, command] of Object.entries(entry.scripts)) {
48
+ const violation = detectBoundaryViolation({
49
+ file: entry.file,
50
+ scriptName,
51
+ command
52
+ });
53
+ if (violation) {
54
+ violations.push(violation);
55
+ }
56
+ }
57
+ }
58
+ return violations.sort((a, b) => {
59
+ const fileCompare = a.file.localeCompare(b.file);
60
+ if (fileCompare !== 0) {
61
+ return fileCompare;
62
+ }
63
+ return a.scriptName.localeCompare(b.scriptName);
64
+ });
65
+ }
66
+ async function readScriptEntries(cwd, options = {}) {
67
+ const entries = [];
68
+ const rootPackagePath = resolve(cwd, "package.json");
69
+ const candidatePaths = [rootPackagePath];
70
+ if (options.appManifestRelativePaths) {
71
+ for (const file of options.appManifestRelativePaths) {
72
+ candidatePaths.push(resolve(cwd, file));
73
+ }
74
+ } else {
75
+ const appPackageGlob = new Bun.Glob("apps/*/package.json");
76
+ for (const match of appPackageGlob.scanSync({ cwd })) {
77
+ candidatePaths.push(resolve(cwd, match));
78
+ }
79
+ }
80
+ const readPackageJson = options.readPackageJson ?? (async (filePath) => await Bun.file(filePath).json());
81
+ for (const filePath of candidatePaths) {
82
+ const isRootManifest = filePath === rootPackagePath;
83
+ try {
84
+ const pkg = await readPackageJson(filePath);
85
+ if (!pkg.scripts) {
86
+ continue;
87
+ }
88
+ entries.push({
89
+ file: relative(cwd, filePath),
90
+ scripts: pkg.scripts
91
+ });
92
+ } catch (error) {
93
+ if (isRootManifest) {
94
+ const message = error instanceof Error ? error.message : "unknown parse error";
95
+ throw new Error(`Failed to read root package manifest (${filePath}): ${message}`);
96
+ }
97
+ }
98
+ }
99
+ return entries;
100
+ }
101
+ async function runCheckBoundaryInvocations() {
102
+ const cwd = process.cwd();
103
+ let entries;
104
+ try {
105
+ entries = await readScriptEntries(cwd);
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : "unknown read failure";
108
+ process.stderr.write(`Boundary invocation check failed before evaluation: ${message}
109
+ `);
110
+ process.exit(1);
111
+ }
112
+ const violations = findBoundaryViolations(entries);
113
+ if (violations.length === 0) {
114
+ process.stdout.write(`No boundary invocation violations detected in root/apps scripts.
115
+ `);
116
+ process.exit(0);
117
+ }
118
+ process.stderr.write(`Boundary invocation violations detected:
119
+
120
+ `);
121
+ for (const violation of violations) {
122
+ process.stderr.write(`- ${violation.file}#${violation.scriptName}: ${violation.command}
123
+ `);
124
+ }
125
+ process.stderr.write("\nUse canonical command surfaces (e.g. `outfitter repo ...` or package bins) instead of executing packages/*/src directly.\n");
126
+ process.exit(1);
127
+ }
128
+
129
+ // src/cli/check-bunup-registry.ts
130
+ import { resolve as resolve2 } from "node:path";
131
+ function extractBunupFilterName(script) {
132
+ const match = script.match(/bunup\s+--filter\s+(\S+)/);
133
+ return match?.[1] ?? null;
134
+ }
135
+ function findUnregisteredPackages(packagesWithFilter, registeredNames) {
136
+ const registered = new Set(registeredNames);
137
+ const missing = packagesWithFilter.filter((name) => !registered.has(name)).sort();
138
+ return {
139
+ ok: missing.length === 0,
140
+ missing
141
+ };
142
+ }
143
+ var COLORS = {
144
+ reset: "\x1B[0m",
145
+ red: "\x1B[31m",
146
+ green: "\x1B[32m",
147
+ yellow: "\x1B[33m",
148
+ blue: "\x1B[34m",
149
+ dim: "\x1B[2m"
150
+ };
151
+ async function runCheckBunupRegistry() {
152
+ const cwd = process.cwd();
153
+ const configPath = resolve2(cwd, "bunup.config.ts");
154
+ let registeredNames;
155
+ try {
156
+ const configModule = await import(configPath);
157
+ const rawConfig = configModule.default;
158
+ if (!Array.isArray(rawConfig)) {
159
+ process.stderr.write(`bunup.config.ts must export a workspace array
160
+ `);
161
+ process.exit(1);
162
+ }
163
+ registeredNames = rawConfig.map((entry) => entry.name);
164
+ } catch {
165
+ process.stderr.write(`Could not load bunup.config.ts from ${cwd}
166
+ `);
167
+ process.exit(1);
168
+ }
169
+ const packagesWithFilter = [];
170
+ const glob = new Bun.Glob("{packages,apps}/*/package.json");
171
+ for (const match of glob.scanSync({ cwd })) {
172
+ const pkgPath = resolve2(cwd, match);
173
+ try {
174
+ const pkg = await Bun.file(pkgPath).json();
175
+ const buildScript = pkg.scripts?.["build"];
176
+ if (!buildScript)
177
+ continue;
178
+ const filterName = extractBunupFilterName(buildScript);
179
+ if (filterName) {
180
+ packagesWithFilter.push(filterName);
181
+ }
182
+ } catch {}
183
+ }
184
+ const result = findUnregisteredPackages(packagesWithFilter, registeredNames);
185
+ if (result.ok) {
186
+ process.stdout.write(`${COLORS.green}All ${packagesWithFilter.length} packages with bunup --filter are registered in bunup.config.ts.${COLORS.reset}
187
+ `);
188
+ process.exit(0);
189
+ }
190
+ 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}
191
+
192
+ `);
193
+ for (const name of result.missing) {
194
+ process.stderr.write(` ${COLORS.yellow}${name}${COLORS.reset} ${COLORS.dim}(missing from workspace array)${COLORS.reset}
195
+ `);
196
+ }
197
+ process.stderr.write(`
198
+ Add the missing entries to ${COLORS.blue}bunup.config.ts${COLORS.reset} defineWorkspace array.
199
+ `);
200
+ process.stderr.write(`Without registration, ${COLORS.dim}bunup --filter <name>${COLORS.reset} silently exits with no output.
201
+ `);
202
+ process.exit(1);
203
+ }
204
+
205
+ // src/cli/check-changeset.ts
206
+ function getChangedPackagePaths(files) {
207
+ const packageNames = new Set;
208
+ const pattern = /^packages\/([^/]+)\/src\//;
209
+ for (const file of files) {
210
+ const match = pattern.exec(file);
211
+ if (match?.[1]) {
212
+ packageNames.add(match[1]);
213
+ }
214
+ }
215
+ return [...packageNames].sort();
216
+ }
217
+ function getChangedChangesetFiles(files) {
218
+ const pattern = /^\.changeset\/([^/]+\.md)$/;
219
+ const results = [];
220
+ for (const file of files) {
221
+ const match = pattern.exec(file);
222
+ if (match?.[1] && match[1] !== "README.md") {
223
+ results.push(match[1]);
224
+ }
225
+ }
226
+ return results.sort();
227
+ }
228
+ function checkChangesetRequired(changedPackages, changesetFiles) {
229
+ if (changedPackages.length === 0) {
230
+ return { ok: true, missingFor: [] };
231
+ }
232
+ if (changesetFiles.length > 0) {
233
+ return { ok: true, missingFor: [] };
234
+ }
235
+ return { ok: false, missingFor: changedPackages };
236
+ }
237
+ var COLORS2 = {
238
+ reset: "\x1B[0m",
239
+ red: "\x1B[31m",
240
+ green: "\x1B[32m",
241
+ yellow: "\x1B[33m",
242
+ blue: "\x1B[34m",
243
+ dim: "\x1B[2m"
244
+ };
245
+ async function runCheckChangeset(options = {}) {
246
+ if (options.skip || process.env["NO_CHANGESET"] === "1") {
247
+ process.stdout.write(`${COLORS2.dim}check-changeset skipped (NO_CHANGESET=1)${COLORS2.reset}
248
+ `);
249
+ process.exit(0);
250
+ }
251
+ if (process.env["GITHUB_EVENT_NAME"] === "push") {
252
+ process.stdout.write(`${COLORS2.dim}check-changeset skipped (push event)${COLORS2.reset}
253
+ `);
254
+ process.exit(0);
255
+ }
256
+ const cwd = process.cwd();
257
+ let changedFiles;
258
+ try {
259
+ const proc = Bun.spawnSync(["git", "diff", "--name-only", "origin/main...HEAD"], { cwd });
260
+ if (proc.exitCode !== 0) {
261
+ process.exit(0);
262
+ }
263
+ changedFiles = proc.stdout.toString().trim().split(`
264
+ `).filter((line) => line.length > 0);
265
+ } catch {
266
+ process.exit(0);
267
+ }
268
+ const changedPackages = getChangedPackagePaths(changedFiles);
269
+ if (changedPackages.length === 0) {
270
+ process.stdout.write(`${COLORS2.green}No package source changes detected.${COLORS2.reset}
271
+ `);
272
+ process.exit(0);
273
+ }
274
+ const changesetFiles = getChangedChangesetFiles(changedFiles);
275
+ const check = checkChangesetRequired(changedPackages, changesetFiles);
276
+ if (check.ok) {
277
+ process.stdout.write(`${COLORS2.green}Changeset found for ${changedPackages.length} changed package(s).${COLORS2.reset}
278
+ `);
279
+ process.exit(0);
280
+ }
281
+ process.stderr.write(`${COLORS2.red}Missing changeset!${COLORS2.reset}
282
+
283
+ `);
284
+ process.stderr.write(`The following packages have source changes but no changeset:
285
+
286
+ `);
287
+ for (const pkg of check.missingFor) {
288
+ process.stderr.write(` ${COLORS2.yellow}@outfitter/${pkg}${COLORS2.reset}
289
+ `);
290
+ }
291
+ process.stderr.write(`
292
+ Run ${COLORS2.blue}bun run changeset${COLORS2.reset} to add a changeset, ` + `or add the ${COLORS2.blue}no-changeset${COLORS2.reset} label to skip.
293
+ `);
294
+ process.exit(1);
295
+ }
296
+
297
+ // src/cli/check-clean-tree.ts
298
+ function parseGitDiff(diffOutput) {
299
+ return diffOutput.split(`
300
+ `).map((line) => line.trim()).filter(Boolean);
301
+ }
302
+ function parseUntrackedFiles(lsOutput) {
303
+ return lsOutput.split(`
304
+ `).map((line) => line.trim()).filter(Boolean);
305
+ }
306
+ var COLORS3 = {
307
+ reset: "\x1B[0m",
308
+ red: "\x1B[31m",
309
+ green: "\x1B[32m",
310
+ dim: "\x1B[2m"
311
+ };
312
+ async function runCheckCleanTree(options = {}) {
313
+ const pathArgs = options.paths ?? [];
314
+ const diffResult = Bun.spawnSync(["git", "diff", "HEAD", "--name-only", "--", ...pathArgs], { stderr: "pipe" });
315
+ if (diffResult.exitCode !== 0) {
316
+ process.stderr.write(`Failed to run git diff
317
+ `);
318
+ process.exit(1);
319
+ }
320
+ const modified = parseGitDiff(diffResult.stdout.toString());
321
+ const lsResult = Bun.spawnSync(["git", "ls-files", "--others", "--exclude-standard", "--", ...pathArgs], { stderr: "pipe" });
322
+ if (lsResult.exitCode !== 0) {
323
+ process.stderr.write(`Failed to run git ls-files
324
+ `);
325
+ process.exit(1);
326
+ }
327
+ const untracked = parseUntrackedFiles(lsResult.stdout.toString());
328
+ const clean = modified.length === 0 && untracked.length === 0;
329
+ const status = { clean, modified, untracked };
330
+ if (status.clean) {
331
+ process.stdout.write(`${COLORS3.green}Working tree is clean.${COLORS3.reset}
332
+ `);
333
+ process.exit(0);
334
+ }
335
+ process.stderr.write(`${COLORS3.red}Working tree is dirty after verification:${COLORS3.reset}
336
+
337
+ `);
338
+ if (modified.length > 0) {
339
+ process.stderr.write(`Modified files:
340
+ `);
341
+ for (const file of modified) {
342
+ process.stderr.write(` ${COLORS3.dim}M${COLORS3.reset} ${file}
343
+ `);
344
+ }
345
+ }
346
+ if (untracked.length > 0) {
347
+ process.stderr.write(`Untracked files:
348
+ `);
349
+ for (const file of untracked) {
350
+ process.stderr.write(` ${COLORS3.dim}?${COLORS3.reset} ${file}
351
+ `);
352
+ }
353
+ }
354
+ process.stderr.write(`
355
+ This likely means a build step produced uncommitted changes.
356
+ `);
357
+ process.stderr.write(`Commit these changes or add them to .gitignore.
358
+ `);
359
+ process.exit(1);
360
+ }
361
+
362
+ // src/cli/check-exports.ts
363
+ import { resolve as resolve3 } from "node:path";
364
+ function entryToSubpath(entry) {
365
+ const stripped = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
366
+ if (stripped === "index") {
367
+ return ".";
368
+ }
369
+ if (stripped.endsWith("/index")) {
370
+ return `./${stripped.slice(0, -"/index".length)}`;
371
+ }
372
+ return `./${stripped}`;
373
+ }
374
+ function compareExports(input) {
375
+ const { name, actual, expected, path } = input;
376
+ const actualKeys = new Set(Object.keys(actual));
377
+ const expectedKeys = new Set(Object.keys(expected));
378
+ const added = [];
379
+ const removed = [];
380
+ const changed = [];
381
+ for (const key of expectedKeys) {
382
+ if (!actualKeys.has(key)) {
383
+ added.push(key);
384
+ }
385
+ }
386
+ for (const key of actualKeys) {
387
+ if (!expectedKeys.has(key)) {
388
+ removed.push(key);
389
+ }
390
+ }
391
+ for (const key of actualKeys) {
392
+ if (expectedKeys.has(key)) {
393
+ const actualValue = actual[key];
394
+ const expectedValue = expected[key];
395
+ if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
396
+ changed.push({ key, expected: expectedValue, actual: actualValue });
397
+ }
398
+ }
399
+ }
400
+ added.sort();
401
+ removed.sort();
402
+ changed.sort((a, b) => a.key.localeCompare(b.key));
403
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
404
+ return { name, status: "ok" };
405
+ }
406
+ return {
407
+ name,
408
+ status: "drift",
409
+ drift: {
410
+ package: name,
411
+ path: path ?? "",
412
+ added,
413
+ removed,
414
+ changed
415
+ }
416
+ };
417
+ }
418
+ function matchesExclude(subpath, excludes) {
419
+ return excludes.some((pattern) => new Bun.Glob(pattern).match(subpath));
420
+ }
421
+ var CLI_EXCLUSION_PATTERNS = [
422
+ "**/cli.ts",
423
+ "**/cli/index.ts",
424
+ "**/bin.ts",
425
+ "**/bin/index.ts"
426
+ ];
427
+ function isCliEntrypoint(entry) {
428
+ return CLI_EXCLUSION_PATTERNS.some((pattern) => new Bun.Glob(pattern).match(entry));
429
+ }
430
+ function buildExportValue(entry) {
431
+ const distPath = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
432
+ return {
433
+ import: {
434
+ types: `./dist/${distPath}.d.ts`,
435
+ default: `./dist/${distPath}.js`
436
+ }
437
+ };
438
+ }
439
+ function discoverEntries(packageRoot) {
440
+ const glob = new Bun.Glob("src/**/*.ts");
441
+ const entries = [];
442
+ for (const match of glob.scanSync({ cwd: packageRoot, dot: false })) {
443
+ if (match.includes("__tests__") || match.endsWith(".test.ts")) {
444
+ continue;
445
+ }
446
+ entries.push(match);
447
+ }
448
+ return entries.sort();
449
+ }
450
+ function addConfigFileExports(expected, pkg) {
451
+ const CONFIG_RE = /\.(json|jsonc|yml|yaml|toml)$/;
452
+ const configFiles = (pkg.files ?? []).filter((file) => CONFIG_RE.test(file) && file !== "package.json");
453
+ for (const file of configFiles) {
454
+ expected[`./${file}`] = `./${file}`;
455
+ let base = file.replace(CONFIG_RE, "");
456
+ const match = base.match(/^(.+)\.preset(?:\.(.+))?$/);
457
+ if (match?.[1]) {
458
+ base = match[2] ? `${match[1]}-${match[2]}` : match[1];
459
+ }
460
+ if (base !== file) {
461
+ expected[`./${base}`] = `./${file}`;
462
+ }
463
+ }
464
+ }
465
+ function computeExpectedExports(packageRoot, workspace, pkg) {
466
+ const entries = discoverEntries(packageRoot);
467
+ const exportsConfig = typeof workspace.config?.exports === "object" ? workspace.config.exports : undefined;
468
+ const excludes = exportsConfig?.exclude ?? [];
469
+ const customExports = exportsConfig?.customExports ?? {};
470
+ const expected = {};
471
+ const subpathEntries = new Map;
472
+ for (const entry of entries) {
473
+ if (isCliEntrypoint(entry))
474
+ continue;
475
+ const subpath = entryToSubpath(entry);
476
+ if (matchesExclude(subpath, excludes))
477
+ continue;
478
+ const existing = subpathEntries.get(subpath);
479
+ if (existing) {
480
+ if (!existing.endsWith("/index.ts") && entry.endsWith("/index.ts")) {
481
+ continue;
482
+ }
483
+ }
484
+ subpathEntries.set(subpath, entry);
485
+ }
486
+ for (const [subpath, entry] of subpathEntries) {
487
+ expected[subpath] = buildExportValue(entry);
488
+ }
489
+ for (const [key, value] of Object.entries(customExports)) {
490
+ expected[`./${key.replace(/^\.\//, "")}`] = value;
491
+ }
492
+ addConfigFileExports(expected, pkg);
493
+ expected["./package.json"] = "./package.json";
494
+ return expected;
495
+ }
496
+ var COLORS4 = {
497
+ reset: "\x1B[0m",
498
+ red: "\x1B[31m",
499
+ green: "\x1B[32m",
500
+ yellow: "\x1B[33m",
501
+ blue: "\x1B[34m",
502
+ dim: "\x1B[2m"
503
+ };
504
+ async function runCheckExports(options = {}) {
505
+ const cwd = process.cwd();
506
+ const configPath = resolve3(cwd, "bunup.config.ts");
507
+ let workspaces;
508
+ try {
509
+ const configModule = await import(configPath);
510
+ const rawConfig = configModule.default;
511
+ if (!Array.isArray(rawConfig)) {
512
+ process.stderr.write(`bunup.config.ts must export a workspace array
513
+ `);
514
+ process.exit(1);
515
+ }
516
+ workspaces = rawConfig;
517
+ } catch {
518
+ process.stderr.write(`Could not load bunup.config.ts from ${cwd}
519
+ `);
520
+ process.exit(1);
521
+ }
522
+ const results = [];
523
+ for (const workspace of workspaces) {
524
+ const packageRoot = resolve3(cwd, workspace.root);
525
+ const pkgPath = resolve3(packageRoot, "package.json");
526
+ let pkg;
527
+ try {
528
+ pkg = await Bun.file(pkgPath).json();
529
+ } catch {
530
+ results.push({ name: workspace.name, status: "ok" });
531
+ continue;
532
+ }
533
+ const actual = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
534
+ const expected = computeExpectedExports(packageRoot, workspace, pkg);
535
+ results.push(compareExports({
536
+ name: workspace.name,
537
+ actual,
538
+ expected,
539
+ path: workspace.root
540
+ }));
541
+ }
542
+ const checkResult = {
543
+ ok: results.every((r) => r.status === "ok"),
544
+ packages: results
545
+ };
546
+ if (options.json) {
547
+ process.stdout.write(`${JSON.stringify(checkResult, null, 2)}
548
+ `);
549
+ } else {
550
+ const drifted = results.filter((r) => r.status === "drift");
551
+ if (drifted.length === 0) {
552
+ process.stdout.write(`${COLORS4.green}All ${results.length} packages have exports in sync.${COLORS4.reset}
553
+ `);
554
+ } else {
555
+ process.stderr.write(`${COLORS4.red}Export drift detected in ${drifted.length} package(s):${COLORS4.reset}
556
+
557
+ `);
558
+ for (const result of drifted) {
559
+ const drift = result.drift;
560
+ if (!drift)
561
+ continue;
562
+ process.stderr.write(` ${COLORS4.yellow}${result.name}${COLORS4.reset} ${COLORS4.dim}(${drift.path})${COLORS4.reset}
563
+ `);
564
+ for (const key of drift.added) {
565
+ process.stderr.write(` ${COLORS4.green}+ ${key}${COLORS4.reset} ${COLORS4.dim}(missing from package.json)${COLORS4.reset}
566
+ `);
567
+ }
568
+ for (const key of drift.removed) {
569
+ process.stderr.write(` ${COLORS4.red}- ${key}${COLORS4.reset} ${COLORS4.dim}(not in source)${COLORS4.reset}
570
+ `);
571
+ }
572
+ for (const entry of drift.changed) {
573
+ process.stderr.write(` ${COLORS4.yellow}~ ${entry.key}${COLORS4.reset} ${COLORS4.dim}(value mismatch)${COLORS4.reset}
574
+ `);
575
+ }
576
+ process.stderr.write(`
577
+ `);
578
+ }
579
+ process.stderr.write(`Run ${COLORS4.blue}bun run build${COLORS4.reset} to regenerate exports.
580
+ `);
581
+ }
582
+ }
583
+ process.exit(checkResult.ok ? 0 : 1);
584
+ }
585
+
25
586
  // src/cli/fix.ts
26
587
  function buildFixCommand(options) {
27
588
  const cmd = ["ultracite", "fix"];
@@ -110,7 +671,7 @@ async function runInit(cwd = process.cwd()) {
110
671
  // src/cli/pre-push.ts
111
672
  import { existsSync, readFileSync } from "node:fs";
112
673
  import { join } from "node:path";
113
- var COLORS = {
674
+ var COLORS5 = {
114
675
  reset: "\x1B[0m",
115
676
  red: "\x1B[31m",
116
677
  green: "\x1B[32m",
@@ -226,13 +787,13 @@ function getChangedFilesForPush() {
226
787
  function maybeSkipForRedPhase(reason, branch) {
227
788
  const changedFiles = getChangedFilesForPush();
228
789
  if (!changedFiles.deterministic) {
229
- log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
790
+ log(`${COLORS5.yellow}RED-phase bypass denied${COLORS5.reset}: could not determine full push diff range`);
230
791
  log("Running strict verification.");
231
792
  log("");
232
793
  return false;
233
794
  }
234
795
  if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
235
- log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
796
+ log(`${COLORS5.yellow}RED-phase bypass denied${COLORS5.reset}: changed files are not test-only`);
236
797
  if (changedFiles.files.length > 0) {
237
798
  log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
238
799
  } else {
@@ -242,11 +803,11 @@ function maybeSkipForRedPhase(reason, branch) {
242
803
  return false;
243
804
  }
244
805
  if (reason === "branch") {
245
- log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
806
+ log(`${COLORS5.yellow}TDD RED phase${COLORS5.reset} detected: ${COLORS5.blue}${branch}${COLORS5.reset}`);
246
807
  } else {
247
- log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
808
+ log(`${COLORS5.yellow}Scaffold branch${COLORS5.reset} with RED phase branch in context: ${COLORS5.blue}${branch}${COLORS5.reset}`);
248
809
  }
249
- log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
810
+ log(`${COLORS5.yellow}Skipping strict verification${COLORS5.reset} - changed files are test-only`);
250
811
  log(`Diff source: ${changedFiles.source}`);
251
812
  log("");
252
813
  log("Remember: GREEN phase (implementation) must make these tests pass!");
@@ -322,14 +883,14 @@ function readPackageScripts(cwd = process.cwd()) {
322
883
  }
323
884
  function runScript(scriptName) {
324
885
  log("");
325
- log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
886
+ log(`Running: ${COLORS5.blue}bun run ${scriptName}${COLORS5.reset}`);
326
887
  const result = Bun.spawnSync(["bun", "run", scriptName], {
327
888
  stdio: ["inherit", "inherit", "inherit"]
328
889
  });
329
890
  return result.exitCode === 0;
330
891
  }
331
892
  async function runPrePush(options = {}) {
332
- log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
893
+ log(`${COLORS5.blue}Pre-push verify${COLORS5.reset} (TDD-aware)`);
333
894
  log("");
334
895
  const branch = getCurrentBranch();
335
896
  if (isRedPhaseBranch(branch)) {
@@ -345,12 +906,12 @@ async function runPrePush(options = {}) {
345
906
  }
346
907
  }
347
908
  if (options.force) {
348
- log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
909
+ log(`${COLORS5.yellow}Force flag set${COLORS5.reset} - skipping strict verification`);
349
910
  process.exit(0);
350
911
  }
351
912
  const plan = createVerificationPlan(readPackageScripts());
352
913
  if (!plan.ok) {
353
- log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
914
+ log(`${COLORS5.red}Strict pre-push verification is not configured${COLORS5.reset}`);
354
915
  log(plan.error);
355
916
  log("");
356
917
  log("Add one of:");
@@ -358,7 +919,7 @@ async function runPrePush(options = {}) {
358
919
  log(" - typecheck + (check or lint) + build + test");
359
920
  process.exit(1);
360
921
  }
361
- log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
922
+ log(`Running strict verification for branch: ${COLORS5.blue}${branch}${COLORS5.reset}`);
362
923
  if (plan.source === "verify:ci") {
363
924
  log("Using `verify:ci` script.");
364
925
  } else {
@@ -369,7 +930,7 @@ async function runPrePush(options = {}) {
369
930
  continue;
370
931
  }
371
932
  log("");
372
- log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
933
+ log(`${COLORS5.red}Verification failed${COLORS5.reset} on script: ${scriptName}`);
373
934
  log("");
374
935
  log("If this is intentional TDD RED phase work, name your branch:");
375
936
  log(" - feature-tests");
@@ -378,14 +939,14 @@ async function runPrePush(options = {}) {
378
939
  process.exit(1);
379
940
  }
380
941
  log("");
381
- log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
942
+ log(`${COLORS5.green}Strict verification passed${COLORS5.reset}`);
382
943
  process.exit(0);
383
944
  }
384
945
 
385
946
  // src/cli/upgrade-bun.ts
386
947
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
387
948
  import { join as join2 } from "node:path";
388
- var COLORS2 = {
949
+ var COLORS6 = {
389
950
  reset: "\x1B[0m",
390
951
  red: "\x1B[31m",
391
952
  green: "\x1B[32m",
@@ -397,15 +958,15 @@ function log2(msg) {
397
958
  `);
398
959
  }
399
960
  function info(msg) {
400
- process.stdout.write(`${COLORS2.blue}▸${COLORS2.reset} ${msg}
961
+ process.stdout.write(`${COLORS6.blue}▸${COLORS6.reset} ${msg}
401
962
  `);
402
963
  }
403
964
  function success(msg) {
404
- process.stdout.write(`${COLORS2.green}✓${COLORS2.reset} ${msg}
965
+ process.stdout.write(`${COLORS6.green}✓${COLORS6.reset} ${msg}
405
966
  `);
406
967
  }
407
968
  function warn(msg) {
408
- process.stdout.write(`${COLORS2.yellow}!${COLORS2.reset} ${msg}
969
+ process.stdout.write(`${COLORS6.yellow}!${COLORS6.reset} ${msg}
409
970
  `);
410
971
  }
411
972
  async function fetchLatestVersion() {
@@ -528,7 +1089,7 @@ async function runUpgradeBun(targetVersion, options = {}) {
528
1089
 
529
1090
  // src/cli/index.ts
530
1091
  var program = new Command;
531
- program.name("tooling").description("Dev tooling configuration management for Outfitter projects").version("0.1.0-rc.1");
1092
+ program.name("tooling").description("Dev tooling configuration management for Outfitter projects").version(VERSION);
532
1093
  program.command("init").description("Initialize tooling config in the current project").action(async () => {
533
1094
  await runInit();
534
1095
  });
@@ -544,4 +1105,23 @@ program.command("upgrade-bun").description("Upgrade Bun version across the proje
544
1105
  program.command("pre-push").description("TDD-aware pre-push strict verification hook").option("-f, --force", "Skip strict verification entirely").action(async (options) => {
545
1106
  await runPrePush(options);
546
1107
  });
1108
+ program.command("check-bunup-registry").description("Validate packages with bunup --filter are registered in bunup.config.ts").action(async () => {
1109
+ await runCheckBunupRegistry();
1110
+ });
1111
+ program.command("check-changeset").description("Validate PRs touching package source include a changeset").option("-s, --skip", "Skip changeset check").action(async (options) => {
1112
+ await runCheckChangeset(options);
1113
+ });
1114
+ program.command("check-exports").description("Validate package.json exports match source entry points").option("--json", "Output results as JSON").action(async (options) => {
1115
+ await runCheckExports(options);
1116
+ });
1117
+ program.command("check-clean-tree").description("Assert working tree is clean (no modified or untracked files)").option("--paths <paths...>", "Limit check to specific paths").action(async (options) => {
1118
+ await runCheckCleanTree(options);
1119
+ });
1120
+ program.command("check-readme-imports").description("Validate README import examples match package exports").option("--json", "Output results as JSON").action(async (options) => {
1121
+ const { runCheckReadmeImports } = await import("../shared/chunk-6a7tjcgm.js");
1122
+ await runCheckReadmeImports(options);
1123
+ });
1124
+ program.command("check-boundary-invocations").description("Validate root/app scripts do not execute packages/*/src entrypoints directly").action(async () => {
1125
+ await runCheckBoundaryInvocations();
1126
+ });
547
1127
  program.parse();