[READ-ONLY] Mirror of https://github.com/bombshell-dev/tools. Internal CLI to standardize tooling across all Bombshell projects
0

Configure Feed

Select the types of activity you want to include in your feed.

wip: add sync command

Nate Moore (Mar 17, 2026, 12:15 AM EDT) 80f2eacb fa54e9fe

+175 -1
+2 -1
src/bin.ts
··· 5 5 import { format } from "./commands/format.ts"; 6 6 import { init } from "./commands/init.ts"; 7 7 import { lint } from "./commands/lint.ts"; 8 + import { sync } from "./commands/sync.ts"; 8 9 import { test } from "./commands/test.ts"; 9 10 10 - const commands = { build, dev, format, init, lint, test }; 11 + const commands = { build, dev, format, init, lint, sync, test }; 11 12 12 13 async function main() { 13 14 const [command, ...args] = argv.slice(2);
+173
src/commands/sync.ts
··· 1 + import { readFile, writeFile, mkdir } from "node:fs/promises"; 2 + import { cwd } from "node:process"; 3 + import { pathToFileURL } from "node:url"; 4 + import type { CommandContext } from "../context.ts"; 5 + 6 + const TEMPLATE_REPO = "bombshell-dev/template"; 7 + const TEMPLATE_REF = "main"; 8 + 9 + /** Files to sync from the template repo */ 10 + const SYNCED_FILES = [ 11 + ".github/workflows/format.yml", 12 + ".github/workflows/preview.yml", 13 + ".github/workflows/publish.yml", 14 + ".gitignore", 15 + "tsconfig.json", 16 + ] as const; 17 + 18 + /** Standard scripts that all bombshell repos should have */ 19 + const SYNCED_SCRIPTS: Record<string, string> = { 20 + dev: "bsh dev", 21 + build: "bsh build", 22 + format: "bsh format", 23 + lint: "bsh lint", 24 + test: "bsh test", 25 + }; 26 + 27 + /** Standard devEngines config */ 28 + const SYNCED_ENGINES = { 29 + runtime: { 30 + name: "node", 31 + version: "22.14.0", 32 + onFail: "error", 33 + }, 34 + packageManager: { 35 + name: "pnpm", 36 + version: "10.7.0", 37 + onFail: "error", 38 + }, 39 + }; 40 + 41 + export async function sync(_ctx: CommandContext) { 42 + const root = pathToFileURL(`${cwd()}/`); 43 + const results: SyncResult[] = []; 44 + 45 + results.push(...(await syncFiles(root))); 46 + results.push(...(await syncPackageJson(root))); 47 + 48 + printResults(results); 49 + } 50 + 51 + interface SyncResult { 52 + file: string; 53 + action: "updated" | "created" | "skipped"; 54 + reason?: string; 55 + } 56 + 57 + /** Fetch a file from the template repo via GitHub API */ 58 + async function fetchTemplateFile(path: string): Promise<string> { 59 + const url = `https://raw.githubusercontent.com/${TEMPLATE_REPO}/${TEMPLATE_REF}/${path}`; 60 + const res = await fetch(url); 61 + if (!res.ok) { 62 + throw new Error(`Failed to fetch ${path}: ${res.status} ${res.statusText}`); 63 + } 64 + return res.text(); 65 + } 66 + 67 + /** Sync files from the template repo */ 68 + async function syncFiles(root: URL): Promise<SyncResult[]> { 69 + const results: SyncResult[] = []; 70 + 71 + for (const file of SYNCED_FILES) { 72 + const dest = new URL(file, root); 73 + const destPath = new URL("./", dest); 74 + 75 + let remote: string; 76 + try { 77 + remote = await fetchTemplateFile(file); 78 + } catch { 79 + results.push({ file, action: "skipped", reason: "not found in template" }); 80 + continue; 81 + } 82 + 83 + let local: string | undefined; 84 + try { 85 + local = await readFile(dest, { encoding: "utf8" }); 86 + } catch { 87 + // file doesn't exist locally yet 88 + } 89 + 90 + if (local === remote) { 91 + results.push({ file, action: "skipped", reason: "already up to date" }); 92 + continue; 93 + } 94 + 95 + await mkdir(destPath, { recursive: true }); 96 + await writeFile(dest, remote, { encoding: "utf8" }); 97 + results.push({ file, action: local === undefined ? "created" : "updated" }); 98 + } 99 + 100 + return results; 101 + } 102 + 103 + /** Sync package.json scripts and devEngines */ 104 + async function syncPackageJson(root: URL): Promise<SyncResult[]> { 105 + const results: SyncResult[] = []; 106 + const pkgPath = new URL("package.json", root); 107 + 108 + let raw: string; 109 + try { 110 + raw = await readFile(pkgPath, { encoding: "utf8" }); 111 + } catch { 112 + results.push({ file: "package.json", action: "skipped", reason: "not found" }); 113 + return results; 114 + } 115 + 116 + const pkg = JSON.parse(raw); 117 + let changed = false; 118 + 119 + // Sync scripts 120 + if (!pkg.scripts) pkg.scripts = {}; 121 + for (const [name, value] of Object.entries(SYNCED_SCRIPTS)) { 122 + if (pkg.scripts[name] !== value) { 123 + pkg.scripts[name] = value; 124 + changed = true; 125 + } 126 + } 127 + 128 + // Sync devEngines 129 + if (JSON.stringify(pkg.devEngines) !== JSON.stringify(SYNCED_ENGINES)) { 130 + pkg.devEngines = SYNCED_ENGINES; 131 + changed = true; 132 + } 133 + 134 + // Remove volta if devEngines is now set 135 + if (pkg.volta) { 136 + delete pkg.volta; 137 + changed = true; 138 + } 139 + 140 + // Ensure @bomb.sh/tools is a devDependency 141 + if (!pkg.devDependencies) pkg.devDependencies = {}; 142 + if (!pkg.devDependencies["@bomb.sh/tools"]) { 143 + pkg.devDependencies["@bomb.sh/tools"] = "latest"; 144 + changed = true; 145 + } 146 + 147 + if (!changed) { 148 + results.push({ file: "package.json", action: "skipped", reason: "already up to date" }); 149 + return results; 150 + } 151 + 152 + await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, { encoding: "utf8" }); 153 + results.push({ file: "package.json", action: "updated" }); 154 + return results; 155 + } 156 + 157 + function printResults(results: SyncResult[]) { 158 + const pad = Math.max(...results.map((r) => r.file.length)); 159 + for (const result of results) { 160 + const label = result.file.padEnd(pad); 161 + switch (result.action) { 162 + case "created": 163 + console.log(` + ${label} \x1b[32mcreated\x1b[0m`); 164 + break; 165 + case "updated": 166 + console.log(` ~ ${label} \x1b[33mupdated\x1b[0m`); 167 + break; 168 + case "skipped": 169 + console.log(` · ${label} \x1b[2m${result.reason}\x1b[0m`); 170 + break; 171 + } 172 + } 173 + }