@outfitter/tooling 0.2.0 → 0.2.2
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/.markdownlint-cli2.jsonc +55 -55
- package/README.md +7 -3
- package/biome.json +79 -72
- package/dist/cli/check.js +1 -1
- package/dist/cli/fix.js +1 -1
- package/dist/cli/index.js +232 -38
- package/dist/cli/init.js +1 -1
- package/dist/cli/pre-push.d.ts +34 -1
- package/dist/cli/pre-push.js +14 -2
- package/dist/cli/upgrade-bun.js +1 -1
- package/dist/registry/build.d.ts +6 -0
- package/dist/registry/build.js +31 -13
- package/dist/shared/@outfitter/{tooling-xx1146e3.js → tooling-0x5q15ec.js} +2 -1
- package/dist/shared/@outfitter/tooling-8sd32ts6.js +277 -0
- package/dist/shared/@outfitter/{tooling-s4eqq91d.js → tooling-9errkcvk.js} +2 -1
- package/dist/shared/@outfitter/{tooling-75j500dv.js → tooling-9yzd08v1.js} +10 -6
- package/dist/shared/@outfitter/{tooling-xaxdr9da.js → tooling-mxwc1n8w.js} +13 -3
- package/lefthook.yml +5 -7
- package/package.json +121 -114
- package/registry/registry.json +78 -76
- package/tsconfig.preset.bun.json +5 -5
- package/tsconfig.preset.json +33 -33
- package/dist/shared/@outfitter/tooling-qm7jeg0d.js +0 -99
package/dist/cli/index.js
CHANGED
|
@@ -13,7 +13,8 @@ function buildCheckCommand(options) {
|
|
|
13
13
|
}
|
|
14
14
|
async function runCheck(paths = []) {
|
|
15
15
|
const cmd = buildCheckCommand({ paths });
|
|
16
|
-
|
|
16
|
+
process.stdout.write(`Running: bun x ${cmd.join(" ")}
|
|
17
|
+
`);
|
|
17
18
|
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
18
19
|
stdio: ["inherit", "inherit", "inherit"]
|
|
19
20
|
});
|
|
@@ -31,7 +32,8 @@ function buildFixCommand(options) {
|
|
|
31
32
|
}
|
|
32
33
|
async function runFix(paths = []) {
|
|
33
34
|
const cmd = buildFixCommand({ paths });
|
|
34
|
-
|
|
35
|
+
process.stdout.write(`Running: bun x ${cmd.join(" ")}
|
|
36
|
+
`);
|
|
35
37
|
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
36
38
|
stdio: ["inherit", "inherit", "inherit"]
|
|
37
39
|
});
|
|
@@ -69,7 +71,15 @@ function detectFrameworks(pkg) {
|
|
|
69
71
|
return ["--frameworks", ...detected];
|
|
70
72
|
}
|
|
71
73
|
function buildUltraciteCommand(options) {
|
|
72
|
-
const cmd = [
|
|
74
|
+
const cmd = [
|
|
75
|
+
"ultracite",
|
|
76
|
+
"init",
|
|
77
|
+
"--linter",
|
|
78
|
+
"biome",
|
|
79
|
+
"--pm",
|
|
80
|
+
"bun",
|
|
81
|
+
"--quiet"
|
|
82
|
+
];
|
|
73
83
|
if (options.frameworks && options.frameworks.length > 0) {
|
|
74
84
|
cmd.push("--frameworks", ...options.frameworks);
|
|
75
85
|
}
|
|
@@ -79,14 +89,16 @@ async function runInit(cwd = process.cwd()) {
|
|
|
79
89
|
const pkgPath = `${cwd}/package.json`;
|
|
80
90
|
const pkgFile = Bun.file(pkgPath);
|
|
81
91
|
if (!await pkgFile.exists()) {
|
|
82
|
-
|
|
92
|
+
process.stderr.write(`No package.json found in current directory
|
|
93
|
+
`);
|
|
83
94
|
process.exit(1);
|
|
84
95
|
}
|
|
85
96
|
const pkg = await pkgFile.json();
|
|
86
97
|
const frameworkFlags = detectFrameworks(pkg);
|
|
87
98
|
const frameworks = frameworkFlags.length > 0 ? frameworkFlags.slice(1) : [];
|
|
88
99
|
const cmd = buildUltraciteCommand({ frameworks });
|
|
89
|
-
|
|
100
|
+
process.stdout.write(`Running: bun x ${cmd.join(" ")}
|
|
101
|
+
`);
|
|
90
102
|
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
91
103
|
cwd,
|
|
92
104
|
stdio: ["inherit", "inherit", "inherit"]
|
|
@@ -96,6 +108,8 @@ async function runInit(cwd = process.cwd()) {
|
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
// src/cli/pre-push.ts
|
|
111
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
112
|
+
import { join } from "node:path";
|
|
99
113
|
var COLORS = {
|
|
100
114
|
reset: "\x1B[0m",
|
|
101
115
|
red: "\x1B[31m",
|
|
@@ -104,18 +118,140 @@ var COLORS = {
|
|
|
104
118
|
blue: "\x1B[34m"
|
|
105
119
|
};
|
|
106
120
|
function log(msg) {
|
|
107
|
-
|
|
121
|
+
process.stdout.write(`${msg}
|
|
122
|
+
`);
|
|
108
123
|
}
|
|
109
124
|
function getCurrentBranch() {
|
|
110
125
|
const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
111
126
|
return result.stdout.toString().trim();
|
|
112
127
|
}
|
|
128
|
+
function runGit(args) {
|
|
129
|
+
try {
|
|
130
|
+
const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
|
|
131
|
+
if (result.exitCode !== 0) {
|
|
132
|
+
return { ok: false, lines: [] };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
lines: result.stdout.toString().split(`
|
|
137
|
+
`).map((line) => line.trim()).filter(Boolean)
|
|
138
|
+
};
|
|
139
|
+
} catch {
|
|
140
|
+
return { ok: false, lines: [] };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
113
143
|
function isRedPhaseBranch(branch) {
|
|
114
144
|
return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
|
|
115
145
|
}
|
|
116
146
|
function isScaffoldBranch(branch) {
|
|
117
147
|
return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
|
|
118
148
|
}
|
|
149
|
+
var TEST_PATH_PATTERNS = [
|
|
150
|
+
/(^|\/)__tests__\//,
|
|
151
|
+
/(^|\/)__snapshots__\//,
|
|
152
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/,
|
|
153
|
+
/\.snap$/,
|
|
154
|
+
/(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
|
|
155
|
+
/(^|\/)tsconfig\.test\.json$/,
|
|
156
|
+
/(^|\/)\.env\.test(\.|$)/
|
|
157
|
+
];
|
|
158
|
+
function isTestOnlyPath(path) {
|
|
159
|
+
const normalized = path.replaceAll("\\", "/");
|
|
160
|
+
return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
161
|
+
}
|
|
162
|
+
function areFilesTestOnly(paths) {
|
|
163
|
+
return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
|
|
164
|
+
}
|
|
165
|
+
function canBypassRedPhaseByChangedFiles(changedFiles) {
|
|
166
|
+
return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
|
|
167
|
+
}
|
|
168
|
+
function resolveBaseRef() {
|
|
169
|
+
const candidates = [
|
|
170
|
+
"origin/main",
|
|
171
|
+
"main",
|
|
172
|
+
"origin/trunk",
|
|
173
|
+
"trunk",
|
|
174
|
+
"origin/master",
|
|
175
|
+
"master"
|
|
176
|
+
];
|
|
177
|
+
for (const candidate of candidates) {
|
|
178
|
+
const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
|
|
179
|
+
if (resolved.ok) {
|
|
180
|
+
return candidate;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
function changedFilesFromRange(range) {
|
|
186
|
+
const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
|
|
187
|
+
return {
|
|
188
|
+
ok: result.ok,
|
|
189
|
+
files: result.lines
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function getChangedFilesForPush() {
|
|
193
|
+
const upstream = runGit([
|
|
194
|
+
"rev-parse",
|
|
195
|
+
"--abbrev-ref",
|
|
196
|
+
"--symbolic-full-name",
|
|
197
|
+
"@{upstream}"
|
|
198
|
+
]);
|
|
199
|
+
if (upstream.ok && upstream.lines[0]) {
|
|
200
|
+
const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
|
|
201
|
+
if (rangeResult.ok) {
|
|
202
|
+
return {
|
|
203
|
+
files: rangeResult.files,
|
|
204
|
+
deterministic: true,
|
|
205
|
+
source: "upstream"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const baseRef = resolveBaseRef();
|
|
210
|
+
if (baseRef) {
|
|
211
|
+
const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
|
|
212
|
+
if (rangeResult.ok) {
|
|
213
|
+
return {
|
|
214
|
+
files: rangeResult.files,
|
|
215
|
+
deterministic: true,
|
|
216
|
+
source: "baseRef"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
files: [],
|
|
222
|
+
deterministic: false,
|
|
223
|
+
source: "undetermined"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function maybeSkipForRedPhase(reason, branch) {
|
|
227
|
+
const changedFiles = getChangedFilesForPush();
|
|
228
|
+
if (!changedFiles.deterministic) {
|
|
229
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
|
|
230
|
+
log("Running strict verification.");
|
|
231
|
+
log("");
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
|
|
235
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
|
|
236
|
+
if (changedFiles.files.length > 0) {
|
|
237
|
+
log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
|
|
238
|
+
} else {
|
|
239
|
+
log(`No changed files detected in ${changedFiles.source} range. Running strict verification.`);
|
|
240
|
+
}
|
|
241
|
+
log("");
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (reason === "branch") {
|
|
245
|
+
log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
246
|
+
} else {
|
|
247
|
+
log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
248
|
+
}
|
|
249
|
+
log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
|
|
250
|
+
log(`Diff source: ${changedFiles.source}`);
|
|
251
|
+
log("");
|
|
252
|
+
log("Remember: GREEN phase (implementation) must make these tests pass!");
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
119
255
|
function hasRedPhaseBranchInContext(currentBranch) {
|
|
120
256
|
let branches = [];
|
|
121
257
|
try {
|
|
@@ -145,44 +281,95 @@ function hasRedPhaseBranchInContext(currentBranch) {
|
|
|
145
281
|
}
|
|
146
282
|
return false;
|
|
147
283
|
}
|
|
148
|
-
function
|
|
284
|
+
function createVerificationPlan(scripts) {
|
|
285
|
+
if (scripts["verify:ci"]) {
|
|
286
|
+
return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
|
|
287
|
+
}
|
|
288
|
+
const requiredScripts = ["typecheck", "build", "test"];
|
|
289
|
+
const missingRequired = requiredScripts.filter((name) => !scripts[name]);
|
|
290
|
+
const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
|
|
291
|
+
if (!checkOrLint || missingRequired.length > 0) {
|
|
292
|
+
const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
scripts: ["typecheck", checkOrLint, "build", "test"],
|
|
301
|
+
source: "fallback"
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function readPackageScripts(cwd = process.cwd()) {
|
|
305
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
306
|
+
if (!existsSync(packageJsonPath)) {
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
311
|
+
const scripts = parsed.scripts ?? {};
|
|
312
|
+
const normalized = {};
|
|
313
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
314
|
+
if (typeof value === "string") {
|
|
315
|
+
normalized[name] = value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return normalized;
|
|
319
|
+
} catch {
|
|
320
|
+
return {};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function runScript(scriptName) {
|
|
149
324
|
log("");
|
|
150
|
-
|
|
325
|
+
log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
|
|
326
|
+
const result = Bun.spawnSync(["bun", "run", scriptName], {
|
|
151
327
|
stdio: ["inherit", "inherit", "inherit"]
|
|
152
328
|
});
|
|
153
329
|
return result.exitCode === 0;
|
|
154
330
|
}
|
|
155
331
|
async function runPrePush(options = {}) {
|
|
156
|
-
log(`${COLORS.blue}Pre-push
|
|
332
|
+
log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
|
|
157
333
|
log("");
|
|
158
334
|
const branch = getCurrentBranch();
|
|
159
335
|
if (isRedPhaseBranch(branch)) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
log("Remember: GREEN phase (implementation) must make these tests pass!");
|
|
164
|
-
process.exit(0);
|
|
336
|
+
if (maybeSkipForRedPhase("branch", branch)) {
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
165
339
|
}
|
|
166
340
|
if (isScaffoldBranch(branch)) {
|
|
167
341
|
if (hasRedPhaseBranchInContext(branch)) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
process.exit(0);
|
|
342
|
+
if (maybeSkipForRedPhase("context", branch)) {
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|
|
172
345
|
}
|
|
173
346
|
}
|
|
174
347
|
if (options.force) {
|
|
175
|
-
log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping
|
|
348
|
+
log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
|
|
176
349
|
process.exit(0);
|
|
177
350
|
}
|
|
178
|
-
|
|
179
|
-
if (
|
|
351
|
+
const plan = createVerificationPlan(readPackageScripts());
|
|
352
|
+
if (!plan.ok) {
|
|
353
|
+
log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
|
|
354
|
+
log(plan.error);
|
|
180
355
|
log("");
|
|
181
|
-
log(
|
|
182
|
-
|
|
356
|
+
log("Add one of:");
|
|
357
|
+
log(" - verify:ci");
|
|
358
|
+
log(" - typecheck + (check or lint) + build + test");
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
362
|
+
if (plan.source === "verify:ci") {
|
|
363
|
+
log("Using `verify:ci` script.");
|
|
183
364
|
} else {
|
|
365
|
+
log(`Using fallback scripts: ${plan.scripts.join(" -> ")}`);
|
|
366
|
+
}
|
|
367
|
+
for (const scriptName of plan.scripts) {
|
|
368
|
+
if (runScript(scriptName)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
184
371
|
log("");
|
|
185
|
-
log(`${COLORS.red}
|
|
372
|
+
log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
|
|
186
373
|
log("");
|
|
187
374
|
log("If this is intentional TDD RED phase work, name your branch:");
|
|
188
375
|
log(" - feature-tests");
|
|
@@ -190,11 +377,14 @@ async function runPrePush(options = {}) {
|
|
|
190
377
|
log(" - feature_tests");
|
|
191
378
|
process.exit(1);
|
|
192
379
|
}
|
|
380
|
+
log("");
|
|
381
|
+
log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
|
|
382
|
+
process.exit(0);
|
|
193
383
|
}
|
|
194
384
|
|
|
195
385
|
// src/cli/upgrade-bun.ts
|
|
196
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
197
|
-
import { join } from "node:path";
|
|
386
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
387
|
+
import { join as join2 } from "node:path";
|
|
198
388
|
var COLORS2 = {
|
|
199
389
|
reset: "\x1B[0m",
|
|
200
390
|
red: "\x1B[31m",
|
|
@@ -203,16 +393,20 @@ var COLORS2 = {
|
|
|
203
393
|
blue: "\x1B[34m"
|
|
204
394
|
};
|
|
205
395
|
function log2(msg) {
|
|
206
|
-
|
|
396
|
+
process.stdout.write(`${msg}
|
|
397
|
+
`);
|
|
207
398
|
}
|
|
208
399
|
function info(msg) {
|
|
209
|
-
|
|
400
|
+
process.stdout.write(`${COLORS2.blue}▸${COLORS2.reset} ${msg}
|
|
401
|
+
`);
|
|
210
402
|
}
|
|
211
403
|
function success(msg) {
|
|
212
|
-
|
|
404
|
+
process.stdout.write(`${COLORS2.green}✓${COLORS2.reset} ${msg}
|
|
405
|
+
`);
|
|
213
406
|
}
|
|
214
407
|
function warn(msg) {
|
|
215
|
-
|
|
408
|
+
process.stdout.write(`${COLORS2.yellow}!${COLORS2.reset} ${msg}
|
|
409
|
+
`);
|
|
216
410
|
}
|
|
217
411
|
async function fetchLatestVersion() {
|
|
218
412
|
const response = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest");
|
|
@@ -228,13 +422,13 @@ function findPackageJsonFiles(dir) {
|
|
|
228
422
|
const glob = new Bun.Glob("**/package.json");
|
|
229
423
|
for (const path of glob.scanSync({ cwd: dir })) {
|
|
230
424
|
if (!path.includes("node_modules")) {
|
|
231
|
-
results.push(
|
|
425
|
+
results.push(join2(dir, path));
|
|
232
426
|
}
|
|
233
427
|
}
|
|
234
428
|
return results;
|
|
235
429
|
}
|
|
236
430
|
function updateEnginesBun(filePath, version) {
|
|
237
|
-
const content =
|
|
431
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
238
432
|
const pattern = /"bun":\s*">=[\d.]+"/;
|
|
239
433
|
if (!pattern.test(content)) {
|
|
240
434
|
return false;
|
|
@@ -247,7 +441,7 @@ function updateEnginesBun(filePath, version) {
|
|
|
247
441
|
return false;
|
|
248
442
|
}
|
|
249
443
|
function updateTypesBun(filePath, version) {
|
|
250
|
-
const content =
|
|
444
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
251
445
|
const pattern = /"@types\/bun":\s*"\^[\d.]+"/;
|
|
252
446
|
if (!pattern.test(content)) {
|
|
253
447
|
return false;
|
|
@@ -261,14 +455,14 @@ function updateTypesBun(filePath, version) {
|
|
|
261
455
|
}
|
|
262
456
|
async function runUpgradeBun(targetVersion, options = {}) {
|
|
263
457
|
const cwd = process.cwd();
|
|
264
|
-
const bunVersionFile =
|
|
458
|
+
const bunVersionFile = join2(cwd, ".bun-version");
|
|
265
459
|
let version = targetVersion;
|
|
266
460
|
if (!version) {
|
|
267
461
|
info("Fetching latest Bun version...");
|
|
268
462
|
version = await fetchLatestVersion();
|
|
269
463
|
log2(`Latest version: ${version}`);
|
|
270
464
|
}
|
|
271
|
-
const currentVersion =
|
|
465
|
+
const currentVersion = existsSync2(bunVersionFile) ? readFileSync2(bunVersionFile, "utf-8").trim() : "unknown";
|
|
272
466
|
log2(`Current version: ${currentVersion}`);
|
|
273
467
|
if (currentVersion === version) {
|
|
274
468
|
success(`Already on version ${version}`);
|
|
@@ -284,13 +478,13 @@ async function runUpgradeBun(targetVersion, options = {}) {
|
|
|
284
478
|
info("Updating engines.bun...");
|
|
285
479
|
for (const file of packageFiles) {
|
|
286
480
|
if (updateEnginesBun(file, version)) {
|
|
287
|
-
log2(` ${file.replace(cwd
|
|
481
|
+
log2(` ${file.replace(`${cwd}/`, "")}`);
|
|
288
482
|
}
|
|
289
483
|
}
|
|
290
484
|
info("Updating @types/bun...");
|
|
291
485
|
for (const file of packageFiles) {
|
|
292
486
|
if (updateTypesBun(file, version)) {
|
|
293
|
-
log2(` ${file.replace(cwd
|
|
487
|
+
log2(` ${file.replace(`${cwd}/`, "")}`);
|
|
294
488
|
}
|
|
295
489
|
}
|
|
296
490
|
if (options.install !== false) {
|
|
@@ -347,7 +541,7 @@ program.command("fix").description("Fix linting issues (wraps ultracite)").argum
|
|
|
347
541
|
program.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) => {
|
|
348
542
|
await runUpgradeBun(version, options);
|
|
349
543
|
});
|
|
350
|
-
program.command("pre-push").description("TDD-aware pre-push
|
|
544
|
+
program.command("pre-push").description("TDD-aware pre-push strict verification hook").option("-f, --force", "Skip strict verification entirely").action(async (options) => {
|
|
351
545
|
await runPrePush(options);
|
|
352
546
|
});
|
|
353
547
|
program.parse();
|
package/dist/cli/init.js
CHANGED
package/dist/cli/pre-push.d.ts
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if branch is a TDD RED phase branch
|
|
3
|
+
*/
|
|
4
|
+
declare function isRedPhaseBranch(branch: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Check if branch is a scaffold branch
|
|
7
|
+
*/
|
|
8
|
+
declare function isScaffoldBranch(branch: string): boolean;
|
|
9
|
+
declare function isTestOnlyPath(path: string): boolean;
|
|
10
|
+
declare function areFilesTestOnly(paths: readonly string[]): boolean;
|
|
11
|
+
interface PushChangedFiles {
|
|
12
|
+
readonly files: readonly string[];
|
|
13
|
+
readonly deterministic: boolean;
|
|
14
|
+
readonly source: "upstream" | "baseRef" | "undetermined";
|
|
15
|
+
}
|
|
16
|
+
declare function canBypassRedPhaseByChangedFiles(changedFiles: PushChangedFiles): boolean;
|
|
17
|
+
type ScriptMap = Readonly<Record<string, string | undefined>>;
|
|
18
|
+
type VerificationPlan = {
|
|
19
|
+
readonly ok: true;
|
|
20
|
+
readonly scripts: readonly string[];
|
|
21
|
+
readonly source: "verify:ci" | "fallback";
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly error: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Derive strict pre-push verification from package scripts.
|
|
28
|
+
*
|
|
29
|
+
* Priority:
|
|
30
|
+
* 1) `verify:ci`
|
|
31
|
+
* 2) fallback sequence: `typecheck`, `check|lint`, `build`, `test`
|
|
32
|
+
*/
|
|
33
|
+
declare function createVerificationPlan(scripts: ScriptMap): VerificationPlan;
|
|
1
34
|
interface PrePushOptions {
|
|
2
35
|
force?: boolean;
|
|
3
36
|
}
|
|
@@ -5,4 +38,4 @@ interface PrePushOptions {
|
|
|
5
38
|
* Main pre-push command
|
|
6
39
|
*/
|
|
7
40
|
declare function runPrePush(options?: PrePushOptions): Promise<void>;
|
|
8
|
-
export { runPrePush, PrePushOptions };
|
|
41
|
+
export { runPrePush, isTestOnlyPath, isScaffoldBranch, isRedPhaseBranch, createVerificationPlan, canBypassRedPhaseByChangedFiles, areFilesTestOnly, VerificationPlan, PushChangedFiles, PrePushOptions };
|
package/dist/cli/pre-push.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
+
areFilesTestOnly,
|
|
4
|
+
canBypassRedPhaseByChangedFiles,
|
|
5
|
+
createVerificationPlan,
|
|
6
|
+
isRedPhaseBranch,
|
|
7
|
+
isScaffoldBranch,
|
|
8
|
+
isTestOnlyPath,
|
|
3
9
|
runPrePush
|
|
4
|
-
} from "../shared/@outfitter/tooling-
|
|
10
|
+
} from "../shared/@outfitter/tooling-8sd32ts6.js";
|
|
5
11
|
export {
|
|
6
|
-
runPrePush
|
|
12
|
+
runPrePush,
|
|
13
|
+
isTestOnlyPath,
|
|
14
|
+
isScaffoldBranch,
|
|
15
|
+
isRedPhaseBranch,
|
|
16
|
+
createVerificationPlan,
|
|
17
|
+
canBypassRedPhaseByChangedFiles,
|
|
18
|
+
areFilesTestOnly
|
|
7
19
|
};
|
package/dist/cli/upgrade-bun.js
CHANGED
package/dist/registry/build.js
CHANGED
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/tooling/src/registry/build.ts
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "fs";
|
|
6
12
|
import { dirname, join } from "path";
|
|
13
|
+
function log(message) {
|
|
14
|
+
process.stdout.write(`${message}
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
7
17
|
function findRepoRoot(startDir) {
|
|
8
18
|
let dir = startDir;
|
|
9
19
|
while (dir !== "/") {
|
|
@@ -76,27 +86,30 @@ var REGISTRY_CONFIG = {
|
|
|
76
86
|
blocks: {
|
|
77
87
|
claude: {
|
|
78
88
|
description: "Claude Code settings and hooks for automated formatting",
|
|
79
|
-
files: [
|
|
80
|
-
".claude/settings.json",
|
|
81
|
-
".claude/hooks/format-code-on-stop.sh"
|
|
82
|
-
]
|
|
89
|
+
files: [".claude/settings.json", ".claude/hooks/format-code-on-stop.sh"]
|
|
83
90
|
},
|
|
84
91
|
biome: {
|
|
85
92
|
description: "Biome linter/formatter configuration via Ultracite",
|
|
86
93
|
files: ["packages/tooling/biome.json"],
|
|
87
94
|
remap: { "packages/tooling/biome.json": "biome.json" },
|
|
88
|
-
devDependencies: { ultracite: "^7.
|
|
95
|
+
devDependencies: { ultracite: "^7.1.1" }
|
|
89
96
|
},
|
|
90
97
|
lefthook: {
|
|
91
98
|
description: "Git hooks via Lefthook for pre-commit and pre-push",
|
|
92
99
|
files: ["packages/tooling/lefthook.yml"],
|
|
93
100
|
remap: { "packages/tooling/lefthook.yml": ".lefthook.yml" },
|
|
94
|
-
devDependencies: {
|
|
101
|
+
devDependencies: {
|
|
102
|
+
"@outfitter/tooling": "^0.2.1",
|
|
103
|
+
lefthook: "^2.0.16",
|
|
104
|
+
ultracite: "^7.1.1"
|
|
105
|
+
}
|
|
95
106
|
},
|
|
96
107
|
markdownlint: {
|
|
97
108
|
description: "Markdown linting configuration via markdownlint-cli2",
|
|
98
109
|
files: ["packages/tooling/.markdownlint-cli2.jsonc"],
|
|
99
|
-
remap: {
|
|
110
|
+
remap: {
|
|
111
|
+
"packages/tooling/.markdownlint-cli2.jsonc": ".markdownlint-cli2.jsonc"
|
|
112
|
+
}
|
|
100
113
|
},
|
|
101
114
|
bootstrap: {
|
|
102
115
|
description: "Project bootstrap script for installing tools and dependencies",
|
|
@@ -113,16 +126,21 @@ function main() {
|
|
|
113
126
|
const repoRoot = findRepoRoot(scriptDir);
|
|
114
127
|
const outputDir = join(repoRoot, "packages/tooling/registry");
|
|
115
128
|
const outputPath = join(outputDir, "registry.json");
|
|
116
|
-
|
|
129
|
+
log(`Building registry from: ${repoRoot}`);
|
|
117
130
|
if (!existsSync(outputDir)) {
|
|
118
131
|
mkdirSync(outputDir, { recursive: true });
|
|
119
132
|
}
|
|
120
133
|
const registry = buildRegistry(REGISTRY_CONFIG, repoRoot);
|
|
121
|
-
writeFileSync(outputPath, JSON.stringify(registry, null,
|
|
134
|
+
writeFileSync(outputPath, `${JSON.stringify(registry, null, "\t")}
|
|
122
135
|
`);
|
|
123
136
|
const blockCount = Object.keys(registry.blocks).length;
|
|
124
137
|
const fileCount = Object.values(registry.blocks).flatMap((b) => b.files ?? []).length;
|
|
125
|
-
|
|
126
|
-
|
|
138
|
+
log(`\u2713 Generated ${outputPath}`);
|
|
139
|
+
log(` ${blockCount} blocks, ${fileCount} files embedded`);
|
|
140
|
+
}
|
|
141
|
+
if (import.meta.main) {
|
|
142
|
+
main();
|
|
127
143
|
}
|
|
128
|
-
|
|
144
|
+
export {
|
|
145
|
+
REGISTRY_CONFIG
|
|
146
|
+
};
|
|
@@ -9,7 +9,8 @@ function buildCheckCommand(options) {
|
|
|
9
9
|
}
|
|
10
10
|
async function runCheck(paths = []) {
|
|
11
11
|
const cmd = buildCheckCommand({ paths });
|
|
12
|
-
|
|
12
|
+
process.stdout.write(`Running: bun x ${cmd.join(" ")}
|
|
13
|
+
`);
|
|
13
14
|
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
14
15
|
stdio: ["inherit", "inherit", "inherit"]
|
|
15
16
|
});
|