@outfitter/tooling 0.3.0 → 0.3.4

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 +21 -21
  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.js +2 -2
  6. package/dist/cli/check-bunup-registry.js +2 -2
  7. package/dist/cli/check-changeset.js +2 -2
  8. package/dist/cli/check-clean-tree.js +2 -2
  9. package/dist/cli/check-exports.js +2 -2
  10. package/dist/cli/check-markdown-links.d.ts +42 -0
  11. package/dist/cli/check-markdown-links.js +13 -0
  12. package/dist/cli/check-readme-imports.d.ts +2 -3
  13. package/dist/cli/check-readme-imports.js +4 -4
  14. package/dist/cli/check-tsdoc.js +2 -2
  15. package/dist/cli/check.js +2 -2
  16. package/dist/cli/fix.js +2 -2
  17. package/dist/cli/index.js +49 -1221
  18. package/dist/cli/init.js +2 -2
  19. package/dist/cli/pre-push.d.ts +13 -1
  20. package/dist/cli/pre-push.js +5 -3
  21. package/dist/cli/upgrade-bun.js +3 -2
  22. package/dist/index.d.ts +6 -186
  23. package/dist/index.js +4 -42
  24. package/dist/registry/build.d.ts +1 -3
  25. package/dist/registry/build.js +187 -58
  26. package/dist/registry/index.js +1 -13
  27. package/dist/registry/schema.js +22 -6
  28. package/dist/shared/@outfitter/{tooling-9errkcvk.js → tooling-1hez6j9d.js} +1 -1
  29. package/dist/shared/@outfitter/{tooling-cj5vsa9k.js → tooling-6cxfdx0q.js} +21 -18
  30. package/dist/shared/@outfitter/{tooling-qk5xgmxr.js → tooling-875svjnz.js} +5 -4
  31. package/dist/shared/@outfitter/{tooling-mxwc1n8w.js → tooling-9ram55dd.js} +4 -3
  32. package/dist/shared/@outfitter/{tooling-0x5q15ec.js → tooling-a4bfx4be.js} +1 -1
  33. package/dist/shared/@outfitter/{tooling-r9976n43.js → tooling-amrbp7cm.js} +6 -4
  34. package/dist/shared/@outfitter/{tooling-2n2dpsaa.js → tooling-d363b88r.js} +38 -12
  35. package/dist/shared/@outfitter/{tooling-1y8w5ahg.js → tooling-gcdvsqqp.js} +7 -4
  36. package/dist/shared/@outfitter/{tooling-enjcenja.js → tooling-h04te11c.js} +6 -4
  37. package/dist/shared/@outfitter/tooling-ja1zg5yc.js +214 -0
  38. package/dist/shared/@outfitter/tooling-mkynjra9.js +23 -0
  39. package/dist/shared/@outfitter/{tooling-9yzd08v1.js → tooling-pq47jv6t.js} +72 -5
  40. package/dist/shared/@outfitter/tooling-vjmhvpjq.d.ts +29 -0
  41. package/dist/shared/@outfitter/{tooling-t17gnh9b.js → tooling-wwm97f47.js} +8 -5
  42. package/dist/version.js +1 -1
  43. package/package.json +134 -130
  44. package/registry/registry.json +18 -11
  45. package/tsconfig.preset.bun.json +5 -5
  46. package/tsconfig.preset.json +33 -33
  47. package/biome.json +0 -81
  48. package/dist/shared/@outfitter/tooling-kcvs6mys.js +0 -1
  49. package/dist/shared/@outfitter/tooling-wv09k6hr.js +0 -23
  50. package/dist/shared/chunk-3s189drz.js +0 -4
  51. package/dist/shared/chunk-7tdgbqb0.js +0 -197
  52. package/dist/shared/chunk-cmde0fwx.js +0 -421
  53. /package/dist/shared/@outfitter/{tooling-dvwh9qve.js → tooling-jnrs9rqd.js} +0 -0
@@ -15,7 +15,7 @@ async function runFix(paths = []) {
15
15
  stdio: ["inherit", "inherit", "inherit"]
16
16
  });
17
17
  const exitCode = await proc.exited;
18
- process.exit(exitCode);
18
+ process.exitCode = exitCode;
19
19
  }
20
20
 
21
21
  export { buildFixCommand, runFix };
@@ -11,7 +11,7 @@ function getChangedPackagePaths(files) {
11
11
  packageNames.add(match[1]);
12
12
  }
13
13
  }
14
- return [...packageNames].sort();
14
+ return [...packageNames].toSorted();
15
15
  }
16
16
  function getChangedChangesetFiles(files) {
17
17
  const pattern = /^\.changeset\/([^/]+\.md)$/;
@@ -22,7 +22,7 @@ function getChangedChangesetFiles(files) {
22
22
  results.push(match[1]);
23
23
  }
24
24
  }
25
- return results.sort();
25
+ return results.toSorted();
26
26
  }
27
27
  function checkChangesetRequired(changedPackages, changesetFiles) {
28
28
  if (changedPackages.length === 0) {
@@ -57,7 +57,7 @@ function parseChangesetFrontmatterPackageNames(markdownContent) {
57
57
  packages.add(match[2]);
58
58
  }
59
59
  }
60
- return [...packages].sort();
60
+ return [...packages].toSorted();
61
61
  }
62
62
  function findIgnoredPackageReferences(input) {
63
63
  if (input.ignoredPackages.length === 0 || input.changesetFiles.length === 0) {
@@ -70,10 +70,10 @@ function findIgnoredPackageReferences(input) {
70
70
  const referencedPackages = parseChangesetFrontmatterPackageNames(content);
71
71
  const invalidReferences = referencedPackages.filter((pkg) => ignored.has(pkg));
72
72
  if (invalidReferences.length > 0) {
73
- results.push({ file, packages: invalidReferences.sort() });
73
+ results.push({ file, packages: invalidReferences.toSorted() });
74
74
  }
75
75
  }
76
- return results.sort((a, b) => a.file.localeCompare(b.file));
76
+ return results.toSorted((a, b) => a.file.localeCompare(b.file));
77
77
  }
78
78
  function loadIgnoredPackages(cwd) {
79
79
  const configPath = join(cwd, ".changeset", "config.json");
@@ -112,38 +112,41 @@ async function runCheckChangeset(options = {}) {
112
112
  if (options.skip || process.env["NO_CHANGESET"] === "1") {
113
113
  process.stdout.write(`${COLORS.dim}check-changeset skipped (NO_CHANGESET=1)${COLORS.reset}
114
114
  `);
115
- process.exit(0);
115
+ process.exitCode = 0;
116
+ return;
116
117
  }
117
118
  if (process.env["GITHUB_EVENT_NAME"] === "push") {
118
119
  process.stdout.write(`${COLORS.dim}check-changeset skipped (push event)${COLORS.reset}
119
120
  `);
120
- process.exit(0);
121
+ process.exitCode = 0;
122
+ return;
121
123
  }
122
124
  const cwd = process.cwd();
123
125
  let changedFiles;
124
126
  try {
125
127
  const proc = Bun.spawnSync(["git", "diff", "--name-only", "origin/main...HEAD"], { cwd });
126
128
  if (proc.exitCode !== 0) {
127
- process.exit(0);
129
+ process.exitCode = 0;
130
+ return;
128
131
  }
129
132
  changedFiles = proc.stdout.toString().trim().split(`
130
133
  `).filter((line) => line.length > 0);
131
134
  } catch {
132
- process.exit(0);
135
+ process.exitCode = 0;
136
+ return;
133
137
  }
134
138
  const changedPackages = getChangedPackagePaths(changedFiles);
135
139
  if (changedPackages.length === 0) {
136
140
  process.stdout.write(`${COLORS.green}No package source changes detected.${COLORS.reset}
137
141
  `);
138
- process.exit(0);
142
+ process.exitCode = 0;
143
+ return;
139
144
  }
140
145
  const changesetFiles = getChangedChangesetFiles(changedFiles);
141
146
  const check = checkChangesetRequired(changedPackages, changesetFiles);
142
147
  if (!check.ok) {
143
- process.stderr.write(`${COLORS.red}Missing changeset!${COLORS.reset}
144
-
145
- `);
146
- process.stderr.write(`The following packages have source changes but no changeset:
148
+ process.stderr.write(`${COLORS.yellow}No changeset found.${COLORS.reset} ` + "Consider adding one with `bun run changeset` for a custom changelog entry.\n\n");
149
+ process.stderr.write(`Packages with source changes:
147
150
 
148
151
  `);
149
152
  for (const pkg of check.missingFor) {
@@ -151,9 +154,8 @@ async function runCheckChangeset(options = {}) {
151
154
  `);
152
155
  }
153
156
  process.stderr.write(`
154
- Run ${COLORS.blue}bun run changeset${COLORS.reset} to add a changeset, ` + `or add the ${COLORS.blue}no-changeset${COLORS.reset} label to skip.
157
+ Run ${COLORS.blue}bun run changeset${COLORS.reset} for a custom changelog entry, ` + `or add ${COLORS.blue}release:none${COLORS.reset} to skip.
155
158
  `);
156
- process.exit(1);
157
159
  }
158
160
  const ignoredReferences = getIgnoredReferencesForChangedChangesets(cwd, changesetFiles);
159
161
  if (ignoredReferences.length > 0) {
@@ -174,11 +176,12 @@ Run ${COLORS.blue}bun run changeset${COLORS.reset} to add a changeset, ` + `or a
174
176
  process.stderr.write(`
175
177
  Update the affected changeset files to remove ignored packages before merging.
176
178
  `);
177
- process.exit(1);
179
+ process.exitCode = 1;
180
+ return;
178
181
  }
179
182
  process.stdout.write(`${COLORS.green}Changeset found for ${changedPackages.length} changed package(s).${COLORS.reset}
180
183
  `);
181
- process.exit(0);
184
+ process.exitCode = 0;
182
185
  }
183
186
 
184
187
  export { getChangedPackagePaths, getChangedChangesetFiles, checkChangesetRequired, parseIgnoredPackagesFromChangesetConfig, parseChangesetFrontmatterPackageNames, findIgnoredPackageReferences, runCheckChangeset };
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  __require
4
- } from "./tooling-dvwh9qve.js";
4
+ } from "./tooling-jnrs9rqd.js";
5
5
 
6
6
  // packages/tooling/src/cli/check-tsdoc.ts
7
7
  import { resolve } from "path";
@@ -225,7 +225,7 @@ function discoverPackages(cwd) {
225
225
  seenEntryPoints.add(entryPoint);
226
226
  } catch {}
227
227
  }
228
- return packages.sort((a, b) => a.name.localeCompare(b.name));
228
+ return packages.toSorted((a, b) => a.name.localeCompare(b.name));
229
229
  }
230
230
  function collectReExportedSourceFiles(sourceFile, program, pkgPath) {
231
231
  const result = [];
@@ -388,7 +388,8 @@ async function runCheckTsdoc(options = {}) {
388
388
  ` + `Searched: packages/*/src/index.ts, apps/*/src/index.ts, src/index.ts
389
389
  ` + `Use --package <path> to specify a package path explicitly.
390
390
  `);
391
- process.exit(1);
391
+ process.exitCode = 1;
392
+ return;
392
393
  }
393
394
  if (resolveJsonMode(options)) {
394
395
  process.stdout.write(`${JSON.stringify(result, null, 2)}
@@ -399,7 +400,7 @@ async function runCheckTsdoc(options = {}) {
399
400
  minCoverage: options.minCoverage
400
401
  });
401
402
  }
402
- process.exit(result.ok ? 0 : 1);
403
+ process.exitCode = result.ok ? 0 : 1;
403
404
  }
404
405
 
405
406
  export { coverageLevelSchema, declarationCoverageSchema, coverageSummarySchema, packageCoverageSchema, tsDocCheckResultSchema, isExportedDeclaration, getDeclarationName, getDeclarationKind, classifyDeclaration, analyzeSourceFile, calculateCoverage, resolveJsonMode, analyzeCheckTsdoc, printCheckTsdocHuman, runCheckTsdoc };
@@ -33,7 +33,7 @@ function buildUltraciteCommand(options) {
33
33
  "ultracite",
34
34
  "init",
35
35
  "--linter",
36
- "biome",
36
+ "oxlint",
37
37
  "--pm",
38
38
  "bun",
39
39
  "--quiet"
@@ -49,7 +49,8 @@ async function runInit(cwd = process.cwd()) {
49
49
  if (!await pkgFile.exists()) {
50
50
  process.stderr.write(`No package.json found in current directory
51
51
  `);
52
- process.exit(1);
52
+ process.exitCode = 1;
53
+ return;
53
54
  }
54
55
  const pkg = await pkgFile.json();
55
56
  const frameworkFlags = detectFrameworks(pkg);
@@ -62,7 +63,7 @@ async function runInit(cwd = process.cwd()) {
62
63
  stdio: ["inherit", "inherit", "inherit"]
63
64
  });
64
65
  const exitCode = await proc.exited;
65
- process.exit(exitCode);
66
+ process.exitCode = exitCode;
66
67
  }
67
68
 
68
69
  export { detectFrameworks, buildUltraciteCommand, runInit };
@@ -15,7 +15,7 @@ async function runCheck(paths = []) {
15
15
  stdio: ["inherit", "inherit", "inherit"]
16
16
  });
17
17
  const exitCode = await proc.exited;
18
- process.exit(exitCode);
18
+ process.exitCode = exitCode;
19
19
  }
20
20
 
21
21
  export { buildCheckCommand, runCheck };
@@ -26,7 +26,7 @@ function findBoundaryViolations(entries) {
26
26
  }
27
27
  }
28
28
  }
29
- return violations.sort((a, b) => {
29
+ return violations.toSorted((a, b) => {
30
30
  const fileCompare = a.file.localeCompare(b.file);
31
31
  if (fileCompare !== 0) {
32
32
  return fileCompare;
@@ -78,13 +78,15 @@ async function runCheckBoundaryInvocations() {
78
78
  const message = error instanceof Error ? error.message : "unknown read failure";
79
79
  process.stderr.write(`Boundary invocation check failed before evaluation: ${message}
80
80
  `);
81
- process.exit(1);
81
+ process.exitCode = 1;
82
+ return;
82
83
  }
83
84
  const violations = findBoundaryViolations(entries);
84
85
  if (violations.length === 0) {
85
86
  process.stdout.write(`No boundary invocation violations detected in root/apps scripts.
86
87
  `);
87
- process.exit(0);
88
+ process.exitCode = 0;
89
+ return;
88
90
  }
89
91
  process.stderr.write(`Boundary invocation violations detected:
90
92
 
@@ -94,7 +96,7 @@ async function runCheckBoundaryInvocations() {
94
96
  `);
95
97
  }
96
98
  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);
99
+ process.exitCode = 1;
98
100
  }
99
101
 
100
102
  export { detectBoundaryViolation, findBoundaryViolations, readScriptEntries, runCheckBoundaryInvocations };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeSourceFile,
4
4
  calculateCoverage
5
- } from "./tooling-qk5xgmxr.js";
5
+ } from "./tooling-875svjnz.js";
6
6
 
7
7
  // packages/tooling/src/cli/pre-push.ts
8
8
  import { existsSync, readFileSync } from "fs";
@@ -255,31 +255,55 @@ function runScript(scriptName) {
255
255
  });
256
256
  return result.exitCode === 0;
257
257
  }
258
+ function checkBunVersion(projectRoot = process.cwd()) {
259
+ const versionFile = join(projectRoot, ".bun-version");
260
+ if (!existsSync(versionFile)) {
261
+ return { matches: true };
262
+ }
263
+ const expected = readFileSync(versionFile, "utf-8").trim();
264
+ const actual = Bun.version;
265
+ if (expected === actual) {
266
+ return { matches: true };
267
+ }
268
+ return { matches: false, expected, actual };
269
+ }
258
270
  async function runPrePush(options = {}) {
259
271
  log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
260
272
  log("");
273
+ if (options.force) {
274
+ log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
275
+ process.exitCode = 0;
276
+ return;
277
+ }
278
+ const versionCheck = checkBunVersion();
279
+ if (!versionCheck.matches) {
280
+ log(`${COLORS.red}Bun version mismatch${COLORS.reset}: running ${versionCheck.actual}, pinned ${versionCheck.expected}`);
281
+ log("Fix: bunx @outfitter/tooling upgrade-bun");
282
+ log("");
283
+ process.exitCode = 1;
284
+ return;
285
+ }
261
286
  const branch = getCurrentBranch();
262
287
  if (isReleaseBranch(branch)) {
263
288
  log(`${COLORS.yellow}Release branch detected${COLORS.reset}: ${COLORS.blue}${branch}${COLORS.reset}`);
264
289
  log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} for automated changeset release push`);
265
- process.exit(0);
290
+ process.exitCode = 0;
291
+ return;
266
292
  }
267
293
  if (isRedPhaseBranch(branch)) {
268
294
  if (maybeSkipForRedPhase("branch", branch)) {
269
- process.exit(0);
295
+ process.exitCode = 0;
296
+ return;
270
297
  }
271
298
  }
272
299
  if (isScaffoldBranch(branch)) {
273
300
  if (hasRedPhaseBranchInContext(branch)) {
274
301
  if (maybeSkipForRedPhase("context", branch)) {
275
- process.exit(0);
302
+ process.exitCode = 0;
303
+ return;
276
304
  }
277
305
  }
278
306
  }
279
- if (options.force) {
280
- log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
281
- process.exit(0);
282
- }
283
307
  const plan = createVerificationPlan(readPackageScripts());
284
308
  if (!plan.ok) {
285
309
  log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
@@ -288,7 +312,8 @@ async function runPrePush(options = {}) {
288
312
  log("Add one of:");
289
313
  log(" - verify:ci");
290
314
  log(" - typecheck + (check or lint) + build + test");
291
- process.exit(1);
315
+ process.exitCode = 1;
316
+ return;
292
317
  }
293
318
  log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
294
319
  if (plan.source === "verify:ci") {
@@ -307,7 +332,8 @@ async function runPrePush(options = {}) {
307
332
  log(" - feature-tests");
308
333
  log(" - feature/tests");
309
334
  log(" - feature_tests");
310
- process.exit(1);
335
+ process.exitCode = 1;
336
+ return;
311
337
  }
312
338
  const changedFiles = getChangedFilesForPush();
313
339
  if (hasPackageSourceChanges(changedFiles)) {
@@ -317,7 +343,7 @@ async function runPrePush(options = {}) {
317
343
  }
318
344
  log("");
319
345
  log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
320
- process.exit(0);
346
+ process.exitCode = 0;
321
347
  }
322
348
 
323
- export { isRedPhaseBranch, isScaffoldBranch, isReleaseBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, hasPackageSourceChanges, createVerificationPlan, runPrePush };
349
+ export { isRedPhaseBranch, isScaffoldBranch, isReleaseBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, hasPackageSourceChanges, createVerificationPlan, checkBunVersion, runPrePush };
@@ -23,14 +23,16 @@ async function runCheckCleanTree(options = {}) {
23
23
  if (diffResult.exitCode !== 0) {
24
24
  process.stderr.write(`Failed to run git diff
25
25
  `);
26
- process.exit(1);
26
+ process.exitCode = 1;
27
+ return;
27
28
  }
28
29
  const modified = parseGitDiff(diffResult.stdout.toString());
29
30
  const lsResult = Bun.spawnSync(["git", "ls-files", "--others", "--exclude-standard", "--", ...pathArgs], { stderr: "pipe" });
30
31
  if (lsResult.exitCode !== 0) {
31
32
  process.stderr.write(`Failed to run git ls-files
32
33
  `);
33
- process.exit(1);
34
+ process.exitCode = 1;
35
+ return;
34
36
  }
35
37
  const untracked = parseUntrackedFiles(lsResult.stdout.toString());
36
38
  const clean = modified.length === 0 && untracked.length === 0;
@@ -38,7 +40,8 @@ async function runCheckCleanTree(options = {}) {
38
40
  if (status.clean) {
39
41
  process.stdout.write(`${COLORS.green}Working tree is clean.${COLORS.reset}
40
42
  `);
41
- process.exit(0);
43
+ process.exitCode = 0;
44
+ return;
42
45
  }
43
46
  process.stderr.write(`${COLORS.red}Working tree is dirty after verification:${COLORS.reset}
44
47
 
@@ -64,7 +67,7 @@ This likely means a build step produced uncommitted changes.
64
67
  `);
65
68
  process.stderr.write(`Commit these changes or add them to .gitignore.
66
69
  `);
67
- process.exit(1);
70
+ process.exitCode = 1;
68
71
  }
69
72
 
70
73
  export { parseGitDiff, parseUntrackedFiles, isCleanTree, runCheckCleanTree };
@@ -85,7 +85,7 @@ function discoverEntries(packageRoot) {
85
85
  }
86
86
  entries.push(match);
87
87
  }
88
- return entries.sort();
88
+ return entries.toSorted();
89
89
  }
90
90
  function addConfigFileExports(expected, pkg) {
91
91
  const CONFIG_RE = /\.(json|jsonc|yml|yaml|toml)$/;
@@ -154,13 +154,15 @@ async function runCheckExports(options = {}) {
154
154
  if (!Array.isArray(rawConfig)) {
155
155
  process.stderr.write(`bunup.config.ts must export a workspace array
156
156
  `);
157
- process.exit(1);
157
+ process.exitCode = 1;
158
+ return;
158
159
  }
159
160
  workspaces = rawConfig;
160
161
  } catch {
161
162
  process.stderr.write(`Could not load bunup.config.ts from ${cwd}
162
163
  `);
163
- process.exit(1);
164
+ process.exitCode = 1;
165
+ return;
164
166
  }
165
167
  const results = [];
166
168
  for (const workspace of workspaces) {
@@ -223,7 +225,7 @@ async function runCheckExports(options = {}) {
223
225
  `);
224
226
  }
225
227
  }
226
- process.exit(checkResult.ok ? 0 : 1);
228
+ process.exitCode = checkResult.ok ? 0 : 1;
227
229
  }
228
230
 
229
231
  export { entryToSubpath, compareExports, resolveJsonMode, runCheckExports };
@@ -0,0 +1,214 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-markdown-links.ts
3
+ import { existsSync } from "fs";
4
+ import { dirname, resolve } from "path";
5
+ var MD_LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
6
+ var SKIP_PROTOCOLS = /^(https?:\/\/|mailto:|data:|tel:)/;
7
+ var FENCE_RE = /^(\s*)((`{3,})|~{3,})(.*)$/;
8
+ function stripInlineCodeSpans(line) {
9
+ let result = "";
10
+ let index = 0;
11
+ while (index < line.length) {
12
+ if (line[index] !== "`") {
13
+ result += line[index];
14
+ index += 1;
15
+ continue;
16
+ }
17
+ let tickCount = 1;
18
+ while (line[index + tickCount] === "`") {
19
+ tickCount += 1;
20
+ }
21
+ const opener = line.slice(index, index + tickCount);
22
+ index += tickCount;
23
+ let closingIndex = -1;
24
+ let cursor = index;
25
+ while (cursor < line.length) {
26
+ if (line[cursor] !== "`") {
27
+ cursor += 1;
28
+ continue;
29
+ }
30
+ let runLength = 1;
31
+ while (line[cursor + runLength] === "`") {
32
+ runLength += 1;
33
+ }
34
+ if (runLength === tickCount) {
35
+ closingIndex = cursor;
36
+ break;
37
+ }
38
+ cursor += runLength;
39
+ }
40
+ if (closingIndex === -1) {
41
+ result += opener;
42
+ continue;
43
+ }
44
+ index = closingIndex + tickCount;
45
+ }
46
+ return result;
47
+ }
48
+ function extractMarkdownLinks(content) {
49
+ const lines = content.split(`
50
+ `);
51
+ const links = [];
52
+ let inCodeFence = false;
53
+ let fenceChar = "";
54
+ let fenceCount = 0;
55
+ for (let i = 0;i < lines.length; i++) {
56
+ const line = lines[i];
57
+ const trimmed = line.trim();
58
+ const fenceMatch = FENCE_RE.exec(trimmed);
59
+ if (fenceMatch) {
60
+ const marker = fenceMatch[2] ?? "";
61
+ const char = marker[0] ?? "`";
62
+ const count = marker.length;
63
+ if (!inCodeFence) {
64
+ inCodeFence = true;
65
+ fenceChar = char;
66
+ fenceCount = count;
67
+ continue;
68
+ }
69
+ if (char === fenceChar && count >= fenceCount && !(fenceMatch[4] ?? "").trim()) {
70
+ inCodeFence = false;
71
+ continue;
72
+ }
73
+ }
74
+ if (inCodeFence)
75
+ continue;
76
+ const stripped = stripInlineCodeSpans(line);
77
+ for (const match of stripped.matchAll(MD_LINK_RE)) {
78
+ let target = match[2];
79
+ if (SKIP_PROTOCOLS.test(target))
80
+ continue;
81
+ if (target.startsWith("#"))
82
+ continue;
83
+ const hashIndex = target.indexOf("#");
84
+ if (hashIndex !== -1) {
85
+ target = target.substring(0, hashIndex);
86
+ }
87
+ if (target === "")
88
+ continue;
89
+ links.push({ target, line: i + 1 });
90
+ }
91
+ }
92
+ return links;
93
+ }
94
+ async function validateLinks(rootDir, files) {
95
+ const broken = [];
96
+ for (const file of files) {
97
+ const absolutePath = resolve(rootDir, file);
98
+ const content = await Bun.file(absolutePath).text();
99
+ const links = extractMarkdownLinks(content);
100
+ const fileDir = dirname(absolutePath);
101
+ for (const link of links) {
102
+ if (link.target.startsWith("docs/packages/") || link.target.startsWith("./docs/packages/") || link.target.startsWith("../docs/packages/")) {
103
+ continue;
104
+ }
105
+ const resolvedTarget = resolve(fileDir, link.target);
106
+ const relativeFromRoot = resolvedTarget.substring(rootDir.length + 1);
107
+ if (relativeFromRoot.startsWith("docs/packages/")) {
108
+ continue;
109
+ }
110
+ if (!existsSync(resolvedTarget)) {
111
+ broken.push({
112
+ source: file,
113
+ target: link.target,
114
+ line: link.line
115
+ });
116
+ }
117
+ }
118
+ }
119
+ return broken;
120
+ }
121
+ var DEFAULT_PATTERNS = [
122
+ "docs/**/*.md",
123
+ "packages/*/README.md",
124
+ "AGENTS.md",
125
+ "CLAUDE.md",
126
+ "README.md",
127
+ ".claude/CLAUDE.md"
128
+ ];
129
+ async function discoverFiles(rootDir, patterns) {
130
+ const files = new Set;
131
+ for (const pattern of patterns) {
132
+ const glob = new Bun.Glob(pattern);
133
+ for await (const match of glob.scan({ cwd: rootDir, absolute: false })) {
134
+ files.add(match);
135
+ }
136
+ }
137
+ return [...files].toSorted();
138
+ }
139
+ var COLORS = {
140
+ reset: "\x1B[0m",
141
+ red: "\x1B[31m",
142
+ green: "\x1B[32m",
143
+ yellow: "\x1B[33m",
144
+ dim: "\x1B[2m",
145
+ cyan: "\x1B[36m"
146
+ };
147
+ async function runCheckMarkdownLinks(rootDir, patterns) {
148
+ const effectivePatterns = patterns ?? DEFAULT_PATTERNS;
149
+ const files = await discoverFiles(rootDir, effectivePatterns);
150
+ if (files.length === 0) {
151
+ process.stdout.write(`No markdown files found.
152
+ `);
153
+ return 0;
154
+ }
155
+ const broken = await validateLinks(rootDir, files);
156
+ if (broken.length === 0) {
157
+ process.stdout.write(`${COLORS.green}All links valid across ${files.length} markdown file(s).${COLORS.reset}
158
+ `);
159
+ return 0;
160
+ }
161
+ const bySource = new Map;
162
+ for (const link of broken) {
163
+ const existing = bySource.get(link.source);
164
+ if (existing) {
165
+ existing.push(link);
166
+ } else {
167
+ bySource.set(link.source, [link]);
168
+ }
169
+ }
170
+ process.stderr.write(`${COLORS.red}Found ${broken.length} broken link(s):${COLORS.reset}
171
+
172
+ `);
173
+ for (const [source, links] of bySource) {
174
+ process.stderr.write(` ${COLORS.yellow}${source}${COLORS.reset}
175
+ `);
176
+ for (const link of links) {
177
+ process.stderr.write(` ${COLORS.cyan}${link.line}${COLORS.reset} ${COLORS.dim}${link.target}${COLORS.reset}
178
+ `);
179
+ }
180
+ process.stderr.write(`
181
+ `);
182
+ }
183
+ return 1;
184
+ }
185
+ async function main() {
186
+ const args = process.argv.slice(2);
187
+ if (args.includes("--help") || args.includes("-h")) {
188
+ process.stdout.write(`Usage: check-markdown-links [dirs/patterns...]
189
+
190
+ Validates that relative links in markdown files resolve to existing files.
191
+
192
+ Arguments:
193
+ dirs/patterns Glob patterns to scan (defaults: docs/, packages/*/README.md, etc.)
194
+
195
+ Options:
196
+ --help Show this help message
197
+
198
+ Exit codes:
199
+ 0 All links valid
200
+ 1 Broken links found
201
+ `);
202
+ process.exitCode = 0;
203
+ return;
204
+ }
205
+ const cwd = process.cwd();
206
+ const patterns = args.length > 0 ? args.filter((a) => !a.startsWith("--")) : undefined;
207
+ const exitCode = await runCheckMarkdownLinks(cwd, patterns);
208
+ process.exitCode = exitCode;
209
+ }
210
+ if (import.meta.main) {
211
+ main();
212
+ }
213
+
214
+ export { extractMarkdownLinks, validateLinks, runCheckMarkdownLinks };
@@ -0,0 +1,23 @@
1
+ // @bun
2
+ // packages/tooling/src/bun-version-compat.ts
3
+ function parseSemver(version) {
4
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
5
+ if (!match?.[1] || !match[2] || !match[3]) {
6
+ return;
7
+ }
8
+ return {
9
+ major: Number.parseInt(match[1], 10),
10
+ minor: Number.parseInt(match[2], 10),
11
+ patch: Number.parseInt(match[3], 10)
12
+ };
13
+ }
14
+ function isTypesBunVersionCompatible(bunVersion, bunTypesVersion) {
15
+ const parsedBun = parseSemver(bunVersion);
16
+ const parsedTypes = parseSemver(bunTypesVersion);
17
+ if (!parsedBun || !parsedTypes) {
18
+ return bunVersion === bunTypesVersion;
19
+ }
20
+ return parsedBun.major === parsedTypes.major && parsedBun.minor === parsedTypes.minor && parsedTypes.patch <= parsedBun.patch;
21
+ }
22
+
23
+ export { parseSemver, isTypesBunVersionCompatible };