forgedock 1.0.9 → 1.0.11

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.
@@ -186,7 +186,7 @@ function detectDefaultBranch(cwd) {
186
186
  );
187
187
  }
188
188
  } catch {
189
- // git not available or detached HEAD
189
+ // git unavailable or not a git repo
190
190
  }
191
191
 
192
192
  // Strategy 3: fall back
@@ -107,8 +107,26 @@ export function parseEnrichedDraft(output, draft) {
107
107
  // Found the closing brace of the top-level object — parse the candidate.
108
108
  try {
109
109
  const enriched = JSON.parse(output.slice(start, i + 1));
110
- // Basic sanity check: must have the required top-level sections.
111
- if (!enriched.project || !enriched.paths || !enriched.branches) {
110
+ // Basic sanity check: must have the required top-level sections and
111
+ // each section must be a plain non-null object. The typeof+null guard
112
+ // rejects null and primitives; Array.isArray() additionally rejects
113
+ // arrays since typeof [] === 'object' would otherwise pass them.
114
+ const isPlainObject = (v) =>
115
+ typeof v === "object" && v !== null && !Array.isArray(v);
116
+ if (
117
+ !isPlainObject(enriched.project) ||
118
+ !isPlainObject(enriched.paths) ||
119
+ !isPlainObject(enriched.branches) ||
120
+ !isPlainObject(enriched.meta)
121
+ ) {
122
+ if (process.env.FORGEDOCK_DEBUG) {
123
+ const failing = ["project", "paths", "branches", "meta"]
124
+ .filter((k) => !isPlainObject(enriched[k]))
125
+ .join(", ");
126
+ console.error(
127
+ ` ${dim("[debug]")} api enrichment: parseEnrichedDraft rejected LLM output — missing/invalid sections: ${failing}. Falling back to baseline draft.`,
128
+ );
129
+ }
112
130
  return draft;
113
131
  }
114
132
  return enriched;
package/bin/tui.mjs CHANGED
@@ -106,9 +106,6 @@ export function box(content, { title = "", padding = 1, width } = {}) {
106
106
  const lines = Array.isArray(content) ? content : String(content).split("\n");
107
107
  const pad = " ".repeat(padding);
108
108
 
109
- // Strip ANSI codes to compute visual width
110
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
111
-
112
109
  const contentWidth = lines.reduce(
113
110
  (max, l) => Math.max(max, stripAnsi(l).length),
114
111
  0,
@@ -659,8 +656,6 @@ export function table(
659
656
  ) {
660
657
  if (!rows || rows.length === 0) return "";
661
658
 
662
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
663
-
664
659
  // Compute column widths
665
660
  const colCount = Math.max(...rows.map((r) => r.length));
666
661
  const widths = Array.from({ length: colCount }, (_, c) =>
@@ -701,6 +696,66 @@ export function table(
701
696
  return lines.join("\n") + "\n";
702
697
  }
703
698
 
699
+ // ---------------------------------------------------------------------------
700
+ // ANSI string utilities (exported for testing)
701
+ // ---------------------------------------------------------------------------
702
+
703
+ /** Strip all ANSI CSI sequences from a string, returning only visible text. */
704
+ export function stripAnsi(s) {
705
+ return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
706
+ }
707
+
708
+ /**
709
+ * Truncate an ANSI-decorated string to at most `maxWidth` visible characters,
710
+ * never bisecting an escape sequence.
711
+ *
712
+ * Tokens from the cut region are suppressed — only ANSI tokens encountered
713
+ * while the visible budget is still live are forwarded to the output.
714
+ * A trailing reset (\x1b[0m) is appended when truncation occurs and the
715
+ * result already contains ANSI sequences, preventing color bleed into
716
+ * adjacent columns.
717
+ */
718
+ export function truncateVisible(str, maxWidth) {
719
+ const ansiRe = /\x1b\[[0-9;]*[A-Za-z]/g;
720
+ let visible = 0;
721
+ let result = "";
722
+ let lastIndex = 0;
723
+ let m;
724
+ ansiRe.lastIndex = 0;
725
+ while ((m = ansiRe.exec(str)) !== null) {
726
+ // Consume plain-text chars before this ANSI token
727
+ const plain = str.slice(lastIndex, m.index);
728
+ const remaining = maxWidth - visible;
729
+ if (remaining > 0) {
730
+ result += plain.slice(0, remaining);
731
+ visible += Math.min(plain.length, remaining);
732
+ }
733
+ // Only include the ANSI token while the visible budget is not yet exhausted.
734
+ // Once the cut point is reached, tokens from the removed region are suppressed;
735
+ // the post-loop reset guard below handles color-state cleanup.
736
+ if (visible < maxWidth) {
737
+ result += m[0];
738
+ }
739
+ lastIndex = ansiRe.lastIndex;
740
+ }
741
+ // Remaining plain text after last ANSI token
742
+ const remaining = maxWidth - visible;
743
+ if (remaining > 0) {
744
+ result += str.slice(lastIndex, lastIndex + remaining);
745
+ }
746
+ // If truncation occurred (or visible length exactly equals maxWidth) and the
747
+ // result contains any ANSI sequences, append a full reset to prevent color from
748
+ // bleeding into adjacent columns. The guard uses >= rather than > to cover the
749
+ // exact-boundary case: when visible === maxWidth the loop guard (visible < maxWidth)
750
+ // suppresses the original trailing RESET token, so the post-loop guard must fire
751
+ // to close any open sequences. Inputs shorter than maxWidth are unaffected because
752
+ // stripAnsi(str).length < maxWidth makes the condition false.
753
+ if (stripAnsi(str).length >= maxWidth && result.includes("\x1b[")) {
754
+ result += "\x1b[0m";
755
+ }
756
+ return result;
757
+ }
758
+
704
759
  // ---------------------------------------------------------------------------
705
760
  // Annotated Review Screen
706
761
  // ---------------------------------------------------------------------------
@@ -821,50 +876,12 @@ export async function annotatedReviewScreen(
821
876
  };
822
877
  }
823
878
 
824
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
825
-
826
879
  // Pad an ANSI-decorated string to `width` visible characters.
827
880
  function padVisible(str, width) {
828
881
  const visual = stripAnsi(str).length;
829
882
  return str + " ".repeat(Math.max(0, width - visual));
830
883
  }
831
884
 
832
- // Truncate an ANSI-decorated string to at most `maxWidth` visible characters,
833
- // never bisecting an escape sequence.
834
- function truncateVisible(str, maxWidth) {
835
- const ansiRe = /\x1b\[[0-9;]*[A-Za-z]/g;
836
- let visible = 0;
837
- let result = "";
838
- let lastIndex = 0;
839
- let m;
840
- ansiRe.lastIndex = 0;
841
- while ((m = ansiRe.exec(str)) !== null) {
842
- // Consume plain-text chars before this ANSI token
843
- const plain = str.slice(lastIndex, m.index);
844
- const remaining = maxWidth - visible;
845
- if (remaining > 0) {
846
- result += plain.slice(0, remaining);
847
- visible += Math.min(plain.length, remaining);
848
- }
849
- // Always include the ANSI token (zero visible width)
850
- result += m[0];
851
- lastIndex = ansiRe.lastIndex;
852
- }
853
- // Remaining plain text after last ANSI token
854
- const remaining = maxWidth - visible;
855
- if (remaining > 0) {
856
- result += str.slice(lastIndex, lastIndex + remaining);
857
- }
858
- // If truncation occurred and the result contains any ANSI sequences,
859
- // append a full reset to prevent color from bleeding into adjacent columns.
860
- // Truncation is detected by checking whether the original string has more
861
- // visible characters than maxWidth (i.e. stripAnsi(str).length > maxWidth).
862
- if (stripAnsi(str).length > maxWidth && result.includes("\x1b[")) {
863
- result += "\x1b[0m";
864
- }
865
- return result;
866
- }
867
-
868
885
  // ── Render the annotated table ────────────────────────────────────────────
869
886
  function renderScreen() {
870
887
  process.stdout.write("\n");