@outfitter/tooling 0.2.2 → 0.2.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 (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 +14 -0
  12. package/dist/cli/check-readme-imports.d.ts +61 -0
  13. package/dist/cli/check-readme-imports.js +198 -0
  14. package/dist/cli/check.js +1 -0
  15. package/dist/cli/fix.js +1 -0
  16. package/dist/cli/index.js +625 -30
  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-enjcenja.js +229 -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-wesswf21.d.ts +59 -0
  34. package/dist/shared/chunk-3s189drz.js +4 -0
  35. package/dist/shared/chunk-7tdgbqb0.js +197 -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 +25 -22
  40. package/registry/registry.json +1 -1
package/dist/cli/init.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  detectFrameworks,
5
5
  runInit
6
6
  } from "../shared/@outfitter/tooling-mxwc1n8w.js";
7
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
7
8
  export {
8
9
  runInit,
9
10
  detectFrameworks,
@@ -8,6 +8,7 @@ import {
8
8
  isTestOnlyPath,
9
9
  runPrePush
10
10
  } from "../shared/@outfitter/tooling-8sd32ts6.js";
11
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
11
12
  export {
12
13
  runPrePush,
13
14
  isTestOnlyPath,
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  runUpgradeBun
4
4
  } from "../shared/@outfitter/tooling-9yzd08v1.js";
5
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
5
6
  export {
6
7
  runUpgradeBun
7
8
  };
package/dist/index.d.ts CHANGED
@@ -106,37 +106,6 @@ interface AddBlockOptions {
106
106
  /** Working directory (defaults to cwd) */
107
107
  cwd?: string;
108
108
  }
109
- /**
110
- * @outfitter/tooling
111
- *
112
- * Dev tooling configuration presets for Outfitter projects.
113
- * Provides standardized biome, TypeScript, lefthook, and markdownlint configurations.
114
- *
115
- * @example
116
- * ```json
117
- * // biome.json
118
- * {
119
- * "extends": ["ultracite/biome/core", "@outfitter/tooling/biome.json"]
120
- * }
121
- * ```
122
- *
123
- * @example
124
- * ```json
125
- * // tsconfig.json
126
- * {
127
- * "extends": "@outfitter/tooling/tsconfig.preset.bun.json"
128
- * }
129
- * ```
130
- *
131
- * @example
132
- * ```yaml
133
- * # .lefthook.yml
134
- * extends:
135
- * - node_modules/@outfitter/tooling/lefthook.yml
136
- * ```
137
- *
138
- * @packageDocumentation
139
- */
140
- /** Package version */
141
- declare const VERSION = "0.1.0-rc.1";
109
+ /** Package version, read from package.json at load time. */
110
+ declare const VERSION: string;
142
111
  export { VERSION, RegistrySchema, RegistryBuildConfig, Registry, FileEntrySchema, FileEntry, BlockSchema, BlockDefinition, Block, AddBlockResult, AddBlockOptions };
package/dist/index.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ VERSION
3
+ } from "./shared/chunk-8aenrm6f.js";
4
+ import"./shared/chunk-3s189drz.js";
5
+
1
6
  // src/registry/schema.ts
2
7
  import { z } from "zod";
3
8
  var FileEntrySchema = z.object({
@@ -18,8 +23,6 @@ var RegistrySchema = z.object({
18
23
  version: z.string(),
19
24
  blocks: z.record(BlockSchema)
20
25
  });
21
- // src/index.ts
22
- var VERSION = "0.1.0-rc.1";
23
26
  export {
24
27
  VERSION,
25
28
  RegistrySchema,
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
3
4
 
4
5
  // packages/tooling/src/registry/build.ts
5
6
  import {
@@ -5,6 +5,7 @@ import {
5
5
  FileEntrySchema,
6
6
  RegistrySchema
7
7
  } from "../shared/@outfitter/tooling-g83d0kjv.js";
8
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
8
9
  export {
9
10
  RegistrySchema,
10
11
  FileEntrySchema,
@@ -4,6 +4,7 @@ import {
4
4
  FileEntrySchema,
5
5
  RegistrySchema
6
6
  } from "../shared/@outfitter/tooling-g83d0kjv.js";
7
+ import"../shared/@outfitter/tooling-dvwh9qve.js";
7
8
  export {
8
9
  RegistrySchema,
9
10
  FileEntrySchema,
@@ -0,0 +1,70 @@
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.exit(1);
27
+ }
28
+ const modified = parseGitDiff(diffResult.stdout.toString());
29
+ const lsResult = Bun.spawnSync(["git", "ls-files", "--others", "--exclude-standard", "--", ...pathArgs], { stderr: "pipe" });
30
+ if (lsResult.exitCode !== 0) {
31
+ process.stderr.write(`Failed to run git ls-files
32
+ `);
33
+ process.exit(1);
34
+ }
35
+ const untracked = parseUntrackedFiles(lsResult.stdout.toString());
36
+ const clean = modified.length === 0 && untracked.length === 0;
37
+ const status = { clean, modified, untracked };
38
+ if (status.clean) {
39
+ process.stdout.write(`${COLORS.green}Working tree is clean.${COLORS.reset}
40
+ `);
41
+ process.exit(0);
42
+ }
43
+ process.stderr.write(`${COLORS.red}Working tree is dirty after verification:${COLORS.reset}
44
+
45
+ `);
46
+ if (modified.length > 0) {
47
+ process.stderr.write(`Modified files:
48
+ `);
49
+ for (const file of modified) {
50
+ process.stderr.write(` ${COLORS.dim}M${COLORS.reset} ${file}
51
+ `);
52
+ }
53
+ }
54
+ if (untracked.length > 0) {
55
+ process.stderr.write(`Untracked files:
56
+ `);
57
+ for (const file of untracked) {
58
+ process.stderr.write(` ${COLORS.dim}?${COLORS.reset} ${file}
59
+ `);
60
+ }
61
+ }
62
+ process.stderr.write(`
63
+ This likely means a build step produced uncommitted changes.
64
+ `);
65
+ process.stderr.write(`Commit these changes or add them to .gitignore.
66
+ `);
67
+ process.exit(1);
68
+ }
69
+
70
+ export { parseGitDiff, parseUntrackedFiles, isCleanTree, runCheckCleanTree };
@@ -0,0 +1,94 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-changeset.ts
3
+ function getChangedPackagePaths(files) {
4
+ const packageNames = new Set;
5
+ const pattern = /^packages\/([^/]+)\/src\//;
6
+ for (const file of files) {
7
+ const match = pattern.exec(file);
8
+ if (match?.[1]) {
9
+ packageNames.add(match[1]);
10
+ }
11
+ }
12
+ return [...packageNames].sort();
13
+ }
14
+ function getChangedChangesetFiles(files) {
15
+ const pattern = /^\.changeset\/([^/]+\.md)$/;
16
+ const results = [];
17
+ for (const file of files) {
18
+ const match = pattern.exec(file);
19
+ if (match?.[1] && match[1] !== "README.md") {
20
+ results.push(match[1]);
21
+ }
22
+ }
23
+ return results.sort();
24
+ }
25
+ function checkChangesetRequired(changedPackages, changesetFiles) {
26
+ if (changedPackages.length === 0) {
27
+ return { ok: true, missingFor: [] };
28
+ }
29
+ if (changesetFiles.length > 0) {
30
+ return { ok: true, missingFor: [] };
31
+ }
32
+ return { ok: false, missingFor: changedPackages };
33
+ }
34
+ var COLORS = {
35
+ reset: "\x1B[0m",
36
+ red: "\x1B[31m",
37
+ green: "\x1B[32m",
38
+ yellow: "\x1B[33m",
39
+ blue: "\x1B[34m",
40
+ dim: "\x1B[2m"
41
+ };
42
+ async function runCheckChangeset(options = {}) {
43
+ if (options.skip || process.env["NO_CHANGESET"] === "1") {
44
+ process.stdout.write(`${COLORS.dim}check-changeset skipped (NO_CHANGESET=1)${COLORS.reset}
45
+ `);
46
+ process.exit(0);
47
+ }
48
+ if (process.env["GITHUB_EVENT_NAME"] === "push") {
49
+ process.stdout.write(`${COLORS.dim}check-changeset skipped (push event)${COLORS.reset}
50
+ `);
51
+ process.exit(0);
52
+ }
53
+ const cwd = process.cwd();
54
+ let changedFiles;
55
+ try {
56
+ const proc = Bun.spawnSync(["git", "diff", "--name-only", "origin/main...HEAD"], { cwd });
57
+ if (proc.exitCode !== 0) {
58
+ process.exit(0);
59
+ }
60
+ changedFiles = proc.stdout.toString().trim().split(`
61
+ `).filter((line) => line.length > 0);
62
+ } catch {
63
+ process.exit(0);
64
+ }
65
+ const changedPackages = getChangedPackagePaths(changedFiles);
66
+ if (changedPackages.length === 0) {
67
+ process.stdout.write(`${COLORS.green}No package source changes detected.${COLORS.reset}
68
+ `);
69
+ process.exit(0);
70
+ }
71
+ const changesetFiles = getChangedChangesetFiles(changedFiles);
72
+ const check = checkChangesetRequired(changedPackages, changesetFiles);
73
+ if (check.ok) {
74
+ process.stdout.write(`${COLORS.green}Changeset found for ${changedPackages.length} changed package(s).${COLORS.reset}
75
+ `);
76
+ process.exit(0);
77
+ }
78
+ process.stderr.write(`${COLORS.red}Missing changeset!${COLORS.reset}
79
+
80
+ `);
81
+ process.stderr.write(`The following packages have source changes but no changeset:
82
+
83
+ `);
84
+ for (const pkg of check.missingFor) {
85
+ process.stderr.write(` ${COLORS.yellow}@outfitter/${pkg}${COLORS.reset}
86
+ `);
87
+ }
88
+ process.stderr.write(`
89
+ Run ${COLORS.blue}bun run changeset${COLORS.reset} to add a changeset, ` + `or add the ${COLORS.blue}no-changeset${COLORS.reset} label to skip.
90
+ `);
91
+ process.exit(1);
92
+ }
93
+
94
+ export { getChangedPackagePaths, getChangedChangesetFiles, checkChangesetRequired, runCheckChangeset };
@@ -0,0 +1,3 @@
1
+ /** Package version, read from package.json at load time. */
2
+ declare const VERSION: string;
3
+ export { VERSION };
@@ -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 };
@@ -0,0 +1,229 @@
1
+ // @bun
2
+ // packages/tooling/src/cli/check-exports.ts
3
+ import { resolve } from "path";
4
+ function entryToSubpath(entry) {
5
+ const stripped = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
6
+ if (stripped === "index") {
7
+ return ".";
8
+ }
9
+ if (stripped.endsWith("/index")) {
10
+ return `./${stripped.slice(0, -"/index".length)}`;
11
+ }
12
+ return `./${stripped}`;
13
+ }
14
+ function compareExports(input) {
15
+ const { name, actual, expected, path } = input;
16
+ const actualKeys = new Set(Object.keys(actual));
17
+ const expectedKeys = new Set(Object.keys(expected));
18
+ const added = [];
19
+ const removed = [];
20
+ const changed = [];
21
+ for (const key of expectedKeys) {
22
+ if (!actualKeys.has(key)) {
23
+ added.push(key);
24
+ }
25
+ }
26
+ for (const key of actualKeys) {
27
+ if (!expectedKeys.has(key)) {
28
+ removed.push(key);
29
+ }
30
+ }
31
+ for (const key of actualKeys) {
32
+ if (expectedKeys.has(key)) {
33
+ const actualValue = actual[key];
34
+ const expectedValue = expected[key];
35
+ if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
36
+ changed.push({ key, expected: expectedValue, actual: actualValue });
37
+ }
38
+ }
39
+ }
40
+ added.sort();
41
+ removed.sort();
42
+ changed.sort((a, b) => a.key.localeCompare(b.key));
43
+ if (added.length === 0 && removed.length === 0 && changed.length === 0) {
44
+ return { name, status: "ok" };
45
+ }
46
+ return {
47
+ name,
48
+ status: "drift",
49
+ drift: {
50
+ package: name,
51
+ path: path ?? "",
52
+ added,
53
+ removed,
54
+ changed
55
+ }
56
+ };
57
+ }
58
+ function matchesExclude(subpath, excludes) {
59
+ return excludes.some((pattern) => new Bun.Glob(pattern).match(subpath));
60
+ }
61
+ var CLI_EXCLUSION_PATTERNS = [
62
+ "**/cli.ts",
63
+ "**/cli/index.ts",
64
+ "**/bin.ts",
65
+ "**/bin/index.ts"
66
+ ];
67
+ function isCliEntrypoint(entry) {
68
+ return CLI_EXCLUSION_PATTERNS.some((pattern) => new Bun.Glob(pattern).match(entry));
69
+ }
70
+ function buildExportValue(entry) {
71
+ const distPath = entry.replace(/^src\//, "").replace(/\.[cm]?[jt]sx?$/, "");
72
+ return {
73
+ import: {
74
+ types: `./dist/${distPath}.d.ts`,
75
+ default: `./dist/${distPath}.js`
76
+ }
77
+ };
78
+ }
79
+ function discoverEntries(packageRoot) {
80
+ const glob = new Bun.Glob("src/**/*.ts");
81
+ const entries = [];
82
+ for (const match of glob.scanSync({ cwd: packageRoot, dot: false })) {
83
+ if (match.includes("__tests__") || match.endsWith(".test.ts")) {
84
+ continue;
85
+ }
86
+ entries.push(match);
87
+ }
88
+ return entries.sort();
89
+ }
90
+ function addConfigFileExports(expected, pkg) {
91
+ const CONFIG_RE = /\.(json|jsonc|yml|yaml|toml)$/;
92
+ const configFiles = (pkg.files ?? []).filter((file) => CONFIG_RE.test(file) && file !== "package.json");
93
+ for (const file of configFiles) {
94
+ expected[`./${file}`] = `./${file}`;
95
+ let base = file.replace(CONFIG_RE, "");
96
+ const match = base.match(/^(.+)\.preset(?:\.(.+))?$/);
97
+ if (match?.[1]) {
98
+ base = match[2] ? `${match[1]}-${match[2]}` : match[1];
99
+ }
100
+ if (base !== file) {
101
+ expected[`./${base}`] = `./${file}`;
102
+ }
103
+ }
104
+ }
105
+ function computeExpectedExports(packageRoot, workspace, pkg) {
106
+ const entries = discoverEntries(packageRoot);
107
+ const exportsConfig = typeof workspace.config?.exports === "object" ? workspace.config.exports : undefined;
108
+ const excludes = exportsConfig?.exclude ?? [];
109
+ const customExports = exportsConfig?.customExports ?? {};
110
+ const expected = {};
111
+ const subpathEntries = new Map;
112
+ for (const entry of entries) {
113
+ if (isCliEntrypoint(entry))
114
+ continue;
115
+ const subpath = entryToSubpath(entry);
116
+ if (matchesExclude(subpath, excludes))
117
+ continue;
118
+ const existing = subpathEntries.get(subpath);
119
+ if (existing) {
120
+ if (!existing.endsWith("/index.ts") && entry.endsWith("/index.ts")) {
121
+ continue;
122
+ }
123
+ }
124
+ subpathEntries.set(subpath, entry);
125
+ }
126
+ for (const [subpath, entry] of subpathEntries) {
127
+ expected[subpath] = buildExportValue(entry);
128
+ }
129
+ for (const [key, value] of Object.entries(customExports)) {
130
+ expected[`./${key.replace(/^\.\//, "")}`] = value;
131
+ }
132
+ addConfigFileExports(expected, pkg);
133
+ expected["./package.json"] = "./package.json";
134
+ return expected;
135
+ }
136
+ var COLORS = {
137
+ reset: "\x1B[0m",
138
+ red: "\x1B[31m",
139
+ green: "\x1B[32m",
140
+ yellow: "\x1B[33m",
141
+ blue: "\x1B[34m",
142
+ dim: "\x1B[2m"
143
+ };
144
+ function resolveJsonMode(options = {}) {
145
+ return options.json ?? process.env["OUTFITTER_JSON"] === "1";
146
+ }
147
+ async function runCheckExports(options = {}) {
148
+ const cwd = process.cwd();
149
+ const configPath = resolve(cwd, "bunup.config.ts");
150
+ let workspaces;
151
+ try {
152
+ const configModule = await import(configPath);
153
+ const rawConfig = configModule.default;
154
+ if (!Array.isArray(rawConfig)) {
155
+ process.stderr.write(`bunup.config.ts must export a workspace array
156
+ `);
157
+ process.exit(1);
158
+ }
159
+ workspaces = rawConfig;
160
+ } catch {
161
+ process.stderr.write(`Could not load bunup.config.ts from ${cwd}
162
+ `);
163
+ process.exit(1);
164
+ }
165
+ const results = [];
166
+ for (const workspace of workspaces) {
167
+ const packageRoot = resolve(cwd, workspace.root);
168
+ const pkgPath = resolve(packageRoot, "package.json");
169
+ let pkg;
170
+ try {
171
+ pkg = await Bun.file(pkgPath).json();
172
+ } catch {
173
+ results.push({ name: workspace.name, status: "ok" });
174
+ continue;
175
+ }
176
+ const actual = typeof pkg.exports === "object" && pkg.exports !== null ? pkg.exports : {};
177
+ const expected = computeExpectedExports(packageRoot, workspace, pkg);
178
+ results.push(compareExports({
179
+ name: workspace.name,
180
+ actual,
181
+ expected,
182
+ path: workspace.root
183
+ }));
184
+ }
185
+ const checkResult = {
186
+ ok: results.every((r) => r.status === "ok"),
187
+ packages: results
188
+ };
189
+ if (resolveJsonMode(options)) {
190
+ process.stdout.write(`${JSON.stringify(checkResult, null, 2)}
191
+ `);
192
+ } else {
193
+ const drifted = results.filter((r) => r.status === "drift");
194
+ if (drifted.length === 0) {
195
+ process.stdout.write(`${COLORS.green}All ${results.length} packages have exports in sync.${COLORS.reset}
196
+ `);
197
+ } else {
198
+ process.stderr.write(`${COLORS.red}Export drift detected in ${drifted.length} package(s):${COLORS.reset}
199
+
200
+ `);
201
+ for (const result of drifted) {
202
+ const drift = result.drift;
203
+ if (!drift)
204
+ continue;
205
+ process.stderr.write(` ${COLORS.yellow}${result.name}${COLORS.reset} ${COLORS.dim}(${drift.path})${COLORS.reset}
206
+ `);
207
+ for (const key of drift.added) {
208
+ process.stderr.write(` ${COLORS.green}+ ${key}${COLORS.reset} ${COLORS.dim}(missing from package.json)${COLORS.reset}
209
+ `);
210
+ }
211
+ for (const key of drift.removed) {
212
+ process.stderr.write(` ${COLORS.red}- ${key}${COLORS.reset} ${COLORS.dim}(not in source)${COLORS.reset}
213
+ `);
214
+ }
215
+ for (const entry of drift.changed) {
216
+ process.stderr.write(` ${COLORS.yellow}~ ${entry.key}${COLORS.reset} ${COLORS.dim}(value mismatch)${COLORS.reset}
217
+ `);
218
+ }
219
+ process.stderr.write(`
220
+ `);
221
+ }
222
+ process.stderr.write(`Run ${COLORS.blue}bun run build${COLORS.reset} to regenerate exports.
223
+ `);
224
+ }
225
+ }
226
+ process.exit(checkResult.ok ? 0 : 1);
227
+ }
228
+
229
+ export { entryToSubpath, compareExports, resolveJsonMode, 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 };