@bensandee/tooling 0.4.0 → 0.5.1
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/dist/bin.mjs +415 -94
- package/package.json +2 -2
package/dist/bin.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain } from "citty";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
4
5
|
import path from "node:path";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
7
|
import { parse } from "jsonc-parser";
|
|
7
8
|
import { z } from "zod";
|
|
8
|
-
import { execSync } from "node:child_process";
|
|
9
9
|
|
|
10
10
|
//#region src/types.ts
|
|
11
11
|
/** Default CI platform when not explicitly chosen. */
|
|
@@ -41,7 +41,7 @@ const TsconfigSchema = z.object({
|
|
|
41
41
|
include: z.array(z.string()).optional(),
|
|
42
42
|
exclude: z.array(z.string()).optional(),
|
|
43
43
|
files: z.array(z.string()).optional(),
|
|
44
|
-
references: z.array(z.object({ path: z.string() })).optional(),
|
|
44
|
+
references: z.array(z.object({ path: z.string() }).passthrough()).optional(),
|
|
45
45
|
compilerOptions: z.record(z.string(), z.unknown()).optional()
|
|
46
46
|
}).loose();
|
|
47
47
|
const RenovateSchema = z.object({
|
|
@@ -395,17 +395,35 @@ function writeFile(targetDir, relativePath, content) {
|
|
|
395
395
|
}
|
|
396
396
|
/**
|
|
397
397
|
* Create a GeneratorContext from a ProjectConfig and a conflict resolution handler.
|
|
398
|
+
* Returns the context and a list of files that were auto-archived before overwriting.
|
|
398
399
|
*/
|
|
399
400
|
function createContext(config, confirmOverwrite) {
|
|
401
|
+
const archivedFiles = [];
|
|
400
402
|
const pkgRaw = readFile(config.targetDir, "package.json");
|
|
401
403
|
return {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
404
|
+
ctx: {
|
|
405
|
+
config,
|
|
406
|
+
targetDir: config.targetDir,
|
|
407
|
+
packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
|
|
408
|
+
exists: (rel) => fileExists(config.targetDir, rel),
|
|
409
|
+
read: (rel) => readFile(config.targetDir, rel),
|
|
410
|
+
write: (rel, content) => {
|
|
411
|
+
if (!rel.startsWith(".tooling-archived/")) {
|
|
412
|
+
const existing = readFile(config.targetDir, rel);
|
|
413
|
+
if (existing !== void 0 && existing !== content) {
|
|
414
|
+
writeFile(config.targetDir, `.tooling-archived/${rel}`, existing);
|
|
415
|
+
archivedFiles.push(rel);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
writeFile(config.targetDir, rel, content);
|
|
419
|
+
},
|
|
420
|
+
remove: (rel) => {
|
|
421
|
+
const fullPath = path.join(config.targetDir, rel);
|
|
422
|
+
if (existsSync(fullPath)) rmSync(fullPath);
|
|
423
|
+
},
|
|
424
|
+
confirmOverwrite
|
|
425
|
+
},
|
|
426
|
+
archivedFiles
|
|
409
427
|
};
|
|
410
428
|
}
|
|
411
429
|
|
|
@@ -419,7 +437,7 @@ const STANDARD_SCRIPTS_SINGLE = {
|
|
|
419
437
|
lint: "oxlint",
|
|
420
438
|
knip: "knip",
|
|
421
439
|
check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip",
|
|
422
|
-
prepare: "
|
|
440
|
+
prepare: "lefthook install"
|
|
423
441
|
};
|
|
424
442
|
const STANDARD_SCRIPTS_MONOREPO = {
|
|
425
443
|
build: "pnpm -r build",
|
|
@@ -428,7 +446,7 @@ const STANDARD_SCRIPTS_MONOREPO = {
|
|
|
428
446
|
lint: "oxlint",
|
|
429
447
|
knip: "knip",
|
|
430
448
|
check: "pnpm typecheck && pnpm build && pnpm lint && pnpm knip",
|
|
431
|
-
prepare: "
|
|
449
|
+
prepare: "lefthook install"
|
|
432
450
|
};
|
|
433
451
|
/** DevDeps that belong in every project (single repo) or per-package (monorepo). */
|
|
434
452
|
const PER_PACKAGE_DEV_DEPS = {
|
|
@@ -439,9 +457,8 @@ const PER_PACKAGE_DEV_DEPS = {
|
|
|
439
457
|
};
|
|
440
458
|
/** DevDeps that belong at the root regardless of structure. */
|
|
441
459
|
const ROOT_DEV_DEPS = {
|
|
442
|
-
husky: "9.1.7",
|
|
443
460
|
knip: "5.85.0",
|
|
444
|
-
|
|
461
|
+
lefthook: "2.1.2",
|
|
445
462
|
oxlint: "1.50.0"
|
|
446
463
|
};
|
|
447
464
|
/**
|
|
@@ -590,6 +607,7 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
590
607
|
const created = results.filter((r) => r.action === "created");
|
|
591
608
|
const updated = results.filter((r) => r.action === "updated");
|
|
592
609
|
const skipped = results.filter((r) => r.action === "skipped");
|
|
610
|
+
const archived = results.filter((r) => r.action === "archived");
|
|
593
611
|
if (created.length > 0) {
|
|
594
612
|
sections.push("**Created:**");
|
|
595
613
|
for (const r of created) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
@@ -600,6 +618,11 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
600
618
|
for (const r of updated) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
601
619
|
sections.push("");
|
|
602
620
|
}
|
|
621
|
+
if (archived.length > 0) {
|
|
622
|
+
sections.push("**Archived:**");
|
|
623
|
+
for (const r of archived) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
624
|
+
sections.push("");
|
|
625
|
+
}
|
|
603
626
|
if (skipped.length > 0) {
|
|
604
627
|
sections.push("**Skipped (review these):**");
|
|
605
628
|
for (const r of skipped) sections.push(`- \`${r.filePath}\` — ${r.description}`);
|
|
@@ -626,6 +649,25 @@ function generateMigratePrompt(results, config, detected) {
|
|
|
626
649
|
}
|
|
627
650
|
sections.push("");
|
|
628
651
|
}
|
|
652
|
+
if (archived.length > 0) {
|
|
653
|
+
sections.push("### Review archived files");
|
|
654
|
+
sections.push("");
|
|
655
|
+
sections.push("The following files were modified or replaced. The originals have been saved to `.tooling-archived/`:");
|
|
656
|
+
sections.push("");
|
|
657
|
+
for (const r of archived) sections.push(`- \`${r.filePath}\` → \`.tooling-archived/${r.filePath}\``);
|
|
658
|
+
sections.push("");
|
|
659
|
+
sections.push("For each archived file, **diff the old version against the new one** and look for features, categories, or modules that were enabled in the original but are missing from the replacement. Focus on broad capability gaps rather than individual rule strictness (in general, being stricter is fine). Examples of what to look for:");
|
|
660
|
+
sections.push("");
|
|
661
|
+
sections.push("- **Lint configs**: enabled plugin categories (e.g. `jsx-a11y`, `import`, `react`, `nextjs`), custom `plugins` or `overrides`, file-scoped rule blocks");
|
|
662
|
+
sections.push("- **TypeScript configs**: compiler features like `jsx`, `paths`, `baseUrl`, or `references` that affect build behavior");
|
|
663
|
+
sections.push("- **Other configs**: feature flags, custom presets, or integrations that go beyond the default template");
|
|
664
|
+
sections.push("");
|
|
665
|
+
sections.push("If the old config had capabilities the new one lacks, port them into the new file. Then:");
|
|
666
|
+
sections.push("");
|
|
667
|
+
sections.push("1. If the project previously used `husky` and `lint-staged`, remove them from `devDependencies`");
|
|
668
|
+
sections.push("2. Delete the `.tooling-archived/` directory when migration is complete");
|
|
669
|
+
sections.push("");
|
|
670
|
+
}
|
|
629
671
|
const oxlintWasSkipped = results.find((r) => r.filePath === "oxlint.config.ts")?.action === "skipped";
|
|
630
672
|
if (detected.hasLegacyOxlintJson) {
|
|
631
673
|
sections.push("### Migrate .oxlintrc.json to oxlint.config.ts");
|
|
@@ -1032,7 +1074,8 @@ const STANDARD_ENTRIES = [
|
|
|
1032
1074
|
".env",
|
|
1033
1075
|
".env.*",
|
|
1034
1076
|
"!.env.example",
|
|
1035
|
-
".tooling-migrate.md"
|
|
1077
|
+
".tooling-migrate.md",
|
|
1078
|
+
".tooling-archived/"
|
|
1036
1079
|
];
|
|
1037
1080
|
/** Normalize a gitignore entry for comparison: strip leading `/` and trailing `/`. */
|
|
1038
1081
|
function normalizeEntry(entry) {
|
|
@@ -1306,6 +1349,8 @@ function buildSettings(ctx) {
|
|
|
1306
1349
|
`Bash(${pm} view *)`,
|
|
1307
1350
|
`Bash(${pm} list)`,
|
|
1308
1351
|
`Bash(${pm} list *)`,
|
|
1352
|
+
`Bash(${pm} ls)`,
|
|
1353
|
+
`Bash(${pm} ls *)`,
|
|
1309
1354
|
"Bash(npm view *)",
|
|
1310
1355
|
"Bash(npm info *)",
|
|
1311
1356
|
"Bash(npm show *)",
|
|
@@ -1345,6 +1390,8 @@ function buildSettings(ctx) {
|
|
|
1345
1390
|
"Bash(head *)",
|
|
1346
1391
|
"Bash(tail *)",
|
|
1347
1392
|
"Bash(wc *)",
|
|
1393
|
+
"Bash(test *)",
|
|
1394
|
+
"Bash([ *)",
|
|
1348
1395
|
"Bash(find *)",
|
|
1349
1396
|
"Bash(which *)",
|
|
1350
1397
|
"Bash(node -e *)",
|
|
@@ -1677,12 +1724,30 @@ async function generateReleaseCi(ctx) {
|
|
|
1677
1724
|
}
|
|
1678
1725
|
|
|
1679
1726
|
//#endregion
|
|
1680
|
-
//#region src/generators/
|
|
1727
|
+
//#region src/generators/lefthook.ts
|
|
1681
1728
|
function buildConfig(formatter) {
|
|
1682
|
-
return
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1729
|
+
return [
|
|
1730
|
+
"pre-commit:",
|
|
1731
|
+
" commands:",
|
|
1732
|
+
" lint:",
|
|
1733
|
+
" run: pnpm exec oxlint {staged_files}",
|
|
1734
|
+
" format:",
|
|
1735
|
+
` run: ${formatter === "prettier" ? "pnpm exec prettier --write {staged_files}" : "pnpm exec oxfmt --no-error-on-unmatched-pattern {staged_files}"}`,
|
|
1736
|
+
" stage_fixed: true",
|
|
1737
|
+
""
|
|
1738
|
+
].join("\n");
|
|
1739
|
+
}
|
|
1740
|
+
const ARCHIVE_DIR = ".tooling-archived";
|
|
1741
|
+
/** Common client-side git hooks that husky may have configured. */
|
|
1742
|
+
const HUSKY_HOOK_NAMES = [
|
|
1743
|
+
"pre-commit",
|
|
1744
|
+
"commit-msg",
|
|
1745
|
+
"pre-push",
|
|
1746
|
+
"post-merge",
|
|
1747
|
+
"post-checkout",
|
|
1748
|
+
"prepare-commit-msg"
|
|
1749
|
+
];
|
|
1750
|
+
/** All known lint-staged config file locations to archive. */
|
|
1686
1751
|
const LINT_STAGED_CONFIG_PATHS = [
|
|
1687
1752
|
"lint-staged.config.mjs",
|
|
1688
1753
|
"lint-staged.config.js",
|
|
@@ -1694,37 +1759,117 @@ const LINT_STAGED_CONFIG_PATHS = [
|
|
|
1694
1759
|
".lintstagedrc.mjs",
|
|
1695
1760
|
".lintstagedrc.cjs"
|
|
1696
1761
|
];
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1762
|
+
/** All known lefthook config file locations, in priority order. */
|
|
1763
|
+
const LEFTHOOK_CONFIG_PATHS = ["lefthook.yml", ".lefthook.yml"];
|
|
1764
|
+
/** Archive all husky hook files found in .husky/ */
|
|
1765
|
+
function archiveHuskyHooks(ctx, results) {
|
|
1766
|
+
let found = false;
|
|
1767
|
+
for (const hook of HUSKY_HOOK_NAMES) {
|
|
1768
|
+
const huskyPath = `.husky/${hook}`;
|
|
1769
|
+
const existing = ctx.read(huskyPath);
|
|
1770
|
+
if (existing !== void 0) {
|
|
1771
|
+
ctx.write(`${ARCHIVE_DIR}/${huskyPath}`, existing);
|
|
1772
|
+
ctx.remove(huskyPath);
|
|
1773
|
+
results.push({
|
|
1774
|
+
filePath: huskyPath,
|
|
1775
|
+
action: "archived",
|
|
1776
|
+
description: `Moved to ${ARCHIVE_DIR}/${huskyPath}`
|
|
1777
|
+
});
|
|
1778
|
+
found = true;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return found;
|
|
1782
|
+
}
|
|
1783
|
+
/** Archive all lint-staged config files. */
|
|
1784
|
+
function archiveLintStagedConfigs(ctx, results) {
|
|
1785
|
+
let found = false;
|
|
1786
|
+
for (const lsPath of LINT_STAGED_CONFIG_PATHS) {
|
|
1787
|
+
const existing = ctx.read(lsPath);
|
|
1788
|
+
if (existing !== void 0) {
|
|
1789
|
+
ctx.write(`${ARCHIVE_DIR}/${lsPath}`, existing);
|
|
1790
|
+
ctx.remove(lsPath);
|
|
1791
|
+
results.push({
|
|
1792
|
+
filePath: lsPath,
|
|
1793
|
+
action: "archived",
|
|
1794
|
+
description: `Moved to ${ARCHIVE_DIR}/${lsPath}`
|
|
1795
|
+
});
|
|
1796
|
+
found = true;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return found;
|
|
1800
|
+
}
|
|
1801
|
+
/** Remove husky/lint-staged from package.json devDependencies and fix prepare script. */
|
|
1802
|
+
function cleanPackageJson(ctx, results) {
|
|
1803
|
+
const raw = ctx.read("package.json");
|
|
1804
|
+
if (!raw) return;
|
|
1805
|
+
const pkg = parsePackageJson(raw);
|
|
1806
|
+
if (!pkg) return;
|
|
1807
|
+
const changes = [];
|
|
1808
|
+
if (pkg.devDependencies) {
|
|
1809
|
+
if ("husky" in pkg.devDependencies) {
|
|
1810
|
+
delete pkg.devDependencies["husky"];
|
|
1811
|
+
changes.push("removed devDependency: husky");
|
|
1812
|
+
}
|
|
1813
|
+
if ("lint-staged" in pkg.devDependencies) {
|
|
1814
|
+
delete pkg.devDependencies["lint-staged"];
|
|
1815
|
+
changes.push("removed devDependency: lint-staged");
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (pkg.scripts?.["prepare"] && /\bhusky\b/.test(pkg.scripts["prepare"])) {
|
|
1819
|
+
pkg.scripts["prepare"] = "lefthook install";
|
|
1820
|
+
changes.push("replaced prepare script: husky → lefthook install");
|
|
1821
|
+
}
|
|
1822
|
+
if (changes.length === 0) return;
|
|
1823
|
+
ctx.write("package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
1824
|
+
results.push({
|
|
1825
|
+
filePath: "package.json",
|
|
1826
|
+
action: "updated",
|
|
1827
|
+
description: changes.join(", ")
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
async function generateLefthook(ctx) {
|
|
1831
|
+
const filePath = "lefthook.yml";
|
|
1700
1832
|
const content = buildConfig(ctx.config.formatter);
|
|
1701
|
-
|
|
1702
|
-
|
|
1833
|
+
const results = [];
|
|
1834
|
+
archiveHuskyHooks(ctx, results);
|
|
1835
|
+
archiveLintStagedConfigs(ctx, results);
|
|
1836
|
+
cleanPackageJson(ctx, results);
|
|
1837
|
+
const existingPath = LEFTHOOK_CONFIG_PATHS.find((p) => ctx.exists(p));
|
|
1703
1838
|
if (existingPath === filePath) {
|
|
1704
1839
|
const existing = ctx.read(filePath);
|
|
1705
1840
|
if (existing) {
|
|
1706
|
-
if (existing === content)
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1841
|
+
if (existing === content) {
|
|
1842
|
+
results.push({
|
|
1843
|
+
filePath,
|
|
1844
|
+
action: "skipped",
|
|
1845
|
+
description: "Already configured"
|
|
1846
|
+
});
|
|
1847
|
+
return results;
|
|
1848
|
+
}
|
|
1849
|
+
if (await ctx.confirmOverwrite(filePath) === "skip") {
|
|
1850
|
+
results.push({
|
|
1851
|
+
filePath,
|
|
1852
|
+
action: "skipped",
|
|
1853
|
+
description: "Existing lefthook config preserved"
|
|
1854
|
+
});
|
|
1855
|
+
return results;
|
|
1856
|
+
}
|
|
1716
1857
|
}
|
|
1717
|
-
} else if (existingPath)
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1858
|
+
} else if (existingPath) {
|
|
1859
|
+
results.push({
|
|
1860
|
+
filePath: existingPath,
|
|
1861
|
+
action: "skipped",
|
|
1862
|
+
description: `Existing config found at ${existingPath}`
|
|
1863
|
+
});
|
|
1864
|
+
return results;
|
|
1865
|
+
}
|
|
1722
1866
|
ctx.write(filePath, content);
|
|
1723
|
-
|
|
1867
|
+
results.push({
|
|
1724
1868
|
filePath,
|
|
1725
1869
|
action: existingPath === filePath ? "updated" : "created",
|
|
1726
|
-
description: "Generated
|
|
1727
|
-
};
|
|
1870
|
+
description: "Generated lefthook pre-commit config"
|
|
1871
|
+
});
|
|
1872
|
+
return results;
|
|
1728
1873
|
}
|
|
1729
1874
|
|
|
1730
1875
|
//#endregion
|
|
@@ -1768,7 +1913,7 @@ const initCommand = defineCommand({
|
|
|
1768
1913
|
});
|
|
1769
1914
|
async function runInit(config, options = {}) {
|
|
1770
1915
|
const detected = detectProject(config.targetDir);
|
|
1771
|
-
const ctx = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
|
|
1916
|
+
const { ctx, archivedFiles } = createContext(config, options.confirmOverwrite ?? (async (relativePath) => {
|
|
1772
1917
|
const result = await p.select({
|
|
1773
1918
|
message: `${relativePath} already exists. What do you want to do?`,
|
|
1774
1919
|
options: [{
|
|
@@ -1791,7 +1936,7 @@ async function runInit(config, options = {}) {
|
|
|
1791
1936
|
results.push(await generateTsdown(ctx));
|
|
1792
1937
|
results.push(await generateOxlint(ctx));
|
|
1793
1938
|
results.push(await generateFormatter(ctx));
|
|
1794
|
-
results.push(await
|
|
1939
|
+
results.push(...await generateLefthook(ctx));
|
|
1795
1940
|
results.push(await generateGitignore(ctx));
|
|
1796
1941
|
results.push(await generateKnip(ctx));
|
|
1797
1942
|
results.push(await generateRenovate(ctx));
|
|
@@ -1802,13 +1947,27 @@ async function runInit(config, options = {}) {
|
|
|
1802
1947
|
results.push(await generateReleaseCi(ctx));
|
|
1803
1948
|
const vitestResults = await generateVitest(ctx);
|
|
1804
1949
|
results.push(...vitestResults);
|
|
1950
|
+
const alreadyArchived = new Set(results.filter((r) => r.action === "archived").map((r) => r.filePath));
|
|
1951
|
+
for (const rel of archivedFiles) if (!alreadyArchived.has(rel)) results.push({
|
|
1952
|
+
filePath: rel,
|
|
1953
|
+
action: "archived",
|
|
1954
|
+
description: `Original saved to .tooling-archived/${rel}`
|
|
1955
|
+
});
|
|
1805
1956
|
s.stop("Done!");
|
|
1957
|
+
if (results.some((r) => r.action === "archived" && r.filePath.startsWith(".husky/"))) try {
|
|
1958
|
+
execSync("git config --unset core.hooksPath", {
|
|
1959
|
+
cwd: config.targetDir,
|
|
1960
|
+
stdio: "ignore"
|
|
1961
|
+
});
|
|
1962
|
+
} catch (_error) {}
|
|
1806
1963
|
const created = results.filter((r) => r.action === "created");
|
|
1807
1964
|
const updated = results.filter((r) => r.action === "updated");
|
|
1808
1965
|
const skipped = results.filter((r) => r.action === "skipped");
|
|
1966
|
+
const archived = results.filter((r) => r.action === "archived");
|
|
1809
1967
|
const summaryLines = [];
|
|
1810
1968
|
if (created.length > 0) summaryLines.push(`Created: ${created.map((r) => r.filePath).join(", ")}`);
|
|
1811
1969
|
if (updated.length > 0) summaryLines.push(`Updated: ${updated.map((r) => r.filePath).join(", ")}`);
|
|
1970
|
+
if (archived.length > 0) summaryLines.push(`Archived: ${archived.map((r) => r.filePath).join(", ")}`);
|
|
1812
1971
|
if (skipped.length > 0) summaryLines.push(`Skipped: ${skipped.map((r) => r.filePath).join(", ")}`);
|
|
1813
1972
|
p.note(summaryLines.join("\n"), "Summary");
|
|
1814
1973
|
if (!options.noPrompt) {
|
|
@@ -1948,10 +2107,6 @@ function createRealExecutor() {
|
|
|
1948
2107
|
}
|
|
1949
2108
|
};
|
|
1950
2109
|
}
|
|
1951
|
-
/** Check whether there are pending changeset files. */
|
|
1952
|
-
function hasChangesets(executor, cwd) {
|
|
1953
|
-
return executor.listChangesetFiles(cwd).length > 0;
|
|
1954
|
-
}
|
|
1955
2110
|
/** Parse "New tag:" lines from changeset publish output. */
|
|
1956
2111
|
function parseNewTags(output) {
|
|
1957
2112
|
const tags = [];
|
|
@@ -2033,6 +2188,22 @@ async function updatePr(executor, conn, prNumber, options) {
|
|
|
2033
2188
|
});
|
|
2034
2189
|
if (!res.ok) throw new TransientError(`Failed to update PR #${String(prNumber)}: ${res.status} ${res.statusText}`);
|
|
2035
2190
|
}
|
|
2191
|
+
/** Merge a pull request by number. */
|
|
2192
|
+
async function mergePr(executor, conn, prNumber, options) {
|
|
2193
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/pulls/${String(prNumber)}/merge`;
|
|
2194
|
+
const res = await executor.fetch(url, {
|
|
2195
|
+
method: "POST",
|
|
2196
|
+
headers: {
|
|
2197
|
+
Authorization: `token ${conn.token}`,
|
|
2198
|
+
"Content-Type": "application/json"
|
|
2199
|
+
},
|
|
2200
|
+
body: JSON.stringify({
|
|
2201
|
+
Do: options?.method ?? "merge",
|
|
2202
|
+
delete_branch_after_merge: options?.deleteBranch ?? true
|
|
2203
|
+
})
|
|
2204
|
+
});
|
|
2205
|
+
if (!res.ok) throw new TransientError(`Failed to merge PR #${String(prNumber)}: ${res.status} ${res.statusText}`);
|
|
2206
|
+
}
|
|
2036
2207
|
/** Check whether a Forgejo release already exists for a given tag. */
|
|
2037
2208
|
async function findRelease(executor, conn, tag) {
|
|
2038
2209
|
const encodedTag = encodeURIComponent(tag);
|
|
@@ -2060,6 +2231,21 @@ async function createRelease(executor, conn, tag) {
|
|
|
2060
2231
|
if (!res.ok) throw new TransientError(`Failed to create release for ${tag}: ${res.status} ${res.statusText}`);
|
|
2061
2232
|
}
|
|
2062
2233
|
|
|
2234
|
+
//#endregion
|
|
2235
|
+
//#region src/release/log.ts
|
|
2236
|
+
/** Log a debug message when verbose mode is enabled. */
|
|
2237
|
+
function debug(config, message) {
|
|
2238
|
+
if (config.verbose) p.log.info(`[debug] ${message}`);
|
|
2239
|
+
}
|
|
2240
|
+
/** Log the result of an exec call when verbose mode is enabled. */
|
|
2241
|
+
function debugExec(config, label, result) {
|
|
2242
|
+
if (!config.verbose) return;
|
|
2243
|
+
const lines = [`[debug] ${label} (exit code ${String(result.exitCode)})`];
|
|
2244
|
+
if (result.stdout.trim()) lines.push(` stdout: ${result.stdout.trim()}`);
|
|
2245
|
+
if (result.stderr.trim()) lines.push(` stderr: ${result.stderr.trim()}`);
|
|
2246
|
+
p.log.info(lines.join("\n"));
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2063
2249
|
//#endregion
|
|
2064
2250
|
//#region src/release/version.ts
|
|
2065
2251
|
const BRANCH = "changeset-release/main";
|
|
@@ -2132,11 +2318,20 @@ function buildPrContent(executor, cwd, packagesBefore) {
|
|
|
2132
2318
|
async function runVersionMode(executor, config) {
|
|
2133
2319
|
p.log.info("Changesets detected — versioning packages");
|
|
2134
2320
|
const packagesBefore = executor.listWorkspacePackages(config.cwd);
|
|
2135
|
-
|
|
2136
|
-
executor.exec("pnpm
|
|
2321
|
+
debug(config, `Packages before versioning: ${packagesBefore.map((pkg) => `${pkg.name}@${pkg.version}`).join(", ") || "(none)"}`);
|
|
2322
|
+
const versionResult = executor.exec("pnpm changeset version", { cwd: config.cwd });
|
|
2323
|
+
debugExec(config, "pnpm changeset version", versionResult);
|
|
2324
|
+
if (versionResult.exitCode !== 0) throw new FatalError(`pnpm changeset version failed (exit code ${String(versionResult.exitCode)}):\n${versionResult.stderr}`);
|
|
2325
|
+
debugExec(config, "pnpm install --no-frozen-lockfile", executor.exec("pnpm install --no-frozen-lockfile", { cwd: config.cwd }));
|
|
2137
2326
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
2327
|
+
debug(config, `PR title: ${title}`);
|
|
2138
2328
|
executor.exec("git add -A", { cwd: config.cwd });
|
|
2139
|
-
|
|
2329
|
+
const remainingChangesets = executor.listChangesetFiles(config.cwd);
|
|
2330
|
+
if (remainingChangesets.length > 0) p.log.warn(`Changeset files still present after versioning: ${remainingChangesets.join(", ")}`);
|
|
2331
|
+
debug(config, `Changeset files after versioning: ${remainingChangesets.length > 0 ? remainingChangesets.join(", ") : "(none — all consumed)"}`);
|
|
2332
|
+
const commitResult = executor.exec("git commit -m \"chore: version packages\"", { cwd: config.cwd });
|
|
2333
|
+
debugExec(config, "git commit", commitResult);
|
|
2334
|
+
if (commitResult.exitCode !== 0) {
|
|
2140
2335
|
p.log.info("Nothing to commit after versioning");
|
|
2141
2336
|
return {
|
|
2142
2337
|
mode: "version",
|
|
@@ -2150,13 +2345,14 @@ async function runVersionMode(executor, config) {
|
|
|
2150
2345
|
pr: "none"
|
|
2151
2346
|
};
|
|
2152
2347
|
}
|
|
2153
|
-
executor.exec(`git push origin "HEAD:refs/heads/${BRANCH}" --force`, { cwd: config.cwd });
|
|
2348
|
+
debugExec(config, "git push", executor.exec(`git push origin "HEAD:refs/heads/${BRANCH}" --force`, { cwd: config.cwd }));
|
|
2154
2349
|
const conn = {
|
|
2155
2350
|
serverUrl: config.serverUrl,
|
|
2156
2351
|
repository: config.repository,
|
|
2157
2352
|
token: config.token
|
|
2158
2353
|
};
|
|
2159
2354
|
const existingPr = await findOpenPr(executor, conn, BRANCH);
|
|
2355
|
+
debug(config, `Existing open PR for ${BRANCH}: ${existingPr === null ? "(none)" : `#${String(existingPr)}`}`);
|
|
2160
2356
|
if (existingPr === null) {
|
|
2161
2357
|
await createPr(executor, conn, {
|
|
2162
2358
|
title,
|
|
@@ -2202,12 +2398,17 @@ async function retryAsync(fn) {
|
|
|
2202
2398
|
async function runPublishMode(executor, config) {
|
|
2203
2399
|
p.log.info("No changesets — publishing packages");
|
|
2204
2400
|
const publishResult = executor.exec("pnpm changeset publish", { cwd: config.cwd });
|
|
2401
|
+
debugExec(config, "pnpm changeset publish", publishResult);
|
|
2205
2402
|
if (publishResult.exitCode !== 0) throw new FatalError(`pnpm changeset publish failed (exit code ${String(publishResult.exitCode)}):\n${publishResult.stderr}`);
|
|
2206
2403
|
const stdoutTags = parseNewTags(publishResult.stdout + "\n" + publishResult.stderr);
|
|
2404
|
+
debug(config, `Tags from publish stdout: ${stdoutTags.length > 0 ? stdoutTags.join(", ") : "(none)"}`);
|
|
2207
2405
|
const expectedTags = computeExpectedTags(executor.listWorkspacePackages(config.cwd));
|
|
2406
|
+
debug(config, `Expected tags from workspace packages: ${expectedTags.length > 0 ? expectedTags.join(", ") : "(none)"}`);
|
|
2208
2407
|
const remoteTags = parseRemoteTags(executor.exec("git ls-remote --tags origin", { cwd: config.cwd }).stdout);
|
|
2408
|
+
debug(config, `Remote tags: ${remoteTags.length > 0 ? remoteTags.join(", ") : "(none)"}`);
|
|
2209
2409
|
const remoteSet = new Set(remoteTags);
|
|
2210
2410
|
const tagsToPush = reconcileTags(expectedTags, remoteTags, stdoutTags);
|
|
2411
|
+
debug(config, `Reconciled tags to push: ${tagsToPush.length > 0 ? tagsToPush.join(", ") : "(none)"}`);
|
|
2211
2412
|
if (config.dryRun) {
|
|
2212
2413
|
if (tagsToPush.length === 0) {
|
|
2213
2414
|
p.log.info("No packages were published");
|
|
@@ -2266,6 +2467,80 @@ async function runPublishMode(executor, config) {
|
|
|
2266
2467
|
};
|
|
2267
2468
|
}
|
|
2268
2469
|
|
|
2470
|
+
//#endregion
|
|
2471
|
+
//#region src/release/connection.ts
|
|
2472
|
+
const RepositorySchema = z.union([z.string(), z.object({ url: z.string() })]);
|
|
2473
|
+
/**
|
|
2474
|
+
* Resolve the hosting platform and connection details.
|
|
2475
|
+
*
|
|
2476
|
+
* Priority:
|
|
2477
|
+
* 1. Environment variables (FORGEJO_SERVER_URL, FORGEJO_REPOSITORY, FORGEJO_TOKEN)
|
|
2478
|
+
* 2. `repository` field in package.json (server URL and owner/repo parsed from the URL)
|
|
2479
|
+
*
|
|
2480
|
+
* For Forgejo, FORGEJO_TOKEN is always required (either from env or explicitly).
|
|
2481
|
+
* If the repository URL hostname is `github.com`, returns `{ type: "github" }`.
|
|
2482
|
+
*/
|
|
2483
|
+
function resolveConnection(cwd) {
|
|
2484
|
+
const serverUrl = process.env["FORGEJO_SERVER_URL"];
|
|
2485
|
+
const repository = process.env["FORGEJO_REPOSITORY"];
|
|
2486
|
+
const token = process.env["FORGEJO_TOKEN"];
|
|
2487
|
+
if (serverUrl && repository && token) return {
|
|
2488
|
+
type: "forgejo",
|
|
2489
|
+
conn: {
|
|
2490
|
+
serverUrl,
|
|
2491
|
+
repository,
|
|
2492
|
+
token
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
const parsed = parseRepositoryUrl(cwd);
|
|
2496
|
+
if (parsed === null) {
|
|
2497
|
+
if (serverUrl) {
|
|
2498
|
+
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
2499
|
+
if (!token) throw new FatalError("FORGEJO_TOKEN environment variable is required");
|
|
2500
|
+
}
|
|
2501
|
+
return { type: "github" };
|
|
2502
|
+
}
|
|
2503
|
+
if (parsed.hostname === "github.com") return { type: "github" };
|
|
2504
|
+
const resolvedToken = token;
|
|
2505
|
+
if (!resolvedToken) throw new FatalError("FORGEJO_TOKEN environment variable is required (server URL and repository were resolved from package.json)");
|
|
2506
|
+
return {
|
|
2507
|
+
type: "forgejo",
|
|
2508
|
+
conn: {
|
|
2509
|
+
serverUrl: serverUrl ?? `${parsed.protocol}//${parsed.hostname}`,
|
|
2510
|
+
repository: repository ?? parsed.repository,
|
|
2511
|
+
token: resolvedToken
|
|
2512
|
+
}
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function parseRepositoryUrl(cwd) {
|
|
2516
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
2517
|
+
let raw;
|
|
2518
|
+
try {
|
|
2519
|
+
raw = readFileSync(pkgPath, "utf-8");
|
|
2520
|
+
} catch {
|
|
2521
|
+
return null;
|
|
2522
|
+
}
|
|
2523
|
+
const pkg = z.object({ repository: RepositorySchema.optional() }).safeParse(JSON.parse(raw));
|
|
2524
|
+
if (!pkg.success) return null;
|
|
2525
|
+
const repo = pkg.data.repository;
|
|
2526
|
+
if (!repo) return null;
|
|
2527
|
+
return parseGitUrl(typeof repo === "string" ? repo : repo.url);
|
|
2528
|
+
}
|
|
2529
|
+
function parseGitUrl(urlStr) {
|
|
2530
|
+
try {
|
|
2531
|
+
const url = new URL(urlStr);
|
|
2532
|
+
const pathname = url.pathname.replace(/\.git$/, "").replace(/^\//, "");
|
|
2533
|
+
if (!pathname.includes("/")) return null;
|
|
2534
|
+
return {
|
|
2535
|
+
protocol: url.protocol,
|
|
2536
|
+
hostname: url.hostname,
|
|
2537
|
+
repository: pathname
|
|
2538
|
+
};
|
|
2539
|
+
} catch {
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2269
2544
|
//#endregion
|
|
2270
2545
|
//#region src/commands/release-changesets.ts
|
|
2271
2546
|
const releaseForgejoCommand = defineCommand({
|
|
@@ -2273,33 +2548,43 @@ const releaseForgejoCommand = defineCommand({
|
|
|
2273
2548
|
name: "release:changesets",
|
|
2274
2549
|
description: "Changesets version/publish for Forgejo CI"
|
|
2275
2550
|
},
|
|
2276
|
-
args: {
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2551
|
+
args: {
|
|
2552
|
+
"dry-run": {
|
|
2553
|
+
type: "boolean",
|
|
2554
|
+
description: "Skip push, API calls, and publishing side effects"
|
|
2555
|
+
},
|
|
2556
|
+
verbose: {
|
|
2557
|
+
type: "boolean",
|
|
2558
|
+
description: "Enable detailed debug logging (also enabled by RELEASE_DEBUG env var)"
|
|
2559
|
+
}
|
|
2560
|
+
},
|
|
2280
2561
|
async run({ args }) {
|
|
2281
|
-
if ((await runRelease(buildReleaseConfig({
|
|
2562
|
+
if ((await runRelease(buildReleaseConfig({
|
|
2563
|
+
dryRun: args["dry-run"] === true,
|
|
2564
|
+
verbose: args.verbose === true || process.env["RELEASE_DEBUG"] === "true"
|
|
2565
|
+
}), createRealExecutor())).mode === "none") process.exitCode = 0;
|
|
2282
2566
|
}
|
|
2283
2567
|
});
|
|
2284
|
-
/** Build release config from environment
|
|
2568
|
+
/** Build release config from environment / package.json and CLI flags. */
|
|
2285
2569
|
function buildReleaseConfig(flags) {
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2288
|
-
const token = process.env["FORGEJO_TOKEN"];
|
|
2289
|
-
if (!serverUrl) throw new FatalError("FORGEJO_SERVER_URL environment variable is required");
|
|
2290
|
-
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
2291
|
-
if (!token) throw new FatalError("FORGEJO_TOKEN environment variable is required");
|
|
2570
|
+
const resolved = resolveConnection(process.cwd());
|
|
2571
|
+
if (resolved.type !== "forgejo") throw new FatalError("release:changesets requires a Forgejo repository");
|
|
2292
2572
|
return {
|
|
2293
|
-
|
|
2294
|
-
repository,
|
|
2295
|
-
token,
|
|
2573
|
+
...resolved.conn,
|
|
2296
2574
|
cwd: process.cwd(),
|
|
2297
|
-
dryRun: flags.dryRun ?? false
|
|
2575
|
+
dryRun: flags.dryRun ?? false,
|
|
2576
|
+
verbose: flags.verbose ?? false
|
|
2298
2577
|
};
|
|
2299
2578
|
}
|
|
2300
2579
|
/** Core release logic — testable with a mock executor. */
|
|
2301
2580
|
async function runRelease(config, executor) {
|
|
2302
|
-
|
|
2581
|
+
const changesetFiles = executor.listChangesetFiles(config.cwd);
|
|
2582
|
+
debug(config, `Changeset files found: ${changesetFiles.length > 0 ? changesetFiles.join(", ") : "(none)"}`);
|
|
2583
|
+
if (changesetFiles.length > 0) {
|
|
2584
|
+
debug(config, "Entering version mode");
|
|
2585
|
+
return runVersionMode(executor, config);
|
|
2586
|
+
}
|
|
2587
|
+
debug(config, "Entering publish mode");
|
|
2303
2588
|
return runPublishMode(executor, config);
|
|
2304
2589
|
}
|
|
2305
2590
|
|
|
@@ -2317,21 +2602,17 @@ const releaseTriggerCommand = defineCommand({
|
|
|
2317
2602
|
} },
|
|
2318
2603
|
async run({ args }) {
|
|
2319
2604
|
const ref = args.ref ?? "main";
|
|
2320
|
-
const
|
|
2321
|
-
if (
|
|
2605
|
+
const resolved = resolveConnection(process.cwd());
|
|
2606
|
+
if (resolved.type === "forgejo") await triggerForgejo(resolved.conn, ref);
|
|
2322
2607
|
else triggerGitHub(ref);
|
|
2323
2608
|
}
|
|
2324
2609
|
});
|
|
2325
|
-
async function triggerForgejo(
|
|
2326
|
-
const
|
|
2327
|
-
const token = process.env["FORGEJO_TOKEN"];
|
|
2328
|
-
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
2329
|
-
if (!token) throw new FatalError("FORGEJO_TOKEN environment variable is required");
|
|
2330
|
-
const url = `${serverUrl}/api/v1/repos/${repository}/actions/workflows/release.yml/dispatches`;
|
|
2610
|
+
async function triggerForgejo(conn, ref) {
|
|
2611
|
+
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/actions/workflows/release.yml/dispatches`;
|
|
2331
2612
|
const res = await fetch(url, {
|
|
2332
2613
|
method: "POST",
|
|
2333
2614
|
headers: {
|
|
2334
|
-
Authorization: `token ${token}`,
|
|
2615
|
+
Authorization: `token ${conn.token}`,
|
|
2335
2616
|
"Content-Type": "application/json"
|
|
2336
2617
|
},
|
|
2337
2618
|
body: JSON.stringify({ ref })
|
|
@@ -2357,18 +2638,10 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
2357
2638
|
required: true
|
|
2358
2639
|
} },
|
|
2359
2640
|
async run({ args }) {
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
const token = process.env["FORGEJO_TOKEN"];
|
|
2363
|
-
if (!serverUrl) throw new FatalError("FORGEJO_SERVER_URL environment variable is required");
|
|
2364
|
-
if (!repository) throw new FatalError("FORGEJO_REPOSITORY environment variable is required");
|
|
2365
|
-
if (!token) throw new FatalError("FORGEJO_TOKEN environment variable is required");
|
|
2641
|
+
const resolved = resolveConnection(process.cwd());
|
|
2642
|
+
if (resolved.type !== "forgejo") throw new FatalError("release:create-forgejo-release requires a Forgejo repository");
|
|
2366
2643
|
const executor = createRealExecutor();
|
|
2367
|
-
const conn =
|
|
2368
|
-
serverUrl,
|
|
2369
|
-
repository,
|
|
2370
|
-
token
|
|
2371
|
-
};
|
|
2644
|
+
const conn = resolved.conn;
|
|
2372
2645
|
if (await findRelease(executor, conn, args.tag)) {
|
|
2373
2646
|
p.log.info(`Release for ${args.tag} already exists — skipping`);
|
|
2374
2647
|
return;
|
|
@@ -2378,12 +2651,57 @@ const createForgejoReleaseCommand = defineCommand({
|
|
|
2378
2651
|
}
|
|
2379
2652
|
});
|
|
2380
2653
|
|
|
2654
|
+
//#endregion
|
|
2655
|
+
//#region src/commands/release-merge.ts
|
|
2656
|
+
const HEAD_BRANCH = "changeset-release/main";
|
|
2657
|
+
const releaseMergeCommand = defineCommand({
|
|
2658
|
+
meta: {
|
|
2659
|
+
name: "release:merge",
|
|
2660
|
+
description: "Merge the open changesets version PR"
|
|
2661
|
+
},
|
|
2662
|
+
args: { "dry-run": {
|
|
2663
|
+
type: "boolean",
|
|
2664
|
+
description: "Show what would be merged without actually merging"
|
|
2665
|
+
} },
|
|
2666
|
+
async run({ args }) {
|
|
2667
|
+
const dryRun = args["dry-run"] === true;
|
|
2668
|
+
const resolved = resolveConnection(process.cwd());
|
|
2669
|
+
if (resolved.type === "forgejo") await mergeForgejo(resolved.conn, dryRun);
|
|
2670
|
+
else mergeGitHub(dryRun);
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
async function mergeForgejo(conn, dryRun) {
|
|
2674
|
+
const executor = createRealExecutor();
|
|
2675
|
+
const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
|
|
2676
|
+
if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
2677
|
+
if (dryRun) {
|
|
2678
|
+
p.log.info(`[dry-run] Would merge PR #${String(prNumber)} and delete branch ${HEAD_BRANCH}`);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
await mergePr(executor, conn, prNumber, {
|
|
2682
|
+
method: "merge",
|
|
2683
|
+
deleteBranch: true
|
|
2684
|
+
});
|
|
2685
|
+
p.log.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
|
|
2686
|
+
}
|
|
2687
|
+
function mergeGitHub(dryRun) {
|
|
2688
|
+
const executor = createRealExecutor();
|
|
2689
|
+
if (dryRun) {
|
|
2690
|
+
const prNum = executor.exec(`gh pr view ${HEAD_BRANCH} --json number --jq .number`, { cwd: process.cwd() }).stdout.trim();
|
|
2691
|
+
if (!prNum) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
|
|
2692
|
+
p.log.info(`[dry-run] Would merge PR #${prNum} and delete branch ${HEAD_BRANCH}`);
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
executor.exec(`gh pr merge ${HEAD_BRANCH} --merge --delete-branch`, { cwd: process.cwd() });
|
|
2696
|
+
p.log.info(`Merged changesets PR and deleted branch ${HEAD_BRANCH}`);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2381
2699
|
//#endregion
|
|
2382
2700
|
//#region src/bin.ts
|
|
2383
|
-
|
|
2701
|
+
const main = defineCommand({
|
|
2384
2702
|
meta: {
|
|
2385
2703
|
name: "tooling",
|
|
2386
|
-
version: "0.1
|
|
2704
|
+
version: "0.5.1",
|
|
2387
2705
|
description: "Bootstrap and maintain standardized TypeScript project tooling"
|
|
2388
2706
|
},
|
|
2389
2707
|
subCommands: {
|
|
@@ -2391,9 +2709,12 @@ runMain(defineCommand({
|
|
|
2391
2709
|
"repo:update": updateCommand,
|
|
2392
2710
|
"release:changesets": releaseForgejoCommand,
|
|
2393
2711
|
"release:trigger": releaseTriggerCommand,
|
|
2394
|
-
"release:create-forgejo-release": createForgejoReleaseCommand
|
|
2712
|
+
"release:create-forgejo-release": createForgejoReleaseCommand,
|
|
2713
|
+
"release:merge": releaseMergeCommand
|
|
2395
2714
|
}
|
|
2396
|
-
})
|
|
2715
|
+
});
|
|
2716
|
+
console.log(`@bensandee/tooling v0.5.1`);
|
|
2717
|
+
runMain(main);
|
|
2397
2718
|
|
|
2398
2719
|
//#endregion
|
|
2399
2720
|
export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensandee/tooling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tooling": "./dist/bin.mjs"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"tsdown": "0.20.3",
|
|
32
32
|
"typescript": "5.9.3",
|
|
33
33
|
"vitest": "4.0.18",
|
|
34
|
-
"@bensandee/config": "0.
|
|
34
|
+
"@bensandee/config": "0.5.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsdown",
|