@packmind/cli 0.4.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 +1613 -194
  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.4.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,39 +257,64 @@ 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
- var ProgrammingLanguage = /* @__PURE__ */ ((ProgrammingLanguage3) => {
263
- ProgrammingLanguage3["JAVASCRIPT"] = "JAVASCRIPT";
264
- ProgrammingLanguage3["JAVASCRIPT_JSX"] = "JAVASCRIPT_JSX";
265
- ProgrammingLanguage3["TYPESCRIPT"] = "TYPESCRIPT";
266
- ProgrammingLanguage3["TYPESCRIPT_TSX"] = "TYPESCRIPT_TSX";
267
- ProgrammingLanguage3["PYTHON"] = "PYTHON";
268
- ProgrammingLanguage3["PHP"] = "PHP";
269
- ProgrammingLanguage3["JAVA"] = "JAVA";
270
- ProgrammingLanguage3["SCSS"] = "SCSS";
271
- ProgrammingLanguage3["HTML"] = "HTML";
272
- ProgrammingLanguage3["CSHARP"] = "CSHARP";
273
- ProgrammingLanguage3["GENERIC"] = "GENERIC";
274
- ProgrammingLanguage3["GO"] = "GO";
275
- ProgrammingLanguage3["C"] = "C";
276
- ProgrammingLanguage3["CPP"] = "CPP";
277
- ProgrammingLanguage3["SQL"] = "SQL";
278
- ProgrammingLanguage3["KOTLIN"] = "KOTLIN";
279
- ProgrammingLanguage3["VUE"] = "VUE";
280
- ProgrammingLanguage3["CSS"] = "CSS";
281
- ProgrammingLanguage3["YAML"] = "YAML";
282
- ProgrammingLanguage3["JSON"] = "JSON";
283
- ProgrammingLanguage3["XML"] = "XML";
284
- ProgrammingLanguage3["BASH"] = "BASH";
285
- ProgrammingLanguage3["MARKDOWN"] = "MARKDOWN";
286
- ProgrammingLanguage3["RUBY"] = "RUBY";
287
- ProgrammingLanguage3["RUST"] = "RUST";
288
- ProgrammingLanguage3["SAP_ABAP"] = "SAP_ABAP";
289
- ProgrammingLanguage3["SAP_CDS"] = "SAP_CDS";
290
- ProgrammingLanguage3["SAP_HANA_SQL"] = "SAP_HANA_SQL";
291
- ProgrammingLanguage3["SWIFT"] = "SWIFT";
292
- ProgrammingLanguage3["PROPERTIES"] = "PROPERTIES";
293
- return ProgrammingLanguage3;
286
+ var ProgrammingLanguage = /* @__PURE__ */ ((ProgrammingLanguage4) => {
287
+ ProgrammingLanguage4["JAVASCRIPT"] = "JAVASCRIPT";
288
+ ProgrammingLanguage4["JAVASCRIPT_JSX"] = "JAVASCRIPT_JSX";
289
+ ProgrammingLanguage4["TYPESCRIPT"] = "TYPESCRIPT";
290
+ ProgrammingLanguage4["TYPESCRIPT_TSX"] = "TYPESCRIPT_TSX";
291
+ ProgrammingLanguage4["PYTHON"] = "PYTHON";
292
+ ProgrammingLanguage4["PHP"] = "PHP";
293
+ ProgrammingLanguage4["JAVA"] = "JAVA";
294
+ ProgrammingLanguage4["SCSS"] = "SCSS";
295
+ ProgrammingLanguage4["HTML"] = "HTML";
296
+ ProgrammingLanguage4["CSHARP"] = "CSHARP";
297
+ ProgrammingLanguage4["GENERIC"] = "GENERIC";
298
+ ProgrammingLanguage4["GO"] = "GO";
299
+ ProgrammingLanguage4["C"] = "C";
300
+ ProgrammingLanguage4["CPP"] = "CPP";
301
+ ProgrammingLanguage4["SQL"] = "SQL";
302
+ ProgrammingLanguage4["KOTLIN"] = "KOTLIN";
303
+ ProgrammingLanguage4["VUE"] = "VUE";
304
+ ProgrammingLanguage4["CSS"] = "CSS";
305
+ ProgrammingLanguage4["YAML"] = "YAML";
306
+ ProgrammingLanguage4["JSON"] = "JSON";
307
+ ProgrammingLanguage4["XML"] = "XML";
308
+ ProgrammingLanguage4["BASH"] = "BASH";
309
+ ProgrammingLanguage4["MARKDOWN"] = "MARKDOWN";
310
+ ProgrammingLanguage4["RUBY"] = "RUBY";
311
+ ProgrammingLanguage4["RUST"] = "RUST";
312
+ ProgrammingLanguage4["SAP_ABAP"] = "SAP_ABAP";
313
+ ProgrammingLanguage4["SAP_CDS"] = "SAP_CDS";
314
+ ProgrammingLanguage4["SAP_HANA_SQL"] = "SAP_HANA_SQL";
315
+ ProgrammingLanguage4["SWIFT"] = "SWIFT";
316
+ ProgrammingLanguage4["PROPERTIES"] = "PROPERTIES";
317
+ return ProgrammingLanguage4;
294
318
  })(ProgrammingLanguage || {});
295
319
  var ProgrammingLanguageDetails = {
296
320
  ["GENERIC" /* GENERIC */]: {
@@ -458,6 +482,262 @@ var createDetectionProgramId = brandedIdFactory();
458
482
  // packages/types/src/linter/RuleDetectionAssessment.ts
459
483
  var createRuleDetectionAssessmentId = brandedIdFactory();
460
484
 
485
+ // packages/types/src/llm/LLMProviderMetadata.ts
486
+ var DEFAULT_OPENAI_MODELS = {
487
+ model: "gpt-5.1",
488
+ fastestModel: "gpt-4.1-mini"
489
+ };
490
+ var DEFAULT_ANTHROPIC_MODELS = {
491
+ model: "claude-sonnet-4-5-20250929",
492
+ fastestModel: "claude-haiku-4-5-20251001"
493
+ };
494
+ var DEFAULT_GEMINI_MODELS = {
495
+ model: "gemini-3-pro-preview",
496
+ fastestModel: "gemini-2.5-flash"
497
+ };
498
+ var DEFAULT_AZURE_OPENAI_API_VERSION = "2024-12-01-preview";
499
+ var LLM_PROVIDER_METADATA = {
500
+ ["openai" /* OPENAI */]: {
501
+ id: "openai" /* OPENAI */,
502
+ displayName: "OpenAI",
503
+ description: "OpenAI GPT models including GPT-4 and GPT-5. Requires an API key from OpenAI.",
504
+ defaultModel: DEFAULT_OPENAI_MODELS.model,
505
+ defaultFastModel: DEFAULT_OPENAI_MODELS.fastestModel,
506
+ documentationUrl: "https://platform.openai.com/docs",
507
+ fields: [
508
+ {
509
+ name: "apiKey",
510
+ label: "API Key",
511
+ type: "password",
512
+ defaultValue: "",
513
+ helpMessage: "Your OpenAI API key from platform.openai.com.",
514
+ optional: false,
515
+ placeholder: "sk-...",
516
+ secret: true
517
+ },
518
+ {
519
+ name: "model",
520
+ label: "Model",
521
+ type: "text",
522
+ defaultValue: DEFAULT_OPENAI_MODELS.model,
523
+ helpMessage: "The primary model to use for standard operations. Defaults to the latest recommended model.",
524
+ optional: true,
525
+ placeholder: "gpt-5.1",
526
+ secret: false
527
+ },
528
+ {
529
+ name: "fastestModel",
530
+ label: "Fast Model",
531
+ type: "text",
532
+ defaultValue: DEFAULT_OPENAI_MODELS.fastestModel,
533
+ helpMessage: "A faster, more economical model for less complex operations.",
534
+ optional: true,
535
+ placeholder: "gpt-4.1-mini",
536
+ secret: false
537
+ }
538
+ ]
539
+ },
540
+ ["anthropic" /* ANTHROPIC */]: {
541
+ id: "anthropic" /* ANTHROPIC */,
542
+ displayName: "Anthropic Claude",
543
+ description: "Anthropic Claude models known for safety and helpfulness. Requires an API key from Anthropic.",
544
+ defaultModel: DEFAULT_ANTHROPIC_MODELS.model,
545
+ defaultFastModel: DEFAULT_ANTHROPIC_MODELS.fastestModel,
546
+ documentationUrl: "https://docs.anthropic.com",
547
+ fields: [
548
+ {
549
+ name: "apiKey",
550
+ label: "API Key",
551
+ type: "password",
552
+ defaultValue: "",
553
+ helpMessage: "Your Anthropic API key from console.anthropic.com.",
554
+ optional: false,
555
+ placeholder: "sk-ant-...",
556
+ secret: true
557
+ },
558
+ {
559
+ name: "model",
560
+ label: "Model",
561
+ type: "text",
562
+ defaultValue: DEFAULT_ANTHROPIC_MODELS.model,
563
+ helpMessage: "The primary Claude model to use. Defaults to the latest Sonnet model.",
564
+ optional: true,
565
+ placeholder: "claude-sonnet-4-5-20250929",
566
+ secret: false
567
+ },
568
+ {
569
+ name: "fastestModel",
570
+ label: "Fast Model",
571
+ type: "text",
572
+ defaultValue: DEFAULT_ANTHROPIC_MODELS.fastestModel,
573
+ helpMessage: "A faster Claude model for less complex operations. Defaults to Haiku.",
574
+ optional: true,
575
+ placeholder: "claude-haiku-4-5-20251001",
576
+ secret: false
577
+ }
578
+ ]
579
+ },
580
+ ["gemini" /* GEMINI */]: {
581
+ id: "gemini" /* GEMINI */,
582
+ displayName: "Google Gemini",
583
+ description: "Google's Gemini models with multimodal capabilities. Requires an API key from Google AI Studio.",
584
+ defaultModel: DEFAULT_GEMINI_MODELS.model,
585
+ defaultFastModel: DEFAULT_GEMINI_MODELS.fastestModel,
586
+ documentationUrl: "https://ai.google.dev/docs",
587
+ fields: [
588
+ {
589
+ name: "apiKey",
590
+ label: "API Key",
591
+ type: "password",
592
+ defaultValue: "",
593
+ helpMessage: "Your Google AI API key from aistudio.google.com.",
594
+ optional: false,
595
+ placeholder: "AIza...",
596
+ secret: true
597
+ },
598
+ {
599
+ name: "model",
600
+ label: "Model",
601
+ type: "text",
602
+ defaultValue: DEFAULT_GEMINI_MODELS.model,
603
+ helpMessage: "The primary Gemini model to use. Defaults to the latest Pro model.",
604
+ optional: true,
605
+ placeholder: "gemini-3-pro-preview",
606
+ secret: false
607
+ },
608
+ {
609
+ name: "fastestModel",
610
+ label: "Fast Model",
611
+ type: "text",
612
+ defaultValue: DEFAULT_GEMINI_MODELS.fastestModel,
613
+ helpMessage: "A faster Gemini model for less complex operations. Defaults to Flash.",
614
+ optional: true,
615
+ placeholder: "gemini-2.5-flash",
616
+ secret: false
617
+ }
618
+ ]
619
+ },
620
+ ["azure-openai" /* AZURE_OPENAI */]: {
621
+ id: "azure-openai" /* AZURE_OPENAI */,
622
+ displayName: "Azure OpenAI",
623
+ description: "Microsoft Azure-hosted OpenAI models. Requires Azure deployment names and credentials.",
624
+ defaultModel: "",
625
+ defaultFastModel: "",
626
+ documentationUrl: "https://learn.microsoft.com/en-us/azure/ai-services/openai/",
627
+ fields: [
628
+ {
629
+ name: "model",
630
+ label: "Model Deployment Name",
631
+ type: "text",
632
+ defaultValue: "",
633
+ helpMessage: "The Azure deployment name for the primary model. This is the name you gave your deployment in Azure Portal.",
634
+ optional: false,
635
+ placeholder: "my-gpt-4-deployment",
636
+ secret: false
637
+ },
638
+ {
639
+ name: "fastestModel",
640
+ label: "Fast Model Deployment Name",
641
+ type: "text",
642
+ defaultValue: "",
643
+ helpMessage: "The Azure deployment name for the fast/economical model.",
644
+ optional: false,
645
+ placeholder: "my-gpt-35-turbo-deployment",
646
+ secret: false
647
+ },
648
+ {
649
+ name: "endpoint",
650
+ label: "Endpoint URL",
651
+ type: "url",
652
+ defaultValue: "",
653
+ helpMessage: "Your Azure OpenAI endpoint URL.",
654
+ optional: false,
655
+ placeholder: "https://your-resource.openai.azure.com",
656
+ secret: false
657
+ },
658
+ {
659
+ name: "apiKey",
660
+ label: "API Key",
661
+ type: "password",
662
+ defaultValue: "",
663
+ helpMessage: "Your Azure OpenAI API key.",
664
+ optional: false,
665
+ placeholder: "",
666
+ secret: true
667
+ },
668
+ {
669
+ name: "apiVersion",
670
+ label: "API Version",
671
+ type: "text",
672
+ defaultValue: DEFAULT_AZURE_OPENAI_API_VERSION,
673
+ helpMessage: "The Azure OpenAI API version to use. Defaults to the latest stable version.",
674
+ optional: true,
675
+ placeholder: "2024-12-01-preview",
676
+ secret: false
677
+ }
678
+ ]
679
+ },
680
+ ["openai-compatible" /* OPENAI_COMPATIBLE */]: {
681
+ id: "openai-compatible" /* OPENAI_COMPATIBLE */,
682
+ displayName: "OpenAI-Compatible",
683
+ description: "Any OpenAI-compatible API endpoint. Use this for local models (Ollama, LM Studio) or other compatible providers.",
684
+ defaultModel: "",
685
+ defaultFastModel: "",
686
+ documentationUrl: void 0,
687
+ fields: [
688
+ {
689
+ name: "llmEndpoint",
690
+ label: "Endpoint URL",
691
+ type: "url",
692
+ defaultValue: "",
693
+ helpMessage: "The base URL of the OpenAI-compatible API endpoint (e.g., http://localhost:11434/v1 for Ollama).",
694
+ optional: false,
695
+ placeholder: "http://localhost:11434/v1",
696
+ secret: false
697
+ },
698
+ {
699
+ name: "llmApiKey",
700
+ label: "API Key",
701
+ type: "password",
702
+ defaultValue: "",
703
+ helpMessage: "API key for authentication. Some local providers may not require this.",
704
+ optional: false,
705
+ placeholder: "",
706
+ secret: true
707
+ },
708
+ {
709
+ name: "model",
710
+ label: "Model",
711
+ type: "text",
712
+ defaultValue: "",
713
+ helpMessage: "The model identifier to use for standard operations (e.g., llama3, mistral).",
714
+ optional: false,
715
+ placeholder: "llama3",
716
+ secret: false
717
+ },
718
+ {
719
+ name: "fastestModel",
720
+ label: "Fast Model",
721
+ type: "text",
722
+ defaultValue: "",
723
+ helpMessage: "A faster model for less complex operations. Can be the same as the primary model.",
724
+ optional: false,
725
+ placeholder: "llama3",
726
+ secret: false
727
+ }
728
+ ]
729
+ },
730
+ ["packmind" /* PACKMIND */]: {
731
+ id: "packmind" /* PACKMIND */,
732
+ displayName: "Packmind (SaaS)",
733
+ description: "Packmind managed LLM service. Uses the platform default provider configuration.",
734
+ defaultModel: DEFAULT_OPENAI_MODELS.model,
735
+ defaultFastModel: DEFAULT_OPENAI_MODELS.fastestModel,
736
+ documentationUrl: void 0,
737
+ fields: []
738
+ }
739
+ };
740
+
461
741
  // packages/types/src/sse/SSEEvent.ts
462
742
  function createProgramStatusChangeEvent(ruleId, language) {
463
743
  return {
@@ -537,33 +817,53 @@ var ExecuteSingleFileAstUseCase = class _ExecuteSingleFileAstUseCase {
537
817
  // apps/cli/src/application/services/GitService.ts
538
818
  var import_child_process = require("child_process");
539
819
  var import_util = require("util");
820
+ var path = __toESM(require("path"));
540
821
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
541
822
  var origin = "GitService";
542
823
  var GitService = class {
543
824
  constructor(logger2 = new PackmindLogger(origin)) {
544
825
  this.logger = logger2;
545
826
  }
546
- async getGitRepositoryRoot(path5) {
827
+ async getGitRepositoryRoot(path8) {
547
828
  try {
548
829
  const { stdout } = await execAsync("git rev-parse --show-toplevel", {
549
- cwd: path5
830
+ cwd: path8
550
831
  });
551
832
  const gitRoot = stdout.trim();
552
833
  this.logger.debug("Resolved git repository root", {
553
- inputPath: path5,
834
+ inputPath: path8,
554
835
  gitRoot
555
836
  });
556
837
  return gitRoot;
557
838
  } catch (error) {
558
839
  if (error instanceof Error) {
559
840
  throw new Error(
560
- `Failed to get Git repository root. The path '${path5}' 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.
561
842
  ${error.message}`
562
843
  );
563
844
  }
564
845
  throw new Error("Failed to get Git repository root: Unknown error");
565
846
  }
566
847
  }
848
+ async tryGetGitRepositoryRoot(path8) {
849
+ try {
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();
863
+ } catch {
864
+ return null;
865
+ }
866
+ }
567
867
  async getCurrentBranches(repoPath) {
568
868
  try {
569
869
  const { stdout } = await execAsync("git branch -a --contains HEAD", {
@@ -581,7 +881,7 @@ ${error.message}`
581
881
  throw new Error("Failed to get Git branches: Unknown error");
582
882
  }
583
883
  }
584
- async getGitRemoteUrl(repoPath, origin10) {
884
+ async getGitRemoteUrl(repoPath, origin11) {
585
885
  try {
586
886
  const { stdout } = await execAsync("git remote -v", {
587
887
  cwd: repoPath
@@ -591,10 +891,10 @@ ${error.message}`
591
891
  throw new Error("No Git remotes found in the repository");
592
892
  }
593
893
  let selectedRemote;
594
- if (origin10) {
595
- const foundRemote = remotes.find((remote) => remote.name === origin10);
894
+ if (origin11) {
895
+ const foundRemote = remotes.find((remote) => remote.name === origin11);
596
896
  if (!foundRemote) {
597
- throw new Error(`Remote '${origin10}' not found in repository`);
897
+ throw new Error(`Remote '${origin11}' not found in repository`);
598
898
  }
599
899
  selectedRemote = foundRemote.url;
600
900
  } else if (remotes.length === 1) {
@@ -664,18 +964,181 @@ ${error.message}`
664
964
  normalizeGitUrl(url) {
665
965
  const sshMatch = url.match(/^git@([^:]+):(.+)$/);
666
966
  if (sshMatch) {
667
- const [, host, path5] = sshMatch;
668
- const cleanPath = path5.replace(/\.git$/, "");
967
+ const [, host, urlPath] = sshMatch;
968
+ const cleanPath = urlPath.replace(/\.git$/, "");
669
969
  return `${host}/${cleanPath}`;
670
970
  }
671
971
  const httpsMatch = url.match(/^https?:\/\/([^/]+)\/(.+)$/);
672
972
  if (httpsMatch) {
673
- const [, host, path5] = httpsMatch;
674
- const cleanPath = path5.replace(/\.git$/, "");
973
+ const [, host, urlPath] = httpsMatch;
974
+ const cleanPath = urlPath.replace(/\.git$/, "");
675
975
  return `${host}/${cleanPath}`;
676
976
  }
677
977
  return url;
678
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
+ }
679
1142
  };
680
1143
 
681
1144
  // apps/cli/src/application/useCases/GetGitRemoteUrlUseCase.ts
@@ -684,14 +1147,14 @@ var GetGitRemoteUrlUseCase = class {
684
1147
  this.gitRemoteUrlService = gitRemoteUrlService;
685
1148
  }
686
1149
  async execute(command3) {
687
- const { path: repoPath, origin: origin10 } = command3;
688
- return this.gitRemoteUrlService.getGitRemoteUrl(repoPath, origin10);
1150
+ const { path: repoPath, origin: origin11 } = command3;
1151
+ return this.gitRemoteUrlService.getGitRemoteUrl(repoPath, origin11);
689
1152
  }
690
1153
  };
691
1154
 
692
1155
  // apps/cli/src/application/services/ListFiles.ts
693
1156
  var fs = __toESM(require("fs/promises"));
694
- var path = __toESM(require("path"));
1157
+ var path2 = __toESM(require("path"));
695
1158
  var ListFiles = class {
696
1159
  async listFilesInDirectory(directoryPath, extensions, excludes = [], skipHidden = true) {
697
1160
  const results = [];
@@ -713,7 +1176,7 @@ var ListFiles = class {
713
1176
  try {
714
1177
  const entries = await fs.readdir(directoryPath, { withFileTypes: true });
715
1178
  for (const entry of entries) {
716
- const fullPath = path.join(directoryPath, entry.name);
1179
+ const fullPath = path2.join(directoryPath, entry.name);
717
1180
  if (this.shouldExcludePath(fullPath, excludes)) {
718
1181
  continue;
719
1182
  }
@@ -730,7 +1193,7 @@ var ListFiles = class {
730
1193
  skipHidden
731
1194
  );
732
1195
  } else if (entry.isFile()) {
733
- const fileExtension = path.extname(entry.name);
1196
+ const fileExtension = path2.extname(entry.name);
734
1197
  if (includeAllExtensions || extensions.includes(fileExtension)) {
735
1198
  results.push({
736
1199
  path: fullPath
@@ -746,7 +1209,7 @@ var ListFiles = class {
746
1209
  if (excludes.length === 0) {
747
1210
  return false;
748
1211
  }
749
- const normalizedPath = path.normalize(filePath).replace(/\\/g, "/");
1212
+ const normalizedPath = path2.normalize(filePath).replace(/\\/g, "/");
750
1213
  for (const exclude of excludes) {
751
1214
  if (this.matchesGlobPattern(normalizedPath, exclude)) {
752
1215
  return true;
@@ -809,7 +1272,7 @@ var ListFilesInDirectoryUseCase = class {
809
1272
 
810
1273
  // apps/cli/src/application/useCases/LintFilesInDirectoryUseCase.ts
811
1274
  var import_minimatch = require("minimatch");
812
- var path2 = __toESM(require("path"));
1275
+ var path3 = __toESM(require("path"));
813
1276
  var fs2 = __toESM(require("fs/promises"));
814
1277
  var origin2 = "LintFilesInDirectoryUseCase";
815
1278
  var LintFilesInDirectoryUseCase = class {
@@ -876,12 +1339,13 @@ var LintFilesInDirectoryUseCase = class {
876
1339
  draftMode,
877
1340
  standardSlug,
878
1341
  ruleId,
879
- language
1342
+ language,
1343
+ diffMode
880
1344
  } = command3;
881
1345
  this.logger.debug(
882
- `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"}"`
883
1347
  );
884
- const absoluteUserPath = path2.isAbsolute(userPath) ? userPath : path2.resolve(process.cwd(), userPath);
1348
+ const absoluteUserPath = path3.isAbsolute(userPath) ? userPath : path3.resolve(process.cwd(), userPath);
885
1349
  let pathStats;
886
1350
  try {
887
1351
  pathStats = await fs2.stat(absoluteUserPath);
@@ -891,7 +1355,7 @@ var LintFilesInDirectoryUseCase = class {
891
1355
  );
892
1356
  }
893
1357
  const isFile = pathStats.isFile();
894
- const directoryForGitOps = isFile ? path2.dirname(absoluteUserPath) : absoluteUserPath;
1358
+ const directoryForGitOps = isFile ? path3.dirname(absoluteUserPath) : absoluteUserPath;
895
1359
  this.logger.debug(
896
1360
  `Path type: ${isFile ? "file" : "directory"}, gitOpsDir="${directoryForGitOps}"`
897
1361
  );
@@ -907,11 +1371,60 @@ var LintFilesInDirectoryUseCase = class {
907
1371
  this.logger.debug(
908
1372
  `Resolved paths: gitRoot="${gitRepoRoot}", lintPath="${absoluteLintPath}"`
909
1373
  );
910
- 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(
911
1419
  absoluteLintPath,
912
1420
  [],
913
1421
  ["node_modules", "dist", ".min.", ".map.", ".git"]
914
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
+ }
915
1428
  const { gitRemoteUrl } = await this.services.gitRemoteUrlService.getGitRemoteUrl(gitRepoRoot);
916
1429
  const { branches } = await this.services.gitRemoteUrlService.getCurrentBranches(gitRepoRoot);
917
1430
  this.logger.debug(
@@ -1108,7 +1621,17 @@ var LintFilesInDirectoryUseCase = class {
1108
1621
  });
1109
1622
  }
1110
1623
  }
1111
- 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(
1112
1635
  (sum, violation) => sum + violation.violations.length,
1113
1636
  0
1114
1637
  );
@@ -1121,10 +1644,10 @@ var LintFilesInDirectoryUseCase = class {
1121
1644
  );
1122
1645
  return {
1123
1646
  gitRemoteUrl,
1124
- violations,
1647
+ violations: filteredViolations,
1125
1648
  summary: {
1126
1649
  totalFiles: files.length,
1127
- violatedFiles: violations.length,
1650
+ violatedFiles: filteredViolations.length,
1128
1651
  totalViolations,
1129
1652
  standardsChecked
1130
1653
  }
@@ -1150,6 +1673,307 @@ var LintFilesInDirectoryUseCase = class {
1150
1673
  }
1151
1674
  };
1152
1675
 
1676
+ // apps/cli/src/application/useCases/LintFilesLocallyUseCase.ts
1677
+ var import_minimatch2 = require("minimatch");
1678
+ var path4 = __toESM(require("path"));
1679
+ var fs3 = __toESM(require("fs/promises"));
1680
+ var origin3 = "LintFilesLocallyUseCase";
1681
+ var LintFilesLocallyUseCase = class {
1682
+ constructor(services, repositories, logger2 = new PackmindLogger(origin3)) {
1683
+ this.services = services;
1684
+ this.repositories = repositories;
1685
+ this.logger = logger2;
1686
+ this.detectionProgramsCache = /* @__PURE__ */ new Map();
1687
+ }
1688
+ fileMatchesTargetAndScope(filePath, targetPath, scopePatterns) {
1689
+ if (!scopePatterns || scopePatterns.length === 0) {
1690
+ const effectivePattern = this.buildEffectivePattern(targetPath, null);
1691
+ return (0, import_minimatch2.minimatch)(filePath, effectivePattern, { matchBase: false });
1692
+ }
1693
+ return scopePatterns.some((scopePattern) => {
1694
+ const effectivePattern = this.buildEffectivePattern(
1695
+ targetPath,
1696
+ scopePattern
1697
+ );
1698
+ return (0, import_minimatch2.minimatch)(filePath, effectivePattern, { matchBase: false });
1699
+ });
1700
+ }
1701
+ buildEffectivePattern(targetPath, scope) {
1702
+ const normalizedTarget = targetPath === "/" ? "/" : targetPath.replace(/\/$/, "");
1703
+ if (!scope) {
1704
+ return normalizedTarget === "/" ? "/**" : normalizedTarget + "/**";
1705
+ }
1706
+ if (scope.startsWith(normalizedTarget + "/") || scope === normalizedTarget) {
1707
+ return scope.endsWith("/") ? scope + "**" : scope;
1708
+ }
1709
+ const cleanScope = scope.startsWith("/") ? scope.substring(1) : scope;
1710
+ let pattern;
1711
+ if (normalizedTarget === "/") {
1712
+ pattern = "/" + cleanScope;
1713
+ } else {
1714
+ pattern = normalizedTarget + "/" + cleanScope;
1715
+ }
1716
+ if (pattern.endsWith("/")) {
1717
+ pattern = pattern + "**";
1718
+ }
1719
+ return pattern;
1720
+ }
1721
+ async execute(command3) {
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);
1728
+ let pathStats;
1729
+ try {
1730
+ pathStats = await fs3.stat(absoluteUserPath);
1731
+ } catch {
1732
+ throw new Error(
1733
+ `The path "${absoluteUserPath}" does not exist or cannot be accessed`
1734
+ );
1735
+ }
1736
+ const isFile = pathStats.isFile();
1737
+ const directoryForConfig = isFile ? path4.dirname(absoluteUserPath) : absoluteUserPath;
1738
+ const gitRepoRoot = await this.services.gitRemoteUrlService.tryGetGitRepositoryRoot(
1739
+ directoryForConfig
1740
+ );
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(
1778
+ directoryForConfig,
1779
+ gitRepoRoot
1780
+ );
1781
+ if (!allConfigs.hasConfigs) {
1782
+ const boundary = gitRepoRoot ?? "filesystem root";
1783
+ throw new Error(
1784
+ `No packmind.json found between ${directoryForConfig} and ${boundary}. Cannot use local linting.`
1785
+ );
1786
+ }
1787
+ const basePath = allConfigs.basePath;
1788
+ this.logger.debug(
1789
+ `Found ${allConfigs.configs.length} packmind.json file(s)`
1790
+ );
1791
+ for (const config of allConfigs.configs) {
1792
+ this.logger.debug(
1793
+ `Using config: ${config.absoluteTargetPath}/packmind.json (target: ${config.targetPath})`
1794
+ );
1795
+ }
1796
+ let files = isFile ? [{ path: absoluteUserPath }] : await this.services.listFiles.listFilesInDirectory(
1797
+ absoluteUserPath,
1798
+ [],
1799
+ ["node_modules", "dist", ".min.", ".map.", ".git"]
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
+ }
1806
+ this.logger.debug(`Found ${files.length} files to lint`);
1807
+ const violations = [];
1808
+ const allStandardsChecked = /* @__PURE__ */ new Set();
1809
+ for (const file of files) {
1810
+ const fileViolations = [];
1811
+ const relativeFilePath = file.path.startsWith(basePath) ? file.path.substring(basePath.length) : file.path;
1812
+ const normalizedFilePath = relativeFilePath.startsWith("/") ? relativeFilePath : "/" + relativeFilePath;
1813
+ this.logger.debug(
1814
+ `Processing file: absolute="${file.path}", relative="${normalizedFilePath}"`
1815
+ );
1816
+ const fileExtension = this.extractExtensionFromFile(file.path);
1817
+ const fileLanguage = this.resolveProgrammingLanguage(fileExtension);
1818
+ if (!fileLanguage) {
1819
+ continue;
1820
+ }
1821
+ const matchingTargets = this.findMatchingTargets(
1822
+ file.path,
1823
+ allConfigs.configs
1824
+ );
1825
+ const programsByLanguage = /* @__PURE__ */ new Map();
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
+ );
1860
+ }
1861
+ }
1862
+ }
1863
+ }
1864
+ }
1865
+ }
1866
+ if (programsByLanguage.size > 0) {
1867
+ try {
1868
+ const fileContent = await this.services.listFiles.readFileContent(
1869
+ file.path
1870
+ );
1871
+ for (const [language, programs] of programsByLanguage.entries()) {
1872
+ try {
1873
+ const result = await this.executeProgramsForFile({
1874
+ filePath: file.path,
1875
+ fileContent,
1876
+ language,
1877
+ programs
1878
+ });
1879
+ fileViolations.push(...result);
1880
+ } catch (error) {
1881
+ console.error(
1882
+ `Error executing programs for file ${file.path} (${language}): ${error}`
1883
+ );
1884
+ }
1885
+ }
1886
+ } catch (error) {
1887
+ console.error(
1888
+ `Error reading file content for ${file.path}: ${error}`
1889
+ );
1890
+ }
1891
+ }
1892
+ if (fileViolations.length > 0) {
1893
+ violations.push({
1894
+ file: file.path,
1895
+ violations: fileViolations
1896
+ });
1897
+ }
1898
+ }
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(
1910
+ (sum, violation) => sum + violation.violations.length,
1911
+ 0
1912
+ );
1913
+ return {
1914
+ violations: filteredViolations,
1915
+ summary: {
1916
+ totalFiles: files.length,
1917
+ violatedFiles: filteredViolations.length,
1918
+ totalViolations,
1919
+ standardsChecked: Array.from(allStandardsChecked)
1920
+ }
1921
+ };
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
+ }
1957
+ resolveProgrammingLanguage(language) {
1958
+ try {
1959
+ return stringToProgrammingLanguage(language);
1960
+ } catch {
1961
+ return null;
1962
+ }
1963
+ }
1964
+ async executeProgramsForFile(command3) {
1965
+ const result = await this.services.linterExecutionUseCase.execute(command3);
1966
+ return result.violations;
1967
+ }
1968
+ extractExtensionFromFile(filePath) {
1969
+ const lastDotIndex = filePath.lastIndexOf(".");
1970
+ if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) {
1971
+ return "";
1972
+ }
1973
+ return filePath.substring(lastDotIndex + 1);
1974
+ }
1975
+ };
1976
+
1153
1977
  // apps/cli/src/infra/repositories/PackmindGateway.ts
1154
1978
  function decodeJwt(jwt) {
1155
1979
  try {
@@ -1256,7 +2080,7 @@ var PackmindGateway = class {
1256
2080
  );
1257
2081
  }
1258
2082
  throw new Error(
1259
- `Failed to pull content: Error: ${err?.message || JSON.stringify(error)}`
2083
+ `Failed to fetch content: Error: ${err?.message || JSON.stringify(error)}`
1260
2084
  );
1261
2085
  }
1262
2086
  };
@@ -1535,7 +2359,117 @@ var PackmindGateway = class {
1535
2359
  `Failed to get package '${slug}': Error: ${err?.message || JSON.stringify(error)}`
1536
2360
  );
1537
2361
  }
1538
- };
2362
+ };
2363
+ this.getDetectionProgramsForPackages = async (params) => {
2364
+ const decodedApiKey = decodeApiKey(this.apiKey);
2365
+ if (!decodedApiKey.isValid) {
2366
+ throw new Error(`Invalid API key: ${decodedApiKey.error}`);
2367
+ }
2368
+ const { host } = decodedApiKey.payload;
2369
+ const url = `${host}/api/v0/detection-programs-for-packages`;
2370
+ const payload = {
2371
+ packagesSlugs: params.packagesSlugs
2372
+ };
2373
+ try {
2374
+ const response = await fetch(url, {
2375
+ method: "POST",
2376
+ headers: {
2377
+ "Content-Type": "application/json",
2378
+ Authorization: `Bearer ${this.apiKey}`
2379
+ },
2380
+ body: JSON.stringify(payload)
2381
+ });
2382
+ if (!response.ok) {
2383
+ let errorMsg = `API request failed: ${response.status} ${response.statusText}`;
2384
+ try {
2385
+ const errorBody = await response.json();
2386
+ if (errorBody && errorBody.message) {
2387
+ errorMsg = `${errorBody.message}`;
2388
+ }
2389
+ } catch {
2390
+ }
2391
+ throw new Error(errorMsg);
2392
+ }
2393
+ const result = await response.json();
2394
+ return result;
2395
+ } catch (error) {
2396
+ const err = error;
2397
+ const code = err?.code || err?.cause?.code;
2398
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || err?.name === "FetchError" || typeof err?.message === "string" && (err.message.includes("Failed to fetch") || err.message.includes("network") || err.message.includes("NetworkError"))) {
2399
+ throw new Error(
2400
+ `Packmind server is not accessible at ${host}. Please check your network connection or the server URL.`
2401
+ );
2402
+ }
2403
+ throw new Error(
2404
+ `Failed to fetch detection programs for packages: Error: ${err?.message || JSON.stringify(error)}`
2405
+ );
2406
+ }
2407
+ };
2408
+ }
2409
+ };
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
+ });
1539
2473
  }
1540
2474
  };
1541
2475
 
@@ -3208,9 +4142,9 @@ var LinterAstAdapter = class {
3208
4142
  var TreeSitter17 = __toESM(require("web-tree-sitter"));
3209
4143
 
3210
4144
  // packages/linter-execution/src/application/useCases/ExecuteLinterProgramsUseCase.ts
3211
- var origin3 = "ExecuteLinterProgramsUseCase";
4145
+ var origin4 = "ExecuteLinterProgramsUseCase";
3212
4146
  var ExecuteLinterProgramsUseCase = class {
3213
- constructor(linterAstAdapter = new LinterAstAdapter(), logger2 = new PackmindLogger(origin3)) {
4147
+ constructor(linterAstAdapter = new LinterAstAdapter(), logger2 = new PackmindLogger(origin4)) {
3214
4148
  this.linterAstAdapter = linterAstAdapter;
3215
4149
  this.logger = logger2;
3216
4150
  }
@@ -3341,9 +4275,9 @@ var ExecuteLinterProgramsUseCase = class {
3341
4275
  let line;
3342
4276
  let character = 0;
3343
4277
  if (typeof value === "number" && Number.isFinite(value)) {
3344
- line = value;
4278
+ line = value + 1;
3345
4279
  } else if (this.isViolationLike(value)) {
3346
- line = value.line;
4280
+ line = value.line + 1;
3347
4281
  character = value.character ?? 0;
3348
4282
  }
3349
4283
  if (!this.isValidLine(line)) {
@@ -3405,14 +4339,30 @@ var ExecuteLinterProgramsUseCase = class {
3405
4339
  }
3406
4340
  };
3407
4341
 
3408
- // packages/node-utils/src/ai/prompts/OpenAIService.ts
3409
- var import_openai = __toESM(require("openai"));
4342
+ // packages/node-utils/src/dataSources/local.ts
4343
+ var import_typeorm = require("typeorm");
4344
+ var dataSource = makeDatasource();
4345
+ function makeDatasource() {
4346
+ try {
4347
+ return new import_typeorm.DataSource({
4348
+ type: "postgres",
4349
+ url: process.env["DATABASE_URL"],
4350
+ entities: [],
4351
+ migrations: []
4352
+ });
4353
+ } catch {
4354
+ return {};
4355
+ }
4356
+ }
4357
+
4358
+ // packages/node-utils/src/cache/Cache.ts
4359
+ var import_ioredis = __toESM(require("ioredis"));
3410
4360
 
3411
4361
  // packages/node-utils/src/config/infra/Infisical/InfisicalConfig.ts
3412
4362
  var import_sdk = require("@infisical/sdk");
3413
- var origin4 = "InfisicalConfig";
4363
+ var origin5 = "InfisicalConfig";
3414
4364
  var InfisicalConfig = class {
3415
- constructor(clientId, clientSecret, env, projectId, logger2 = new PackmindLogger(origin4)) {
4365
+ constructor(clientId, clientSecret, env, projectId, logger2 = new PackmindLogger(origin5)) {
3416
4366
  this.clientId = clientId;
3417
4367
  this.clientSecret = clientSecret;
3418
4368
  this.env = env;
@@ -3487,7 +4437,7 @@ var InfisicalConfig = class {
3487
4437
  };
3488
4438
 
3489
4439
  // packages/node-utils/src/config/config/Configuration.ts
3490
- var origin5 = "Configuration";
4440
+ var origin6 = "Configuration";
3491
4441
  var Configuration = class _Configuration {
3492
4442
  constructor() {
3493
4443
  this.initialized = false;
@@ -3495,7 +4445,7 @@ var Configuration = class _Configuration {
3495
4445
  }
3496
4446
  static {
3497
4447
  this.logger = new PackmindLogger(
3498
- origin5,
4448
+ origin6,
3499
4449
  "info" /* INFO */
3500
4450
  );
3501
4451
  }
@@ -3642,8 +4592,7 @@ var Configuration = class _Configuration {
3642
4592
  };
3643
4593
 
3644
4594
  // packages/node-utils/src/cache/Cache.ts
3645
- var import_ioredis = __toESM(require("ioredis"));
3646
- var origin6 = "Cache";
4595
+ var origin7 = "Cache";
3647
4596
  var Cache = class _Cache {
3648
4597
  constructor() {
3649
4598
  this.initialized = false;
@@ -3655,7 +4604,7 @@ var Cache = class _Cache {
3655
4604
  }
3656
4605
  static {
3657
4606
  this.logger = new PackmindLogger(
3658
- origin6,
4607
+ origin7,
3659
4608
  "info" /* INFO */
3660
4609
  );
3661
4610
  }
@@ -3837,14 +4786,14 @@ var import_common2 = require("@nestjs/common");
3837
4786
 
3838
4787
  // packages/node-utils/src/sse/RedisSSEClient.ts
3839
4788
  var import_ioredis2 = __toESM(require("ioredis"));
3840
- var origin7 = "RedisSSEClient";
4789
+ var origin8 = "RedisSSEClient";
3841
4790
  var RedisSSEClient = class _RedisSSEClient {
3842
4791
  // eslint-disable-next-line @typescript-eslint/no-empty-function
3843
4792
  constructor() {
3844
4793
  this.initialized = false;
3845
4794
  }
3846
4795
  static {
3847
- this.logger = new PackmindLogger(origin7);
4796
+ this.logger = new PackmindLogger(origin8);
3848
4797
  }
3849
4798
  static getInstance() {
3850
4799
  _RedisSSEClient.logger.debug("Getting RedisSSEClient instance");
@@ -4028,10 +4977,10 @@ function serializeSSERedisMessage(message) {
4028
4977
  }
4029
4978
 
4030
4979
  // packages/node-utils/src/sse/SSEEventPublisher.ts
4031
- var origin8 = "SSEEventPublisher";
4980
+ var origin9 = "SSEEventPublisher";
4032
4981
  var SSEEventPublisher = class _SSEEventPublisher {
4033
4982
  static {
4034
- this.logger = new PackmindLogger(origin8);
4983
+ this.logger = new PackmindLogger(origin9);
4035
4984
  }
4036
4985
  /**
4037
4986
  * Get the singleton instance
@@ -4270,25 +5219,9 @@ ${sectionBlock}
4270
5219
  return result;
4271
5220
  }
4272
5221
 
4273
- // packages/node-utils/src/dataSources/local.ts
4274
- var import_typeorm = require("typeorm");
4275
- var dataSource = makeDatasource();
4276
- function makeDatasource() {
4277
- try {
4278
- return new import_typeorm.DataSource({
4279
- type: "postgres",
4280
- url: process.env["DATABASE_URL"],
4281
- entities: [],
4282
- migrations: []
4283
- });
4284
- } catch {
4285
- return {};
4286
- }
4287
- }
4288
-
4289
5222
  // apps/cli/src/application/useCases/PullDataUseCase.ts
4290
- var fs3 = __toESM(require("fs/promises"));
4291
- var path3 = __toESM(require("path"));
5223
+ var fs4 = __toESM(require("fs/promises"));
5224
+ var path5 = __toESM(require("path"));
4292
5225
  var PullDataUseCase = class {
4293
5226
  constructor(packmindGateway) {
4294
5227
  this.packmindGateway = packmindGateway;
@@ -4299,13 +5232,27 @@ var PullDataUseCase = class {
4299
5232
  filesCreated: 0,
4300
5233
  filesUpdated: 0,
4301
5234
  filesDeleted: 0,
4302
- errors: []
5235
+ errors: [],
5236
+ recipesCount: 0,
5237
+ standardsCount: 0
4303
5238
  };
4304
5239
  const response = await this.packmindGateway.getPullData({
4305
5240
  packagesSlugs: command3.packagesSlugs
4306
5241
  });
5242
+ const uniqueFilesMap = /* @__PURE__ */ new Map();
5243
+ for (const file of response.fileUpdates.createOrUpdate) {
5244
+ uniqueFilesMap.set(file.path, file);
5245
+ }
5246
+ const uniqueFiles = Array.from(uniqueFilesMap.values());
5247
+ for (const file of uniqueFiles) {
5248
+ if (file.path.includes(".packmind/recipes/") && file.path.endsWith(".md")) {
5249
+ result.recipesCount++;
5250
+ } else if (file.path.includes(".packmind/standards/") && file.path.endsWith(".md")) {
5251
+ result.standardsCount++;
5252
+ }
5253
+ }
4307
5254
  try {
4308
- for (const file of response.fileUpdates.createOrUpdate) {
5255
+ for (const file of uniqueFiles) {
4309
5256
  try {
4310
5257
  await this.createOrUpdateFile(baseDirectory, file, result);
4311
5258
  } catch (error) {
@@ -4330,9 +5277,9 @@ var PullDataUseCase = class {
4330
5277
  return result;
4331
5278
  }
4332
5279
  async createOrUpdateFile(baseDirectory, file, result) {
4333
- const fullPath = path3.join(baseDirectory, file.path);
4334
- const directory = path3.dirname(fullPath);
4335
- await fs3.mkdir(directory, { recursive: true });
5280
+ const fullPath = path5.join(baseDirectory, file.path);
5281
+ const directory = path5.dirname(fullPath);
5282
+ await fs4.mkdir(directory, { recursive: true });
4336
5283
  const fileExists = await this.fileExists(fullPath);
4337
5284
  if (file.content !== void 0) {
4338
5285
  await this.handleFullContentUpdate(
@@ -4352,7 +5299,7 @@ var PullDataUseCase = class {
4352
5299
  }
4353
5300
  async handleFullContentUpdate(fullPath, content, fileExists, result) {
4354
5301
  if (fileExists) {
4355
- const existingContent = await fs3.readFile(fullPath, "utf-8");
5302
+ const existingContent = await fs4.readFile(fullPath, "utf-8");
4356
5303
  const commentMarker = this.extractCommentMarker(content);
4357
5304
  let finalContent;
4358
5305
  if (!commentMarker) {
@@ -4364,23 +5311,23 @@ var PullDataUseCase = class {
4364
5311
  commentMarker
4365
5312
  );
4366
5313
  }
4367
- await fs3.writeFile(fullPath, finalContent, "utf-8");
5314
+ await fs4.writeFile(fullPath, finalContent, "utf-8");
4368
5315
  result.filesUpdated++;
4369
5316
  } else {
4370
- await fs3.writeFile(fullPath, content, "utf-8");
5317
+ await fs4.writeFile(fullPath, content, "utf-8");
4371
5318
  result.filesCreated++;
4372
5319
  }
4373
5320
  }
4374
5321
  async handleSectionsUpdate(fullPath, sections, fileExists, result) {
4375
5322
  let currentContent = "";
4376
5323
  if (fileExists) {
4377
- currentContent = await fs3.readFile(fullPath, "utf-8");
5324
+ currentContent = await fs4.readFile(fullPath, "utf-8");
4378
5325
  }
4379
5326
  const mergedContent = mergeSectionsIntoFileContent(
4380
5327
  currentContent,
4381
5328
  sections
4382
5329
  );
4383
- await fs3.writeFile(fullPath, mergedContent, "utf-8");
5330
+ await fs4.writeFile(fullPath, mergedContent, "utf-8");
4384
5331
  if (fileExists) {
4385
5332
  result.filesUpdated++;
4386
5333
  } else {
@@ -4388,16 +5335,16 @@ var PullDataUseCase = class {
4388
5335
  }
4389
5336
  }
4390
5337
  async deleteFile(baseDirectory, filePath, result) {
4391
- const fullPath = path3.join(baseDirectory, filePath);
5338
+ const fullPath = path5.join(baseDirectory, filePath);
4392
5339
  const fileExists = await this.fileExists(fullPath);
4393
5340
  if (fileExists) {
4394
- await fs3.unlink(fullPath);
5341
+ await fs4.unlink(fullPath);
4395
5342
  result.filesDeleted++;
4396
5343
  }
4397
5344
  }
4398
5345
  async fileExists(filePath) {
4399
5346
  try {
4400
- await fs3.access(filePath);
5347
+ await fs4.access(filePath);
4401
5348
  return true;
4402
5349
  } catch {
4403
5350
  return false;
@@ -4470,6 +5417,235 @@ var GetPackageSummaryUseCase = class {
4470
5417
  }
4471
5418
  };
4472
5419
 
5420
+ // apps/cli/src/infra/repositories/ConfigFileRepository.ts
5421
+ var fs5 = __toESM(require("fs/promises"));
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
5453
+ var ConfigFileRepository = class {
5454
+ constructor() {
5455
+ this.CONFIG_FILENAME = "packmind.json";
5456
+ this.warnedFiles = /* @__PURE__ */ new Set();
5457
+ this.EXCLUDED_DIRECTORIES = [
5458
+ "node_modules",
5459
+ ".git",
5460
+ "dist",
5461
+ "build",
5462
+ "coverage",
5463
+ ".nx"
5464
+ ];
5465
+ }
5466
+ async writeConfig(baseDirectory, config) {
5467
+ const configPath = path6.join(baseDirectory, this.CONFIG_FILENAME);
5468
+ const configContent = JSON.stringify(config, null, 2) + "\n";
5469
+ await fs5.writeFile(configPath, configContent, "utf-8");
5470
+ }
5471
+ async readConfig(baseDirectory) {
5472
+ const configPath = path6.join(baseDirectory, this.CONFIG_FILENAME);
5473
+ try {
5474
+ const configContent = await fs5.readFile(configPath, "utf-8");
5475
+ const config = JSON.parse(configContent);
5476
+ if (!config.packages || typeof config.packages !== "object") {
5477
+ throw new Error(
5478
+ "Invalid packmind.json structure. Expected { packages: { ... } }"
5479
+ );
5480
+ }
5481
+ return config;
5482
+ } catch (error) {
5483
+ if (error.code === "ENOENT") {
5484
+ return null;
5485
+ }
5486
+ if (!this.warnedFiles.has(configPath)) {
5487
+ this.warnedFiles.add(configPath);
5488
+ logWarningConsole(`\u26A0 Skipping malformed config file: ${configPath}`);
5489
+ }
5490
+ return null;
5491
+ }
5492
+ }
5493
+ /**
5494
+ * Recursively finds all directories containing packmind.json in descendant folders.
5495
+ * Excludes common build/dependency directories (node_modules, .git, dist, etc.)
5496
+ *
5497
+ * @param directory - The root directory to search from
5498
+ * @returns Array of directory paths that contain a packmind.json file
5499
+ */
5500
+ async findDescendantConfigs(directory) {
5501
+ const normalizedDir = path6.resolve(directory);
5502
+ const results = [];
5503
+ const searchRecursively = async (currentDir) => {
5504
+ let entries;
5505
+ try {
5506
+ entries = await fs5.readdir(currentDir, { withFileTypes: true });
5507
+ } catch {
5508
+ return;
5509
+ }
5510
+ for (const entry of entries) {
5511
+ if (!entry.isDirectory()) {
5512
+ continue;
5513
+ }
5514
+ if (this.EXCLUDED_DIRECTORIES.includes(entry.name)) {
5515
+ continue;
5516
+ }
5517
+ const entryPath = path6.join(currentDir, entry.name);
5518
+ const config = await this.readConfig(entryPath);
5519
+ if (config) {
5520
+ results.push(entryPath);
5521
+ }
5522
+ await searchRecursively(entryPath);
5523
+ }
5524
+ };
5525
+ await searchRecursively(normalizedDir);
5526
+ return results;
5527
+ }
5528
+ /**
5529
+ * Reads all packmind.json files from startDirectory up to stopDirectory (inclusive)
5530
+ * and merges their package configurations.
5531
+ *
5532
+ * @param startDirectory - Directory to start searching from (typically the lint target)
5533
+ * @param stopDirectory - Directory to stop searching at (typically git repo root), or null to walk to filesystem root
5534
+ * @returns Merged configuration from all found packmind.json files
5535
+ */
5536
+ async readHierarchicalConfig(startDirectory, stopDirectory) {
5537
+ const configs = [];
5538
+ const configPaths = [];
5539
+ const normalizedStart = path6.resolve(startDirectory);
5540
+ const normalizedStop = stopDirectory ? path6.resolve(stopDirectory) : null;
5541
+ let currentDir = normalizedStart;
5542
+ while (true) {
5543
+ const config = await this.readConfig(currentDir);
5544
+ if (config) {
5545
+ configs.push(config);
5546
+ configPaths.push(path6.join(currentDir, this.CONFIG_FILENAME));
5547
+ }
5548
+ if (normalizedStop !== null && currentDir === normalizedStop) {
5549
+ break;
5550
+ }
5551
+ const parentDir = path6.dirname(currentDir);
5552
+ if (parentDir === currentDir) {
5553
+ break;
5554
+ }
5555
+ currentDir = parentDir;
5556
+ }
5557
+ const mergedPackages = {};
5558
+ for (const config of configs) {
5559
+ for (const [slug, version] of Object.entries(config.packages)) {
5560
+ if (!(slug in mergedPackages)) {
5561
+ mergedPackages[slug] = version;
5562
+ }
5563
+ }
5564
+ }
5565
+ return {
5566
+ packages: mergedPackages,
5567
+ configPaths,
5568
+ hasConfigs: configs.length > 0
5569
+ };
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
+ }
5647
+ };
5648
+
4473
5649
  // apps/cli/src/PackmindCliHexaFactory.ts
4474
5650
  var PackmindCliHexaFactory = class {
4475
5651
  constructor(logger2) {
@@ -4477,12 +5653,14 @@ var PackmindCliHexaFactory = class {
4477
5653
  this.repositories = {
4478
5654
  packmindGateway: new PackmindGateway(
4479
5655
  process.env.PACKMIND_API_KEY_V3 || ""
4480
- )
5656
+ ),
5657
+ configFileRepository: new ConfigFileRepository()
4481
5658
  };
4482
5659
  this.services = {
4483
5660
  listFiles: new ListFiles(),
4484
5661
  gitRemoteUrlService: new GitService(this.logger),
4485
- linterExecutionUseCase: new ExecuteLinterProgramsUseCase()
5662
+ linterExecutionUseCase: new ExecuteLinterProgramsUseCase(),
5663
+ diffViolationFilterService: new DiffViolationFilterService()
4486
5664
  };
4487
5665
  this.useCases = {
4488
5666
  executeSingleFileAst: new ExecuteSingleFileAstUseCase(
@@ -4495,6 +5673,11 @@ var PackmindCliHexaFactory = class {
4495
5673
  this.repositories,
4496
5674
  this.logger
4497
5675
  ),
5676
+ lintFilesLocally: new LintFilesLocallyUseCase(
5677
+ this.services,
5678
+ this.repositories,
5679
+ this.logger
5680
+ ),
4498
5681
  pullData: new PullDataUseCase(this.repositories.packmindGateway),
4499
5682
  listPackages: new ListPackagesUseCase(this.repositories.packmindGateway),
4500
5683
  getPackageBySlug: new GetPackageSummaryUseCase(
@@ -4505,9 +5688,9 @@ var PackmindCliHexaFactory = class {
4505
5688
  };
4506
5689
 
4507
5690
  // apps/cli/src/PackmindCliHexa.ts
4508
- var origin9 = "PackmindCliHexa";
5691
+ var origin10 = "PackmindCliHexa";
4509
5692
  var PackmindCliHexa = class {
4510
- constructor(logger2 = new PackmindLogger(origin9)) {
5693
+ constructor(logger2 = new PackmindLogger(origin10)) {
4511
5694
  this.logger = logger2;
4512
5695
  try {
4513
5696
  this.hexa = new PackmindCliHexaFactory(this.logger);
@@ -4537,6 +5720,9 @@ var PackmindCliHexa = class {
4537
5720
  async lintFilesInDirectory(command3) {
4538
5721
  return this.hexa.useCases.lintFilesInDirectory.execute(command3);
4539
5722
  }
5723
+ async lintFilesLocally(command3) {
5724
+ return this.hexa.useCases.lintFilesLocally.execute(command3);
5725
+ }
4540
5726
  async pullData(command3) {
4541
5727
  return this.hexa.useCases.pullData.execute(command3);
4542
5728
  }
@@ -4546,6 +5732,57 @@ var PackmindCliHexa = class {
4546
5732
  async getPackageBySlug(command3) {
4547
5733
  return this.hexa.useCases.getPackageBySlug.execute(command3);
4548
5734
  }
5735
+ async writeConfig(baseDirectory, packagesSlugs) {
5736
+ const config = {
5737
+ packages: packagesSlugs.reduce(
5738
+ (acc, slug) => {
5739
+ acc[slug] = "*";
5740
+ return acc;
5741
+ },
5742
+ {}
5743
+ )
5744
+ };
5745
+ await this.hexa.repositories.configFileRepository.writeConfig(
5746
+ baseDirectory,
5747
+ config
5748
+ );
5749
+ }
5750
+ async readConfig(baseDirectory) {
5751
+ const config = await this.hexa.repositories.configFileRepository.readConfig(
5752
+ baseDirectory
5753
+ );
5754
+ if (!config) return [];
5755
+ const hasNonWildcardVersions = Object.values(config.packages).some(
5756
+ (version) => version !== "*"
5757
+ );
5758
+ if (hasNonWildcardVersions) {
5759
+ logWarningConsole(
5760
+ "Package versions are not supported yet, getting the latest version"
5761
+ );
5762
+ }
5763
+ return Object.keys(config.packages);
5764
+ }
5765
+ async readHierarchicalConfig(startDirectory, stopDirectory) {
5766
+ return this.hexa.repositories.configFileRepository.readHierarchicalConfig(
5767
+ startDirectory,
5768
+ stopDirectory
5769
+ );
5770
+ }
5771
+ async findDescendantConfigs(directory) {
5772
+ return this.hexa.repositories.configFileRepository.findDescendantConfigs(
5773
+ directory
5774
+ );
5775
+ }
5776
+ async getGitRepositoryRoot(directory) {
5777
+ return this.hexa.services.gitRemoteUrlService.getGitRepositoryRoot(
5778
+ directory
5779
+ );
5780
+ }
5781
+ async tryGetGitRepositoryRoot(directory) {
5782
+ return this.hexa.services.gitRemoteUrlService.tryGetGitRepositoryRoot(
5783
+ directory
5784
+ );
5785
+ }
4549
5786
  };
4550
5787
 
4551
5788
  // apps/cli/src/infra/repositories/IDELintLogger.ts
@@ -4565,7 +5802,6 @@ var IDELintLogger = class {
4565
5802
  };
4566
5803
 
4567
5804
  // apps/cli/src/infra/repositories/HumanReadableLogger.ts
4568
- var import_chalk = __toESM(require("chalk"));
4569
5805
  var HumanReadableLogger = class {
4570
5806
  logViolations(violations) {
4571
5807
  violations.forEach((violation) => {
@@ -4576,29 +5812,117 @@ var HumanReadableLogger = class {
4576
5812
  (acc, violation) => acc + violation.violations.length,
4577
5813
  0
4578
5814
  );
4579
- console.log(
4580
- import_chalk.default.bgRed.bold("packmind-cli"),
4581
- import_chalk.default.red(
4582
- `\u274C Found ${import_chalk.default.bold(totalViolationCount)} violation(s) in ${import_chalk.default.bold(violations.length)} file(s)`
4583
- )
5815
+ logErrorConsole(
5816
+ `\u274C Found ${formatBold(String(totalViolationCount))} violation(s) in ${formatBold(String(violations.length))} file(s)`
4584
5817
  );
4585
5818
  } else {
4586
- console.log(
4587
- import_chalk.default.bgGreen.bold("packmind-cli"),
4588
- import_chalk.default.green.bold(`\u2705 No violations found`)
4589
- );
5819
+ logSuccessConsole(`\u2705 No violations found`);
4590
5820
  }
4591
5821
  }
4592
5822
  logViolation(violation) {
4593
- console.log(import_chalk.default.underline.gray(violation.file));
5823
+ console.log(formatFilePath(violation.file));
4594
5824
  violation.violations.forEach(({ line, character, standard, rule }) => {
4595
5825
  console.log(
4596
- import_chalk.default.red(` ${line}:${character} error @${standard}/${rule}`)
5826
+ formatError(` ${line}:${character} error @${standard}/${rule}`)
4597
5827
  );
4598
5828
  });
4599
5829
  }
4600
5830
  };
4601
5831
 
5832
+ // apps/cli/src/infra/commands/LinterCommand.ts
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
+
4602
5926
  // apps/cli/src/infra/commands/LinterCommand.ts
4603
5927
  var Logger = {
4604
5928
  from: async (input) => {
@@ -4627,6 +5951,19 @@ var RuleID = {
4627
5951
  };
4628
5952
  }
4629
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
+ };
4630
5967
  var lintCommand = (0, import_cmd_ts.command)({
4631
5968
  name: "lint",
4632
5969
  description: "Lint code at the specified path",
@@ -4660,33 +5997,34 @@ var lintCommand = (0, import_cmd_ts.command)({
4660
5997
  debug: (0, import_cmd_ts.flag)({
4661
5998
  long: "debug",
4662
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)
4663
6013
  })
4664
6014
  },
4665
- handler: async ({ path: path5, draft, rule, debug, language, logger: logger2 }) => {
4666
- if (draft && !rule) {
4667
- throw new Error("option --rule is required to use --draft mode");
4668
- }
4669
- const startedAt = Date.now();
6015
+ handler: async (args2) => {
4670
6016
  const packmindLogger = new PackmindLogger(
4671
6017
  "PackmindCLI",
4672
- debug ? "debug" /* DEBUG */ : "info" /* INFO */
6018
+ args2.debug ? "debug" /* DEBUG */ : "info" /* INFO */
4673
6019
  );
4674
- const packmindCliHexa = new PackmindCliHexa(packmindLogger);
4675
- const { violations } = await packmindCliHexa.lintFilesInDirectory({
4676
- path: path5 ?? ".",
4677
- draftMode: draft,
4678
- standardSlug: rule?.standardSlug,
4679
- ruleId: rule?.ruleId,
4680
- language
4681
- });
4682
- (logger2 === "ide" /* ide */ ? new IDELintLogger() : new HumanReadableLogger()).logViolations(violations);
4683
- const durationSeconds = (Date.now() - startedAt) / 1e3;
4684
- console.log(`Lint completed in ${durationSeconds.toFixed(2)}s`);
4685
- if (violations.length > 0) {
4686
- process.exit(1);
4687
- } else {
4688
- process.exit(0);
4689
- }
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);
4690
6028
  }
4691
6029
  });
4692
6030
 
@@ -4746,8 +6084,8 @@ function extractWasmFiles() {
4746
6084
 
4747
6085
  // apps/cli/src/main.ts
4748
6086
  var import_dotenv = require("dotenv");
4749
- var fs4 = __toESM(require("fs"));
4750
- var path4 = __toESM(require("path"));
6087
+ var fs6 = __toESM(require("fs"));
6088
+ var path7 = __toESM(require("path"));
4751
6089
 
4752
6090
  // apps/cli/src/infra/commands/PullCommand.ts
4753
6091
  var import_cmd_ts2 = require("cmd-ts");
@@ -4782,10 +6120,28 @@ var pullCommand = (0, import_cmd_ts2.command)({
4782
6120
  console.log("No packages found.");
4783
6121
  process.exit(0);
4784
6122
  }
4785
- console.log("Available packages:");
4786
- packages.forEach((pkg) => {
4787
- console.log(` - ${pkg.slug}: ${pkg.description || pkg.name}`);
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}`);
6130
+ if (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) {
6139
+ console.log("");
6140
+ }
4788
6141
  });
6142
+ const exampleSlug = formatSlug(sortedPackages[0].slug);
6143
+ console.log("\nHow to install a package:\n");
6144
+ console.log(` $ packmind-cli install ${exampleSlug}`);
4789
6145
  process.exit(0);
4790
6146
  } catch (error) {
4791
6147
  console.error("\n\u274C Failed to list packages:");
@@ -4841,30 +6197,59 @@ var pullCommand = (0, import_cmd_ts2.command)({
4841
6197
  process.exit(1);
4842
6198
  }
4843
6199
  }
4844
- if (packagesSlugs.length === 0) {
4845
- console.log("Usage: packmind-cli pull <package-slug> [package-slug...]");
4846
- console.log(" packmind-cli pull --list");
6200
+ let configPackages;
6201
+ let configExists = false;
6202
+ try {
6203
+ configPackages = await packmindCliHexa.readConfig(process.cwd());
6204
+ configExists = configPackages.length > 0;
6205
+ } catch (error) {
6206
+ console.error("ERROR Failed to parse packmind.json");
6207
+ if (error instanceof Error) {
6208
+ console.error(`ERROR ${error.message}`);
6209
+ } else {
6210
+ console.error(`ERROR ${String(error)}`);
6211
+ }
6212
+ console.error(
6213
+ "\n\u{1F4A1} Please fix the packmind.json file or delete it to continue."
6214
+ );
6215
+ process.exit(1);
6216
+ }
6217
+ const allPackages = [.../* @__PURE__ */ new Set([...configPackages, ...packagesSlugs])];
6218
+ if (allPackages.length === 0) {
6219
+ logWarningConsole("config packmind.json not found");
6220
+ console.log(
6221
+ "Usage: packmind-cli install <package-slug> [package-slug...]"
6222
+ );
6223
+ console.log(" packmind-cli install --list");
4847
6224
  console.log("");
4848
6225
  console.log("Examples:");
4849
- console.log(" packmind-cli pull backend");
4850
- console.log(" packmind-cli pull backend frontend");
4851
- console.log(" packmind-cli pull --list # Show available packages");
6226
+ console.log(" packmind-cli install backend");
6227
+ console.log(" packmind-cli install backend frontend");
6228
+ console.log(" packmind-cli install --list # Show available packages");
4852
6229
  console.log("");
4853
- console.log("Pull recipes and standards from the specified packages.");
6230
+ console.log("Install recipes and standards from the specified packages.");
4854
6231
  process.exit(0);
4855
6232
  }
4856
- console.log(
4857
- `Pulling content from packages: ${packagesSlugs.join(", ")}...`
4858
- );
6233
+ if (!configExists && packagesSlugs.length > 0) {
6234
+ console.log("INFO initializing packmind.json");
6235
+ }
4859
6236
  try {
6237
+ const packageCount = allPackages.length;
6238
+ const packageWord = packageCount === 1 ? "package" : "packages";
6239
+ console.log(
6240
+ `Fetching ${packageCount} ${packageWord}: ${allPackages.join(", ")}...`
6241
+ );
4860
6242
  const result = await packmindCliHexa.pullData({
4861
6243
  baseDirectory: process.cwd(),
4862
- packagesSlugs
6244
+ packagesSlugs: allPackages
4863
6245
  });
4864
- console.log("\n\u2705 Pull completed successfully!");
4865
- console.log(` Files created: ${result.filesCreated}`);
4866
- console.log(` Files updated: ${result.filesUpdated}`);
4867
- console.log(` Files deleted: ${result.filesDeleted}`);
6246
+ console.log(
6247
+ `Installing ${result.recipesCount} recipes and ${result.standardsCount} standards...`
6248
+ );
6249
+ console.log(
6250
+ `
6251
+ added ${result.filesCreated} files, changed ${result.filesUpdated} files, removed ${result.filesDeleted} files`
6252
+ );
4868
6253
  if (result.errors.length > 0) {
4869
6254
  console.log("\n\u26A0\uFE0F Errors encountered:");
4870
6255
  result.errors.forEach((error) => {
@@ -4872,15 +6257,54 @@ var pullCommand = (0, import_cmd_ts2.command)({
4872
6257
  });
4873
6258
  process.exit(1);
4874
6259
  }
6260
+ await packmindCliHexa.writeConfig(process.cwd(), allPackages);
4875
6261
  } catch (error) {
4876
- console.error("\n\u274C Failed to pull content:");
6262
+ console.error("\n\u274C Failed to install content:");
4877
6263
  if (error instanceof Error) {
4878
6264
  const errorObj = error;
4879
6265
  if (errorObj.statusCode === 404) {
4880
6266
  console.error(` ${errorObj.message}`);
4881
- console.error(
4882
- "\n\u{1F4A1} Use `packmind-cli pull --list` to show available packages"
4883
- );
6267
+ if (configExists && configPackages.length > 0) {
6268
+ const missingPackages = allPackages.filter(
6269
+ (pkg) => configPackages.includes(pkg)
6270
+ );
6271
+ if (missingPackages.length > 0) {
6272
+ console.error(
6273
+ "\n\u{1F4A1} Either remove the following package(s) from packmind.json:"
6274
+ );
6275
+ missingPackages.forEach((pkg) => {
6276
+ console.error(` "${pkg}"`);
6277
+ });
6278
+ console.error(" Or ensure that:");
6279
+ console.error(
6280
+ " - The package slug exists and is correctly spelled"
6281
+ );
6282
+ console.error(" - The package exists in your organization");
6283
+ console.error(" - You have the correct API key configured");
6284
+ } else {
6285
+ console.error("\n\u{1F4A1} Troubleshooting tips:");
6286
+ console.error(
6287
+ " - Check if the package slug exists and is correctly spelled"
6288
+ );
6289
+ console.error(
6290
+ " - Check that the package exists in your organization"
6291
+ );
6292
+ console.error(
6293
+ " - Ensure you have the correct API key configured"
6294
+ );
6295
+ }
6296
+ } else {
6297
+ console.error("\n\u{1F4A1} Troubleshooting tips:");
6298
+ console.error(
6299
+ " - Check if the package slug exists and is correctly spelled"
6300
+ );
6301
+ console.error(
6302
+ " - Check that the package exists in your organization"
6303
+ );
6304
+ console.error(
6305
+ " - Ensure you have the correct API key configured"
6306
+ );
6307
+ }
4884
6308
  } else {
4885
6309
  console.error(` ${errorObj.message}`);
4886
6310
  const apiErrorObj = error;
@@ -4906,29 +6330,23 @@ var pullCommand = (0, import_cmd_ts2.command)({
4906
6330
  // apps/cli/src/main.ts
4907
6331
  var { version: CLI_VERSION } = require_package();
4908
6332
  function findEnvFile() {
4909
- let currentDir = process.cwd();
4910
- const startDir = currentDir;
4911
- 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;
4912
6338
  let searchDir = currentDir;
4913
- while (searchDir !== path4.parse(searchDir).root) {
4914
- if (fs4.existsSync(path4.join(searchDir, ".git"))) {
4915
- gitRootFound = true;
4916
- break;
4917
- }
4918
- searchDir = path4.dirname(searchDir);
4919
- }
4920
- while (currentDir !== path4.parse(currentDir).root) {
4921
- const envPath2 = path4.join(currentDir, ".env");
4922
- if (fs4.existsSync(envPath2)) {
6339
+ let parentDir = path7.dirname(searchDir);
6340
+ while (searchDir !== parentDir) {
6341
+ const envPath2 = path7.join(searchDir, ".env");
6342
+ if (fs6.existsSync(envPath2)) {
4923
6343
  return envPath2;
4924
6344
  }
4925
- if (gitRootFound && fs4.existsSync(path4.join(currentDir, ".git"))) {
4926
- break;
4927
- }
4928
- if (!gitRootFound && currentDir !== startDir) {
4929
- break;
6345
+ if (searchDir === stopDir) {
6346
+ return null;
4930
6347
  }
4931
- currentDir = path4.dirname(currentDir);
6348
+ searchDir = parentDir;
6349
+ parentDir = path7.dirname(searchDir);
4932
6350
  }
4933
6351
  return null;
4934
6352
  }
@@ -4953,10 +6371,11 @@ var app = (0, import_cmd_ts3.subcommands)({
4953
6371
  description: "Packmind CLI tool",
4954
6372
  cmds: {
4955
6373
  lint: lintCommand,
4956
- pull: pullCommand
6374
+ pull: pullCommand,
6375
+ install: pullCommand
4957
6376
  }
4958
6377
  });
4959
6378
  (0, import_cmd_ts3.run)(app, args).catch((error) => {
4960
- console.error(import_chalk2.default.bgRed.bold("packmind-cli"), import_chalk2.default.red(error.message));
6379
+ logErrorConsole(error.message);
4961
6380
  process.exit(1);
4962
6381
  });