@forge-ts/cli 0.16.0 → 0.18.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-DV33Y4E2.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";
@@ -899,18 +961,8 @@ var DEFAULT_CONFIG_CONTENT = `import { defineConfig } from "@forge-ts/core";
899
961
 
900
962
  export default defineConfig({
901
963
  rootDir: ".",
902
- tsconfig: "tsconfig.json",
903
964
  outDir: "docs/generated",
904
- enforce: {
905
- enabled: true,
906
- minVisibility: "public",
907
- strict: false,
908
- },
909
965
  gen: {
910
- enabled: true,
911
- formats: ["mdx"],
912
- llmsTxt: true,
913
- readmeSync: false,
914
966
  ssgTarget: "mintlify",
915
967
  },
916
968
  });
@@ -1193,43 +1245,136 @@ async function runDoctor(args) {
1193
1245
  });
1194
1246
  }
1195
1247
  const huskyPreCommit = join(rootDir, ".husky", "pre-commit");
1248
+ const huskyPrePush = join(rootDir, ".husky", "pre-push");
1196
1249
  const lefthookYml = join(rootDir, "lefthook.yml");
1197
- let hookConfigured = false;
1198
- let hookLocation = "";
1250
+ let hookManagerType = "none";
1251
+ let preCommitConfigured = false;
1252
+ let prePushConfigured = false;
1199
1253
  if (existsSync(huskyPreCommit)) {
1254
+ hookManagerType = "husky";
1200
1255
  try {
1201
1256
  const content = readFileSync(huskyPreCommit, "utf8");
1202
1257
  if (content.includes("forge-ts check")) {
1203
- hookConfigured = true;
1204
- 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;
1205
1268
  }
1206
1269
  } catch {
1207
1270
  }
1208
1271
  }
1209
- if (!hookConfigured && existsSync(lefthookYml)) {
1272
+ if (hookManagerType === "none" && existsSync(lefthookYml)) {
1273
+ hookManagerType = "lefthook";
1210
1274
  try {
1211
1275
  const content = readFileSync(lefthookYml, "utf8");
1212
1276
  if (content.includes("forge-ts check")) {
1213
- hookConfigured = true;
1214
- hookLocation = "lefthook";
1277
+ preCommitConfigured = true;
1278
+ }
1279
+ if (content.includes("forge-ts prepublish")) {
1280
+ prePushConfigured = true;
1215
1281
  }
1216
1282
  } catch {
1217
1283
  }
1218
1284
  }
1219
- if (hookConfigured) {
1220
- checks.push({
1221
- name: "Git hooks",
1222
- status: "pass",
1223
- message: `Git hooks \u2014 forge-ts check in ${hookLocation}`,
1224
- fixable: false
1225
- });
1226
- } 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) {
1227
1320
  checks.push({
1228
1321
  name: "Git hooks",
1229
1322
  status: "warn",
1230
- 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)",
1231
1324
  fixable: false
1232
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
+ }
1233
1378
  }
1234
1379
  const summary = {
1235
1380
  passed: checks.filter((c) => c.status === "pass").length,
@@ -1607,15 +1752,19 @@ function detectHookManager(rootDir) {
1607
1752
  }
1608
1753
  return "none";
1609
1754
  }
1610
- var HUSKY_PRE_COMMIT = `#!/usr/bin/env sh
1611
- . "$(dirname -- "$0")/_/husky.sh"
1612
-
1613
- npx forge-ts check
1755
+ var HUSKY_PRE_COMMIT = `npx forge-ts check
1756
+ `;
1757
+ var HUSKY_PRE_PUSH = `npx forge-ts prepublish
1614
1758
  `;
1615
1759
  var LEFTHOOK_BLOCK = `pre-commit:
1616
1760
  commands:
1617
1761
  forge-ts-check:
1618
1762
  run: npx forge-ts check
1763
+
1764
+ pre-push:
1765
+ commands:
1766
+ forge-ts-prepublish:
1767
+ run: npx forge-ts prepublish
1619
1768
  `;
1620
1769
  async function runInitHooks(args) {
1621
1770
  const start = Date.now();
@@ -1626,54 +1775,125 @@ async function runInitHooks(args) {
1626
1775
  const warnings = [];
1627
1776
  const instructions = [];
1628
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
+ }
1629
1789
  const huskyDir = join3(rootDir, ".husky");
1630
- const hookPath = join3(huskyDir, "pre-commit");
1631
- const relativePath = ".husky/pre-commit";
1632
- if (existsSync3(hookPath) && !args.force) {
1633
- 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");
1634
1794
  if (existing.includes("forge-ts check")) {
1635
- skippedFiles.push(relativePath);
1795
+ skippedFiles.push(preCommitRel);
1636
1796
  warnings.push({
1637
1797
  code: "HOOKS_ALREADY_EXISTS",
1638
- 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.`
1639
1799
  });
1640
1800
  } else {
1641
1801
  const appended = `${existing.trimEnd()}
1642
1802
 
1643
1803
  npx forge-ts check
1644
1804
  `;
1645
- await writeFile3(hookPath, appended, { mode: 493 });
1646
- writtenFiles.push(relativePath);
1805
+ await writeFile3(preCommitPath, appended, { mode: 493 });
1806
+ writtenFiles.push(preCommitRel);
1647
1807
  }
1648
1808
  } else {
1649
1809
  await mkdir3(huskyDir, { recursive: true });
1650
- await writeFile3(hookPath, HUSKY_PRE_COMMIT, { mode: 493 });
1651
- 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
+ }
1652
1846
  }
1653
1847
  if (hookManager === "none") {
1654
1848
  instructions.push(
1655
- "No hook manager detected. Wrote .husky/pre-commit as a starting point.",
1656
- "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)"
1657
1851
  );
1658
1852
  } else {
1659
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.");
1660
1855
  }
1661
1856
  } else if (hookManager === "lefthook") {
1662
1857
  const lefthookPath = join3(rootDir, "lefthook.yml");
1663
1858
  const relativePath = "lefthook.yml";
1664
1859
  if (existsSync3(lefthookPath)) {
1665
1860
  const existing = await readFile(lefthookPath, "utf8");
1666
- 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) {
1667
1864
  skippedFiles.push(relativePath);
1668
1865
  warnings.push({
1669
1866
  code: "HOOKS_ALREADY_EXISTS",
1670
- 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.`
1671
1868
  });
1672
- } 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
+ `;
1673
1876
  const appended = `${existing.trimEnd()}
1674
- forge-ts-check:
1675
- 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
1676
1892
  `;
1893
+ } else if (!hasPrepublish) {
1894
+ appended += "\n forge-ts-prepublish:\n run: npx forge-ts prepublish";
1895
+ }
1896
+ appended += "\n";
1677
1897
  await writeFile3(lefthookPath, appended, "utf8");
1678
1898
  writtenFiles.push(relativePath);
1679
1899
  } else {
@@ -1688,6 +1908,7 @@ ${LEFTHOOK_BLOCK}`;
1688
1908
  writtenFiles.push(relativePath);
1689
1909
  }
1690
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.");
1691
1912
  }
1692
1913
  const data = {
1693
1914
  success: true,
@@ -2351,7 +2572,7 @@ var initCommand2 = defineCommand13({
2351
2572
  if (hasSubCommand) {
2352
2573
  return;
2353
2574
  }
2354
- const { runInitProject: runInitProject2 } = await import("./init-project-5AWZ2LAI.js");
2575
+ const { runInitProject: runInitProject2 } = await import("./init-project-6CWF4CCX.js");
2355
2576
  const { emitResult: emitResult2, resolveExitCode: resolveExitCode2 } = await import("./output-OSCHMPOX.js");
2356
2577
  const { forgeLogger: forgeLogger2 } = await import("./forge-logger-RTOBEKWH.js");
2357
2578
  const output = await runInitProject2({