@packmind/cli 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/main.cjs +794 -207
  2. package/package.json +1 -1
package/main.cjs CHANGED
@@ -38,7 +38,7 @@ var require_package = __commonJS({
38
38
  "apps/cli/package.json"(exports2, module2) {
39
39
  module2.exports = {
40
40
  name: "@packmind/cli",
41
- version: "0.5.0",
41
+ version: "0.6.0",
42
42
  description: "A command-line interface for Packmind linting and code quality checks",
43
43
  private: false,
44
44
  bin: {
@@ -76,7 +76,6 @@ var require_package = __commonJS({
76
76
  });
77
77
 
78
78
  // apps/cli/src/main.ts
79
- var import_chalk2 = __toESM(require("chalk"));
80
79
  var import_cmd_ts3 = require("cmd-ts");
81
80
 
82
81
  // apps/cli/src/infra/commands/LinterCommand.ts
@@ -258,6 +257,31 @@ var createRuleId = brandedIdFactory();
258
257
  // packages/types/src/standards/RuleExampleId.ts
259
258
  var createRuleExampleId = brandedIdFactory();
260
259
 
260
+ // packages/types/src/events/PackmindEvent.ts
261
+ var PackmindEvent = class {
262
+ constructor(payload) {
263
+ this.payload = payload;
264
+ }
265
+ /**
266
+ * Get the event name from the static property.
267
+ * Used internally by the event emitter.
268
+ */
269
+ get name() {
270
+ return this.constructor.eventName;
271
+ }
272
+ };
273
+
274
+ // packages/types/src/events/UserEvent.ts
275
+ var UserEvent = class extends PackmindEvent {
276
+ };
277
+
278
+ // packages/types/src/standards/events/StandardUpdatedEvent.ts
279
+ var StandardUpdatedEvent = class extends UserEvent {
280
+ static {
281
+ this.eventName = "standards.standard.updated";
282
+ }
283
+ };
284
+
261
285
  // packages/types/src/languages/ProgrammingLanguage.ts
262
286
  var ProgrammingLanguage = /* @__PURE__ */ ((ProgrammingLanguage4) => {
263
287
  ProgrammingLanguage4["JAVASCRIPT"] = "JAVASCRIPT";
@@ -793,36 +817,49 @@ var ExecuteSingleFileAstUseCase = class _ExecuteSingleFileAstUseCase {
793
817
  // apps/cli/src/application/services/GitService.ts
794
818
  var import_child_process = require("child_process");
795
819
  var import_util = require("util");
820
+ var path = __toESM(require("path"));
796
821
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
797
822
  var origin = "GitService";
798
823
  var GitService = class {
799
824
  constructor(logger2 = new PackmindLogger(origin)) {
800
825
  this.logger = logger2;
801
826
  }
802
- async getGitRepositoryRoot(path7) {
827
+ async getGitRepositoryRoot(path8) {
803
828
  try {
804
829
  const { stdout } = await execAsync("git rev-parse --show-toplevel", {
805
- cwd: path7
830
+ cwd: path8
806
831
  });
807
832
  const gitRoot = stdout.trim();
808
833
  this.logger.debug("Resolved git repository root", {
809
- inputPath: path7,
834
+ inputPath: path8,
810
835
  gitRoot
811
836
  });
812
837
  return gitRoot;
813
838
  } catch (error) {
814
839
  if (error instanceof Error) {
815
840
  throw new Error(
816
- `Failed to get Git repository root. The path '${path7}' does not appear to be inside a Git repository.
841
+ `Failed to get Git repository root. The path '${path8}' does not appear to be inside a Git repository.
817
842
  ${error.message}`
818
843
  );
819
844
  }
820
845
  throw new Error("Failed to get Git repository root: Unknown error");
821
846
  }
822
847
  }
823
- async tryGetGitRepositoryRoot(path7) {
848
+ async tryGetGitRepositoryRoot(path8) {
824
849
  try {
825
- return await this.getGitRepositoryRoot(path7);
850
+ return await this.getGitRepositoryRoot(path8);
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ getGitRepositoryRootSync(cwd) {
856
+ try {
857
+ const result = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
858
+ cwd,
859
+ stdio: ["pipe", "pipe", "pipe"],
860
+ encoding: "utf-8"
861
+ });
862
+ return result.trim();
826
863
  } catch {
827
864
  return null;
828
865
  }
@@ -927,18 +964,181 @@ ${error.message}`
927
964
  normalizeGitUrl(url) {
928
965
  const sshMatch = url.match(/^git@([^:]+):(.+)$/);
929
966
  if (sshMatch) {
930
- const [, host, path7] = sshMatch;
931
- const cleanPath = path7.replace(/\.git$/, "");
967
+ const [, host, urlPath] = sshMatch;
968
+ const cleanPath = urlPath.replace(/\.git$/, "");
932
969
  return `${host}/${cleanPath}`;
933
970
  }
934
971
  const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+)$/);
935
972
  if (httpsMatch) {
936
- const [, host, path7] = httpsMatch;
937
- const cleanPath = path7.replace(/\.git$/, "");
973
+ const [, host, urlPath] = httpsMatch;
974
+ const cleanPath = urlPath.replace(/\.git$/, "");
938
975
  return `${host}/${cleanPath}`;
939
976
  }
940
977
  return url;
941
978
  }
979
+ /**
980
+ * Gets files that have been modified (staged + unstaged) compared to HEAD.
981
+ * Returns absolute file paths.
982
+ */
983
+ async getModifiedFiles(repoPath) {
984
+ const gitRoot = await this.getGitRepositoryRoot(repoPath);
985
+ const trackedFiles = await this.getTrackedModifiedFiles(gitRoot);
986
+ const untrackedFiles = await this.getUntrackedFiles(gitRoot);
987
+ const allFiles = [.../* @__PURE__ */ new Set([...trackedFiles, ...untrackedFiles])];
988
+ this.logger.debug("Found modified files", {
989
+ trackedCount: trackedFiles.length,
990
+ untrackedCount: untrackedFiles.length,
991
+ totalCount: allFiles.length
992
+ });
993
+ return allFiles;
994
+ }
995
+ /**
996
+ * Gets tracked files that have been modified (staged + unstaged) compared to HEAD.
997
+ * Returns absolute file paths.
998
+ */
999
+ async getTrackedModifiedFiles(gitRoot) {
1000
+ try {
1001
+ const { stdout } = await execAsync("git diff --name-only HEAD", {
1002
+ cwd: gitRoot
1003
+ });
1004
+ return stdout.trim().split("\n").filter((line) => line.length > 0).map((relativePath) => path.join(gitRoot, relativePath));
1005
+ } catch (error) {
1006
+ if (error instanceof Error && error.message.includes("unknown revision")) {
1007
+ this.logger.debug(
1008
+ "HEAD does not exist (first commit), getting staged files only"
1009
+ );
1010
+ return this.getStagedFilesWithoutHead(gitRoot);
1011
+ }
1012
+ throw error;
1013
+ }
1014
+ }
1015
+ /**
1016
+ * Gets staged files when HEAD doesn't exist (first commit scenario).
1017
+ */
1018
+ async getStagedFilesWithoutHead(gitRoot) {
1019
+ const { stdout } = await execAsync("git diff --cached --name-only", {
1020
+ cwd: gitRoot
1021
+ });
1022
+ return stdout.trim().split("\n").filter((line) => line.length > 0).map((relativePath) => path.join(gitRoot, relativePath));
1023
+ }
1024
+ /**
1025
+ * Gets untracked files (new files not yet added to git).
1026
+ * Returns absolute file paths.
1027
+ */
1028
+ async getUntrackedFiles(repoPath) {
1029
+ const gitRoot = await this.getGitRepositoryRoot(repoPath);
1030
+ const { stdout } = await execAsync(
1031
+ "git ls-files --others --exclude-standard",
1032
+ {
1033
+ cwd: gitRoot
1034
+ }
1035
+ );
1036
+ return stdout.trim().split("\n").filter((line) => line.length > 0).map((relativePath) => path.join(gitRoot, relativePath));
1037
+ }
1038
+ /**
1039
+ * Gets line-level diff information for modified files.
1040
+ * For untracked files, all lines are considered modified (new file).
1041
+ * Returns ModifiedLine objects with absolute file paths.
1042
+ */
1043
+ async getModifiedLines(repoPath) {
1044
+ const gitRoot = await this.getGitRepositoryRoot(repoPath);
1045
+ const modifiedLines = [];
1046
+ const trackedModifications = await this.getTrackedModifiedLines(gitRoot);
1047
+ modifiedLines.push(...trackedModifications);
1048
+ const untrackedFiles = await this.getUntrackedFiles(gitRoot);
1049
+ for (const filePath of untrackedFiles) {
1050
+ const lineCount = await this.countFileLines(filePath);
1051
+ if (lineCount > 0) {
1052
+ modifiedLines.push({
1053
+ file: filePath,
1054
+ startLine: 1,
1055
+ lineCount
1056
+ });
1057
+ }
1058
+ }
1059
+ this.logger.debug("Found modified lines", {
1060
+ trackedEntries: trackedModifications.length,
1061
+ untrackedFiles: untrackedFiles.length,
1062
+ totalEntries: modifiedLines.length
1063
+ });
1064
+ return modifiedLines;
1065
+ }
1066
+ /**
1067
+ * Parses git diff output to extract line-level modifications.
1068
+ */
1069
+ async getTrackedModifiedLines(gitRoot) {
1070
+ try {
1071
+ const { stdout } = await execAsync("git diff HEAD --unified=0", {
1072
+ cwd: gitRoot,
1073
+ maxBuffer: 50 * 1024 * 1024
1074
+ // 50MB buffer for large diffs
1075
+ });
1076
+ return this.parseDiffOutput(stdout, gitRoot);
1077
+ } catch (error) {
1078
+ if (error instanceof Error && error.message.includes("unknown revision")) {
1079
+ this.logger.debug(
1080
+ "HEAD does not exist (first commit), getting staged diff only"
1081
+ );
1082
+ return this.getStagedModifiedLinesWithoutHead(gitRoot);
1083
+ }
1084
+ throw error;
1085
+ }
1086
+ }
1087
+ /**
1088
+ * Gets modified lines from staged files when HEAD doesn't exist.
1089
+ */
1090
+ async getStagedModifiedLinesWithoutHead(gitRoot) {
1091
+ const { stdout } = await execAsync("git diff --cached --unified=0", {
1092
+ cwd: gitRoot,
1093
+ maxBuffer: 50 * 1024 * 1024
1094
+ });
1095
+ return this.parseDiffOutput(stdout, gitRoot);
1096
+ }
1097
+ /**
1098
+ * Parses unified diff output to extract modified line ranges.
1099
+ * Format: @@ -oldStart,oldCount +newStart,newCount @@
1100
+ */
1101
+ parseDiffOutput(diffOutput, gitRoot) {
1102
+ const modifiedLines = [];
1103
+ const lines = diffOutput.split("\n");
1104
+ let currentFile = null;
1105
+ for (const line of lines) {
1106
+ const fileMatch = line.match(/^diff --git a\/(.+) b\/(.+)$/);
1107
+ if (fileMatch) {
1108
+ currentFile = path.join(gitRoot, fileMatch[2]);
1109
+ continue;
1110
+ }
1111
+ const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
1112
+ if (hunkMatch && currentFile) {
1113
+ const startLine = parseInt(hunkMatch[1], 10);
1114
+ const lineCount = hunkMatch[2] ? parseInt(hunkMatch[2], 10) : 1;
1115
+ if (lineCount > 0) {
1116
+ modifiedLines.push({
1117
+ file: currentFile,
1118
+ startLine,
1119
+ lineCount
1120
+ });
1121
+ }
1122
+ }
1123
+ }
1124
+ return modifiedLines;
1125
+ }
1126
+ /**
1127
+ * Counts the number of lines in a file.
1128
+ */
1129
+ async countFileLines(filePath) {
1130
+ try {
1131
+ const { stdout } = await execAsync(`wc -l < "${filePath}"`);
1132
+ const count = parseInt(stdout.trim(), 10);
1133
+ if (count === 0) {
1134
+ const { stdout: content } = await execAsync(`head -c 1 "${filePath}"`);
1135
+ return content.length > 0 ? 1 : 0;
1136
+ }
1137
+ return count;
1138
+ } catch {
1139
+ return 0;
1140
+ }
1141
+ }
942
1142
  };
943
1143
 
944
1144
  // apps/cli/src/application/useCases/GetGitRemoteUrlUseCase.ts
@@ -954,7 +1154,7 @@ var GetGitRemoteUrlUseCase = class {
954
1154
 
955
1155
  // apps/cli/src/application/services/ListFiles.ts
956
1156
  var fs = __toESM(require("fs/promises"));
957
- var path = __toESM(require("path"));
1157
+ var path2 = __toESM(require("path"));
958
1158
  var ListFiles = class {
959
1159
  async listFilesInDirectory(directoryPath, extensions, excludes = [], skipHidden = true) {
960
1160
  const results = [];
@@ -976,7 +1176,7 @@ var ListFiles = class {
976
1176
  try {
977
1177
  const entries = await fs.readdir(directoryPath, { withFileTypes: true });
978
1178
  for (const entry of entries) {
979
- const fullPath = path.join(directoryPath, entry.name);
1179
+ const fullPath = path2.join(directoryPath, entry.name);
980
1180
  if (this.shouldExcludePath(fullPath, excludes)) {
981
1181
  continue;
982
1182
  }
@@ -993,7 +1193,7 @@ var ListFiles = class {
993
1193
  skipHidden
994
1194
  );
995
1195
  } else if (entry.isFile()) {
996
- const fileExtension = path.extname(entry.name);
1196
+ const fileExtension = path2.extname(entry.name);
997
1197
  if (includeAllExtensions || extensions.includes(fileExtension)) {
998
1198
  results.push({
999
1199
  path: fullPath
@@ -1009,7 +1209,7 @@ var ListFiles = class {
1009
1209
  if (excludes.length === 0) {
1010
1210
  return false;
1011
1211
  }
1012
- const normalizedPath = path.normalize(filePath).replace(/\\/g, "/");
1212
+ const normalizedPath = path2.normalize(filePath).replace(/\\/g, "/");
1013
1213
  for (const exclude of excludes) {
1014
1214
  if (this.matchesGlobPattern(normalizedPath, exclude)) {
1015
1215
  return true;
@@ -1072,7 +1272,7 @@ var ListFilesInDirectoryUseCase = class {
1072
1272
 
1073
1273
  // apps/cli/src/application/useCases/LintFilesInDirectoryUseCase.ts
1074
1274
  var import_minimatch = require("minimatch");
1075
- var path2 = __toESM(require("path"));
1275
+ var path3 = __toESM(require("path"));
1076
1276
  var fs2 = __toESM(require("fs/promises"));
1077
1277
  var origin2 = "LintFilesInDirectoryUseCase";
1078
1278
  var LintFilesInDirectoryUseCase = class {
@@ -1139,12 +1339,13 @@ var LintFilesInDirectoryUseCase = class {
1139
1339
  draftMode,
1140
1340
  standardSlug,
1141
1341
  ruleId,
1142
- language
1342
+ language,
1343
+ diffMode
1143
1344
  } = command3;
1144
1345
  this.logger.debug(
1145
- `Starting linting: path="${userPath}", draftMode=${!!draftMode}, standardSlug="${standardSlug || "N/A"}", ruleId="${ruleId || "N/A"}", language="${language || "N/A"}"`
1346
+ `Starting linting: path="${userPath}", draftMode=${!!draftMode}, standardSlug="${standardSlug || "N/A"}", ruleId="${ruleId || "N/A"}", language="${language || "N/A"}", diffMode="${diffMode ?? "none"}"`
1146
1347
  );
1147
- const absoluteUserPath = path2.isAbsolute(userPath) ? userPath : path2.resolve(process.cwd(), userPath);
1348
+ const absoluteUserPath = path3.isAbsolute(userPath) ? userPath : path3.resolve(process.cwd(), userPath);
1148
1349
  let pathStats;
1149
1350
  try {
1150
1351
  pathStats = await fs2.stat(absoluteUserPath);
@@ -1154,7 +1355,7 @@ var LintFilesInDirectoryUseCase = class {
1154
1355
  );
1155
1356
  }
1156
1357
  const isFile = pathStats.isFile();
1157
- const directoryForGitOps = isFile ? path2.dirname(absoluteUserPath) : absoluteUserPath;
1358
+ const directoryForGitOps = isFile ? path3.dirname(absoluteUserPath) : absoluteUserPath;
1158
1359
  this.logger.debug(
1159
1360
  `Path type: ${isFile ? "file" : "directory"}, gitOpsDir="${directoryForGitOps}"`
1160
1361
  );
@@ -1170,11 +1371,60 @@ var LintFilesInDirectoryUseCase = class {
1170
1371
  this.logger.debug(
1171
1372
  `Resolved paths: gitRoot="${gitRepoRoot}", lintPath="${absoluteLintPath}"`
1172
1373
  );
1173
- const files = isFile ? [{ path: absoluteLintPath }] : await this.services.listFiles.listFilesInDirectory(
1374
+ let modifiedFiles = null;
1375
+ let modifiedLines = null;
1376
+ if (diffMode) {
1377
+ if (diffMode === "files" /* FILES */) {
1378
+ modifiedFiles = await this.services.gitRemoteUrlService.getModifiedFiles(gitRepoRoot);
1379
+ this.logger.debug(`Found ${modifiedFiles.length} modified files`);
1380
+ if (modifiedFiles.length === 0) {
1381
+ const { gitRemoteUrl: gitRemoteUrl2 } = await this.services.gitRemoteUrlService.getGitRemoteUrl(
1382
+ gitRepoRoot
1383
+ );
1384
+ return {
1385
+ gitRemoteUrl: gitRemoteUrl2,
1386
+ violations: [],
1387
+ summary: {
1388
+ totalFiles: 0,
1389
+ violatedFiles: 0,
1390
+ totalViolations: 0,
1391
+ standardsChecked: []
1392
+ }
1393
+ };
1394
+ }
1395
+ } else if (diffMode === "lines" /* LINES */) {
1396
+ modifiedLines = await this.services.gitRemoteUrlService.getModifiedLines(gitRepoRoot);
1397
+ modifiedFiles = [...new Set(modifiedLines.map((ml) => ml.file))];
1398
+ this.logger.debug(
1399
+ `Found ${modifiedLines.length} modified line ranges in ${modifiedFiles.length} files`
1400
+ );
1401
+ if (modifiedFiles.length === 0) {
1402
+ const { gitRemoteUrl: gitRemoteUrl2 } = await this.services.gitRemoteUrlService.getGitRemoteUrl(
1403
+ gitRepoRoot
1404
+ );
1405
+ return {
1406
+ gitRemoteUrl: gitRemoteUrl2,
1407
+ violations: [],
1408
+ summary: {
1409
+ totalFiles: 0,
1410
+ violatedFiles: 0,
1411
+ totalViolations: 0,
1412
+ standardsChecked: []
1413
+ }
1414
+ };
1415
+ }
1416
+ }
1417
+ }
1418
+ let files = isFile ? [{ path: absoluteLintPath }] : await this.services.listFiles.listFilesInDirectory(
1174
1419
  absoluteLintPath,
1175
1420
  [],
1176
1421
  ["node_modules", "dist", ".min.", ".map.", ".git"]
1177
1422
  );
1423
+ if (modifiedFiles) {
1424
+ const modifiedFilesSet = new Set(modifiedFiles);
1425
+ files = files.filter((file) => modifiedFilesSet.has(file.path));
1426
+ this.logger.debug(`Filtered to ${files.length} modified files`);
1427
+ }
1178
1428
  const { gitRemoteUrl } = await this.services.gitRemoteUrlService.getGitRemoteUrl(gitRepoRoot);
1179
1429
  const { branches } = await this.services.gitRemoteUrlService.getCurrentBranches(gitRepoRoot);
1180
1430
  this.logger.debug(
@@ -1371,7 +1621,17 @@ var LintFilesInDirectoryUseCase = class {
1371
1621
  });
1372
1622
  }
1373
1623
  }
1374
- const totalViolations = violations.reduce(
1624
+ let filteredViolations = violations;
1625
+ if (diffMode === "lines" /* LINES */ && modifiedLines) {
1626
+ filteredViolations = this.services.diffViolationFilterService.filterByLines(
1627
+ violations,
1628
+ modifiedLines
1629
+ );
1630
+ this.logger.debug(
1631
+ `Filtered violations by lines: ${violations.length} -> ${filteredViolations.length}`
1632
+ );
1633
+ }
1634
+ const totalViolations = filteredViolations.reduce(
1375
1635
  (sum, violation) => sum + violation.violations.length,
1376
1636
  0
1377
1637
  );
@@ -1384,10 +1644,10 @@ var LintFilesInDirectoryUseCase = class {
1384
1644
  );
1385
1645
  return {
1386
1646
  gitRemoteUrl,
1387
- violations,
1647
+ violations: filteredViolations,
1388
1648
  summary: {
1389
1649
  totalFiles: files.length,
1390
- violatedFiles: violations.length,
1650
+ violatedFiles: filteredViolations.length,
1391
1651
  totalViolations,
1392
1652
  standardsChecked
1393
1653
  }
@@ -1415,7 +1675,7 @@ var LintFilesInDirectoryUseCase = class {
1415
1675
 
1416
1676
  // apps/cli/src/application/useCases/LintFilesLocallyUseCase.ts
1417
1677
  var import_minimatch2 = require("minimatch");
1418
- var path3 = __toESM(require("path"));
1678
+ var path4 = __toESM(require("path"));
1419
1679
  var fs3 = __toESM(require("fs/promises"));
1420
1680
  var origin3 = "LintFilesLocallyUseCase";
1421
1681
  var LintFilesLocallyUseCase = class {
@@ -1423,6 +1683,7 @@ var LintFilesLocallyUseCase = class {
1423
1683
  this.services = services;
1424
1684
  this.repositories = repositories;
1425
1685
  this.logger = logger2;
1686
+ this.detectionProgramsCache = /* @__PURE__ */ new Map();
1426
1687
  }
1427
1688
  fileMatchesTargetAndScope(filePath, targetPath, scopePatterns) {
1428
1689
  if (!scopePatterns || scopePatterns.length === 0) {
@@ -1458,9 +1719,12 @@ var LintFilesLocallyUseCase = class {
1458
1719
  return pattern;
1459
1720
  }
1460
1721
  async execute(command3) {
1461
- const { path: userPath } = command3;
1462
- this.logger.debug(`Starting local linting: path="${userPath}"`);
1463
- const absoluteUserPath = path3.isAbsolute(userPath) ? userPath : path3.resolve(process.cwd(), userPath);
1722
+ const { path: userPath, diffMode } = command3;
1723
+ this.logger.debug(
1724
+ `Starting local linting: path="${userPath}", diffMode="${diffMode ?? "none"}"`
1725
+ );
1726
+ this.detectionProgramsCache.clear();
1727
+ const absoluteUserPath = path4.isAbsolute(userPath) ? userPath : path4.resolve(process.cwd(), userPath);
1464
1728
  let pathStats;
1465
1729
  try {
1466
1730
  pathStats = await fs3.stat(absoluteUserPath);
@@ -1470,44 +1734,78 @@ var LintFilesLocallyUseCase = class {
1470
1734
  );
1471
1735
  }
1472
1736
  const isFile = pathStats.isFile();
1473
- const directoryForConfig = isFile ? path3.dirname(absoluteUserPath) : absoluteUserPath;
1737
+ const directoryForConfig = isFile ? path4.dirname(absoluteUserPath) : absoluteUserPath;
1474
1738
  const gitRepoRoot = await this.services.gitRemoteUrlService.tryGetGitRepositoryRoot(
1475
1739
  directoryForConfig
1476
1740
  );
1477
- const hierarchicalConfig = await this.repositories.configFileRepository.readHierarchicalConfig(
1741
+ let modifiedFiles = null;
1742
+ let modifiedLines = null;
1743
+ if (diffMode && gitRepoRoot) {
1744
+ if (diffMode === "files" /* FILES */) {
1745
+ modifiedFiles = await this.services.gitRemoteUrlService.getModifiedFiles(gitRepoRoot);
1746
+ this.logger.debug(`Found ${modifiedFiles.length} modified files`);
1747
+ if (modifiedFiles.length === 0) {
1748
+ return {
1749
+ violations: [],
1750
+ summary: {
1751
+ totalFiles: 0,
1752
+ violatedFiles: 0,
1753
+ totalViolations: 0,
1754
+ standardsChecked: []
1755
+ }
1756
+ };
1757
+ }
1758
+ } else if (diffMode === "lines" /* LINES */) {
1759
+ modifiedLines = await this.services.gitRemoteUrlService.getModifiedLines(gitRepoRoot);
1760
+ modifiedFiles = [...new Set(modifiedLines.map((ml) => ml.file))];
1761
+ this.logger.debug(
1762
+ `Found ${modifiedLines.length} modified line ranges in ${modifiedFiles.length} files`
1763
+ );
1764
+ if (modifiedFiles.length === 0) {
1765
+ return {
1766
+ violations: [],
1767
+ summary: {
1768
+ totalFiles: 0,
1769
+ violatedFiles: 0,
1770
+ totalViolations: 0,
1771
+ standardsChecked: []
1772
+ }
1773
+ };
1774
+ }
1775
+ }
1776
+ }
1777
+ const allConfigs = await this.repositories.configFileRepository.findAllConfigsInTree(
1478
1778
  directoryForConfig,
1479
1779
  gitRepoRoot
1480
1780
  );
1481
- if (!hierarchicalConfig.hasConfigs) {
1781
+ if (!allConfigs.hasConfigs) {
1482
1782
  const boundary = gitRepoRoot ?? "filesystem root";
1483
1783
  throw new Error(
1484
1784
  `No packmind.json found between ${directoryForConfig} and ${boundary}. Cannot use local linting.`
1485
1785
  );
1486
1786
  }
1487
- const basePath = gitRepoRoot ?? directoryForConfig;
1787
+ const basePath = allConfigs.basePath;
1488
1788
  this.logger.debug(
1489
- `Found ${hierarchicalConfig.configPaths.length} packmind.json file(s)`
1789
+ `Found ${allConfigs.configs.length} packmind.json file(s)`
1490
1790
  );
1491
- for (const configPath of hierarchicalConfig.configPaths) {
1492
- this.logger.debug(`Using config: ${configPath}`);
1791
+ for (const config of allConfigs.configs) {
1792
+ this.logger.debug(
1793
+ `Using config: ${config.absoluteTargetPath}/packmind.json (target: ${config.targetPath})`
1794
+ );
1493
1795
  }
1494
- const packageSlugs = Object.keys(hierarchicalConfig.packages);
1495
- this.logger.debug(
1496
- `Merged ${packageSlugs.length} packages from configuration files`
1497
- );
1498
- const detectionPrograms = await this.repositories.packmindGateway.getDetectionProgramsForPackages({
1499
- packagesSlugs: packageSlugs
1500
- });
1501
- this.logger.debug(
1502
- `Retrieved detection programs: targetsCount=${detectionPrograms.targets.length}`
1503
- );
1504
- const files = isFile ? [{ path: absoluteUserPath }] : await this.services.listFiles.listFilesInDirectory(
1796
+ let files = isFile ? [{ path: absoluteUserPath }] : await this.services.listFiles.listFilesInDirectory(
1505
1797
  absoluteUserPath,
1506
1798
  [],
1507
1799
  ["node_modules", "dist", ".min.", ".map.", ".git"]
1508
1800
  );
1801
+ if (modifiedFiles) {
1802
+ const modifiedFilesSet = new Set(modifiedFiles);
1803
+ files = files.filter((file) => modifiedFilesSet.has(file.path));
1804
+ this.logger.debug(`Filtered to ${files.length} modified files`);
1805
+ }
1509
1806
  this.logger.debug(`Found ${files.length} files to lint`);
1510
1807
  const violations = [];
1808
+ const allStandardsChecked = /* @__PURE__ */ new Set();
1511
1809
  for (const file of files) {
1512
1810
  const fileViolations = [];
1513
1811
  const relativeFilePath = file.path.startsWith(basePath) ? file.path.substring(basePath.length) : file.path;
@@ -1520,38 +1818,46 @@ var LintFilesLocallyUseCase = class {
1520
1818
  if (!fileLanguage) {
1521
1819
  continue;
1522
1820
  }
1821
+ const matchingTargets = this.findMatchingTargets(
1822
+ file.path,
1823
+ allConfigs.configs
1824
+ );
1523
1825
  const programsByLanguage = /* @__PURE__ */ new Map();
1524
- for (const target of detectionPrograms.targets) {
1525
- for (const standard of target.standards) {
1526
- if (!this.fileMatchesTargetAndScope(
1527
- normalizedFilePath,
1528
- target.path,
1529
- standard.scope
1530
- )) {
1531
- continue;
1532
- }
1533
- for (const rule of standard.rules) {
1534
- for (const activeProgram of rule.activeDetectionPrograms) {
1535
- try {
1536
- const programLanguage = this.resolveProgrammingLanguage(
1537
- activeProgram.language
1538
- );
1539
- if (!programLanguage || programLanguage !== fileLanguage) {
1540
- continue;
1826
+ for (const targetConfig of matchingTargets) {
1827
+ const detectionPrograms = await this.getDetectionProgramsForTarget(targetConfig);
1828
+ for (const target of detectionPrograms.targets) {
1829
+ for (const standard of target.standards) {
1830
+ if (!this.fileMatchesTargetAndScope(
1831
+ normalizedFilePath,
1832
+ targetConfig.targetPath,
1833
+ standard.scope
1834
+ )) {
1835
+ continue;
1836
+ }
1837
+ allStandardsChecked.add(standard.slug);
1838
+ for (const rule of standard.rules) {
1839
+ for (const activeProgram of rule.activeDetectionPrograms) {
1840
+ try {
1841
+ const programLanguage = this.resolveProgrammingLanguage(
1842
+ activeProgram.language
1843
+ );
1844
+ if (!programLanguage || programLanguage !== fileLanguage) {
1845
+ continue;
1846
+ }
1847
+ const programsForLanguage = programsByLanguage.get(programLanguage) ?? [];
1848
+ programsForLanguage.push({
1849
+ code: activeProgram.detectionProgram.code,
1850
+ ruleContent: rule.content,
1851
+ standardSlug: standard.slug,
1852
+ sourceCodeState: activeProgram.detectionProgram.sourceCodeState,
1853
+ language: fileLanguage
1854
+ });
1855
+ programsByLanguage.set(programLanguage, programsForLanguage);
1856
+ } catch (error) {
1857
+ console.error(
1858
+ `Error preparing program for file ${file.path}: ${error}`
1859
+ );
1541
1860
  }
1542
- const programsForLanguage = programsByLanguage.get(programLanguage) ?? [];
1543
- programsForLanguage.push({
1544
- code: activeProgram.detectionProgram.code,
1545
- ruleContent: rule.content,
1546
- standardSlug: standard.slug,
1547
- sourceCodeState: activeProgram.detectionProgram.sourceCodeState,
1548
- language: fileLanguage
1549
- });
1550
- programsByLanguage.set(programLanguage, programsForLanguage);
1551
- } catch (error) {
1552
- console.error(
1553
- `Error preparing program for file ${file.path}: ${error}`
1554
- );
1555
1861
  }
1556
1862
  }
1557
1863
  }
@@ -1590,27 +1896,64 @@ var LintFilesLocallyUseCase = class {
1590
1896
  });
1591
1897
  }
1592
1898
  }
1593
- const totalViolations = violations.reduce(
1899
+ let filteredViolations = violations;
1900
+ if (diffMode === "lines" /* LINES */ && modifiedLines) {
1901
+ filteredViolations = this.services.diffViolationFilterService.filterByLines(
1902
+ violations,
1903
+ modifiedLines
1904
+ );
1905
+ this.logger.debug(
1906
+ `Filtered violations by lines: ${violations.length} -> ${filteredViolations.length}`
1907
+ );
1908
+ }
1909
+ const totalViolations = filteredViolations.reduce(
1594
1910
  (sum, violation) => sum + violation.violations.length,
1595
1911
  0
1596
1912
  );
1597
- const standardsChecked = Array.from(
1598
- new Set(
1599
- detectionPrograms.targets.flatMap(
1600
- (target) => target.standards.map((standard) => standard.slug)
1601
- )
1602
- )
1603
- );
1604
1913
  return {
1605
- violations,
1914
+ violations: filteredViolations,
1606
1915
  summary: {
1607
1916
  totalFiles: files.length,
1608
- violatedFiles: violations.length,
1917
+ violatedFiles: filteredViolations.length,
1609
1918
  totalViolations,
1610
- standardsChecked
1919
+ standardsChecked: Array.from(allStandardsChecked)
1611
1920
  }
1612
1921
  };
1613
1922
  }
1923
+ /**
1924
+ * Finds all targets (configs) that are ancestors of the given file path.
1925
+ * A target matches if the file is located within or under the target's directory.
1926
+ */
1927
+ findMatchingTargets(absoluteFilePath, configs) {
1928
+ return configs.filter(
1929
+ (config) => absoluteFilePath.startsWith(config.absoluteTargetPath + "/") || absoluteFilePath === config.absoluteTargetPath
1930
+ );
1931
+ }
1932
+ /**
1933
+ * Gets detection programs for a target, using cache to avoid redundant API calls.
1934
+ * Cache key is the sorted package slugs to handle identical package sets.
1935
+ */
1936
+ async getDetectionProgramsForTarget(targetConfig) {
1937
+ const packageSlugs = Object.keys(targetConfig.packages).sort(
1938
+ (a, b) => a.localeCompare(b)
1939
+ );
1940
+ const cacheKey = packageSlugs.join(",");
1941
+ const cached = this.detectionProgramsCache.get(cacheKey);
1942
+ if (cached) {
1943
+ this.logger.debug(
1944
+ `Using cached detection programs for packages: ${cacheKey}`
1945
+ );
1946
+ return cached;
1947
+ }
1948
+ this.logger.debug(
1949
+ `Fetching detection programs for packages: ${packageSlugs.join(", ")}`
1950
+ );
1951
+ const detectionPrograms = await this.repositories.packmindGateway.getDetectionProgramsForPackages({
1952
+ packagesSlugs: packageSlugs
1953
+ });
1954
+ this.detectionProgramsCache.set(cacheKey, detectionPrograms);
1955
+ return detectionPrograms;
1956
+ }
1614
1957
  resolveProgrammingLanguage(language) {
1615
1958
  try {
1616
1959
  return stringToProgrammingLanguage(language);
@@ -2065,6 +2408,71 @@ var PackmindGateway = class {
2065
2408
  }
2066
2409
  };
2067
2410
 
2411
+ // apps/cli/src/application/services/DiffViolationFilterService.ts
2412
+ var DiffViolationFilterService = class {
2413
+ /**
2414
+ * Filters violations to only include those in modified files.
2415
+ * @param violations - The list of violations to filter
2416
+ * @param modifiedFiles - The list of absolute paths of modified files
2417
+ * @returns Violations that occur in modified files
2418
+ */
2419
+ filterByFiles(violations, modifiedFiles) {
2420
+ const modifiedFilesSet = new Set(modifiedFiles);
2421
+ return violations.filter(
2422
+ (violation) => modifiedFilesSet.has(violation.file)
2423
+ );
2424
+ }
2425
+ /**
2426
+ * Filters violations to only include those on modified lines.
2427
+ * @param violations - The list of violations to filter
2428
+ * @param modifiedLines - The list of modified line ranges
2429
+ * @returns Violations that occur on modified lines
2430
+ */
2431
+ filterByLines(violations, modifiedLines) {
2432
+ const modifiedLinesByFile = this.groupModifiedLinesByFile(modifiedLines);
2433
+ return violations.map((violation) => {
2434
+ const fileModifications = modifiedLinesByFile.get(violation.file);
2435
+ if (!fileModifications) {
2436
+ return null;
2437
+ }
2438
+ const filteredViolations = violation.violations.filter(
2439
+ (singleViolation) => this.isLineInModifiedRanges(
2440
+ singleViolation.line,
2441
+ fileModifications
2442
+ )
2443
+ );
2444
+ if (filteredViolations.length === 0) {
2445
+ return null;
2446
+ }
2447
+ return {
2448
+ file: violation.file,
2449
+ violations: filteredViolations
2450
+ };
2451
+ }).filter((v) => v !== null);
2452
+ }
2453
+ /**
2454
+ * Groups modified lines by file path for efficient lookup.
2455
+ */
2456
+ groupModifiedLinesByFile(modifiedLines) {
2457
+ const byFile = /* @__PURE__ */ new Map();
2458
+ for (const modification of modifiedLines) {
2459
+ const existing = byFile.get(modification.file) ?? [];
2460
+ existing.push(modification);
2461
+ byFile.set(modification.file, existing);
2462
+ }
2463
+ return byFile;
2464
+ }
2465
+ /**
2466
+ * Checks if a line number falls within any of the modified line ranges.
2467
+ */
2468
+ isLineInModifiedRanges(line, modifications) {
2469
+ return modifications.some((mod) => {
2470
+ const endLine = mod.startLine + mod.lineCount - 1;
2471
+ return line >= mod.startLine && line <= endLine;
2472
+ });
2473
+ }
2474
+ };
2475
+
2068
2476
  // packages/linter-ast/src/core/ParserError.ts
2069
2477
  var ParserNotAvailableError = class extends Error {
2070
2478
  constructor(language, cause) {
@@ -3867,9 +4275,9 @@ var ExecuteLinterProgramsUseCase = class {
3867
4275
  let line;
3868
4276
  let character = 0;
3869
4277
  if (typeof value === "number" && Number.isFinite(value)) {
3870
- line = value;
4278
+ line = value + 1;
3871
4279
  } else if (this.isViolationLike(value)) {
3872
- line = value.line;
4280
+ line = value.line + 1;
3873
4281
  character = value.character ?? 0;
3874
4282
  }
3875
4283
  if (!this.isValidLine(line)) {
@@ -4813,7 +5221,7 @@ ${sectionBlock}
4813
5221
 
4814
5222
  // apps/cli/src/application/useCases/PullDataUseCase.ts
4815
5223
  var fs4 = __toESM(require("fs/promises"));
4816
- var path4 = __toESM(require("path"));
5224
+ var path5 = __toESM(require("path"));
4817
5225
  var PullDataUseCase = class {
4818
5226
  constructor(packmindGateway) {
4819
5227
  this.packmindGateway = packmindGateway;
@@ -4869,8 +5277,8 @@ var PullDataUseCase = class {
4869
5277
  return result;
4870
5278
  }
4871
5279
  async createOrUpdateFile(baseDirectory, file, result) {
4872
- const fullPath = path4.join(baseDirectory, file.path);
4873
- const directory = path4.dirname(fullPath);
5280
+ const fullPath = path5.join(baseDirectory, file.path);
5281
+ const directory = path5.dirname(fullPath);
4874
5282
  await fs4.mkdir(directory, { recursive: true });
4875
5283
  const fileExists = await this.fileExists(fullPath);
4876
5284
  if (file.content !== void 0) {
@@ -4927,7 +5335,7 @@ var PullDataUseCase = class {
4927
5335
  }
4928
5336
  }
4929
5337
  async deleteFile(baseDirectory, filePath, result) {
4930
- const fullPath = path4.join(baseDirectory, filePath);
5338
+ const fullPath = path5.join(baseDirectory, filePath);
4931
5339
  const fileExists = await this.fileExists(fullPath);
4932
5340
  if (fileExists) {
4933
5341
  await fs4.unlink(fullPath);
@@ -5011,10 +5419,41 @@ var GetPackageSummaryUseCase = class {
5011
5419
 
5012
5420
  // apps/cli/src/infra/repositories/ConfigFileRepository.ts
5013
5421
  var fs5 = __toESM(require("fs/promises"));
5014
- var path5 = __toESM(require("path"));
5422
+ var path6 = __toESM(require("path"));
5423
+
5424
+ // apps/cli/src/infra/utils/consoleLogger.ts
5425
+ var import_chalk = __toESM(require("chalk"));
5426
+ var CLI_PREFIX = "packmind-cli";
5427
+ function logWarningConsole(message) {
5428
+ console.warn(import_chalk.default.bgYellow.bold(CLI_PREFIX), import_chalk.default.yellow(message));
5429
+ }
5430
+ function logErrorConsole(message) {
5431
+ console.error(import_chalk.default.bgRed.bold(CLI_PREFIX), import_chalk.default.red(message));
5432
+ }
5433
+ function logSuccessConsole(message) {
5434
+ console.log(import_chalk.default.bgGreen.bold(CLI_PREFIX), import_chalk.default.green.bold(message));
5435
+ }
5436
+ function formatSlug(text) {
5437
+ return import_chalk.default.blue.bold(text);
5438
+ }
5439
+ function formatLabel(text) {
5440
+ return import_chalk.default.dim(text);
5441
+ }
5442
+ function formatError(text) {
5443
+ return import_chalk.default.red(text);
5444
+ }
5445
+ function formatBold(text) {
5446
+ return import_chalk.default.bold(text);
5447
+ }
5448
+ function formatFilePath(text) {
5449
+ return import_chalk.default.underline.gray(text);
5450
+ }
5451
+
5452
+ // apps/cli/src/infra/repositories/ConfigFileRepository.ts
5015
5453
  var ConfigFileRepository = class {
5016
5454
  constructor() {
5017
5455
  this.CONFIG_FILENAME = "packmind.json";
5456
+ this.warnedFiles = /* @__PURE__ */ new Set();
5018
5457
  this.EXCLUDED_DIRECTORIES = [
5019
5458
  "node_modules",
5020
5459
  ".git",
@@ -5025,12 +5464,12 @@ var ConfigFileRepository = class {
5025
5464
  ];
5026
5465
  }
5027
5466
  async writeConfig(baseDirectory, config) {
5028
- const configPath = path5.join(baseDirectory, this.CONFIG_FILENAME);
5467
+ const configPath = path6.join(baseDirectory, this.CONFIG_FILENAME);
5029
5468
  const configContent = JSON.stringify(config, null, 2) + "\n";
5030
5469
  await fs5.writeFile(configPath, configContent, "utf-8");
5031
5470
  }
5032
5471
  async readConfig(baseDirectory) {
5033
- const configPath = path5.join(baseDirectory, this.CONFIG_FILENAME);
5472
+ const configPath = path6.join(baseDirectory, this.CONFIG_FILENAME);
5034
5473
  try {
5035
5474
  const configContent = await fs5.readFile(configPath, "utf-8");
5036
5475
  const config = JSON.parse(configContent);
@@ -5044,9 +5483,11 @@ var ConfigFileRepository = class {
5044
5483
  if (error.code === "ENOENT") {
5045
5484
  return null;
5046
5485
  }
5047
- throw new Error(
5048
- `Failed to read packmind.json: ${error.message}`
5049
- );
5486
+ if (!this.warnedFiles.has(configPath)) {
5487
+ this.warnedFiles.add(configPath);
5488
+ logWarningConsole(`\u26A0 Skipping malformed config file: ${configPath}`);
5489
+ }
5490
+ return null;
5050
5491
  }
5051
5492
  }
5052
5493
  /**
@@ -5057,7 +5498,7 @@ var ConfigFileRepository = class {
5057
5498
  * @returns Array of directory paths that contain a packmind.json file
5058
5499
  */
5059
5500
  async findDescendantConfigs(directory) {
5060
- const normalizedDir = path5.resolve(directory);
5501
+ const normalizedDir = path6.resolve(directory);
5061
5502
  const results = [];
5062
5503
  const searchRecursively = async (currentDir) => {
5063
5504
  let entries;
@@ -5073,7 +5514,7 @@ var ConfigFileRepository = class {
5073
5514
  if (this.EXCLUDED_DIRECTORIES.includes(entry.name)) {
5074
5515
  continue;
5075
5516
  }
5076
- const entryPath = path5.join(currentDir, entry.name);
5517
+ const entryPath = path6.join(currentDir, entry.name);
5077
5518
  const config = await this.readConfig(entryPath);
5078
5519
  if (config) {
5079
5520
  results.push(entryPath);
@@ -5095,19 +5536,19 @@ var ConfigFileRepository = class {
5095
5536
  async readHierarchicalConfig(startDirectory, stopDirectory) {
5096
5537
  const configs = [];
5097
5538
  const configPaths = [];
5098
- const normalizedStart = path5.resolve(startDirectory);
5099
- const normalizedStop = stopDirectory ? path5.resolve(stopDirectory) : null;
5539
+ const normalizedStart = path6.resolve(startDirectory);
5540
+ const normalizedStop = stopDirectory ? path6.resolve(stopDirectory) : null;
5100
5541
  let currentDir = normalizedStart;
5101
5542
  while (true) {
5102
5543
  const config = await this.readConfig(currentDir);
5103
5544
  if (config) {
5104
5545
  configs.push(config);
5105
- configPaths.push(path5.join(currentDir, this.CONFIG_FILENAME));
5546
+ configPaths.push(path6.join(currentDir, this.CONFIG_FILENAME));
5106
5547
  }
5107
5548
  if (normalizedStop !== null && currentDir === normalizedStop) {
5108
5549
  break;
5109
5550
  }
5110
- const parentDir = path5.dirname(currentDir);
5551
+ const parentDir = path6.dirname(currentDir);
5111
5552
  if (parentDir === currentDir) {
5112
5553
  break;
5113
5554
  }
@@ -5127,6 +5568,82 @@ var ConfigFileRepository = class {
5127
5568
  hasConfigs: configs.length > 0
5128
5569
  };
5129
5570
  }
5571
+ /**
5572
+ * Finds all packmind.json files in the tree (both ancestors and descendants)
5573
+ * and returns each config with its target path.
5574
+ *
5575
+ * @param startDirectory - Directory to start searching from (typically the lint target)
5576
+ * @param stopDirectory - Directory to stop ancestor search at (typically git repo root), also used as base for descendants search
5577
+ * @returns All configs found with their target paths
5578
+ */
5579
+ async findAllConfigsInTree(startDirectory, stopDirectory) {
5580
+ const normalizedStart = path6.resolve(startDirectory);
5581
+ const normalizedStop = stopDirectory ? path6.resolve(stopDirectory) : null;
5582
+ const basePath = normalizedStop ?? normalizedStart;
5583
+ const configsMap = /* @__PURE__ */ new Map();
5584
+ let currentDir = normalizedStart;
5585
+ while (true) {
5586
+ const config = await this.readConfig(currentDir);
5587
+ if (config) {
5588
+ const targetPath = this.computeRelativeTargetPath(currentDir, basePath);
5589
+ configsMap.set(currentDir, {
5590
+ targetPath,
5591
+ absoluteTargetPath: currentDir,
5592
+ packages: config.packages
5593
+ });
5594
+ }
5595
+ if (normalizedStop !== null && currentDir === normalizedStop) {
5596
+ break;
5597
+ }
5598
+ const parentDir = path6.dirname(currentDir);
5599
+ if (parentDir === currentDir) {
5600
+ break;
5601
+ }
5602
+ currentDir = parentDir;
5603
+ }
5604
+ const searchRoot = normalizedStop ?? normalizedStart;
5605
+ const descendantDirs = await this.findDescendantConfigs(searchRoot);
5606
+ for (const descendantDir of descendantDirs) {
5607
+ if (configsMap.has(descendantDir)) {
5608
+ continue;
5609
+ }
5610
+ const config = await this.readConfig(descendantDir);
5611
+ if (config) {
5612
+ const targetPath = this.computeRelativeTargetPath(
5613
+ descendantDir,
5614
+ basePath
5615
+ );
5616
+ configsMap.set(descendantDir, {
5617
+ targetPath,
5618
+ absoluteTargetPath: descendantDir,
5619
+ packages: config.packages
5620
+ });
5621
+ }
5622
+ }
5623
+ if (!configsMap.has(searchRoot)) {
5624
+ const rootConfig = await this.readConfig(searchRoot);
5625
+ if (rootConfig) {
5626
+ configsMap.set(searchRoot, {
5627
+ targetPath: "/",
5628
+ absoluteTargetPath: searchRoot,
5629
+ packages: rootConfig.packages
5630
+ });
5631
+ }
5632
+ }
5633
+ const configs = Array.from(configsMap.values());
5634
+ return {
5635
+ configs,
5636
+ hasConfigs: configs.length > 0,
5637
+ basePath
5638
+ };
5639
+ }
5640
+ computeRelativeTargetPath(absolutePath, basePath) {
5641
+ if (absolutePath === basePath) {
5642
+ return "/";
5643
+ }
5644
+ const relativePath = absolutePath.substring(basePath.length);
5645
+ return relativePath.startsWith("/") ? relativePath : "/" + relativePath;
5646
+ }
5130
5647
  };
5131
5648
 
5132
5649
  // apps/cli/src/PackmindCliHexaFactory.ts
@@ -5142,7 +5659,8 @@ var PackmindCliHexaFactory = class {
5142
5659
  this.services = {
5143
5660
  listFiles: new ListFiles(),
5144
5661
  gitRemoteUrlService: new GitService(this.logger),
5145
- linterExecutionUseCase: new ExecuteLinterProgramsUseCase()
5662
+ linterExecutionUseCase: new ExecuteLinterProgramsUseCase(),
5663
+ diffViolationFilterService: new DiffViolationFilterService()
5146
5664
  };
5147
5665
  this.useCases = {
5148
5666
  executeSingleFileAst: new ExecuteSingleFileAstUseCase(
@@ -5238,8 +5756,8 @@ var PackmindCliHexa = class {
5238
5756
  (version) => version !== "*"
5239
5757
  );
5240
5758
  if (hasNonWildcardVersions) {
5241
- console.log(
5242
- "WARN: Package versions are not supported yet, getting the latest version"
5759
+ logWarningConsole(
5760
+ "Package versions are not supported yet, getting the latest version"
5243
5761
  );
5244
5762
  }
5245
5763
  return Object.keys(config.packages);
@@ -5284,7 +5802,6 @@ var IDELintLogger = class {
5284
5802
  };
5285
5803
 
5286
5804
  // apps/cli/src/infra/repositories/HumanReadableLogger.ts
5287
- var import_chalk = __toESM(require("chalk"));
5288
5805
  var HumanReadableLogger = class {
5289
5806
  logViolations(violations) {
5290
5807
  violations.forEach((violation) => {
@@ -5295,24 +5812,18 @@ var HumanReadableLogger = class {
5295
5812
  (acc, violation) => acc + violation.violations.length,
5296
5813
  0
5297
5814
  );
5298
- console.log(
5299
- import_chalk.default.bgRed.bold("packmind-cli"),
5300
- import_chalk.default.red(
5301
- `\u274C Found ${import_chalk.default.bold(totalViolationCount)} violation(s) in ${import_chalk.default.bold(violations.length)} file(s)`
5302
- )
5815
+ logErrorConsole(
5816
+ `\u274C Found ${formatBold(String(totalViolationCount))} violation(s) in ${formatBold(String(violations.length))} file(s)`
5303
5817
  );
5304
5818
  } else {
5305
- console.log(
5306
- import_chalk.default.bgGreen.bold("packmind-cli"),
5307
- import_chalk.default.green.bold(`\u2705 No violations found`)
5308
- );
5819
+ logSuccessConsole(`\u2705 No violations found`);
5309
5820
  }
5310
5821
  }
5311
5822
  logViolation(violation) {
5312
- console.log(import_chalk.default.underline.gray(violation.file));
5823
+ console.log(formatFilePath(violation.file));
5313
5824
  violation.violations.forEach(({ line, character, standard, rule }) => {
5314
5825
  console.log(
5315
- import_chalk.default.red(` ${line}:${character} error @${standard}/${rule}`)
5826
+ formatError(` ${line}:${character} error @${standard}/${rule}`)
5316
5827
  );
5317
5828
  });
5318
5829
  }
@@ -5320,6 +5831,99 @@ var HumanReadableLogger = class {
5320
5831
 
5321
5832
  // apps/cli/src/infra/commands/LinterCommand.ts
5322
5833
  var pathModule = __toESM(require("path"));
5834
+
5835
+ // apps/cli/src/infra/commands/lintHandler.ts
5836
+ var MISSING_API_KEY_ERROR = "Please set the PACKMIND_API_KEY_V3 environment variable";
5837
+ function isMissingApiKeyError(error) {
5838
+ if (error instanceof Error) {
5839
+ return error.message.includes(MISSING_API_KEY_ERROR);
5840
+ }
5841
+ return false;
5842
+ }
5843
+ async function lintHandler(args2, deps) {
5844
+ const {
5845
+ path: path8,
5846
+ draft,
5847
+ rule,
5848
+ language,
5849
+ logger: logger2,
5850
+ continueOnError,
5851
+ continueOnMissingKey,
5852
+ diff
5853
+ } = args2;
5854
+ const {
5855
+ packmindCliHexa,
5856
+ humanReadableLogger,
5857
+ ideLintLogger,
5858
+ resolvePath,
5859
+ exit
5860
+ } = deps;
5861
+ if (draft && !rule) {
5862
+ throw new Error("option --rule is required to use --draft mode");
5863
+ }
5864
+ const startedAt = Date.now();
5865
+ const targetPath = path8 ?? ".";
5866
+ const hasArguments = !!(draft || rule || language);
5867
+ const absolutePath = resolvePath(targetPath);
5868
+ if (diff) {
5869
+ const gitRoot = await packmindCliHexa.tryGetGitRepositoryRoot(absolutePath);
5870
+ if (!gitRoot) {
5871
+ throw new Error(
5872
+ "The --diff option requires the project to be in a Git repository"
5873
+ );
5874
+ }
5875
+ }
5876
+ let useLocalLinting = false;
5877
+ if (!hasArguments) {
5878
+ const stopDirectory = await packmindCliHexa.tryGetGitRepositoryRoot(absolutePath);
5879
+ const hierarchicalConfig = await packmindCliHexa.readHierarchicalConfig(
5880
+ absolutePath,
5881
+ stopDirectory
5882
+ );
5883
+ if (hierarchicalConfig.hasConfigs) {
5884
+ useLocalLinting = true;
5885
+ }
5886
+ }
5887
+ let violations = [];
5888
+ try {
5889
+ if (useLocalLinting) {
5890
+ const result = await packmindCliHexa.lintFilesLocally({
5891
+ path: absolutePath,
5892
+ diffMode: diff
5893
+ });
5894
+ violations = result.violations;
5895
+ } else {
5896
+ const result = await packmindCliHexa.lintFilesInDirectory({
5897
+ path: targetPath,
5898
+ draftMode: draft,
5899
+ standardSlug: rule?.standardSlug,
5900
+ ruleId: rule?.ruleId,
5901
+ language,
5902
+ diffMode: diff
5903
+ });
5904
+ violations = result.violations;
5905
+ }
5906
+ } catch (error) {
5907
+ if (isMissingApiKeyError(error) && continueOnMissingKey) {
5908
+ console.warn("Warning: No PACKMIND_API_KEY_V3 set, linting is skipped.");
5909
+ exit(0);
5910
+ return;
5911
+ }
5912
+ throw error;
5913
+ }
5914
+ (logger2 === "ide" /* ide */ ? ideLintLogger : humanReadableLogger).logViolations(
5915
+ violations
5916
+ );
5917
+ const durationSeconds = (Date.now() - startedAt) / 1e3;
5918
+ console.log(`Lint completed in ${durationSeconds.toFixed(2)}s`);
5919
+ if (violations.length > 0 && !continueOnError) {
5920
+ exit(1);
5921
+ } else {
5922
+ exit(0);
5923
+ }
5924
+ }
5925
+
5926
+ // apps/cli/src/infra/commands/LinterCommand.ts
5323
5927
  var Logger = {
5324
5928
  from: async (input) => {
5325
5929
  switch (input) {
@@ -5347,6 +5951,19 @@ var RuleID = {
5347
5951
  };
5348
5952
  }
5349
5953
  };
5954
+ var DiffModeType = {
5955
+ from: async (input) => {
5956
+ switch (input) {
5957
+ case "files":
5958
+ return "files" /* FILES */;
5959
+ case "lines":
5960
+ return "lines" /* LINES */;
5961
+ }
5962
+ throw new Error(
5963
+ `${input} is not a valid value for the --diff option. Expected values are: files, lines`
5964
+ );
5965
+ }
5966
+ };
5350
5967
  var lintCommand = (0, import_cmd_ts.command)({
5351
5968
  name: "lint",
5352
5969
  description: "Lint code at the specified path",
@@ -5380,72 +5997,34 @@ var lintCommand = (0, import_cmd_ts.command)({
5380
5997
  debug: (0, import_cmd_ts.flag)({
5381
5998
  long: "debug",
5382
5999
  description: "Enable debug logging"
6000
+ }),
6001
+ continueOnError: (0, import_cmd_ts.flag)({
6002
+ long: "continue-on-error",
6003
+ description: "Exit with status code 0 even if violations are found"
6004
+ }),
6005
+ continueOnMissingKey: (0, import_cmd_ts.flag)({
6006
+ long: "continue-on-missing-key",
6007
+ description: "Skip linting and exit with status code 0 if PACKMIND_API_KEY_V3 is not set"
6008
+ }),
6009
+ diff: (0, import_cmd_ts.option)({
6010
+ long: "diff",
6011
+ description: "Filter violations by git diff (files | lines)",
6012
+ type: (0, import_cmd_ts.optional)(DiffModeType)
5383
6013
  })
5384
6014
  },
5385
- handler: async ({ path: path7, draft, rule, debug, language, logger: logger2 }) => {
5386
- if (draft && !rule) {
5387
- throw new Error("option --rule is required to use --draft mode");
5388
- }
5389
- const startedAt = Date.now();
6015
+ handler: async (args2) => {
5390
6016
  const packmindLogger = new PackmindLogger(
5391
6017
  "PackmindCLI",
5392
- debug ? "debug" /* DEBUG */ : "info" /* INFO */
6018
+ args2.debug ? "debug" /* DEBUG */ : "info" /* INFO */
5393
6019
  );
5394
- const packmindCliHexa = new PackmindCliHexa(packmindLogger);
5395
- const targetPath = path7 ?? ".";
5396
- const hasArguments = !!(draft || rule || language);
5397
- const absolutePath = pathModule.isAbsolute(targetPath) ? targetPath : pathModule.resolve(process.cwd(), targetPath);
5398
- let useLocalLinting = false;
5399
- let lintTargets = [];
5400
- if (!hasArguments) {
5401
- const stopDirectory = await packmindCliHexa.tryGetGitRepositoryRoot(absolutePath);
5402
- const hierarchicalConfig = await packmindCliHexa.readHierarchicalConfig(
5403
- absolutePath,
5404
- stopDirectory
5405
- );
5406
- if (hierarchicalConfig.hasConfigs) {
5407
- useLocalLinting = true;
5408
- const rootConfig = await packmindCliHexa.readHierarchicalConfig(
5409
- absolutePath,
5410
- absolutePath
5411
- );
5412
- if (rootConfig.hasConfigs) {
5413
- lintTargets.push(absolutePath);
5414
- }
5415
- const descendantTargets = await packmindCliHexa.findDescendantConfigs(absolutePath);
5416
- lintTargets = [...lintTargets, ...descendantTargets];
5417
- if (lintTargets.length === 0) {
5418
- lintTargets.push(absolutePath);
5419
- }
5420
- }
5421
- }
5422
- let violations = [];
5423
- if (useLocalLinting && lintTargets.length > 0) {
5424
- for (const target of lintTargets) {
5425
- packmindLogger.debug(`Linting target: ${target}`);
5426
- const result = await packmindCliHexa.lintFilesLocally({
5427
- path: target
5428
- });
5429
- violations = [...violations, ...result.violations];
5430
- }
5431
- } else {
5432
- const result = await packmindCliHexa.lintFilesInDirectory({
5433
- path: targetPath,
5434
- draftMode: draft,
5435
- standardSlug: rule?.standardSlug,
5436
- ruleId: rule?.ruleId,
5437
- language
5438
- });
5439
- violations = result.violations;
5440
- }
5441
- (logger2 === "ide" /* ide */ ? new IDELintLogger() : new HumanReadableLogger()).logViolations(violations);
5442
- const durationSeconds = (Date.now() - startedAt) / 1e3;
5443
- console.log(`Lint completed in ${durationSeconds.toFixed(2)}s`);
5444
- if (violations.length > 0) {
5445
- process.exit(1);
5446
- } else {
5447
- process.exit(0);
5448
- }
6020
+ const deps = {
6021
+ packmindCliHexa: new PackmindCliHexa(packmindLogger),
6022
+ humanReadableLogger: new HumanReadableLogger(),
6023
+ ideLintLogger: new IDELintLogger(),
6024
+ resolvePath: (targetPath) => pathModule.isAbsolute(targetPath) ? targetPath : pathModule.resolve(process.cwd(), targetPath),
6025
+ exit: (code) => process.exit(code)
6026
+ };
6027
+ await lintHandler(args2, deps);
5449
6028
  }
5450
6029
  });
5451
6030
 
@@ -5506,7 +6085,7 @@ function extractWasmFiles() {
5506
6085
  // apps/cli/src/main.ts
5507
6086
  var import_dotenv = require("dotenv");
5508
6087
  var fs6 = __toESM(require("fs"));
5509
- var path6 = __toESM(require("path"));
6088
+ var path7 = __toESM(require("path"));
5510
6089
 
5511
6090
  // apps/cli/src/infra/commands/PullCommand.ts
5512
6091
  var import_cmd_ts2 = require("cmd-ts");
@@ -5541,14 +6120,28 @@ var pullCommand = (0, import_cmd_ts2.command)({
5541
6120
  console.log("No packages found.");
5542
6121
  process.exit(0);
5543
6122
  }
5544
- console.log("Available packages:");
5545
- packages.forEach((pkg) => {
5546
- console.log(`- ${pkg.name} (${pkg.slug})`);
6123
+ const sortedPackages = [...packages].sort(
6124
+ (a, b) => a.slug.localeCompare(b.slug)
6125
+ );
6126
+ console.log("Available packages:\n");
6127
+ sortedPackages.forEach((pkg, index) => {
6128
+ console.log(`- ${formatSlug(pkg.slug)}`);
6129
+ console.log(` ${formatLabel("Name:")} ${pkg.name}`);
5547
6130
  if (pkg.description) {
5548
- console.log(` ${pkg.description}`);
6131
+ const descriptionLines = pkg.description.trim().split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6132
+ const [firstLine, ...restLines] = descriptionLines;
6133
+ console.log(` ${formatLabel("Description:")} ${firstLine}`);
6134
+ restLines.forEach((line) => {
6135
+ console.log(` ${line}`);
6136
+ });
6137
+ }
6138
+ if (index < sortedPackages.length - 1) {
5549
6139
  console.log("");
5550
6140
  }
5551
6141
  });
6142
+ const exampleSlug = formatSlug(sortedPackages[0].slug);
6143
+ console.log("\nHow to install a package:\n");
6144
+ console.log(` $ packmind-cli install ${exampleSlug}`);
5552
6145
  process.exit(0);
5553
6146
  } catch (error) {
5554
6147
  console.error("\n\u274C Failed to list packages:");
@@ -5623,7 +6216,7 @@ var pullCommand = (0, import_cmd_ts2.command)({
5623
6216
  }
5624
6217
  const allPackages = [.../* @__PURE__ */ new Set([...configPackages, ...packagesSlugs])];
5625
6218
  if (allPackages.length === 0) {
5626
- console.log("WARN config packmind.json not found");
6219
+ logWarningConsole("config packmind.json not found");
5627
6220
  console.log(
5628
6221
  "Usage: packmind-cli install <package-slug> [package-slug...]"
5629
6222
  );
@@ -5737,29 +6330,23 @@ added ${result.filesCreated} files, changed ${result.filesUpdated} files, remove
5737
6330
  // apps/cli/src/main.ts
5738
6331
  var { version: CLI_VERSION } = require_package();
5739
6332
  function findEnvFile() {
5740
- let currentDir = process.cwd();
5741
- const startDir = currentDir;
5742
- let gitRootFound = false;
6333
+ const currentDir = process.cwd();
6334
+ const gitService = new GitService();
6335
+ const gitRoot = gitService.getGitRepositoryRootSync(currentDir);
6336
+ const filesystemRoot = path7.parse(currentDir).root;
6337
+ const stopDir = gitRoot ?? filesystemRoot;
5743
6338
  let searchDir = currentDir;
5744
- while (searchDir !== path6.parse(searchDir).root) {
5745
- if (fs6.existsSync(path6.join(searchDir, ".git"))) {
5746
- gitRootFound = true;
5747
- break;
5748
- }
5749
- searchDir = path6.dirname(searchDir);
5750
- }
5751
- while (currentDir !== path6.parse(currentDir).root) {
5752
- const envPath2 = path6.join(currentDir, ".env");
6339
+ let parentDir = path7.dirname(searchDir);
6340
+ while (searchDir !== parentDir) {
6341
+ const envPath2 = path7.join(searchDir, ".env");
5753
6342
  if (fs6.existsSync(envPath2)) {
5754
6343
  return envPath2;
5755
6344
  }
5756
- if (gitRootFound && fs6.existsSync(path6.join(currentDir, ".git"))) {
5757
- break;
5758
- }
5759
- if (!gitRootFound && currentDir !== startDir) {
5760
- break;
6345
+ if (searchDir === stopDir) {
6346
+ return null;
5761
6347
  }
5762
- currentDir = path6.dirname(currentDir);
6348
+ searchDir = parentDir;
6349
+ parentDir = path7.dirname(searchDir);
5763
6350
  }
5764
6351
  return null;
5765
6352
  }
@@ -5789,6 +6376,6 @@ var app = (0, import_cmd_ts3.subcommands)({
5789
6376
  }
5790
6377
  });
5791
6378
  (0, import_cmd_ts3.run)(app, args).catch((error) => {
5792
- console.error(import_chalk2.default.bgRed.bold("packmind-cli"), import_chalk2.default.red(error.message));
6379
+ logErrorConsole(error.message);
5793
6380
  process.exit(1);
5794
6381
  });