@outfitter/tooling 0.2.0
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 +66 -0
- package/README.md +168 -0
- package/biome.json +74 -0
- package/dist/cli/check.d.ts +19 -0
- package/dist/cli/check.js +9 -0
- package/dist/cli/fix.d.ts +19 -0
- package/dist/cli/fix.js +9 -0
- package/dist/cli/index.js +353 -0
- package/dist/cli/init.d.ts +31 -0
- package/dist/cli/init.js +11 -0
- package/dist/cli/pre-push.d.ts +8 -0
- package/dist/cli/pre-push.js +7 -0
- package/dist/cli/upgrade-bun.d.ts +8 -0
- package/dist/cli/upgrade-bun.js +7 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +28 -0
- package/dist/registry/build.js +128 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +12 -0
- package/dist/registry/schema.d.ts +2 -0
- package/dist/registry/schema.js +11 -0
- package/dist/shared/@outfitter/tooling-75j500dv.js +142 -0
- package/dist/shared/@outfitter/tooling-g83d0kjv.js +23 -0
- package/dist/shared/@outfitter/tooling-kcvs6mys.js +1 -0
- package/dist/shared/@outfitter/tooling-qm7jeg0d.js +99 -0
- package/dist/shared/@outfitter/tooling-s4eqq91d.js +20 -0
- package/dist/shared/@outfitter/tooling-sjm8nebx.d.ts +109 -0
- package/dist/shared/@outfitter/tooling-xaxdr9da.js +58 -0
- package/dist/shared/@outfitter/tooling-xx1146e3.js +20 -0
- package/lefthook.yml +30 -0
- package/package.json +116 -0
- package/registry/registry.json +78 -0
- package/tsconfig.preset.bun.json +7 -0
- package/tsconfig.preset.json +40 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// packages/tooling/src/registry/build.ts
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
function findRepoRoot(startDir) {
|
|
8
|
+
let dir = startDir;
|
|
9
|
+
while (dir !== "/") {
|
|
10
|
+
const pkgPath = join(dir, "package.json");
|
|
11
|
+
if (existsSync(pkgPath)) {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
13
|
+
if (pkg.workspaces) {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
dir = dirname(dir);
|
|
18
|
+
}
|
|
19
|
+
throw new Error("Could not find repository root");
|
|
20
|
+
}
|
|
21
|
+
function isExecutable(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const stats = statSync(filePath);
|
|
24
|
+
return (stats.mode & 64) !== 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function readFileEntry(repoRoot, sourcePath, destPath) {
|
|
30
|
+
const fullPath = join(repoRoot, sourcePath);
|
|
31
|
+
if (!existsSync(fullPath)) {
|
|
32
|
+
throw new Error(`Source file not found: ${fullPath}`);
|
|
33
|
+
}
|
|
34
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
35
|
+
const executable = isExecutable(fullPath);
|
|
36
|
+
return {
|
|
37
|
+
path: destPath,
|
|
38
|
+
content,
|
|
39
|
+
...executable && { executable: true }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buildBlock(repoRoot, name, def) {
|
|
43
|
+
const block = {
|
|
44
|
+
name,
|
|
45
|
+
description: def.description
|
|
46
|
+
};
|
|
47
|
+
if (def.files && def.files.length > 0) {
|
|
48
|
+
block.files = def.files.map((sourcePath) => {
|
|
49
|
+
const destPath = def.remap?.[sourcePath] ?? sourcePath;
|
|
50
|
+
return readFileEntry(repoRoot, sourcePath, destPath);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (def.dependencies && Object.keys(def.dependencies).length > 0) {
|
|
54
|
+
block.dependencies = def.dependencies;
|
|
55
|
+
}
|
|
56
|
+
if (def.devDependencies && Object.keys(def.devDependencies).length > 0) {
|
|
57
|
+
block.devDependencies = def.devDependencies;
|
|
58
|
+
}
|
|
59
|
+
if (def.extends && def.extends.length > 0) {
|
|
60
|
+
block.extends = def.extends;
|
|
61
|
+
}
|
|
62
|
+
return block;
|
|
63
|
+
}
|
|
64
|
+
function buildRegistry(config, repoRoot) {
|
|
65
|
+
const blocks = {};
|
|
66
|
+
for (const [name, def] of Object.entries(config.blocks)) {
|
|
67
|
+
blocks[name] = buildBlock(repoRoot, name, def);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
version: config.version,
|
|
71
|
+
blocks
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
var REGISTRY_CONFIG = {
|
|
75
|
+
version: "1.0.0",
|
|
76
|
+
blocks: {
|
|
77
|
+
claude: {
|
|
78
|
+
description: "Claude Code settings and hooks for automated formatting",
|
|
79
|
+
files: [
|
|
80
|
+
".claude/settings.json",
|
|
81
|
+
".claude/hooks/format-code-on-stop.sh"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
biome: {
|
|
85
|
+
description: "Biome linter/formatter configuration via Ultracite",
|
|
86
|
+
files: ["packages/tooling/biome.json"],
|
|
87
|
+
remap: { "packages/tooling/biome.json": "biome.json" },
|
|
88
|
+
devDependencies: { ultracite: "^7.0.0" }
|
|
89
|
+
},
|
|
90
|
+
lefthook: {
|
|
91
|
+
description: "Git hooks via Lefthook for pre-commit and pre-push",
|
|
92
|
+
files: ["packages/tooling/lefthook.yml"],
|
|
93
|
+
remap: { "packages/tooling/lefthook.yml": ".lefthook.yml" },
|
|
94
|
+
devDependencies: { lefthook: "^2.0.0" }
|
|
95
|
+
},
|
|
96
|
+
markdownlint: {
|
|
97
|
+
description: "Markdown linting configuration via markdownlint-cli2",
|
|
98
|
+
files: ["packages/tooling/.markdownlint-cli2.jsonc"],
|
|
99
|
+
remap: { "packages/tooling/.markdownlint-cli2.jsonc": ".markdownlint-cli2.jsonc" }
|
|
100
|
+
},
|
|
101
|
+
bootstrap: {
|
|
102
|
+
description: "Project bootstrap script for installing tools and dependencies",
|
|
103
|
+
files: ["scripts/bootstrap.sh"]
|
|
104
|
+
},
|
|
105
|
+
scaffolding: {
|
|
106
|
+
description: "Full starter kit: Claude settings, Biome, Lefthook, markdownlint, and bootstrap script",
|
|
107
|
+
extends: ["claude", "biome", "lefthook", "markdownlint", "bootstrap"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
function main() {
|
|
112
|
+
const scriptDir = dirname(new URL(import.meta.url).pathname);
|
|
113
|
+
const repoRoot = findRepoRoot(scriptDir);
|
|
114
|
+
const outputDir = join(repoRoot, "packages/tooling/registry");
|
|
115
|
+
const outputPath = join(outputDir, "registry.json");
|
|
116
|
+
console.log(`Building registry from: ${repoRoot}`);
|
|
117
|
+
if (!existsSync(outputDir)) {
|
|
118
|
+
mkdirSync(outputDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
const registry = buildRegistry(REGISTRY_CONFIG, repoRoot);
|
|
121
|
+
writeFileSync(outputPath, JSON.stringify(registry, null, 2) + `
|
|
122
|
+
`);
|
|
123
|
+
const blockCount = Object.keys(registry.blocks).length;
|
|
124
|
+
const fileCount = Object.values(registry.blocks).flatMap((b) => b.files ?? []).length;
|
|
125
|
+
console.log(`\u2713 Generated ${outputPath}`);
|
|
126
|
+
console.log(` ${blockCount} blocks, ${fileCount} files embedded`);
|
|
127
|
+
}
|
|
128
|
+
main();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import "../shared/@outfitter/tooling-xqwn46sx";
|
|
2
|
+
import { AddBlockOptions, AddBlockResult, Block, BlockDefinition, BlockSchema, FileEntry, FileEntrySchema, Registry, RegistryBuildConfig, RegistrySchema } from "../shared/@outfitter/tooling-sjm8nebx";
|
|
3
|
+
export { RegistrySchema, RegistryBuildConfig, Registry, FileEntrySchema, FileEntry, BlockSchema, BlockDefinition, Block, AddBlockResult, AddBlockOptions };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { AddBlockOptions, AddBlockResult, Block, BlockDefinition, BlockSchema, FileEntry, FileEntrySchema, Registry, RegistryBuildConfig, RegistrySchema } from "../shared/@outfitter/tooling-sjm8nebx";
|
|
2
|
+
export { RegistrySchema, RegistryBuildConfig, Registry, FileEntrySchema, FileEntry, BlockSchema, BlockDefinition, Block, AddBlockResult, AddBlockOptions };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/upgrade-bun.ts
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } 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
|
+
console.log(msg);
|
|
14
|
+
}
|
|
15
|
+
function info(msg) {
|
|
16
|
+
console.log(`${COLORS.blue}\u25B8${COLORS.reset} ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
function success(msg) {
|
|
19
|
+
console.log(`${COLORS.green}\u2713${COLORS.reset} ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
function warn(msg) {
|
|
22
|
+
console.log(`${COLORS.yellow}!${COLORS.reset} ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
async function fetchLatestVersion() {
|
|
25
|
+
const response = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest");
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
const match = data.tag_name.match(/bun-v(.+)/);
|
|
28
|
+
if (!match?.[1]) {
|
|
29
|
+
throw new Error(`Could not parse version from tag: ${data.tag_name}`);
|
|
30
|
+
}
|
|
31
|
+
return match[1];
|
|
32
|
+
}
|
|
33
|
+
function findPackageJsonFiles(dir) {
|
|
34
|
+
const results = [];
|
|
35
|
+
const glob = new Bun.Glob("**/package.json");
|
|
36
|
+
for (const path of glob.scanSync({ cwd: dir })) {
|
|
37
|
+
if (!path.includes("node_modules")) {
|
|
38
|
+
results.push(join(dir, path));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
function updateEnginesBun(filePath, version) {
|
|
44
|
+
const content = readFileSync(filePath, "utf-8");
|
|
45
|
+
const pattern = /"bun":\s*">=[\d.]+"/;
|
|
46
|
+
if (!pattern.test(content)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const updated = content.replace(pattern, `"bun": ">=${version}"`);
|
|
50
|
+
if (updated !== content) {
|
|
51
|
+
writeFileSync(filePath, updated);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function updateTypesBun(filePath, version) {
|
|
57
|
+
const content = readFileSync(filePath, "utf-8");
|
|
58
|
+
const pattern = /"@types\/bun":\s*"\^[\d.]+"/;
|
|
59
|
+
if (!pattern.test(content)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const updated = content.replace(pattern, `"@types/bun": "^${version}"`);
|
|
63
|
+
if (updated !== content) {
|
|
64
|
+
writeFileSync(filePath, updated);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function runUpgradeBun(targetVersion, options = {}) {
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
const bunVersionFile = join(cwd, ".bun-version");
|
|
72
|
+
let version = targetVersion;
|
|
73
|
+
if (!version) {
|
|
74
|
+
info("Fetching latest Bun version...");
|
|
75
|
+
version = await fetchLatestVersion();
|
|
76
|
+
log(`Latest version: ${version}`);
|
|
77
|
+
}
|
|
78
|
+
const currentVersion = existsSync(bunVersionFile) ? readFileSync(bunVersionFile, "utf-8").trim() : "unknown";
|
|
79
|
+
log(`Current version: ${currentVersion}`);
|
|
80
|
+
if (currentVersion === version) {
|
|
81
|
+
success(`Already on version ${version}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
log("");
|
|
85
|
+
info(`Upgrading Bun: ${currentVersion} \u2192 ${version}`);
|
|
86
|
+
log("");
|
|
87
|
+
writeFileSync(bunVersionFile, `${version}
|
|
88
|
+
`);
|
|
89
|
+
success("Updated .bun-version");
|
|
90
|
+
const packageFiles = findPackageJsonFiles(cwd);
|
|
91
|
+
info("Updating engines.bun...");
|
|
92
|
+
for (const file of packageFiles) {
|
|
93
|
+
if (updateEnginesBun(file, version)) {
|
|
94
|
+
log(` ${file.replace(cwd + "/", "")}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
info("Updating @types/bun...");
|
|
98
|
+
for (const file of packageFiles) {
|
|
99
|
+
if (updateTypesBun(file, version)) {
|
|
100
|
+
log(` ${file.replace(cwd + "/", "")}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (options.install !== false) {
|
|
104
|
+
log("");
|
|
105
|
+
info(`Installing Bun ${version}...`);
|
|
106
|
+
const installResult = Bun.spawnSync([
|
|
107
|
+
"bash",
|
|
108
|
+
"-c",
|
|
109
|
+
`curl -fsSL https://bun.sh/install | bash -s "bun-v${version}"`
|
|
110
|
+
]);
|
|
111
|
+
if (installResult.exitCode !== 0) {
|
|
112
|
+
warn("Could not install Bun automatically");
|
|
113
|
+
log("Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
114
|
+
} else {
|
|
115
|
+
success(`Bun ${version} installed`);
|
|
116
|
+
log("");
|
|
117
|
+
info("Updating lockfile...");
|
|
118
|
+
const bunInstall = Bun.spawnSync(["bun", "install"], {
|
|
119
|
+
cwd,
|
|
120
|
+
env: {
|
|
121
|
+
...process.env,
|
|
122
|
+
BUN_INSTALL: `${process.env["HOME"]}/.bun`,
|
|
123
|
+
PATH: `${process.env["HOME"]}/.bun/bin:${process.env["PATH"]}`
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (bunInstall.exitCode === 0) {
|
|
127
|
+
success("Lockfile updated");
|
|
128
|
+
} else {
|
|
129
|
+
warn("Could not update lockfile - run 'bun install' manually");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
log("");
|
|
134
|
+
success("Done! Changes ready to commit:");
|
|
135
|
+
log(" - .bun-version");
|
|
136
|
+
log(" - package.json files (engines.bun, @types/bun)");
|
|
137
|
+
log(" - bun.lock");
|
|
138
|
+
log("");
|
|
139
|
+
log(`Commit with: git add -A && git commit -m 'chore: upgrade Bun to ${version}'`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { runUpgradeBun };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/registry/schema.ts
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var FileEntrySchema = z.object({
|
|
5
|
+
path: z.string().min(1),
|
|
6
|
+
content: z.string(),
|
|
7
|
+
executable: z.boolean().optional(),
|
|
8
|
+
template: z.boolean().optional()
|
|
9
|
+
});
|
|
10
|
+
var BlockSchema = z.object({
|
|
11
|
+
name: z.string().min(1),
|
|
12
|
+
description: z.string().min(1),
|
|
13
|
+
files: z.array(FileEntrySchema).optional(),
|
|
14
|
+
dependencies: z.record(z.string()).optional(),
|
|
15
|
+
devDependencies: z.record(z.string()).optional(),
|
|
16
|
+
extends: z.array(z.string()).optional()
|
|
17
|
+
});
|
|
18
|
+
var RegistrySchema = z.object({
|
|
19
|
+
version: z.string(),
|
|
20
|
+
blocks: z.record(BlockSchema)
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export { FileEntrySchema, BlockSchema, RegistrySchema };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/pre-push.ts
|
|
3
|
+
var COLORS = {
|
|
4
|
+
reset: "\x1B[0m",
|
|
5
|
+
red: "\x1B[31m",
|
|
6
|
+
green: "\x1B[32m",
|
|
7
|
+
yellow: "\x1B[33m",
|
|
8
|
+
blue: "\x1B[34m"
|
|
9
|
+
};
|
|
10
|
+
function log(msg) {
|
|
11
|
+
console.log(msg);
|
|
12
|
+
}
|
|
13
|
+
function getCurrentBranch() {
|
|
14
|
+
const result = Bun.spawnSync(["git", "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
15
|
+
return result.stdout.toString().trim();
|
|
16
|
+
}
|
|
17
|
+
function isRedPhaseBranch(branch) {
|
|
18
|
+
return branch.endsWith("-tests") || branch.endsWith("/tests") || branch.endsWith("_tests");
|
|
19
|
+
}
|
|
20
|
+
function isScaffoldBranch(branch) {
|
|
21
|
+
return branch.endsWith("-scaffold") || branch.endsWith("/scaffold") || branch.endsWith("_scaffold");
|
|
22
|
+
}
|
|
23
|
+
function hasRedPhaseBranchInContext(currentBranch) {
|
|
24
|
+
let branches = [];
|
|
25
|
+
try {
|
|
26
|
+
const gtResult = Bun.spawnSync(["gt", "ls"], { stderr: "pipe" });
|
|
27
|
+
if (gtResult.exitCode === 0) {
|
|
28
|
+
branches = gtResult.stdout.toString().split(`
|
|
29
|
+
`).map((line) => line.replace(/^[\u2502\u251C\u2514\u2500\u25C9\u25EF ]*/g, "").replace(/ \(.*/, "")).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
} catch {}
|
|
32
|
+
if (branches.length === 0) {
|
|
33
|
+
const gitResult = Bun.spawnSync([
|
|
34
|
+
"git",
|
|
35
|
+
"branch",
|
|
36
|
+
"--list",
|
|
37
|
+
"cli/*",
|
|
38
|
+
"types/*",
|
|
39
|
+
"contracts/*"
|
|
40
|
+
]);
|
|
41
|
+
branches = gitResult.stdout.toString().split(`
|
|
42
|
+
`).map((line) => line.replace(/^[* ]+/, "")).filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
for (const branch of branches) {
|
|
45
|
+
if (branch === currentBranch)
|
|
46
|
+
continue;
|
|
47
|
+
if (isRedPhaseBranch(branch))
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function runTests() {
|
|
53
|
+
log("");
|
|
54
|
+
const result = Bun.spawnSync(["bun", "run", "test"], {
|
|
55
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
56
|
+
});
|
|
57
|
+
return result.exitCode === 0;
|
|
58
|
+
}
|
|
59
|
+
async function runPrePush(options = {}) {
|
|
60
|
+
log(`${COLORS.blue}Pre-push test${COLORS.reset} (TDD-aware)`);
|
|
61
|
+
log("");
|
|
62
|
+
const branch = getCurrentBranch();
|
|
63
|
+
if (isRedPhaseBranch(branch)) {
|
|
64
|
+
log(`${COLORS.yellow}TDD RED phase${COLORS.reset} detected: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
65
|
+
log(`${COLORS.yellow}Skipping test execution${COLORS.reset} - tests are expected to fail in RED phase`);
|
|
66
|
+
log("");
|
|
67
|
+
log("Remember: GREEN phase (implementation) must make these tests pass!");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
if (isScaffoldBranch(branch)) {
|
|
71
|
+
if (hasRedPhaseBranchInContext(branch)) {
|
|
72
|
+
log(`${COLORS.yellow}Scaffold branch${COLORS.reset} with RED phase branch in context: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
73
|
+
log(`${COLORS.yellow}Skipping test execution${COLORS.reset} - RED phase tests expected to fail`);
|
|
74
|
+
log("");
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (options.force) {
|
|
79
|
+
log(`${COLORS.yellow}Force flag set${COLORS.reset} - skipping tests`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
log(`Running tests for branch: ${COLORS.blue}${branch}${COLORS.reset}`);
|
|
83
|
+
if (runTests()) {
|
|
84
|
+
log("");
|
|
85
|
+
log(`${COLORS.green}All tests passed${COLORS.reset}`);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
} else {
|
|
88
|
+
log("");
|
|
89
|
+
log(`${COLORS.red}Tests failed${COLORS.reset}`);
|
|
90
|
+
log("");
|
|
91
|
+
log("If this is intentional TDD RED phase work, name your branch:");
|
|
92
|
+
log(" - feature-tests");
|
|
93
|
+
log(" - feature/tests");
|
|
94
|
+
log(" - feature_tests");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { runPrePush };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/fix.ts
|
|
3
|
+
function buildFixCommand(options) {
|
|
4
|
+
const cmd = ["ultracite", "fix"];
|
|
5
|
+
if (options.paths && options.paths.length > 0) {
|
|
6
|
+
cmd.push(...options.paths);
|
|
7
|
+
}
|
|
8
|
+
return cmd;
|
|
9
|
+
}
|
|
10
|
+
async function runFix(paths = []) {
|
|
11
|
+
const cmd = buildFixCommand({ paths });
|
|
12
|
+
console.log(`Running: bun x ${cmd.join(" ")}`);
|
|
13
|
+
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
14
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
15
|
+
});
|
|
16
|
+
const exitCode = await proc.exited;
|
|
17
|
+
process.exit(exitCode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { buildFixCommand, runFix };
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
}
|
|
109
|
+
export { FileEntry, FileEntrySchema, Block, BlockSchema, Registry, RegistrySchema, BlockDefinition, RegistryBuildConfig, AddBlockResult, AddBlockOptions };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/init.ts
|
|
3
|
+
var FRAMEWORK_DETECTORS = {
|
|
4
|
+
react: ["react", "react-dom"],
|
|
5
|
+
next: ["next"],
|
|
6
|
+
vue: ["vue"],
|
|
7
|
+
nuxt: ["nuxt"],
|
|
8
|
+
svelte: ["svelte"],
|
|
9
|
+
angular: ["@angular/core"],
|
|
10
|
+
solid: ["solid-js"],
|
|
11
|
+
astro: ["astro"],
|
|
12
|
+
remix: ["@remix-run/react"],
|
|
13
|
+
qwik: ["@builder.io/qwik"]
|
|
14
|
+
};
|
|
15
|
+
function detectFrameworks(pkg) {
|
|
16
|
+
const allDeps = {
|
|
17
|
+
...pkg.dependencies,
|
|
18
|
+
...pkg.devDependencies
|
|
19
|
+
};
|
|
20
|
+
const detected = [];
|
|
21
|
+
for (const [framework, packages] of Object.entries(FRAMEWORK_DETECTORS)) {
|
|
22
|
+
if (packages.some((p) => (p in allDeps))) {
|
|
23
|
+
detected.push(framework);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (detected.length === 0) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return ["--frameworks", ...detected];
|
|
30
|
+
}
|
|
31
|
+
function buildUltraciteCommand(options) {
|
|
32
|
+
const cmd = ["ultracite", "init", "--linter", "biome", "--pm", "bun", "--quiet"];
|
|
33
|
+
if (options.frameworks && options.frameworks.length > 0) {
|
|
34
|
+
cmd.push("--frameworks", ...options.frameworks);
|
|
35
|
+
}
|
|
36
|
+
return cmd;
|
|
37
|
+
}
|
|
38
|
+
async function runInit(cwd = process.cwd()) {
|
|
39
|
+
const pkgPath = `${cwd}/package.json`;
|
|
40
|
+
const pkgFile = Bun.file(pkgPath);
|
|
41
|
+
if (!await pkgFile.exists()) {
|
|
42
|
+
console.error("No package.json found in current directory");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const pkg = await pkgFile.json();
|
|
46
|
+
const frameworkFlags = detectFrameworks(pkg);
|
|
47
|
+
const frameworks = frameworkFlags.length > 0 ? frameworkFlags.slice(1) : [];
|
|
48
|
+
const cmd = buildUltraciteCommand({ frameworks });
|
|
49
|
+
console.log(`Running: bun x ${cmd.join(" ")}`);
|
|
50
|
+
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
51
|
+
cwd,
|
|
52
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
53
|
+
});
|
|
54
|
+
const exitCode = await proc.exited;
|
|
55
|
+
process.exit(exitCode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { detectFrameworks, buildUltraciteCommand, runInit };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/tooling/src/cli/check.ts
|
|
3
|
+
function buildCheckCommand(options) {
|
|
4
|
+
const cmd = ["ultracite", "check"];
|
|
5
|
+
if (options.paths && options.paths.length > 0) {
|
|
6
|
+
cmd.push(...options.paths);
|
|
7
|
+
}
|
|
8
|
+
return cmd;
|
|
9
|
+
}
|
|
10
|
+
async function runCheck(paths = []) {
|
|
11
|
+
const cmd = buildCheckCommand({ paths });
|
|
12
|
+
console.log(`Running: bun x ${cmd.join(" ")}`);
|
|
13
|
+
const proc = Bun.spawn(["bun", "x", ...cmd], {
|
|
14
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
15
|
+
});
|
|
16
|
+
const exitCode = await proc.exited;
|
|
17
|
+
process.exit(exitCode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { buildCheckCommand, runCheck };
|
package/lefthook.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Lefthook configuration preset for Outfitter projects
|
|
2
|
+
# https://github.com/evilmartians/lefthook
|
|
3
|
+
#
|
|
4
|
+
# Usage: In your project's .lefthook.yml, add:
|
|
5
|
+
# extends:
|
|
6
|
+
# - node_modules/@outfitter/tooling/lefthook.yml
|
|
7
|
+
|
|
8
|
+
pre-commit:
|
|
9
|
+
parallel: true
|
|
10
|
+
commands:
|
|
11
|
+
ultracite:
|
|
12
|
+
glob: "*.{js,jsx,ts,tsx,json,jsonc,css}"
|
|
13
|
+
run: bun x ultracite fix {staged_files}
|
|
14
|
+
stage_fixed: true
|
|
15
|
+
|
|
16
|
+
typecheck:
|
|
17
|
+
glob: "*.{ts,tsx}"
|
|
18
|
+
run: bun run typecheck
|
|
19
|
+
|
|
20
|
+
pre-push:
|
|
21
|
+
parallel: false
|
|
22
|
+
commands:
|
|
23
|
+
build:
|
|
24
|
+
run: bun run build
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
# TDD-aware: skips tests on RED phase branches (*-tests, */tests, *_tests)
|
|
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
|
|
30
|
+
run: bunx @outfitter/tooling pre-push
|