commit f04afd6cca7fc4c51b27d65ab043683d82ffd0d9 Author: Errilaz Date: Wed Apr 29 00:55:53 2026 +0200 feat(regime) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..41754b8 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# REGIME + +> Tooling and unified configuration for managing a bunch of repositories and packages. + +## TODO + +- `check` action + - `check` bun script + - `test` bun script +- `release` action + - build with bun? + - run semantic release diff --git a/actions/checks/action.yml b/actions/checks/action.yml new file mode 100644 index 0000000..b898b6c --- /dev/null +++ b/actions/checks/action.yml @@ -0,0 +1,25 @@ +name: Checks +description: Run lint, type-check, and tests + +runs: + using: composite + steps: + - name: Setup Bun + shell: bash + run: curl -fsSL https://bun.sh/install | bash && echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + + - name: Install dependencies + shell: bash + run: bun install --frozen-lockfile + + - name: Lint + shell: bash + run: bun run lint + + - name: Type check + shell: bash + run: bun run check + + - name: Test + shell: bash + run: bun run test diff --git a/actions/mirror/action.yml b/actions/mirror/action.yml new file mode 100644 index 0000000..12eedba --- /dev/null +++ b/actions/mirror/action.yml @@ -0,0 +1,55 @@ +name: Mirror to GitHub +description: Mirror the current repo to GitHub, optionally filtering paths listed in .mirrorignore + +inputs: + target: + description: "GitHub repo (e.g. owner/repo)" + required: true + source: + description: "Authenticated clone URL for the source repo" + required: true + token: + description: "GitHub personal access token with push access" + required: true + +runs: + using: composite + steps: + - name: Install git-filter-repo + shell: bash + run: pip install --break-system-packages git-filter-repo + + - name: Clone mirror + shell: bash + run: git clone --bare "$GITHUB_WORKSPACE" /tmp/mirror-repo + + - name: Filter ignored paths + shell: bash + run: | + MIRRORIGNORE="$GITHUB_WORKSPACE/.mirrorignore" + if [ ! -f "$MIRRORIGNORE" ]; then + echo "No .mirrorignore found, skipping filter" + exit 0 + fi + + ARGS="" + while IFS= read -r line || [ -n "$line" ]; do + line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [ -z "$line" ] && continue + [[ "$line" == \#* ]] && continue + ARGS="$ARGS --path $line" + done < "$MIRRORIGNORE" + + # Always filter .mirrorignore itself + ARGS="$ARGS --path .mirrorignore" + + if [ -n "$ARGS" ]; then + cd /tmp/mirror-repo + git filter-repo $ARGS --invert-paths --force + fi + + - name: Push mirror + shell: bash + run: | + cd /tmp/mirror-repo + git push --mirror "https://${{ inputs.token }}@github.com/${{ inputs.target }}.git" diff --git a/actions/release/action.yml b/actions/release/action.yml new file mode 100644 index 0000000..ba2b717 --- /dev/null +++ b/actions/release/action.yml @@ -0,0 +1,32 @@ +name: Semantic Release +description: Run multi-semantic-release for per-package versioning + +inputs: + gitea-token: + description: "Forgejo API token with push + API access" + required: true + gitea-url: + description: "Forgejo instance URL" + required: true + npm-token: + description: "npm registry auth token" + required: true + +runs: + using: composite + steps: + - name: Setup Bun + shell: bash + run: curl -fsSL https://bun.sh/install | bash && echo "$HOME/.bun/bin" >> "$GITHUB_PATH" + + - name: Install dependencies + shell: bash + run: bun install --frozen-lockfile + + - name: Run multi-semantic-release + shell: bash + env: + GITEA_TOKEN: ${{ inputs.gitea-token }} + GITEA_URL: ${{ inputs.gitea-url }} + NPM_TOKEN: ${{ inputs.npm-token }} + run: bunx multi-semantic-release diff --git a/bin/regime b/bin/regime new file mode 100755 index 0000000..794f23c --- /dev/null +++ b/bin/regime @@ -0,0 +1,25 @@ +#!/usr/bin/env bun +import { resolve } from "path" +import { check } from "../src/check" +import { sync } from "../src/sync" +import { promote } from "../src/promote" + +const [command, ...rawArgs] = process.argv.slice(2) +const hasYes = rawArgs.includes("--yes") +const args = rawArgs.filter(a => a !== "--yes") +const targetDir = resolve(args[0] ?? process.cwd()) + +switch (command) { + case "check": + await check(targetDir) + break + case "sync": + await sync(targetDir) + break + case "promote": + await promote(targetDir, hasYes) + break + default: + console.error("Usage: regime [path] [--yes]") + process.exit(1) +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..83ea551 --- /dev/null +++ b/bun.lock @@ -0,0 +1,20 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "devDependencies": { + "@types/bun": "^1.3.13", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + + "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e489ae9 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@types/bun": "^1.3.13" + } +} \ No newline at end of file diff --git a/src/check.ts b/src/check.ts new file mode 100644 index 0000000..82de3ee --- /dev/null +++ b/src/check.ts @@ -0,0 +1,90 @@ +import { dirname, relative, join } from "node:path" +import { existsSync } from "node:fs" +import { + type RegimeConfig, + findRegimeConfigs, + resolveTemplateChain, + getStrategy, + interpolate, + readFileSync, + diffJson, + mergeTemplateJsonFiles, +} from "./shared" + +const red = Bun.color("red", "ansi") +const orange = Bun.color("orange", "ansi") +const green = Bun.color("green", "ansi") +const purple = Bun.color("purple", "ansi") +const reset = "\x1b[0m" + +export async function check(targetDir: string): Promise { + const rcFiles = await findRegimeConfigs(targetDir) + + if (rcFiles.length === 0) { + console.log("No regime.config.json files found.") + return + } + + for (const rcFile of rcFiles) { + const rcDir = dirname(rcFile) + const relDir = relative(targetDir, rcDir) || "." + console.log(`\n${purple}${relDir}/${reset}`) + + const rc: RegimeConfig = JSON.parse(readFileSync(rcFile)) + const templateNames = Array.isArray(rc.templates) ? rc.templates : [rc.templates] + const vars = rc.vars ?? {} + + const { files, patterns } = resolveTemplateChain(templateNames) + + if (files.size === 0) { + console.log(" (no template files)") + continue + } + + let synced = true + + for (const [relPath, templatePaths] of files) { + const targetPath = join(rcDir, relPath) + const strategy = getStrategy(relPath, patterns) + + if (!existsSync(targetPath)) { + console.log(` ${relPath}: ${red}missing${reset}`) + synced = false + continue + } + + const existingContent = readFileSync(targetPath) + + if (strategy === "merge json") { + try { + const templateObj = mergeTemplateJsonFiles(templatePaths, vars, relPath) + const existingObj = JSON.parse(existingContent) + const diffs = diffJson(templateObj, existingObj) + + if (diffs.length > 0) { + synced = false + console.log(` ${relPath}:`) + for (const d of diffs) { + const exp = JSON.stringify(d.expected) + const act = d.actual === undefined ? `${red}missing${reset}` : `${orange}${JSON.stringify(d.actual)}${reset}` + console.log(` ${d.field}: ${act} -> ${green}${exp}${reset}`) + } + } + } catch (e) { + console.log(` ${relPath}: ${red}failed to parse JSON${reset} - ${e}`) + synced = false + } + } else if (strategy === "overwrite") { + const templateContent = interpolate(readFileSync(templatePaths[templatePaths.length - 1]), vars, relPath) + if (existingContent !== templateContent) { + console.log(` ${relPath}: ${orange}differs${reset}`) + synced = false + } + } + } + + if (synced) { + console.log(` ${green}in sync${reset}`) + } + } +} diff --git a/src/promote.ts b/src/promote.ts new file mode 100644 index 0000000..02e0c04 --- /dev/null +++ b/src/promote.ts @@ -0,0 +1,163 @@ +import { dirname, join } from "node:path" +import { existsSync } from "node:fs" +import { writeFile } from "node:fs/promises" +import { $ } from "bun" +import { + type RegimeConfig, + findRegimeConfigs, + resolveTemplateChain, + getStrategy, + interpolate, + deinterpolate, + readFileSync, + templatesDir, +} from "./shared" + +const green = Bun.color("green", "ansi") +const orange = Bun.color("orange", "ansi") +const purple = Bun.color("purple", "ansi") +const reset = "\x1b[0m" + +async function gumStdin(command: string, input: string): Promise { + const proc = Bun.spawn(["gum", command], { + stdin: Buffer.from(input), + stdout: "pipe", + stderr: "inherit", + }) + const output = await new Response(proc.stdout).text() + await proc.exited + return output +} + +interface PromotableFile { + relPath: string // relative path within template/repo (e.g. "tsconfig.json") + repoPath: string // absolute path in the repo + configDir: string // directory containing regime.config.json + vars: Record + templateNames: string[] // all templates in the inheritance tree +} + +export async function promote(targetDir: string, yes = false): Promise { + // Step 1: Find regime configs + const rcFiles = await findRegimeConfigs(targetDir) + if (rcFiles.length === 0) { + console.error("No regime.config.json files found.") + process.exit(1) + } + + // Step 2: Collect promotable files (overwrite-strategy files that differ) + const promotable: PromotableFile[] = [] + + for (const rcFile of rcFiles) { + const rcDir = dirname(rcFile) + const rc: RegimeConfig = JSON.parse(readFileSync(rcFile)) + const templateNames = Array.isArray(rc.templates) ? rc.templates : [rc.templates] + const vars = rc.vars ?? {} + + const { files, patterns, templateNames: chainNames } = resolveTemplateChain(templateNames) + + for (const [relPath, templatePaths] of files) { + const strategy = getStrategy(relPath, patterns) + if (strategy !== "overwrite") continue + + const repoPath = join(rcDir, relPath) + if (!existsSync(repoPath)) continue + + const repoContent = readFileSync(repoPath) + const templateContent = interpolate( + readFileSync(templatePaths[templatePaths.length - 1]), + vars, + relPath, + ) + + if (repoContent !== templateContent) { + promotable.push({ + relPath, + repoPath, + configDir: rcDir, + vars, + templateNames: chainNames, + }) + } + } + } + + if (promotable.length === 0) { + console.log("Nothing to promote.") + return + } + + // Step 3: Interactive file selection via gum filter + const fileList = promotable.map(p => p.relPath).join("\n") + const selectedFile = (await gumStdin("filter", fileList)).trim() + + if (!selectedFile) { + console.log("No file selected.") + return + } + + const entry = promotable.find(p => p.relPath === selectedFile) + if (!entry) { + console.error(`File "${selectedFile}" not found in promotable list.`) + process.exit(1) + } + + // Step 4: Interactive template selection via gum choose + let selectedTemplate: string + if (entry.templateNames.length === 1) { + selectedTemplate = entry.templateNames[0] + } else { + const templateList = entry.templateNames.join("\n") + selectedTemplate = (await gumStdin("choose", templateList)).trim() + } + + if (!selectedTemplate) { + console.log("No template selected.") + return + } + + // Step 5: De-interpolate + const repoContent = readFileSync(entry.repoPath) + const promoted = deinterpolate(repoContent, entry.vars) + + // Step 6: Show diff + const templateFilePath = join(templatesDir, selectedTemplate, entry.relPath) + + console.log(`\n${purple}Promoting${reset} ${entry.relPath} -> ${green}${selectedTemplate}${reset}`) + + if (existsSync(templateFilePath)) { + const tmpFile = `/tmp/regime-promote-${Date.now()}` + await writeFile(tmpFile, promoted) + try { + const diff = await $`diff -u ${templateFilePath} ${tmpFile} || true`.text() + if (diff.trim()) { + console.log(diff) + } else { + console.log("No changes detected after de-interpolation.") + return + } + } finally { + await $`rm -f ${tmpFile}`.quiet() + } + } else { + console.log(`${orange}New file${reset} — will be created in template "${selectedTemplate}"`) + console.log(promoted) + } + + // Step 7: Confirm + if (!yes) { + const confirmed = await $`gum confirm "Write to template?"`.nothrow() + if (confirmed.exitCode !== 0) { + console.log("Cancelled.") + return + } + } + + // Step 8: Write + const targetFileDir = dirname(templateFilePath) + if (!existsSync(targetFileDir)) { + await $`mkdir -p ${targetFileDir}`.quiet() + } + await writeFile(templateFilePath, promoted) + console.log(`${green}Written${reset} ${templateFilePath}`) +} diff --git a/src/shared.ts b/src/shared.ts new file mode 100644 index 0000000..1270d1e --- /dev/null +++ b/src/shared.ts @@ -0,0 +1,236 @@ +import { resolve, join, dirname } from "node:path" +import { existsSync } from "node:fs" +import { readdir } from "node:fs/promises" +import { Glob } from "bun" + +// --- Types --- + +export interface RegimeConfig { + templates: string | string[] + vars?: Record +} + +export interface TemplateConfig { + inherits?: string[] + patterns?: Record +} + +export interface CollectedTemplate { + files: Map // relative path -> absolute paths (in chain order) + patterns: Record + templateNames: string[] // ordered list of visited template names +} + +// --- Constants --- + +export const regimeDir = resolve(dirname(import.meta.dir)) +export const templatesDir = join(regimeDir, "templates") + +// --- Utilities --- + +export function readFileSync(path: string): string { + return require("fs").readFileSync(path, "utf-8") +} + +export function readdirSyncRecursive(dir: string, prefix = ""): string[] { + const fs = require("fs") + const results: string[] = [] + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name + if (entry.isDirectory()) { + results.push(...readdirSyncRecursive(join(dir, entry.name), rel)) + } else { + results.push(rel) + } + } + return results +} + +// --- Template resolution --- + +export function resolveTemplateConfig(name: string): TemplateConfig { + const configPath = join(templatesDir, name, ".regime-template.json") + if (!existsSync(configPath)) return {} + const raw = JSON.parse(readFileSync(configPath)) + return raw as TemplateConfig +} + +export function resolveTemplateChain(names: string[]): CollectedTemplate { + const visited = new Set() + const files = new Map() + const patterns: Record = {} + const templateNames: string[] = [] + + function walk(name: string) { + if (visited.has(name)) return + visited.add(name) + + const dir = join(templatesDir, name) + if (!existsSync(dir)) { + console.error(` warning: template "${name}" not found at ${dir}`) + return + } + + const config = resolveTemplateConfig(name) + + // Walk parents first so children override + if (config.inherits) { + for (const parent of config.inherits) { + walk(parent) + } + } + + templateNames.push(name) + + // Collect patterns + if (config.patterns) { + Object.assign(patterns, config.patterns) + } + + // Collect files (skip .regime-template.json) + const entries = readdirSyncRecursive(dir) + for (const entry of entries) { + if (entry === ".regime-template.json") continue + const existing = files.get(entry) ?? [] + existing.push(join(dir, entry)) + files.set(entry, existing) + } + } + + for (const name of names) { + walk(name) + } + + return { files, patterns, templateNames } +} + +// --- Strategy matching --- + +export function getStrategy(filePath: string, patterns: Record): string { + // Check exact match first + if (patterns[filePath]) return patterns[filePath] + + // Check glob patterns + for (const [pattern, strategy] of Object.entries(patterns)) { + if (pattern.includes("*")) { + const glob = new Glob(pattern) + if (glob.match(filePath)) return strategy + } + } + + return "overwrite" // default +} + +// --- Variable interpolation --- + +export function interpolate(content: string, vars: Record, context?: string): string { + return content.replace(/<<(\w+)>>/g, (_, key) => { + if (!(key in vars)) { + console.error(` warning: undeclared var "<<${key}>>"${context ? ` in ${context}` : ""}`) + } + return vars[key] ?? `<<${key}>>` + }) +} + +export function deinterpolate(content: string, vars: Record): string { + let result = content + for (const [key, value] of Object.entries(vars)) { + result = result.replaceAll(value, `<<${key}>>`) + } + return result +} + +// --- Deep merge (template values win; existing-only fields preserved) --- + +export function deepMerge(base: any, overlay: any): any { + if (typeof base !== "object" || base === null || Array.isArray(base)) return overlay + if (typeof overlay !== "object" || overlay === null || Array.isArray(overlay)) return overlay + + const result = { ...base } + for (const key of Object.keys(overlay)) { + if (key in result) { + result[key] = deepMerge(result[key], overlay[key]) + } else { + result[key] = overlay[key] + } + } + return result +} + +// --- Merge all template JSON files into one combined object --- + +export function mergeTemplateJsonFiles(paths: string[], vars: Record, relPath: string): any { + let merged: any = {} + for (const p of paths) { + const content = interpolate(readFileSync(p), vars, relPath) + merged = deepMerge(merged, JSON.parse(content)) + } + return merged +} + +// --- Indentation detection --- + +export function detectIndent(content: string): string { + const match = content.match(/^(\s+)/m) + return match?.[1] ?? " " +} + +// --- Diff reporting --- + +export function diffJson( + templateObj: any, + existingObj: any, + path: string[] = [], +): { field: string; expected: any; actual: any }[] { + const diffs: { field: string; expected: any; actual: any }[] = [] + + for (const key of Object.keys(templateObj)) { + const fieldPath = [...path, key].join(".") + const expected = templateObj[key] + const actual = existingObj?.[key] + + if (actual === undefined) { + diffs.push({ field: fieldPath, expected, actual: undefined }) + } else if ( + typeof expected === "object" && + expected !== null && + !Array.isArray(expected) && + typeof actual === "object" && + actual !== null && + !Array.isArray(actual) + ) { + diffs.push(...diffJson(expected, actual, [...path, key])) + } else if (JSON.stringify(expected) !== JSON.stringify(actual)) { + diffs.push({ field: fieldPath, expected, actual }) + } + } + + return diffs +} + +// --- Find all regime.config.json files in a repo --- + +export async function findRegimeConfigs(repoDir: string): Promise { + const results: string[] = [] + + async function walk(dir: string) { + let entries + try { + entries = await readdir(dir, { withFileTypes: true }) + } catch { + return + } + for (const entry of entries) { + if (entry.name === "node_modules" || entry.name === ".git") continue + const full = join(dir, entry.name) + if (entry.isDirectory()) { + await walk(full) + } else if (entry.name === "regime.config.json") { + results.push(full) + } + } + } + + await walk(repoDir) + return results +} diff --git a/src/sync.ts b/src/sync.ts new file mode 100644 index 0000000..05898b6 --- /dev/null +++ b/src/sync.ts @@ -0,0 +1,124 @@ +import { dirname, relative, join } from "node:path" +import { existsSync } from "node:fs" +import { writeFile } from "node:fs/promises" +import { + type RegimeConfig, + findRegimeConfigs, + resolveTemplateChain, + getStrategy, + interpolate, + readFileSync, + deepMerge, + mergeTemplateJsonFiles, + detectIndent, +} from "./shared" + +const green = Bun.color("green", "ansi") +const yellow = Bun.color("orange", "ansi") +const red = Bun.color("red", "ansi") +const purple = Bun.color("purple", "ansi") +const reset = "\x1b[0m" + +export async function sync(targetDir: string): Promise { + const rcFiles = await findRegimeConfigs(targetDir) + + if (rcFiles.length === 0) { + console.log("No regime.config.json files found.") + return + } + + for (const rcFile of rcFiles) { + const rcDir = dirname(rcFile) + const relDir = relative(targetDir, rcDir) || "." + console.log(`\n${purple}${relDir}/${reset}`) + + const rc: RegimeConfig = JSON.parse(readFileSync(rcFile)) + const templateNames = Array.isArray(rc.templates) ? rc.templates : [rc.templates] + const vars = rc.vars ?? {} + + const { files, patterns } = resolveTemplateChain(templateNames) + + if (files.size === 0) { + console.log(" (no template files)") + continue + } + + let allSynced = true + + for (const [relPath, templatePaths] of files) { + const targetPath = join(rcDir, relPath) + const strategy = getStrategy(relPath, patterns) + + // Check target directory exists + const targetFileDir = dirname(targetPath) + if (!existsSync(targetFileDir)) { + console.log(` ${relPath}: ${yellow}warning: directory does not exist, skipping${reset}`) + continue + } + + if (strategy === "merge json") { + let templateObj: any + try { + templateObj = mergeTemplateJsonFiles(templatePaths, vars, relPath) + } catch (e) { + console.log(` ${relPath}: ${red}failed to parse template JSON${reset} - ${e}`) + continue + } + + if (existsSync(targetPath)) { + const existingContent = readFileSync(targetPath) + let existingObj: any + try { + existingObj = JSON.parse(existingContent) + } catch (e) { + console.log(` ${relPath}: ${red}failed to parse existing JSON${reset} - ${e}`) + continue + } + + const merged = deepMerge(existingObj, templateObj) + const indent = detectIndent(existingContent) + const mergedContent = JSON.stringify(merged, null, indent) + "\n" + + if (mergedContent === existingContent) { + // already in sync + continue + } + + await writeFile(targetPath, mergedContent) + console.log(` ${relPath}: ${green}updated${reset}`) + allSynced = false + } else { + const content = JSON.stringify(templateObj, null, " ") + "\n" + await writeFile(targetPath, content) + console.log(` ${relPath}: ${green}created${reset}`) + allSynced = false + } + } else if (strategy === "overwrite") { + const templateContent = interpolate( + readFileSync(templatePaths[templatePaths.length - 1]), + vars, + relPath, + ) + + if (existsSync(targetPath)) { + const existingContent = readFileSync(targetPath) + if (existingContent === templateContent) { + // already in sync + continue + } + await writeFile(targetPath, templateContent) + console.log(` ${relPath}: ${green}updated${reset}`) + allSynced = false + } else { + await writeFile(targetPath, templateContent) + console.log(` ${relPath}: ${green}created${reset}`) + allSynced = false + } + } + } + + if (allSynced) { + console.log(` ${green}in sync${reset}`) + } + } +} diff --git a/templates/monorepo-package/.regime-template.json b/templates/monorepo-package/.regime-template.json new file mode 100644 index 0000000..28730d5 --- /dev/null +++ b/templates/monorepo-package/.regime-template.json @@ -0,0 +1,6 @@ +{ + "inherits": [ + "shared", + "package" + ] +} \ No newline at end of file diff --git a/templates/monorepo-root/.regime-template.json b/templates/monorepo-root/.regime-template.json new file mode 100644 index 0000000..a2badd9 --- /dev/null +++ b/templates/monorepo-root/.regime-template.json @@ -0,0 +1,6 @@ +{ + "inherits": [ + "shared", + "repo" + ] +} \ No newline at end of file diff --git a/templates/monorepo-root/package.json b/templates/monorepo-root/package.json new file mode 100644 index 0000000..f8330bd --- /dev/null +++ b/templates/monorepo-root/package.json @@ -0,0 +1,11 @@ +{ + "workspaces": { + "catalog": { + "@types/bun": "1.3.11" + } + }, + "scripts": { + "test": "bun run --workspaces --parallel --no-exit-on-error test", + "check": "bun run --workspaces --parallel --no-exit-on-error check" + } +} \ No newline at end of file diff --git a/templates/package/.regime-template.json b/templates/package/.regime-template.json new file mode 100644 index 0000000..24aa72f --- /dev/null +++ b/templates/package/.regime-template.json @@ -0,0 +1,5 @@ +{ + "inherits": [ + "tools-ts" + ] +} \ No newline at end of file diff --git a/templates/package/package.json b/templates/package/package.json new file mode 100644 index 0000000..da3025c --- /dev/null +++ b/templates/package/package.json @@ -0,0 +1,8 @@ +{ + "scripts": { + "test": "bun test --pass-with-no-tests" + }, + "files": [ + "src" + ] +} \ No newline at end of file diff --git a/templates/package/tsconfig.json b/templates/package/tsconfig.json new file mode 100644 index 0000000..a087da2 --- /dev/null +++ b/templates/package/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/templates/repo/.regime-template.json b/templates/repo/.regime-template.json new file mode 100644 index 0000000..fa4c69d --- /dev/null +++ b/templates/repo/.regime-template.json @@ -0,0 +1,9 @@ +{ + "inherits": [ + "tools-oxc", + "tools-mirror", + "tools-commitlint", + "tools-semantic-release", + "tools-checks" + ] +} \ No newline at end of file diff --git a/templates/repo/package.json b/templates/repo/package.json new file mode 100644 index 0000000..414e337 --- /dev/null +++ b/templates/repo/package.json @@ -0,0 +1,5 @@ +{ + "scripts": { + "lint": "oxlint" + } +} \ No newline at end of file diff --git a/templates/shared/.regime-template.json b/templates/shared/.regime-template.json new file mode 100644 index 0000000..d2e3429 --- /dev/null +++ b/templates/shared/.regime-template.json @@ -0,0 +1,6 @@ +{ + "patterns": { + "package.json": "merge json", + "tsconfig.json": "merge json" + } +} diff --git a/templates/shared/LICENSE b/templates/shared/LICENSE new file mode 100644 index 0000000..9d5730c --- /dev/null +++ b/templates/shared/LICENSE @@ -0,0 +1,7 @@ +Copyright © 2026 Sigitex + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/templates/shared/package.json b/templates/shared/package.json new file mode 100644 index 0000000..b7dd1f5 --- /dev/null +++ b/templates/shared/package.json @@ -0,0 +1,11 @@ +{ + "license": "MIT", + "author": { + "name": "Sigitex", + "url": "http://github.com/sigitex" + }, + "repository": { + "type": "git", + "url": "https://github.com/sigitex/<>.git" + } +} diff --git a/templates/solo-package/.regime-template.json b/templates/solo-package/.regime-template.json new file mode 100644 index 0000000..9ad0890 --- /dev/null +++ b/templates/solo-package/.regime-template.json @@ -0,0 +1,7 @@ +{ + "inherits": [ + "shared", + "repo", + "package" + ] +} \ No newline at end of file diff --git a/templates/tools-checks/.forgejo/workflows/checks.yml b/templates/tools-checks/.forgejo/workflows/checks.yml new file mode 100644 index 0000000..cdd66d8 --- /dev/null +++ b/templates/tools-checks/.forgejo/workflows/checks.yml @@ -0,0 +1,27 @@ +name: Checks + +on: + push: + branches: ["*"] + pull_request: + +jobs: + checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: https://${{ secrets.FORGE_TOKEN }}@code.quickbasic.org/sigitex/regime/actions/checks@main + + release: + runs-on: ubuntu-latest + needs: checks + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: https://${{ secrets.FORGE_TOKEN }}@code.quickbasic.org/sigitex/regime/actions/release@main + with: + gitea-token: ${{ secrets.FORGE_TOKEN }} + gitea-url: https://code.quickbasic.org + npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/templates/tools-commitlint/commitlint.config.ts b/templates/tools-commitlint/commitlint.config.ts new file mode 100644 index 0000000..38b3e5b --- /dev/null +++ b/templates/tools-commitlint/commitlint.config.ts @@ -0,0 +1,3 @@ +export default { + extends: ["@commitlint/config-conventional"] +} diff --git a/templates/tools-commitlint/package.json b/templates/tools-commitlint/package.json new file mode 100644 index 0000000..fc4cc54 --- /dev/null +++ b/templates/tools-commitlint/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "@commitlint/cli": "^20.5.3", + "@commitlint/config-conventional": "^20.5.3", + "husky": "^9.1.7" + } +} \ No newline at end of file diff --git a/templates/tools-mirror/.forgejo/workflows/mirror.yml b/templates/tools-mirror/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..5e1aeac --- /dev/null +++ b/templates/tools-mirror/.forgejo/workflows/mirror.yml @@ -0,0 +1,16 @@ +name: Mirror to GitHub + +on: + push: + branches: + - main + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: https://${{ secrets.FORGE_TOKEN }}@code.quickbasic.org/sigitex/regime/actions/mirror@main + with: + target: sigitex/<> + token: ${{ secrets.MIRROR_TOKEN }} diff --git a/templates/tools-mirror/.mirrorignore b/templates/tools-mirror/.mirrorignore new file mode 100644 index 0000000..2731680 --- /dev/null +++ b/templates/tools-mirror/.mirrorignore @@ -0,0 +1,3 @@ +AGENTS.md +.forgejo +openspec diff --git a/templates/tools-oxc/oxfmt.config.ts b/templates/tools-oxc/oxfmt.config.ts new file mode 100644 index 0000000..17cbf10 --- /dev/null +++ b/templates/tools-oxc/oxfmt.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "oxfmt" + +export default defineConfig({ + useTabs: false, + tabWidth: 2, + printWidth: 80, + singleQuote: false, + jsxSingleQuote: false, + quoteProps: "as-needed", + trailingComma: "all", + semi: false, + arrowParens: "always", + bracketSameLine: false, + bracketSpacing: true, + ignorePatterns: ["**/*.gen.ts"], +}); diff --git a/templates/tools-oxc/oxlint.config.ts b/templates/tools-oxc/oxlint.config.ts new file mode 100644 index 0000000..882bb32 --- /dev/null +++ b/templates/tools-oxc/oxlint.config.ts @@ -0,0 +1,47 @@ +import { defineConfig } from "oxlint" + +export default defineConfig({ + plugins: ["typescript", "unicorn", "oxc"], + categories: { + correctness: "error", + suspicious: "warn", + perf: "warn", + style: "warn", + restriction: "error", + }, + rules: { + "no-shadow-restricted-names": "off", + "prefer-template": "off", + "typescript/no-non-null-assertion": "off", + "typescript/no-empty-interface": "off", + "prefer-ternary": "off", + "sort-keys": "off", + "typescript/consistent-type-definitions": ["error", "type"], + "typescript/explicit-function-return-type": "off", + "typescript/explicit-member-accessibility": "off", + "typescript/explicit-module-boundary-types": "off", + "no-use-before-define": "off", + "no-dynamic-delete": "off", + "id-length": "off", + "new-cap": "off", + "no-shadow": "off", + "no-ternary": "off", + "func-style": ["error", "declaration"], + "typescript/prefer-function-type": "off", + "typescript/consistent-indexed-object-style": "off", + "no-undefined": "off", + "filename-case": "off", + "no-magic-numbers": "off", + "max-statements": "off", + "no-plusplus": "off", + "no-array-for-each": "off", + "sort-imports": "off", + "no-optional-chaining": "off", + "default-case": "off", + "prefer-for-of": "off", + "switch-case-braces": "off", + "require-module-specifiers": "off", + "typescript/no-namespace": "off", + }, + ignorePatterns: ["**/*.gen.ts"], +}) diff --git a/templates/tools-oxc/package.json b/templates/tools-oxc/package.json new file mode 100644 index 0000000..ee325ce --- /dev/null +++ b/templates/tools-oxc/package.json @@ -0,0 +1,9 @@ +{ + "devDependencies": { + "oxfmt": "^0.47.0", + "oxlint": "^1.62.0" + }, + "scripts": { + "prepare": "husky" + } +} \ No newline at end of file diff --git a/templates/tools-semantic-release/package.json b/templates/tools-semantic-release/package.json new file mode 100644 index 0000000..b107c82 --- /dev/null +++ b/templates/tools-semantic-release/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "semantic-release": "^25.0.3", + "multi-semantic-release": "^3.1.0", + "@markwylde/semantic-release-gitea": "^2.2.0", + "@semantic-release/npm": "^13.1.5" + } +} \ No newline at end of file diff --git a/templates/tools-semantic-release/release.config.cjs b/templates/tools-semantic-release/release.config.cjs new file mode 100644 index 0000000..f59a98e --- /dev/null +++ b/templates/tools-semantic-release/release.config.cjs @@ -0,0 +1,13 @@ +/** + * @type {import('semantic-release').GlobalConfig} + */ +module.exports = { + repositoryUrl: "https://code.quickbasic.org/sigitex/<>.git", + branches: ["main"], + plugins: [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + ["@semantic-release/npm", { npmPublish: false }], + "@markwylde/semantic-release-gitea", + ], +}; diff --git a/templates/tools-ts/package.json b/templates/tools-ts/package.json new file mode 100644 index 0000000..4cc7c82 --- /dev/null +++ b/templates/tools-ts/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "@typescript/native-preview": "beta" + }, + "scripts": { + "check": "tsgo --noEmit" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e7e21ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "composite": true, + "module": "esnext", + "target": "esnext", + "lib": ["esnext"], + "types": ["bun"], + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "strict": true, + "outDir": "lib" + }, + "include": [ + "src/**/*", + "./bin/regime" + ] +}