@outfitter/tooling 0.2.1 → 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 +527 -18
- 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/index.d.ts +108 -2
- package/dist/index.js +21 -9
- 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 -121
- 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/index.d.ts
CHANGED
|
@@ -1,5 +1,111 @@
|
|
|
1
|
-
import "
|
|
2
|
-
|
|
1
|
+
import { ZodType } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* File entry in a block.
|
|
4
|
+
*/
|
|
5
|
+
interface FileEntry {
|
|
6
|
+
/** Destination path relative to project root */
|
|
7
|
+
path: string;
|
|
8
|
+
/** File contents (embedded in registry) */
|
|
9
|
+
content: string;
|
|
10
|
+
/** Whether to chmod +x after copying */
|
|
11
|
+
executable?: boolean | undefined;
|
|
12
|
+
/** Whether to process as a template (future) */
|
|
13
|
+
template?: boolean | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Schema for a file entry in a block.
|
|
17
|
+
* Represents a file that will be copied to the user's project.
|
|
18
|
+
*/
|
|
19
|
+
declare const FileEntrySchema: ZodType<FileEntry>;
|
|
20
|
+
/**
|
|
21
|
+
* Block in the registry.
|
|
22
|
+
*/
|
|
23
|
+
interface Block {
|
|
24
|
+
/** Block name (matches the key in blocks record) */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Human-readable description */
|
|
27
|
+
description: string;
|
|
28
|
+
/** Files included in this block */
|
|
29
|
+
files?: FileEntry[] | undefined;
|
|
30
|
+
/** npm dependencies to add to package.json */
|
|
31
|
+
dependencies?: Record<string, string> | undefined;
|
|
32
|
+
/** npm devDependencies to add to package.json */
|
|
33
|
+
devDependencies?: Record<string, string> | undefined;
|
|
34
|
+
/** Other blocks this block extends (for composite blocks) */
|
|
35
|
+
extends?: string[] | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Schema for a block in the registry.
|
|
39
|
+
* A block is a collection of related files that can be added together.
|
|
40
|
+
*/
|
|
41
|
+
declare const BlockSchema: ZodType<Block>;
|
|
42
|
+
/**
|
|
43
|
+
* Complete registry structure.
|
|
44
|
+
*/
|
|
45
|
+
interface Registry {
|
|
46
|
+
/** Registry schema version */
|
|
47
|
+
version: string;
|
|
48
|
+
/** Map of block name to block definition */
|
|
49
|
+
blocks: Record<string, Block>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Schema for the complete registry.
|
|
53
|
+
* Contains all available blocks with their files and metadata.
|
|
54
|
+
*/
|
|
55
|
+
declare const RegistrySchema: ZodType<Registry>;
|
|
56
|
+
/**
|
|
57
|
+
* Block definition used in the build script.
|
|
58
|
+
* Specifies how to collect source files into a block.
|
|
59
|
+
*/
|
|
60
|
+
interface BlockDefinition {
|
|
61
|
+
/** Human-readable description */
|
|
62
|
+
description: string;
|
|
63
|
+
/** Source file paths (relative to repo root) */
|
|
64
|
+
files?: string[];
|
|
65
|
+
/** Remap source paths to destination paths */
|
|
66
|
+
remap?: Record<string, string>;
|
|
67
|
+
/** npm dependencies */
|
|
68
|
+
dependencies?: Record<string, string>;
|
|
69
|
+
/** npm devDependencies */
|
|
70
|
+
devDependencies?: Record<string, string>;
|
|
71
|
+
/** Other blocks this block extends */
|
|
72
|
+
extends?: string[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Configuration for the registry build.
|
|
76
|
+
*/
|
|
77
|
+
interface RegistryBuildConfig {
|
|
78
|
+
/** Registry schema version */
|
|
79
|
+
version: string;
|
|
80
|
+
/** Block definitions */
|
|
81
|
+
blocks: Record<string, BlockDefinition>;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Result of adding a block to a project.
|
|
85
|
+
*/
|
|
86
|
+
interface AddBlockResult {
|
|
87
|
+
/** Files that were created */
|
|
88
|
+
created: string[];
|
|
89
|
+
/** Files that were skipped (already exist) */
|
|
90
|
+
skipped: string[];
|
|
91
|
+
/** Files that were overwritten (with --force) */
|
|
92
|
+
overwritten: string[];
|
|
93
|
+
/** Dependencies added to package.json */
|
|
94
|
+
dependencies: Record<string, string>;
|
|
95
|
+
/** devDependencies added to package.json */
|
|
96
|
+
devDependencies: Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Options for the add command.
|
|
100
|
+
*/
|
|
101
|
+
interface AddBlockOptions {
|
|
102
|
+
/** Overwrite existing files */
|
|
103
|
+
force?: boolean;
|
|
104
|
+
/** Show what would be added without making changes */
|
|
105
|
+
dryRun?: boolean;
|
|
106
|
+
/** Working directory (defaults to cwd) */
|
|
107
|
+
cwd?: string;
|
|
108
|
+
}
|
|
3
109
|
/**
|
|
4
110
|
* @outfitter/tooling
|
|
5
111
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
import"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// src/registry/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var FileEntrySchema = z.object({
|
|
4
|
+
path: z.string().min(1),
|
|
5
|
+
content: z.string(),
|
|
6
|
+
executable: z.boolean().optional(),
|
|
7
|
+
template: z.boolean().optional()
|
|
8
|
+
});
|
|
9
|
+
var BlockSchema = z.object({
|
|
10
|
+
name: z.string().min(1),
|
|
11
|
+
description: z.string().min(1),
|
|
12
|
+
files: z.array(FileEntrySchema).optional(),
|
|
13
|
+
dependencies: z.record(z.string()).optional(),
|
|
14
|
+
devDependencies: z.record(z.string()).optional(),
|
|
15
|
+
extends: z.array(z.string()).optional()
|
|
16
|
+
});
|
|
17
|
+
var RegistrySchema = z.object({
|
|
18
|
+
version: z.string(),
|
|
19
|
+
blocks: z.record(BlockSchema)
|
|
20
|
+
});
|
|
21
|
+
// src/index.ts
|
|
10
22
|
var VERSION = "0.1.0-rc.1";
|
|
11
23
|
export {
|
|
12
24
|
VERSION,
|
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
|
});
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/pre-push.ts
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var COLORS = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
red: "\x1B[31m",
|
|
8
|
+
green: "\x1B[32m",
|
|
9
|
+
yellow: "\x1B[33m",
|
|
10
|
+
blue: "\x1B[34m"
|
|
11
|
+
};
|
|
12
|
+
function log(msg) {
|
|
13
|
+
process.stdout.write(`${msg}
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
function getCurrentBranch() {
|
|
17
|
+
const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
18
|
+
return result.stdout.toString().trim();
|
|
19
|
+
}
|
|
20
|
+
function runGit(args) {
|
|
21
|
+
try {
|
|
22
|
+
const result = Bun.spawnSync(["git", ...args], { stderr: "ignore" });
|
|
23
|
+
if (result.exitCode !== 0) {
|
|
24
|
+
return { ok: false, lines: [] };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ok: true,
|
|
28
|
+
lines: result.stdout.toString().split(`
|
|
29
|
+
`).map((line) => line.trim()).filter(Boolean)
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return { ok: false, lines: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isRedPhaseBranch(branch) {
|
|
36
|
+
return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
|
|
37
|
+
}
|
|
38
|
+
function isScaffoldBranch(branch) {
|
|
39
|
+
return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
|
|
40
|
+
}
|
|
41
|
+
var TEST_PATH_PATTERNS = [
|
|
42
|
+
/(^|\/)__tests__\//,
|
|
43
|
+
/(^|\/)__snapshots__\//,
|
|
44
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/,
|
|
45
|
+
/\.snap$/,
|
|
46
|
+
/(^|\/)(vitest|jest|bun)\.config\.[cm]?[jt]s$/,
|
|
47
|
+
/(^|\/)tsconfig\.test\.json$/,
|
|
48
|
+
/(^|\/)\.env\.test(\.|$)/
|
|
49
|
+
];
|
|
50
|
+
function isTestOnlyPath(path) {
|
|
51
|
+
const normalized = path.replaceAll("\\", "/");
|
|
52
|
+
return TEST_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
53
|
+
}
|
|
54
|
+
function areFilesTestOnly(paths) {
|
|
55
|
+
return paths.length > 0 && paths.every((path) => isTestOnlyPath(path));
|
|
56
|
+
}
|
|
57
|
+
function canBypassRedPhaseByChangedFiles(changedFiles) {
|
|
58
|
+
return changedFiles.deterministic && areFilesTestOnly(changedFiles.files);
|
|
59
|
+
}
|
|
60
|
+
function resolveBaseRef() {
|
|
61
|
+
const candidates = [
|
|
62
|
+
"origin/main",
|
|
63
|
+
"main",
|
|
64
|
+
"origin/trunk",
|
|
65
|
+
"trunk",
|
|
66
|
+
"origin/master",
|
|
67
|
+
"master"
|
|
68
|
+
];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const resolved = runGit(["rev-parse", "--verify", "--quiet", candidate]);
|
|
71
|
+
if (resolved.ok) {
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
function changedFilesFromRange(range) {
|
|
78
|
+
const result = runGit(["diff", "--name-only", "--diff-filter=d", range]);
|
|
79
|
+
return {
|
|
80
|
+
ok: result.ok,
|
|
81
|
+
files: result.lines
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function getChangedFilesForPush() {
|
|
85
|
+
const upstream = runGit([
|
|
86
|
+
"rev-parse",
|
|
87
|
+
"--abbrev-ref",
|
|
88
|
+
"--symbolic-full-name",
|
|
89
|
+
"@{upstream}"
|
|
90
|
+
]);
|
|
91
|
+
if (upstream.ok && upstream.lines[0]) {
|
|
92
|
+
const rangeResult = changedFilesFromRange(`${upstream.lines[0]}...HEAD`);
|
|
93
|
+
if (rangeResult.ok) {
|
|
94
|
+
return {
|
|
95
|
+
files: rangeResult.files,
|
|
96
|
+
deterministic: true,
|
|
97
|
+
source: "upstream"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const baseRef = resolveBaseRef();
|
|
102
|
+
if (baseRef) {
|
|
103
|
+
const rangeResult = changedFilesFromRange(`${baseRef}...HEAD`);
|
|
104
|
+
if (rangeResult.ok) {
|
|
105
|
+
return {
|
|
106
|
+
files: rangeResult.files,
|
|
107
|
+
deterministic: true,
|
|
108
|
+
source: "baseRef"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
files: [],
|
|
114
|
+
deterministic: false,
|
|
115
|
+
source: "undetermined"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function maybeSkipForRedPhase(reason, branch) {
|
|
119
|
+
const changedFiles = getChangedFilesForPush();
|
|
120
|
+
if (!changedFiles.deterministic) {
|
|
121
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: could not determine full push diff range`);
|
|
122
|
+
log("Running strict verification.");
|
|
123
|
+
log("");
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (!canBypassRedPhaseByChangedFiles(changedFiles)) {
|
|
127
|
+
log(`${COLORS.yellow}RED-phase bypass denied${COLORS.reset}: changed files are not test-only`);
|
|
128
|
+
if (changedFiles.files.length > 0) {
|
|
129
|
+
log(`Changed files (${changedFiles.source}): ${changedFiles.files.join(", ")}`);
|
|
130
|
+
} else {
|
|
131
|
+
log(`No changed files detected in ${changedFiles.source} range. Running strict verification.`);
|
|
132
|
+
}
|
|
133
|
+
log("");
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (reason === "branch") {
|
|
137
|
+
log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
138
|
+
} else {
|
|
139
|
+
log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
140
|
+
}
|
|
141
|
+
log(`${COLORS.yellow}Skipping strict verification${COLORS.reset} - changed files are test-only`);
|
|
142
|
+
log(`Diff source: ${changedFiles.source}`);
|
|
143
|
+
log("");
|
|
144
|
+
log("Remember: GREEN phase (implementation) must make these tests pass!");
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
function hasRedPhaseBranchInContext(currentBranch) {
|
|
148
|
+
let branches = [];
|
|
149
|
+
try {
|
|
150
|
+
const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
|
|
151
|
+
if (gtResult.exitCode === 0) {
|
|
152
|
+
branches = gtResult.stdout.toString().split(`
|
|
153
|
+
`).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
if (branches.length === 0) {
|
|
157
|
+
const gitResult = Bun.spawnSync([
|
|
158
|
+
"git",
|
|
159
|
+
"branch",
|
|
160
|
+
"--list",
|
|
161
|
+
"cli/*",
|
|
162
|
+
"types/*",
|
|
163
|
+
"contracts/*"
|
|
164
|
+
]);
|
|
165
|
+
branches = gitResult.stdout.toString().split(`
|
|
166
|
+
`).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
|
|
167
|
+
}
|
|
168
|
+
for (const branch of branches) {
|
|
169
|
+
if (branch === currentBranch)
|
|
170
|
+
continue;
|
|
171
|
+
if (isRedPhaseBranch(branch))
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
function createVerificationPlan(scripts) {
|
|
177
|
+
if (scripts["verify:ci"]) {
|
|
178
|
+
return { ok: true, scripts: ["verify:ci"], source: "verify:ci" };
|
|
179
|
+
}
|
|
180
|
+
const requiredScripts = ["typecheck", "build", "test"];
|
|
181
|
+
const missingRequired = requiredScripts.filter((name) => !scripts[name]);
|
|
182
|
+
const checkOrLint = scripts["check"] ? "check" : scripts["lint"] ? "lint" : undefined;
|
|
183
|
+
if (!checkOrLint || missingRequired.length > 0) {
|
|
184
|
+
const missing = checkOrLint ? missingRequired : [...missingRequired, "check|lint"];
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
error: `Missing required scripts for strict pre-push verification: ${missing.join(", ")}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
scripts: ["typecheck", checkOrLint, "build", "test"],
|
|
193
|
+
source: "fallback"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function readPackageScripts(cwd = process.cwd()) {
|
|
197
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
198
|
+
if (!existsSync(packageJsonPath)) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
203
|
+
const scripts = parsed.scripts ?? {};
|
|
204
|
+
const normalized = {};
|
|
205
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
206
|
+
if (typeof value === "string") {
|
|
207
|
+
normalized[name] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return normalized;
|
|
211
|
+
} catch {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function runScript(scriptName) {
|
|
216
|
+
log("");
|
|
217
|
+
log(`Running: ${COLORS.blue}bun run ${scriptName}${COLORS.reset}`);
|
|
218
|
+
const result = Bun.spawnSync(["bun", "run", scriptName], {
|
|
219
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
220
|
+
});
|
|
221
|
+
return result.exitCode === 0;
|
|
222
|
+
}
|
|
223
|
+
async function runPrePush(options = {}) {
|
|
224
|
+
log(`${COLORS.blue}Pre-push verify${COLORS.reset} (TDD-aware)`);
|
|
225
|
+
log("");
|
|
226
|
+
const branch = getCurrentBranch();
|
|
227
|
+
if (isRedPhaseBranch(branch)) {
|
|
228
|
+
if (maybeSkipForRedPhase("branch", branch)) {
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (isScaffoldBranch(branch)) {
|
|
233
|
+
if (hasRedPhaseBranchInContext(branch)) {
|
|
234
|
+
if (maybeSkipForRedPhase("context", branch)) {
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (options.force) {
|
|
240
|
+
log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping strict verification`);
|
|
241
|
+
process.exit(0);
|
|
242
|
+
}
|
|
243
|
+
const plan = createVerificationPlan(readPackageScripts());
|
|
244
|
+
if (!plan.ok) {
|
|
245
|
+
log(`${COLORS.red}Strict pre-push verification is not configured${COLORS.reset}`);
|
|
246
|
+
log(plan.error);
|
|
247
|
+
log("");
|
|
248
|
+
log("Add one of:");
|
|
249
|
+
log(" - verify:ci");
|
|
250
|
+
log(" - typecheck + (check or lint) + build + test");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
log(`Running strict verification for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
254
|
+
if (plan.source === "verify:ci") {
|
|
255
|
+
log("Using `verify:ci` script.");
|
|
256
|
+
} else {
|
|
257
|
+
log(`Using fallback scripts: ${plan.scripts.join(" -> ")}`);
|
|
258
|
+
}
|
|
259
|
+
for (const scriptName of plan.scripts) {
|
|
260
|
+
if (runScript(scriptName)) {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
log("");
|
|
264
|
+
log(`${COLORS.red}Verification failed${COLORS.reset} on script: ${scriptName}`);
|
|
265
|
+
log("");
|
|
266
|
+
log("If this is intentional TDD RED phase work, name your branch:");
|
|
267
|
+
log(" - feature-tests");
|
|
268
|
+
log(" - feature/tests");
|
|
269
|
+
log(" - feature_tests");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
log("");
|
|
273
|
+
log(`${COLORS.green}Strict verification passed${COLORS.reset}`);
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { isRedPhaseBranch, isScaffoldBranch, isTestOnlyPath, areFilesTestOnly, canBypassRedPhaseByChangedFiles, createVerificationPlan, runPrePush };
|
|
@@ -9,7 +9,8 @@ function buildFixCommand(options) {
|
|
|
9
9
|
}
|
|
10
10
|
async function runFix(paths = []) {
|
|
11
11
|
const cmd = buildFixCommand({ 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
|
});
|
|
@@ -10,16 +10,20 @@ var COLORS = {
|
|
|
10
10
|
blue: "\x1B[34m"
|
|
11
11
|
};
|
|
12
12
|
function log(msg) {
|
|
13
|
-
|
|
13
|
+
process.stdout.write(`${msg}
|
|
14
|
+
`);
|
|
14
15
|
}
|
|
15
16
|
function info(msg) {
|
|
16
|
-
|
|
17
|
+
process.stdout.write(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}
|
|
18
|
+
`);
|
|
17
19
|
}
|
|
18
20
|
function success(msg) {
|
|
19
|
-
|
|
21
|
+
process.stdout.write(`${COLORS.green}\u2713${COLORS.reset} ${msg}
|
|
22
|
+
`);
|
|
20
23
|
}
|
|
21
24
|
function warn(msg) {
|
|
22
|
-
|
|
25
|
+
process.stdout.write(`${COLORS.yellow}!${COLORS.reset} ${msg}
|
|
26
|
+
`);
|
|
23
27
|
}
|
|
24
28
|
async function fetchLatestVersion() {
|
|
25
29
|
const response = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest");
|
|
@@ -91,13 +95,13 @@ async function runUpgradeBun(targetVersion, options = {}) {
|
|
|
91
95
|
info("Updating engines.bun...");
|
|
92
96
|
for (const file of packageFiles) {
|
|
93
97
|
if (updateEnginesBun(file, version)) {
|
|
94
|
-
log(` ${file.replace(cwd
|
|
98
|
+
log(` ${file.replace(`${cwd}/`, "")}`);
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
info("Updating @types/bun...");
|
|
98
102
|
for (const file of packageFiles) {
|
|
99
103
|
if (updateTypesBun(file, version)) {
|
|
100
|
-
log(` ${file.replace(cwd
|
|
104
|
+
log(` ${file.replace(`${cwd}/`, "")}`);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
if (options.install !== false) {
|
|
@@ -29,7 +29,15 @@ function detectFrameworks(pkg) {
|
|
|
29
29
|
return ["--frameworks", ...detected];
|
|
30
30
|
}
|
|
31
31
|
function buildUltraciteCommand(options) {
|
|
32
|
-
const cmd = [
|
|
32
|
+
const cmd = [
|
|
33
|
+
"ultracite",
|
|
34
|
+
"init",
|
|
35
|
+
"--linter",
|
|
36
|
+
"biome",
|
|
37
|
+
"--pm",
|
|
38
|
+
"bun",
|
|
39
|
+
"--quiet"
|
|
40
|
+
];
|
|
33
41
|
if (options.frameworks && options.frameworks.length > 0) {
|
|
34
42
|
cmd.push("--frameworks", ...options.frameworks);
|
|
35
43
|
}
|
|
@@ -39,14 +47,16 @@ async function runInit(cwd = process.cwd()) {
|
|
|
39
47
|
const pkgPath = `${cwd}/package.json`;
|
|
40
48
|
const pkgFile = Bun.file(pkgPath);
|
|
41
49
|
if (!await pkgFile.exists()) {
|
|
42
|
-
|
|
50
|
+
process.stderr.write(`No package.json found in current directory
|
|
51
|
+
`);
|
|
43
52
|
process.exit(1);
|
|
44
53
|
}
|
|
45
54
|
const pkg = await pkgFile.json();
|
|
46
55
|
const frameworkFlags = detectFrameworks(pkg);
|
|
47
56
|
const frameworks = frameworkFlags.length > 0 ? frameworkFlags.slice(1) : [];
|
|
48
57
|
const cmd = buildUltraciteCommand({ frameworks });
|
|
49
|
-
|
|
58
|
+
process.stdout.write(`Running: bun x ${cmd.join(" ")}
|
|
59
|
+
`);
|
|
50
60
|
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
51
61
|
cwd,
|
|
52
62
|
stdio: ["inherit", "inherit", "inherit"]
|
package/lefthook.yml
CHANGED
|
@@ -20,11 +20,9 @@ pre-commit:
|
|
|
20
20
|
pre-push:
|
|
21
21
|
parallel: false
|
|
22
22
|
commands:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
# Override with `run: bun run test` if you don't want TDD support
|
|
29
|
-
# Requires: @outfitter/tooling must be a devDependency in your project
|
|
23
|
+
verify:
|
|
24
|
+
# TDD-aware: skips strict verification on explicit RED phase branches
|
|
25
|
+
# (*-tests, */tests, *_tests). Otherwise runs verify:ci (or a strict
|
|
26
|
+
# fallback: typecheck/check-or-lint/build/test).
|
|
27
|
+
# Requires: @outfitter/tooling as a dev dependency.
|
|
30
28
|
run: bunx @outfitter/tooling pre-push
|