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