@outfitter/tooling 0.3.3 → 0.3.5

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 (94) hide show
  1. package/.markdownlint-cli2.jsonc +55 -55
  2. package/README.md +33 -24
  3. package/dist/bun-version-compat.d.ts +2 -0
  4. package/dist/bun-version-compat.js +10 -0
  5. package/dist/cli/check-boundary-invocations.d.ts +34 -0
  6. package/dist/cli/check-boundary-invocations.js +14 -0
  7. package/dist/cli/check-bunup-registry.d.ts +36 -0
  8. package/dist/cli/check-bunup-registry.js +12 -0
  9. package/dist/cli/check-changeset.d.ts +82 -0
  10. package/dist/cli/check-changeset.js +24 -0
  11. package/dist/cli/check-clean-tree.d.ts +36 -0
  12. package/dist/cli/check-clean-tree.js +14 -0
  13. package/dist/cli/check-exports.d.ts +3 -0
  14. package/dist/cli/check-exports.js +17 -0
  15. package/dist/cli/check-home-paths.d.ts +31 -0
  16. package/dist/cli/check-home-paths.js +12 -0
  17. package/dist/cli/check-markdown-links.d.ts +42 -0
  18. package/dist/cli/check-markdown-links.js +13 -0
  19. package/dist/cli/check-readme-imports.d.ts +61 -0
  20. package/dist/{shared/chunk-7tdgbqb0.js → cli/check-readme-imports.js} +7 -6
  21. package/dist/cli/check-tsdoc.d.ts +5 -0
  22. package/dist/cli/check-tsdoc.js +42 -0
  23. package/dist/cli/check.d.ts +19 -0
  24. package/dist/cli/check.js +10 -0
  25. package/dist/cli/fix.d.ts +19 -0
  26. package/dist/cli/fix.js +10 -0
  27. package/dist/cli/index.js +61 -1218
  28. package/dist/cli/init.d.ts +31 -0
  29. package/dist/cli/init.js +12 -0
  30. package/dist/cli/internal/exports-analysis.d.ts +2 -0
  31. package/dist/cli/internal/exports-analysis.js +10 -0
  32. package/dist/cli/internal/exports-fs.d.ts +17 -0
  33. package/dist/cli/internal/exports-fs.js +9 -0
  34. package/dist/cli/internal/pre-push-checks.d.ts +2 -0
  35. package/dist/cli/internal/pre-push-checks.js +37 -0
  36. package/dist/cli/internal/tsdoc-analysis.d.ts +3 -0
  37. package/dist/cli/internal/tsdoc-analysis.js +26 -0
  38. package/dist/cli/internal/tsdoc-formatting.d.ts +3 -0
  39. package/dist/cli/internal/tsdoc-formatting.js +10 -0
  40. package/dist/cli/internal/tsdoc-types.d.ts +2 -0
  41. package/dist/cli/internal/tsdoc-types.js +16 -0
  42. package/dist/cli/pre-push.d.ts +7 -0
  43. package/dist/cli/pre-push.js +29 -0
  44. package/dist/cli/upgrade-bun.d.ts +8 -0
  45. package/dist/cli/upgrade-bun.js +9 -0
  46. package/dist/index.d.ts +9 -186
  47. package/dist/index.js +4 -42
  48. package/dist/registry/build.d.ts +4 -0
  49. package/dist/registry/build.js +279 -0
  50. package/dist/registry/index.d.ts +3 -0
  51. package/dist/registry/index.js +1 -0
  52. package/dist/registry/schema.d.ts +2 -0
  53. package/dist/registry/schema.js +28 -0
  54. package/dist/shared/@outfitter/tooling-0zjz8eg9.js +106 -0
  55. package/dist/shared/@outfitter/tooling-1hez6j9d.js +21 -0
  56. package/dist/shared/@outfitter/tooling-2vv5y3s4.js +145 -0
  57. package/dist/shared/{chunk-cmde0fwx.js → @outfitter/tooling-5xxctk9b.js} +12 -138
  58. package/dist/shared/@outfitter/tooling-5ynz680q.js +59 -0
  59. package/dist/shared/@outfitter/tooling-7437rmy6.js +39 -0
  60. package/dist/shared/@outfitter/tooling-8qcwr06t.d.ts +74 -0
  61. package/dist/shared/@outfitter/tooling-9ram55dd.js +69 -0
  62. package/dist/shared/@outfitter/tooling-9vs606gq.d.ts +3 -0
  63. package/dist/shared/@outfitter/tooling-a4bfx4be.js +21 -0
  64. package/dist/shared/@outfitter/tooling-a59br34g.js +32 -0
  65. package/dist/shared/@outfitter/tooling-a6q3zh7t.js +86 -0
  66. package/dist/shared/@outfitter/tooling-amrbp7cm.js +102 -0
  67. package/dist/shared/@outfitter/tooling-ayps7c4x.js +58 -0
  68. package/dist/shared/@outfitter/tooling-c8q6mj8z.js +228 -0
  69. package/dist/shared/@outfitter/tooling-cb0b8wsx.d.ts +57 -0
  70. package/dist/shared/@outfitter/tooling-ctmgnap5.js +19 -0
  71. package/dist/shared/@outfitter/tooling-f8q38e9z.d.ts +16 -0
  72. package/dist/shared/@outfitter/tooling-gcdvsqqp.js +73 -0
  73. package/dist/shared/@outfitter/tooling-h5dnevjw.js +139 -0
  74. package/dist/shared/@outfitter/tooling-j8d1h2zd.d.ts +10 -0
  75. package/dist/shared/@outfitter/tooling-ja1zg5yc.js +214 -0
  76. package/dist/shared/@outfitter/tooling-jnrs9rqd.js +4 -0
  77. package/dist/shared/@outfitter/tooling-mkynjra9.js +23 -0
  78. package/dist/shared/@outfitter/tooling-mq2xvz96.js +285 -0
  79. package/dist/shared/@outfitter/tooling-pq47jv6t.js +213 -0
  80. package/dist/shared/@outfitter/tooling-sjm8nebx.d.ts +109 -0
  81. package/dist/shared/@outfitter/tooling-stgnc2zx.d.ts +85 -0
  82. package/dist/shared/@outfitter/tooling-tj9p41vj.d.ts +55 -0
  83. package/dist/shared/@outfitter/tooling-vjmhvpjq.d.ts +29 -0
  84. package/dist/shared/@outfitter/tooling-wwm97f47.js +81 -0
  85. package/dist/shared/@outfitter/tooling-y43b117h.d.ts +13 -0
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.js +8 -0
  88. package/lefthook.yml +5 -1
  89. package/package.json +140 -131
  90. package/registry/registry.json +19 -12
  91. package/tsconfig.preset.bun.json +5 -5
  92. package/tsconfig.preset.json +33 -33
  93. package/biome.json +0 -81
  94. package/dist/shared/chunk-3s189drz.js +0 -4
@@ -0,0 +1,102 @@
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.toSorted((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.exitCode = 1;
82
+ return;
83
+ }
84
+ const violations = findBoundaryViolations(entries);
85
+ if (violations.length === 0) {
86
+ process.stdout.write(`No boundary invocation violations detected in root/apps scripts.
87
+ `);
88
+ process.exitCode = 0;
89
+ return;
90
+ }
91
+ process.stderr.write(`Boundary invocation violations detected:
92
+
93
+ `);
94
+ for (const violation of violations) {
95
+ process.stderr.write(`- ${violation.file}#${violation.scriptName}: ${violation.command}
96
+ `);
97
+ }
98
+ process.stderr.write("\nUse canonical command surfaces (e.g. `outfitter repo ...` or package bins) instead of executing packages/*/src directly.\n");
99
+ process.exitCode = 1;
100
+ }
101
+
102
+ export { detectBoundaryViolation, findBoundaryViolations, readScriptEntries, runCheckBoundaryInvocations };
@@ -0,0 +1,58 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/internal/exports-analysis.ts
3
+ function entryToSubpath(entry) {
4
+ const stripped = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
5
+ if (stripped === "index") {
6
+ return ".";
7
+ }
8
+ if (stripped.endsWith("/index")) {
9
+ return `./${stripped.slice(0, -"/index".length)}`;
10
+ }
11
+ return `./${stripped}`;
12
+ }
13
+ function compareExports(input) {
14
+ const { name, actual, expected, path } = input;
15
+ const actualKeys = new Set(Object.keys(actual));
16
+ const expectedKeys = new Set(Object.keys(expected));
17
+ const added = [];
18
+ const removed = [];
19
+ const changed = [];
20
+ for (const key of expectedKeys) {
21
+ if (!actualKeys.has(key)) {
22
+ added.push(key);
23
+ }
24
+ }
25
+ for (const key of actualKeys) {
26
+ if (!expectedKeys.has(key)) {
27
+ removed.push(key);
28
+ }
29
+ }
30
+ for (const key of actualKeys) {
31
+ if (expectedKeys.has(key)) {
32
+ const actualValue = actual[key];
33
+ const expectedValue = expected[key];
34
+ if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
35
+ changed.push({ key, expected: expectedValue, actual: actualValue });
36
+ }
37
+ }
38
+ }
39
+ added.sort();
40
+ removed.sort();
41
+ changed.sort((a, b) => a.key.localeCompare(b.key));
42
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
43
+ return { name, status: "ok" };
44
+ }
45
+ return {
46
+ name,
47
+ status: "drift",
48
+ drift: {
49
+ package: name,
50
+ path: path ?? "",
51
+ added,
52
+ removed,
53
+ changed
54
+ }
55
+ };
56
+ }
57
+
58
+ export { entryToSubpath, compareExports };
@@ -0,0 +1,228 @@
1
+ // @bun
2
+ import {
3
+ analyzeSourceFile,
4
+ calculateCoverage
5
+ } from "./tooling-5xxctk9b.js";
6
+
7
+ // packages/tooling/src/cli/internal/pre-push-checks.ts
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { join, resolve } from "path";
10
+ import ts from "typescript";
11
+ function getCurrentBranch() {
12
+ const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
13
+ return result.stdout.toString().trim();
14
+ }
15
+ function runGit(args) {
16
+ try {
17
+ const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
18
+ if (result.exitCode !== 0) {
19
+ return { ok: false, lines: [] };
20
+ }
21
+ return {
22
+ ok: true,
23
+ lines: result.stdout.toString().split(`
24
+ `).map((line) => line.trim()).filter(Boolean)
25
+ };
26
+ } catch {
27
+ return { ok: false, lines: [] };
28
+ }
29
+ }
30
+ function isRedPhaseBranch(branch) {
31
+ return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
32
+ }
33
+ function isScaffoldBranch(branch) {
34
+ return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
35
+ }
36
+ function isReleaseBranch(branch) {
37
+ return branch.startsWith("changeset-release/");
38
+ }
39
+ var TEST_PATH_PATTERNS = [
40
+ /(^|\/)__tests__\//,
41
+ /(^|\/)__snapshots__\//,
42
+ /\.(test|spec)\.[cm]?[jt]sx?$/,
43
+ /\.snap$/,
44
+ /(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
45
+ /(^|\/)tsconfig\.test\.json$/,
46
+ /(^|\/)\.env\.test(\.|$)/
47
+ ];
48
+ function isTestOnlyPath(path) {
49
+ const normalized = path.replaceAll("\\", "/");
50
+ return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
51
+ }
52
+ function areFilesTestOnly(paths) {
53
+ return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
54
+ }
55
+ function canBypassRedPhaseByChangedFiles(changedFiles) {
56
+ return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
57
+ }
58
+ function hasPackageSourceChanges(changedFiles) {
59
+ const packageSrcPattern = /^packages\/[^/]+\/src\//;
60
+ return changedFiles.files.some((f) => packageSrcPattern.test(f));
61
+ }
62
+ function resolveBaseRef() {
63
+ const candidates = [
64
+ "origin/main",
65
+ "main",
66
+ "origin/trunk",
67
+ "trunk",
68
+ "origin/master",
69
+ "master"
70
+ ];
71
+ for (const candidate of candidates) {
72
+ const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
73
+ if (resolved.ok) {
74
+ return candidate;
75
+ }
76
+ }
77
+ return;
78
+ }
79
+ function changedFilesFromRange(range) {
80
+ const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
81
+ return {
82
+ ok: result.ok,
83
+ files: result.lines
84
+ };
85
+ }
86
+ function getChangedFilesForPush() {
87
+ const upstream = runGit([
88
+ "rev-parse",
89
+ "--abbrev-ref",
90
+ "--symbolic-full-name",
91
+ "@{upstream}"
92
+ ]);
93
+ if (upstream.ok && upstream.lines[0]) {
94
+ const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
95
+ if (rangeResult.ok) {
96
+ return {
97
+ files: rangeResult.files,
98
+ deterministic: true,
99
+ source: "upstream"
100
+ };
101
+ }
102
+ }
103
+ const baseRef = resolveBaseRef();
104
+ if (baseRef) {
105
+ const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
106
+ if (rangeResult.ok) {
107
+ return {
108
+ files: rangeResult.files,
109
+ deterministic: true,
110
+ source: "baseRef"
111
+ };
112
+ }
113
+ }
114
+ return {
115
+ files: [],
116
+ deterministic: false,
117
+ source: "undetermined"
118
+ };
119
+ }
120
+ function hasRedPhaseBranchInContext(currentBranch) {
121
+ let branches = [];
122
+ try {
123
+ const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
124
+ if (gtResult.exitCode === 0) {
125
+ branches = gtResult.stdout.toString().split(`
126
+ `).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
127
+ }
128
+ } catch {}
129
+ if (branches.length === 0) {
130
+ const gitResult = Bun.spawnSync([
131
+ "git",
132
+ "branch",
133
+ "--list",
134
+ "cli/*",
135
+ "types/*",
136
+ "contracts/*"
137
+ ]);
138
+ branches = gitResult.stdout.toString().split(`
139
+ `).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
140
+ }
141
+ for (const branch of branches) {
142
+ if (branch === currentBranch)
143
+ continue;
144
+ if (isRedPhaseBranch(branch))
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ function createVerificationPlan(scripts) {
150
+ if (scripts["verify:push"]) {
151
+ return { ok: true, scripts: ["verify:push"], source: "verify:push" };
152
+ }
153
+ if (scripts["verify:ci"]) {
154
+ return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
155
+ }
156
+ const requiredScripts = ["typecheck", "build", "test"];
157
+ const missingRequired = requiredScripts.filter((name) => !scripts[name]);
158
+ const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
159
+ if (!checkOrLint || missingRequired.length > 0) {
160
+ const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
161
+ return {
162
+ ok: false,
163
+ error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
164
+ };
165
+ }
166
+ return {
167
+ ok: true,
168
+ scripts: ["typecheck", checkOrLint, "build", "test"],
169
+ source: "fallback"
170
+ };
171
+ }
172
+ function readPackageScripts(cwd = process.cwd()) {
173
+ const packageJsonPath = join(cwd, "package.json");
174
+ if (!existsSync(packageJsonPath)) {
175
+ return {};
176
+ }
177
+ try {
178
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
179
+ const scripts = parsed.scripts ?? {};
180
+ const normalized = {};
181
+ for (const [name, value] of Object.entries(scripts)) {
182
+ if (typeof value === "string") {
183
+ normalized[name] = value;
184
+ }
185
+ }
186
+ return normalized;
187
+ } catch {
188
+ return {};
189
+ }
190
+ }
191
+ function checkBunVersion(projectRoot = process.cwd()) {
192
+ const versionFile = join(projectRoot, ".bun-version");
193
+ if (!existsSync(versionFile)) {
194
+ return { matches: true };
195
+ }
196
+ const expected = readFileSync(versionFile, "utf-8").trim();
197
+ const actual = Bun.version;
198
+ if (expected === actual) {
199
+ return { matches: true };
200
+ }
201
+ return { matches: false, expected, actual };
202
+ }
203
+ async function printTsdocSummary(log) {
204
+ const glob = new Bun.Glob("packages/*/src/index.ts");
205
+ const cwd = process.cwd();
206
+ const allDeclarations = [];
207
+ for (const entry of glob.scanSync({ cwd })) {
208
+ const filePath = resolve(cwd, entry);
209
+ const content = await Bun.file(filePath).text();
210
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
211
+ allDeclarations.push(...analyzeSourceFile(sourceFile));
212
+ }
213
+ if (allDeclarations.length === 0)
214
+ return;
215
+ const coverage = calculateCoverage(allDeclarations);
216
+ const parts = [];
217
+ if (coverage.documented > 0)
218
+ parts.push(`${coverage.documented} documented`);
219
+ if (coverage.partial > 0)
220
+ parts.push(`${coverage.partial} partial`);
221
+ if (coverage.undocumented > 0)
222
+ parts.push(`${coverage.undocumented} undocumented`);
223
+ const BLUE = "\x1B[34m";
224
+ const RESET = "\x1B[0m";
225
+ log(`${BLUE}TSDoc${RESET}: ${coverage.percentage}% coverage (${parts.join(", ")} of ${coverage.total} total)`);
226
+ }
227
+
228
+ export { getCurrentBranch, runGit, isRedPhaseBranch, isScaffoldBranch, isReleaseBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, hasPackageSourceChanges, getChangedFilesForPush, hasRedPhaseBranchInContext, createVerificationPlan, readPackageScripts, checkBunVersion, printTsdocSummary };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pure analysis functions for comparing maps.
3
+ *
4
+ * Types and stateless comparison logic used by the check-exports command.
5
+ * No filesystem access — all inputs are passed as arguments.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /** A package.json map: keys are subpaths, values are conditions or strings */
10
+ type ExportMap = Record<string, unknown>;
11
+ /** Describes drift between expected and actual exports for a single package */
12
+ interface ExportDrift {
13
+ readonly package: string;
14
+ readonly path: string;
15
+ readonly added: string[];
16
+ readonly removed: string[];
17
+ readonly changed: Array<{
18
+ readonly key: string;
19
+ readonly expected: unknown;
20
+ readonly actual: unknown;
21
+ }>;
22
+ }
23
+ /** Per-package comparison result */
24
+ interface PackageResult {
25
+ readonly name: string;
26
+ readonly status: "ok" | "drift";
27
+ readonly drift?: ExportDrift;
28
+ }
29
+ /** Aggregated result across all checked packages */
30
+ interface CheckResult {
31
+ readonly ok: boolean;
32
+ readonly packages: PackageResult[];
33
+ }
34
+ /** Input for comparing a single package's exports */
35
+ interface CompareInput {
36
+ readonly name: string;
37
+ readonly actual: ExportMap;
38
+ readonly expected: ExportMap;
39
+ readonly path?: string;
40
+ }
41
+ /**
42
+ * Convert a source entry file path to its subpath.
43
+ *
44
+ * @example
45
+ * entryToSubpath("src/index.ts") // "."
46
+ * entryToSubpath("src/branded.ts") // "./branded"
47
+ * entryToSubpath("src/cli/index.ts") // "./cli"
48
+ * entryToSubpath("src/cli/check.ts") // "./cli/check"
49
+ */
50
+ declare function entryToSubpath(entry: string): string;
51
+ /**
52
+ * Compare actual vs expected exports for a single package.
53
+ *
54
+ * Returns a PackageResult with status "ok" or "drift" and detailed diff.
55
+ */
56
+ declare function compareExports(input: CompareInput): PackageResult;
57
+ export { ExportMap, ExportDrift, PackageResult, CheckResult, CompareInput, entryToSubpath, compareExports };
@@ -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,16 @@
1
+ import { CheckTsDocOptions, TsDocCheckResult } from "./tooling-tj9p41vj.js";
2
+ /** Resolve whether JSON output mode is active. */
3
+ declare function resolveJsonMode(options?: CheckTsDocOptions): boolean;
4
+ /**
5
+ * Print a TSDoc coverage result in human-readable format.
6
+ *
7
+ * Renders a bar chart per package with summary statistics. Writes to stdout/stderr.
8
+ *
9
+ * @param result - The coverage result to print
10
+ * @param options - Display options (strict mode, coverage threshold for warning)
11
+ */
12
+ declare function printCheckTsdocHuman(result: TsDocCheckResult, options?: {
13
+ strict?: boolean | undefined;
14
+ minCoverage?: number | undefined;
15
+ }): void;
16
+ export { resolveJsonMode, printCheckTsdocHuman };
@@ -0,0 +1,73 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-clean-tree.ts
3
+ function parseGitDiff(diffOutput) {
4
+ return diffOutput.split(`
5
+ `).map((line) => line.trim()).filter(Boolean);
6
+ }
7
+ function parseUntrackedFiles(lsOutput) {
8
+ return lsOutput.split(`
9
+ `).map((line) => line.trim()).filter(Boolean);
10
+ }
11
+ function isCleanTree(status) {
12
+ return status.modified.length === 0 && status.untracked.length === 0;
13
+ }
14
+ var COLORS = {
15
+ reset: "\x1B[0m",
16
+ red: "\x1B[31m",
17
+ green: "\x1B[32m",
18
+ dim: "\x1B[2m"
19
+ };
20
+ async function runCheckCleanTree(options = {}) {
21
+ const pathArgs = options.paths ?? [];
22
+ const diffResult = Bun.spawnSync(["git", "diff", "HEAD", "--name-only", "--", ...pathArgs], { stderr: "pipe" });
23
+ if (diffResult.exitCode !== 0) {
24
+ process.stderr.write(`Failed to run git diff
25
+ `);
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ const modified = parseGitDiff(diffResult.stdout.toString());
30
+ const lsResult = Bun.spawnSync(["git", "ls-files", "--others", "--exclude-standard", "--", ...pathArgs], { stderr: "pipe" });
31
+ if (lsResult.exitCode !== 0) {
32
+ process.stderr.write(`Failed to run git ls-files
33
+ `);
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ const untracked = parseUntrackedFiles(lsResult.stdout.toString());
38
+ const clean = modified.length === 0 && untracked.length === 0;
39
+ const status = { clean, modified, untracked };
40
+ if (status.clean) {
41
+ process.stdout.write(`${COLORS.green}Working tree is clean.${COLORS.reset}
42
+ `);
43
+ process.exitCode = 0;
44
+ return;
45
+ }
46
+ process.stderr.write(`${COLORS.red}Working tree is dirty after verification:${COLORS.reset}
47
+
48
+ `);
49
+ if (modified.length > 0) {
50
+ process.stderr.write(`Modified files:
51
+ `);
52
+ for (const file of modified) {
53
+ process.stderr.write(` ${COLORS.dim}M${COLORS.reset} ${file}
54
+ `);
55
+ }
56
+ }
57
+ if (untracked.length > 0) {
58
+ process.stderr.write(`Untracked files:
59
+ `);
60
+ for (const file of untracked) {
61
+ process.stderr.write(` ${COLORS.dim}?${COLORS.reset} ${file}
62
+ `);
63
+ }
64
+ }
65
+ process.stderr.write(`
66
+ This likely means a build step produced uncommitted changes.
67
+ `);
68
+ process.stderr.write(`Commit these changes or add them to .gitignore.
69
+ `);
70
+ process.exitCode = 1;
71
+ }
72
+
73
+ export { parseGitDiff, parseUntrackedFiles, isCleanTree, runCheckCleanTree };
@@ -0,0 +1,139 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-home-paths.ts
3
+ import { readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { resolve } from "path";
6
+ function formatLeakLineText(lineText) {
7
+ return lineText.trimEnd();
8
+ }
9
+ function writeLeakSummary(stderr, leaks) {
10
+ stderr.write(`Hardcoded home directory paths detected:
11
+ `);
12
+ for (const leak of leaks) {
13
+ stderr.write(` ${leak.filePath}:${leak.line}:${leak.column} ${formatLeakLineText(leak.lineText)}
14
+ `);
15
+ }
16
+ }
17
+ function writeReplacementHint(stderr, leaks) {
18
+ const matchedText = leaks[0]?.matchedText;
19
+ if (!matchedText) {
20
+ return;
21
+ }
22
+ stderr.write(`
23
+ Replace ${JSON.stringify(matchedText)} with a repo-relative or home-agnostic path before committing.
24
+ `);
25
+ }
26
+ function escapeRegExp(value) {
27
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28
+ }
29
+ function buildHomePathPattern(homePath) {
30
+ if (homePath.trim().length === 0) {
31
+ return;
32
+ }
33
+ const patternSource = homePath.split(/(\\+)/).map((segment) => segment.includes("\\") ? "\\\\+" : escapeRegExp(segment)).join("");
34
+ return new RegExp(`${patternSource}(?![\\w.-])`, "g");
35
+ }
36
+ function findHomePathLeaks(content, homePathOrPattern) {
37
+ let pattern;
38
+ if (homePathOrPattern instanceof RegExp) {
39
+ if (!homePathOrPattern.flags.includes("g")) {
40
+ throw new TypeError("findHomePathLeaks: RegExp must have the global (g) flag set");
41
+ }
42
+ pattern = homePathOrPattern;
43
+ } else {
44
+ pattern = buildHomePathPattern(homePathOrPattern);
45
+ }
46
+ if (!pattern) {
47
+ return [];
48
+ }
49
+ const leaks = [];
50
+ const lines = content.split(/\r?\n/);
51
+ for (let index = 0;index < lines.length; index += 1) {
52
+ const line = lines[index] ?? "";
53
+ for (const match of line.matchAll(pattern)) {
54
+ leaks.push({
55
+ line: index + 1,
56
+ column: (match.index ?? 0) + 1,
57
+ matchedText: match[0],
58
+ lineText: line
59
+ });
60
+ }
61
+ }
62
+ return leaks;
63
+ }
64
+ function scanFilesForHardcodedHomePaths(filePaths, options = {}) {
65
+ const cwd = options.cwd ?? process.cwd();
66
+ const homePath = options.homeDir ?? homedir();
67
+ const readFile = options.readFile ?? readFileSync;
68
+ const leaks = [];
69
+ const failures = [];
70
+ const pattern = buildHomePathPattern(homePath);
71
+ if (!pattern) {
72
+ return { leaks, failures };
73
+ }
74
+ for (const filePath of filePaths) {
75
+ const absolutePath = resolve(cwd, filePath);
76
+ let fileContent;
77
+ try {
78
+ fileContent = readFile(absolutePath, "utf-8");
79
+ } catch (error) {
80
+ const errno = error;
81
+ if (errno.code === "ENOENT") {
82
+ continue;
83
+ }
84
+ failures.push({
85
+ filePath,
86
+ reason: error instanceof Error ? error.message : String(error)
87
+ });
88
+ continue;
89
+ }
90
+ const fileLeaks = findHomePathLeaks(fileContent, pattern);
91
+ for (const leak of fileLeaks) {
92
+ leaks.push({
93
+ filePath,
94
+ ...leak
95
+ });
96
+ }
97
+ }
98
+ return { leaks, failures };
99
+ }
100
+ function runCheckHomePaths(paths, options = {}) {
101
+ const stderr = options.stderr ?? process.stderr;
102
+ const setExitCode = options.setExitCode ?? ((code) => {
103
+ process.exitCode = code;
104
+ });
105
+ const { failures, leaks } = scanFilesForHardcodedHomePaths(paths, options.scanOptions);
106
+ if (failures.length > 0) {
107
+ const unreadableTarget = failures.length === 1 ? "file" : "files";
108
+ stderr.write(`Unreadable ${unreadableTarget} while scanning for hardcoded home paths:
109
+ `);
110
+ for (const failure of failures) {
111
+ stderr.write(` ${failure.filePath}: ${failure.reason}
112
+ `);
113
+ }
114
+ if (leaks.length > 0) {
115
+ stderr.write(`
116
+ `);
117
+ writeLeakSummary(stderr, leaks);
118
+ writeReplacementHint(stderr, leaks);
119
+ stderr.write(`
120
+ `);
121
+ } else {
122
+ stderr.write(`
123
+ `);
124
+ }
125
+ stderr.write(`Fix file permissions or remove the unreadable ${unreadableTarget} before committing.
126
+ `);
127
+ setExitCode(1);
128
+ return;
129
+ }
130
+ if (leaks.length === 0) {
131
+ setExitCode(0);
132
+ return;
133
+ }
134
+ writeLeakSummary(stderr, leaks);
135
+ writeReplacementHint(stderr, leaks);
136
+ setExitCode(1);
137
+ }
138
+
139
+ export { findHomePathLeaks, scanFilesForHardcodedHomePaths, runCheckHomePaths };