@forge-ts/cli 0.17.0 → 0.19.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/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ addScripts,
3
4
  initProjectCommand,
4
- runInitProject
5
- } from "./chunk-57QAENSK.js";
5
+ readPkgJson,
6
+ runInitProject,
7
+ writePkgJson
8
+ } from "./chunk-JVI2NAXX.js";
6
9
  import {
7
10
  configureLogger,
8
11
  forgeLogger
@@ -473,9 +476,22 @@ var bypassCommand = defineCommand3({
473
476
  });
474
477
 
475
478
  // src/commands/check.ts
479
+ import { execSync } from "child_process";
476
480
  import { loadConfig as loadConfig3 } from "@forge-ts/core";
477
481
  import { enforce } from "@forge-ts/enforcer";
478
482
  import { defineCommand as defineCommand4 } from "citty";
483
+ function getStagedFiles(cwd) {
484
+ try {
485
+ const output = execSync("git diff --cached --name-only --diff-filter=d", {
486
+ cwd,
487
+ encoding: "utf8",
488
+ stdio: ["pipe", "pipe", "pipe"]
489
+ });
490
+ return output.split("\n").map((f) => f.trim()).filter((f) => f.length > 0 && (f.endsWith(".ts") || f.endsWith(".tsx")));
491
+ } catch {
492
+ return null;
493
+ }
494
+ }
479
495
  var RULE_NAMES = {
480
496
  E001: "require-summary",
481
497
  E002: "require-param",
@@ -634,27 +650,66 @@ async function runCheck(args) {
634
650
  if (args.strict !== void 0) {
635
651
  config.enforce.strict = args.strict;
636
652
  }
653
+ const fileFilter = args.file;
654
+ let stagedPaths;
655
+ if (args.staged) {
656
+ const rootDir = config.rootDir;
657
+ const staged = getStagedFiles(rootDir);
658
+ if (staged !== null && staged.length > 0) {
659
+ stagedPaths = staged;
660
+ } else {
661
+ const data2 = {
662
+ success: true,
663
+ summary: { errors: 0, warnings: 0, files: 0, symbols: 0, duration: 0 }
664
+ };
665
+ return { operation: "check", success: true, data: data2, duration: 0 };
666
+ }
667
+ }
637
668
  const result = await enforce(config);
638
669
  const mviLevel = args.mvi ?? "standard";
639
670
  const limit = args.limit ?? 20;
640
671
  const offset = args.offset ?? 0;
641
- const filters = { rule: args.rule, file: args.file };
672
+ const filters = { rule: args.rule, file: fileFilter };
673
+ const CROSS_FILE_RULES = /* @__PURE__ */ new Set([
674
+ "E005",
675
+ "E008",
676
+ "E009",
677
+ "E010",
678
+ "E011",
679
+ "E012",
680
+ "W007",
681
+ "W008",
682
+ "W009"
683
+ ]);
684
+ let filteredErrors = result.errors;
685
+ let filteredWarnings = result.warnings;
686
+ if (stagedPaths && stagedPaths.length > 0) {
687
+ const stagedSet = new Set(stagedPaths);
688
+ const matchesStaged = (filePath) => stagedPaths?.some((sp) => filePath.endsWith(sp)) ?? false;
689
+ filteredErrors = result.errors.filter(
690
+ (e) => CROSS_FILE_RULES.has(e.code) || stagedSet.has(e.filePath) || matchesStaged(e.filePath)
691
+ );
692
+ filteredWarnings = result.warnings.filter(
693
+ (w) => CROSS_FILE_RULES.has(w.code) || stagedSet.has(w.filePath) || matchesStaged(w.filePath)
694
+ );
695
+ }
642
696
  const exportedSymbolCount = result.symbols.filter((s) => s.exported).length;
643
697
  const data = buildCheckResult(
644
- result.errors,
645
- result.warnings,
698
+ filteredErrors,
699
+ filteredWarnings,
646
700
  exportedSymbolCount,
647
701
  result.duration,
648
- result.success,
702
+ filteredErrors.length === 0,
649
703
  mviLevel,
650
704
  filters,
651
705
  limit,
652
706
  offset
653
707
  );
654
- const cliErrors = result.success ? void 0 : [
708
+ const checkSuccess = filteredErrors.length === 0;
709
+ const cliErrors = checkSuccess ? void 0 : [
655
710
  {
656
711
  code: "FORGE_CHECK_FAILED",
657
- message: `TSDoc coverage check failed: ${result.errors.length} error(s), ${result.warnings.length} warning(s) across ${data.summary.files} file(s)`
712
+ message: `TSDoc coverage check failed: ${filteredErrors.length} error(s), ${filteredWarnings.length} warning(s) across ${data.summary.files} file(s)`
658
713
  }
659
714
  ];
660
715
  const cliWarnings = config._configWarnings?.map((msg) => ({
@@ -666,7 +721,7 @@ async function runCheck(args) {
666
721
  }));
667
722
  return {
668
723
  operation: "check",
669
- success: result.success,
724
+ success: checkSuccess,
670
725
  data,
671
726
  errors: cliErrors,
672
727
  warnings: cliWarnings,
@@ -769,6 +824,11 @@ var checkCommand = defineCommand4({
769
824
  type: "string",
770
825
  description: "Filter by file path (substring match)"
771
826
  },
827
+ staged: {
828
+ type: "boolean",
829
+ description: "Only check symbols from git-staged .ts/.tsx files",
830
+ default: false
831
+ },
772
832
  limit: {
773
833
  type: "string",
774
834
  description: "Max file groups in output (default: 20)"
@@ -805,6 +865,7 @@ var checkCommand = defineCommand4({
805
865
  mvi: args.mvi,
806
866
  rule: args.rule,
807
867
  file: args.file,
868
+ staged: args.staged,
808
869
  limit: args.limit ? parseInt(args.limit, 10) : void 0,
809
870
  offset: args.offset ? parseInt(args.offset, 10) : void 0
810
871
  });
@@ -880,6 +941,7 @@ var docsDevCommand = defineCommand5({
880
941
  });
881
942
 
882
943
  // src/commands/doctor.ts
944
+ import { execSync as execSync2 } from "child_process";
883
945
  import { existsSync, readFileSync } from "fs";
884
946
  import { mkdir, writeFile } from "fs/promises";
885
947
  import { join } from "path";
@@ -1183,43 +1245,136 @@ async function runDoctor(args) {
1183
1245
  });
1184
1246
  }
1185
1247
  const huskyPreCommit = join(rootDir, ".husky", "pre-commit");
1248
+ const huskyPrePush = join(rootDir, ".husky", "pre-push");
1186
1249
  const lefthookYml = join(rootDir, "lefthook.yml");
1187
- let hookConfigured = false;
1188
- let hookLocation = "";
1250
+ let hookManagerType = "none";
1251
+ let preCommitConfigured = false;
1252
+ let prePushConfigured = false;
1189
1253
  if (existsSync(huskyPreCommit)) {
1254
+ hookManagerType = "husky";
1190
1255
  try {
1191
1256
  const content = readFileSync(huskyPreCommit, "utf8");
1192
1257
  if (content.includes("forge-ts check")) {
1193
- hookConfigured = true;
1194
- hookLocation = "husky pre-commit";
1258
+ preCommitConfigured = true;
1259
+ }
1260
+ } catch {
1261
+ }
1262
+ }
1263
+ if (hookManagerType === "husky" && existsSync(huskyPrePush)) {
1264
+ try {
1265
+ const content = readFileSync(huskyPrePush, "utf8");
1266
+ if (content.includes("forge-ts prepublish")) {
1267
+ prePushConfigured = true;
1195
1268
  }
1196
1269
  } catch {
1197
1270
  }
1198
1271
  }
1199
- if (!hookConfigured && existsSync(lefthookYml)) {
1272
+ if (hookManagerType === "none" && existsSync(lefthookYml)) {
1273
+ hookManagerType = "lefthook";
1200
1274
  try {
1201
1275
  const content = readFileSync(lefthookYml, "utf8");
1202
1276
  if (content.includes("forge-ts check")) {
1203
- hookConfigured = true;
1204
- hookLocation = "lefthook";
1277
+ preCommitConfigured = true;
1278
+ }
1279
+ if (content.includes("forge-ts prepublish")) {
1280
+ prePushConfigured = true;
1205
1281
  }
1206
1282
  } catch {
1207
1283
  }
1208
1284
  }
1209
- if (hookConfigured) {
1210
- checks.push({
1211
- name: "Git hooks",
1212
- status: "pass",
1213
- message: `Git hooks \u2014 forge-ts check in ${hookLocation}`,
1214
- fixable: false
1215
- });
1216
- } else {
1285
+ let huskyInstalled = false;
1286
+ if (hookManagerType === "husky" || hookManagerType === "none") {
1287
+ const huskyBin = join(rootDir, "node_modules", ".bin", "husky");
1288
+ if (existsSync(huskyBin)) {
1289
+ huskyInstalled = true;
1290
+ } else {
1291
+ const pkgPathHooks = join(rootDir, "package.json");
1292
+ const hooksPkg = readJsonSafe(pkgPathHooks);
1293
+ if (hooksPkg) {
1294
+ const allDeps = { ...hooksPkg.dependencies, ...hooksPkg.devDependencies };
1295
+ if ("husky" in allDeps) {
1296
+ huskyInstalled = true;
1297
+ }
1298
+ }
1299
+ }
1300
+ }
1301
+ let prepareScriptExists = false;
1302
+ {
1303
+ const pkgPathPrep = join(rootDir, "package.json");
1304
+ const prepPkg = readJsonSafe(pkgPathPrep);
1305
+ if (prepPkg?.scripts?.prepare) {
1306
+ prepareScriptExists = true;
1307
+ }
1308
+ }
1309
+ if (hookManagerType === "husky") {
1310
+ try {
1311
+ execSync2("git config core.hooksPath", {
1312
+ cwd: rootDir,
1313
+ encoding: "utf8",
1314
+ stdio: ["pipe", "pipe", "pipe"]
1315
+ });
1316
+ } catch {
1317
+ }
1318
+ }
1319
+ if (hookManagerType === "none" && !preCommitConfigured && !prePushConfigured) {
1217
1320
  checks.push({
1218
1321
  name: "Git hooks",
1219
1322
  status: "warn",
1220
- message: "Git hooks \u2014 forge-ts check not in pre-commit (run forge-ts init hooks)",
1323
+ message: "Git hooks \u2014 no hook manager detected (run forge-ts init hooks)",
1221
1324
  fixable: false
1222
1325
  });
1326
+ } else if (hookManagerType === "husky") {
1327
+ if (!huskyInstalled) {
1328
+ checks.push({
1329
+ name: "Git hooks (husky)",
1330
+ status: "warn",
1331
+ message: "Git hooks \u2014 hook files exist but husky is not installed (run: npm install -D husky)",
1332
+ fixable: false
1333
+ });
1334
+ } else if (!prepareScriptExists) {
1335
+ checks.push({
1336
+ name: "Git hooks (husky)",
1337
+ status: "warn",
1338
+ message: 'Git hooks \u2014 husky installed but "prepare" script missing in package.json',
1339
+ fixable: false
1340
+ });
1341
+ } else if (preCommitConfigured && prePushConfigured) {
1342
+ checks.push({
1343
+ name: "Git hooks (husky)",
1344
+ status: "pass",
1345
+ message: "Git hooks \u2014 husky installed, prepare script wired, pre-commit and pre-push configured",
1346
+ fixable: false
1347
+ });
1348
+ } else {
1349
+ const missing = [];
1350
+ if (!preCommitConfigured) missing.push("pre-commit (forge-ts check)");
1351
+ if (!prePushConfigured) missing.push("pre-push (forge-ts prepublish)");
1352
+ checks.push({
1353
+ name: "Git hooks (husky)",
1354
+ status: "warn",
1355
+ message: `Git hooks \u2014 husky installed but missing: ${missing.join(", ")}`,
1356
+ fixable: false
1357
+ });
1358
+ }
1359
+ } else if (hookManagerType === "lefthook") {
1360
+ if (preCommitConfigured && prePushConfigured) {
1361
+ checks.push({
1362
+ name: "Git hooks (lefthook)",
1363
+ status: "pass",
1364
+ message: "Git hooks \u2014 lefthook pre-commit and pre-push configured",
1365
+ fixable: false
1366
+ });
1367
+ } else {
1368
+ const missing = [];
1369
+ if (!preCommitConfigured) missing.push("pre-commit (forge-ts check)");
1370
+ if (!prePushConfigured) missing.push("pre-push (forge-ts prepublish)");
1371
+ checks.push({
1372
+ name: "Git hooks (lefthook)",
1373
+ status: "warn",
1374
+ message: `Git hooks \u2014 lefthook detected but missing: ${missing.join(", ")}`,
1375
+ fixable: false
1376
+ });
1377
+ }
1223
1378
  }
1224
1379
  const summary = {
1225
1380
  passed: checks.filter((c) => c.status === "pass").length,
@@ -1597,15 +1752,19 @@ function detectHookManager(rootDir) {
1597
1752
  }
1598
1753
  return "none";
1599
1754
  }
1600
- var HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
1601
- . "$(dirname -- "$0")/_/husky.sh"
1602
-
1603
- npx forge-ts check
1755
+ var HUSKY_PRE_COMMIT = `npx forge-ts check
1756
+ `;
1757
+ var HUSKY_PRE_PUSH = `npx forge-ts prepublish
1604
1758
  `;
1605
1759
  var LEFTHOOK_BLOCK = `pre-commit:
1606
1760
  commands:
1607
1761
  forge-ts-check:
1608
1762
  run: npx forge-ts check
1763
+
1764
+ pre-push:
1765
+ commands:
1766
+ forge-ts-prepublish:
1767
+ run: npx forge-ts prepublish
1609
1768
  `;
1610
1769
  async function runInitHooks(args) {
1611
1770
  const start = Date.now();
@@ -1616,54 +1775,125 @@ async function runInitHooks(args) {
1616
1775
  const warnings = [];
1617
1776
  const instructions = [];
1618
1777
  if (hookManager === "husky" || hookManager === "none") {
1778
+ const huskyBin = join3(rootDir, "node_modules", ".bin", "husky");
1779
+ const pkg2 = readPkgJson(rootDir);
1780
+ const huskyInDeps = pkg2 !== null && ("husky" in (pkg2.obj.devDependencies ?? {}) || "husky" in (pkg2.obj.dependencies ?? {}));
1781
+ const huskyInstalled = existsSync3(huskyBin) || huskyInDeps;
1782
+ if (!huskyInstalled) {
1783
+ warnings.push({
1784
+ code: "HOOKS_HUSKY_NOT_INSTALLED",
1785
+ message: "husky not installed. Run: npm install -D husky"
1786
+ });
1787
+ instructions.push("husky is not installed. Run: npm install -D husky (or pnpm add -D husky)");
1788
+ }
1619
1789
  const huskyDir = join3(rootDir, ".husky");
1620
- const hookPath = join3(huskyDir, "pre-commit");
1621
- const relativePath = ".husky/pre-commit";
1622
- if (existsSync3(hookPath) && !args.force) {
1623
- const existing = await readFile(hookPath, "utf8");
1790
+ const preCommitPath = join3(huskyDir, "pre-commit");
1791
+ const preCommitRel = ".husky/pre-commit";
1792
+ if (existsSync3(preCommitPath) && !args.force) {
1793
+ const existing = await readFile(preCommitPath, "utf8");
1624
1794
  if (existing.includes("forge-ts check")) {
1625
- skippedFiles.push(relativePath);
1795
+ skippedFiles.push(preCommitRel);
1626
1796
  warnings.push({
1627
1797
  code: "HOOKS_ALREADY_EXISTS",
1628
- message: `${relativePath} already contains forge-ts check \u2014 skipping. Use --force to overwrite.`
1798
+ message: `${preCommitRel} already contains forge-ts check \u2014 skipping. Use --force to overwrite.`
1629
1799
  });
1630
1800
  } else {
1631
1801
  const appended = `${existing.trimEnd()}
1632
1802
 
1633
1803
  npx forge-ts check
1634
1804
  `;
1635
- await writeFile3(hookPath, appended, { mode: 493 });
1636
- writtenFiles.push(relativePath);
1805
+ await writeFile3(preCommitPath, appended, { mode: 493 });
1806
+ writtenFiles.push(preCommitRel);
1637
1807
  }
1638
1808
  } else {
1639
1809
  await mkdir3(huskyDir, { recursive: true });
1640
- await writeFile3(hookPath, HUSKY_PRE_COMMIT, { mode: 493 });
1641
- writtenFiles.push(relativePath);
1810
+ await writeFile3(preCommitPath, HUSKY_PRE_COMMIT, { mode: 493 });
1811
+ writtenFiles.push(preCommitRel);
1812
+ }
1813
+ const prePushPath = join3(huskyDir, "pre-push");
1814
+ const prePushRel = ".husky/pre-push";
1815
+ if (existsSync3(prePushPath) && !args.force) {
1816
+ const existing = await readFile(prePushPath, "utf8");
1817
+ if (existing.includes("forge-ts prepublish")) {
1818
+ skippedFiles.push(prePushRel);
1819
+ warnings.push({
1820
+ code: "HOOKS_ALREADY_EXISTS",
1821
+ message: `${prePushRel} already contains forge-ts prepublish \u2014 skipping. Use --force to overwrite.`
1822
+ });
1823
+ } else {
1824
+ const appended = `${existing.trimEnd()}
1825
+
1826
+ npx forge-ts prepublish
1827
+ `;
1828
+ await writeFile3(prePushPath, appended, { mode: 493 });
1829
+ writtenFiles.push(prePushRel);
1830
+ }
1831
+ } else {
1832
+ await mkdir3(huskyDir, { recursive: true });
1833
+ await writeFile3(prePushPath, HUSKY_PRE_PUSH, { mode: 493 });
1834
+ writtenFiles.push(prePushRel);
1835
+ }
1836
+ const pkgData = readPkgJson(rootDir);
1837
+ if (pkgData) {
1838
+ const added = addScripts(pkgData, { prepare: "husky" });
1839
+ if (added.length > 0) {
1840
+ await writePkgJson(pkgData);
1841
+ writtenFiles.push("package.json (prepare script)");
1842
+ instructions.push('Added "prepare": "husky" script to package.json.');
1843
+ } else {
1844
+ skippedFiles.push("package.json (prepare script already exists)");
1845
+ }
1642
1846
  }
1643
1847
  if (hookManager === "none") {
1644
1848
  instructions.push(
1645
- "No hook manager detected. Wrote .husky/pre-commit as a starting point.",
1646
- "Install husky to activate: npx husky-init && npm install (or pnpm dlx husky-init && pnpm install)"
1849
+ "No hook manager detected. Wrote .husky/pre-commit and .husky/pre-push as a starting point.",
1850
+ "Install husky to activate: npm install -D husky && npx husky (or pnpm add -D husky && pnpm exec husky)"
1647
1851
  );
1648
1852
  } else {
1649
1853
  instructions.push("Husky pre-commit hook configured to run forge-ts check.");
1854
+ instructions.push("Husky pre-push hook configured to run forge-ts prepublish.");
1650
1855
  }
1651
1856
  } else if (hookManager === "lefthook") {
1652
1857
  const lefthookPath = join3(rootDir, "lefthook.yml");
1653
1858
  const relativePath = "lefthook.yml";
1654
1859
  if (existsSync3(lefthookPath)) {
1655
1860
  const existing = await readFile(lefthookPath, "utf8");
1656
- if (existing.includes("forge-ts check") && !args.force) {
1861
+ const hasCheck = existing.includes("forge-ts check");
1862
+ const hasPrepublish = existing.includes("forge-ts prepublish");
1863
+ if (hasCheck && hasPrepublish && !args.force) {
1657
1864
  skippedFiles.push(relativePath);
1658
1865
  warnings.push({
1659
1866
  code: "HOOKS_ALREADY_EXISTS",
1660
- message: `${relativePath} already contains forge-ts check \u2014 skipping. Use --force to overwrite.`
1867
+ message: `${relativePath} already contains forge-ts check and prepublish \u2014 skipping. Use --force to overwrite.`
1661
1868
  });
1662
- } else if (existing.includes("pre-commit:") && !args.force) {
1869
+ } else if (hasCheck && !hasPrepublish && !args.force) {
1870
+ const prePushBlock = `
1871
+ pre-push:
1872
+ commands:
1873
+ forge-ts-prepublish:
1874
+ run: npx forge-ts prepublish
1875
+ `;
1663
1876
  const appended = `${existing.trimEnd()}
1664
- forge-ts-check:
1665
- run: npx forge-ts check
1877
+ ${prePushBlock}`;
1878
+ await writeFile3(lefthookPath, appended, "utf8");
1879
+ writtenFiles.push(relativePath);
1880
+ } else if (existing.includes("pre-commit:") && !args.force) {
1881
+ let appended = existing.trimEnd();
1882
+ if (!hasCheck) {
1883
+ appended += "\n forge-ts-check:\n run: npx forge-ts check";
1884
+ }
1885
+ if (!existing.includes("pre-push:")) {
1886
+ appended += `
1887
+
1888
+ pre-push:
1889
+ commands:
1890
+ forge-ts-prepublish:
1891
+ run: npx forge-ts prepublish
1666
1892
  `;
1893
+ } else if (!hasPrepublish) {
1894
+ appended += "\n forge-ts-prepublish:\n run: npx forge-ts prepublish";
1895
+ }
1896
+ appended += "\n";
1667
1897
  await writeFile3(lefthookPath, appended, "utf8");
1668
1898
  writtenFiles.push(relativePath);
1669
1899
  } else {
@@ -1678,6 +1908,7 @@ ${LEFTHOOK_BLOCK}`;
1678
1908
  writtenFiles.push(relativePath);
1679
1909
  }
1680
1910
  instructions.push("Lefthook pre-commit hook configured to run forge-ts check.");
1911
+ instructions.push("Lefthook pre-push hook configured to run forge-ts prepublish.");
1681
1912
  }
1682
1913
  const data = {
1683
1914
  success: true,
@@ -2341,7 +2572,7 @@ var initCommand2 = defineCommand13({
2341
2572
  if (hasSubCommand) {
2342
2573
  return;
2343
2574
  }
2344
- const { runInitProject: runInitProject2 } = await import("./init-project-PT4XOWV2.js");
2575
+ const { runInitProject: runInitProject2 } = await import("./init-project-6CWF4CCX.js");
2345
2576
  const { emitResult: emitResult2, resolveExitCode: resolveExitCode2 } = await import("./output-OSCHMPOX.js");
2346
2577
  const { forgeLogger: forgeLogger2 } = await import("./forge-logger-RTOBEKWH.js");
2347
2578
  const output = await runInitProject2({