forgedock 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/forge-utils.mjs +164 -0
- package/bin/forgedock.mjs +1914 -394
- package/bin/hooks/session-start.mjs +317 -0
- package/bin/init-detect.mjs +325 -0
- package/bin/init-enrich-api.mjs +203 -0
- package/bin/registry.mjs +358 -0
- package/bin/tui.mjs +453 -2
- package/commands/forgedock-init.md +305 -384
- package/docs/CONFIG.md +167 -0
- package/docs/superpowers/specs/2026-06-09-intelligent-onboarding-design.md +172 -0
- package/package.json +5 -1
- package/scripts/verify-route-registration.sh +23 -17
package/bin/tui.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export function box(content, { title = "", padding = 1, width } = {}) {
|
|
|
107
107
|
const pad = " ".repeat(padding);
|
|
108
108
|
|
|
109
109
|
// Strip ANSI codes to compute visual width
|
|
110
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*
|
|
110
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
111
111
|
|
|
112
112
|
const contentWidth = lines.reduce(
|
|
113
113
|
(max, l) => Math.max(max, stripAnsi(l).length),
|
|
@@ -659,7 +659,7 @@ export function table(
|
|
|
659
659
|
) {
|
|
660
660
|
if (!rows || rows.length === 0) return "";
|
|
661
661
|
|
|
662
|
-
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*
|
|
662
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
663
663
|
|
|
664
664
|
// Compute column widths
|
|
665
665
|
const colCount = Math.max(...rows.map((r) => r.length));
|
|
@@ -700,3 +700,454 @@ export function table(
|
|
|
700
700
|
|
|
701
701
|
return lines.join("\n") + "\n";
|
|
702
702
|
}
|
|
703
|
+
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
// Annotated Review Screen
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Confidence badge — returns a short colored indicator string.
|
|
710
|
+
* Uses function wrappers (not raw ANSI constants) so NO_COLOR is respected.
|
|
711
|
+
*
|
|
712
|
+
* @param {'high'|'medium'|'low'} confidence
|
|
713
|
+
* @returns {string}
|
|
714
|
+
*/
|
|
715
|
+
function confidenceBadge(confidence) {
|
|
716
|
+
switch (confidence) {
|
|
717
|
+
case "high":
|
|
718
|
+
return green("[high]");
|
|
719
|
+
case "medium":
|
|
720
|
+
return yellow("[med] ");
|
|
721
|
+
case "low":
|
|
722
|
+
return red("[low] ");
|
|
723
|
+
default:
|
|
724
|
+
return dim("[???] ");
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* All ConfigDraft fields in display order with their human-readable labels
|
|
730
|
+
* and the forge.yaml key used for TODO comments.
|
|
731
|
+
*/
|
|
732
|
+
const REVIEW_FIELDS = [
|
|
733
|
+
{ key: "owner", label: "GitHub Owner", section: "project", draftPath: ["project", "owner"] },
|
|
734
|
+
{ key: "repo", label: "Repository Name", section: "project", draftPath: ["project", "repo"] },
|
|
735
|
+
{ key: "name", label: "Project Name", section: "project", draftPath: ["project", "name"] },
|
|
736
|
+
{ key: "description", label: "Description", section: "project", draftPath: null },
|
|
737
|
+
{ key: "root", label: "Repository Root", section: "paths", draftPath: ["paths", "root"] },
|
|
738
|
+
{ key: "worktreeBase", label: "Worktree Base", section: "paths", draftPath: ["paths", "worktreeBase"] },
|
|
739
|
+
{ key: "defaultBranch", label: "Default Branch", section: "branches", draftPath: ["branches", "default"] },
|
|
740
|
+
{ key: "stagingBranch", label: "Staging Branch", section: "branches", draftPath: ["branches", "staging"] },
|
|
741
|
+
];
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Render the annotated review screen for a ConfigDraft.
|
|
745
|
+
*
|
|
746
|
+
* Shows every required forge.yaml field with its detected value, confidence
|
|
747
|
+
* badge, source, and a plain-language "why" explanation. The user presses
|
|
748
|
+
* Enter to accept all values, or types a field number to edit that field
|
|
749
|
+
* inline. Fields with low confidence are flagged — the caller can use the
|
|
750
|
+
* returned metadata to inject \`# TODO(forgedock:<field>)\` YAML comments.
|
|
751
|
+
*
|
|
752
|
+
* @param {import('./init-detect.mjs').ConfigDraft} draft
|
|
753
|
+
* The ConfigDraft returned by detectConfig().
|
|
754
|
+
* @param {object} [opts]
|
|
755
|
+
* @param {boolean} [opts.hasExistingConfig=false]
|
|
756
|
+
* When true, shows a "diff-style" header noting an existing forge.yaml will
|
|
757
|
+
* be overwritten.
|
|
758
|
+
* @param {string} [opts.existingContent=""]
|
|
759
|
+
* Serialized content of the existing forge.yaml for diff context display.
|
|
760
|
+
* @param {boolean} [opts.showSources=false]
|
|
761
|
+
* When true (e.g. \`--verbose\` mode), renders the Notes/why block for ALL
|
|
762
|
+
* fields — including high-confidence ones — so the user can see every
|
|
763
|
+
* detection source and reasoning string.
|
|
764
|
+
*
|
|
765
|
+
* @returns {Promise<{
|
|
766
|
+
* owner: string,
|
|
767
|
+
* repo: string,
|
|
768
|
+
* name: string,
|
|
769
|
+
* description: string,
|
|
770
|
+
* root: string,
|
|
771
|
+
* worktreeBase: string,
|
|
772
|
+
* defaultBranch: string,
|
|
773
|
+
* stagingBranch: string,
|
|
774
|
+
* lowConfidenceKeys: string[],
|
|
775
|
+
* }>}
|
|
776
|
+
* Resolves with the accepted (or edited) values, plus the list of field keys
|
|
777
|
+
* that had low confidence at the time of the screen render (so the caller can
|
|
778
|
+
* inject TODO comments).
|
|
779
|
+
*/
|
|
780
|
+
export async function annotatedReviewScreen(
|
|
781
|
+
draft,
|
|
782
|
+
{ hasExistingConfig = false, existingContent = "", showSources = false } = {},
|
|
783
|
+
) {
|
|
784
|
+
// Helper — pull a field from the draft by path, or return a low-confidence placeholder.
|
|
785
|
+
function getField(draftPath) {
|
|
786
|
+
if (!draftPath) return { value: "", confidence: "low", source: "none", why: "Not auto-detected" };
|
|
787
|
+
let node = draft;
|
|
788
|
+
for (const key of draftPath) {
|
|
789
|
+
if (node && typeof node === "object" && key in node) {
|
|
790
|
+
node = node[key];
|
|
791
|
+
} else {
|
|
792
|
+
return { value: "", confidence: "low", source: "none", why: "Not auto-detected" };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (node && typeof node === "object" && "value" in node) return node;
|
|
796
|
+
return { value: "", confidence: "low", source: "none", why: "Not auto-detected" };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Build the mutable values map (starts from draft detections).
|
|
800
|
+
// Description is not part of ConfigDraft — always starts empty with low confidence.
|
|
801
|
+
const values = {};
|
|
802
|
+
const confidences = {};
|
|
803
|
+
const sources = {};
|
|
804
|
+
const whys = {};
|
|
805
|
+
|
|
806
|
+
for (const fd of REVIEW_FIELDS) {
|
|
807
|
+
const field = getField(fd.draftPath);
|
|
808
|
+
values[fd.key] = field.value;
|
|
809
|
+
confidences[fd.key] = field.confidence;
|
|
810
|
+
sources[fd.key] = field.source;
|
|
811
|
+
whys[fd.key] = field.why;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Non-TTY: return detect values directly without interaction.
|
|
815
|
+
if (!process.stdin.isTTY) {
|
|
816
|
+
return {
|
|
817
|
+
...values,
|
|
818
|
+
lowConfidenceKeys: REVIEW_FIELDS.filter(
|
|
819
|
+
(fd) => confidences[fd.key] === "low",
|
|
820
|
+
).map((fd) => fd.key),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
825
|
+
|
|
826
|
+
// Pad an ANSI-decorated string to `width` visible characters.
|
|
827
|
+
function padVisible(str, width) {
|
|
828
|
+
const visual = stripAnsi(str).length;
|
|
829
|
+
return str + " ".repeat(Math.max(0, width - visual));
|
|
830
|
+
}
|
|
831
|
+
|
|
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
|
+
// ── Render the annotated table ────────────────────────────────────────────
|
|
869
|
+
function renderScreen() {
|
|
870
|
+
process.stdout.write("\n");
|
|
871
|
+
|
|
872
|
+
if (hasExistingConfig) {
|
|
873
|
+
process.stdout.write(
|
|
874
|
+
box(
|
|
875
|
+
[
|
|
876
|
+
"",
|
|
877
|
+
` ${yellow("forge.yaml already exists.")} The values below will ${bold("overwrite")} it.`,
|
|
878
|
+
` ${dim("A backup will be created before writing.")}`,
|
|
879
|
+
"",
|
|
880
|
+
],
|
|
881
|
+
{ title: "Overwrite Mode" },
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Header
|
|
887
|
+
process.stdout.write(
|
|
888
|
+
`${bold(" forge.yaml configuration")} ${dim("(detected from this repository)")}\n\n`,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Table header
|
|
892
|
+
const NUM_W = 3;
|
|
893
|
+
const KEY_W = 16;
|
|
894
|
+
const VAL_W = 36;
|
|
895
|
+
const BADGE_W = 7; // "[high]" + space = 7 visible chars
|
|
896
|
+
const SOURCE_W = 32;
|
|
897
|
+
|
|
898
|
+
const hdr = [
|
|
899
|
+
dim("#".padEnd(NUM_W)),
|
|
900
|
+
dim("Field".padEnd(KEY_W)),
|
|
901
|
+
dim("Value".padEnd(VAL_W)),
|
|
902
|
+
dim("Conf".padEnd(BADGE_W)),
|
|
903
|
+
dim("Source"),
|
|
904
|
+
].join(" ");
|
|
905
|
+
process.stdout.write(" " + hdr + "\n");
|
|
906
|
+
process.stdout.write(
|
|
907
|
+
" " +
|
|
908
|
+
dim("─".repeat(NUM_W + KEY_W + VAL_W + BADGE_W + SOURCE_W + 8)) +
|
|
909
|
+
"\n",
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
for (let i = 0; i < REVIEW_FIELDS.length; i++) {
|
|
913
|
+
const fd = REVIEW_FIELDS[i];
|
|
914
|
+
const num = dim(String(i + 1).padEnd(NUM_W));
|
|
915
|
+
const key = fd.label.padEnd(KEY_W);
|
|
916
|
+
const rawVal = values[fd.key] || dim("(empty)");
|
|
917
|
+
// Pad/truncate on visible width only — ANSI escape bytes must not be counted.
|
|
918
|
+
const displayVal = stripAnsi(rawVal).length > VAL_W
|
|
919
|
+
? truncateVisible(rawVal, VAL_W - 1) + dim("…")
|
|
920
|
+
: padVisible(rawVal, VAL_W);
|
|
921
|
+
const badge = confidenceBadge(confidences[fd.key]);
|
|
922
|
+
// Truncate long source strings
|
|
923
|
+
const rawSrc = sources[fd.key] || "";
|
|
924
|
+
const displaySrc = rawSrc.length > SOURCE_W
|
|
925
|
+
? rawSrc.slice(0, SOURCE_W - 1) + "…"
|
|
926
|
+
: rawSrc;
|
|
927
|
+
|
|
928
|
+
process.stdout.write(
|
|
929
|
+
` ${num} ${key} ${displayVal} ${badge} ${dim(displaySrc)}\n`,
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
process.stdout.write("\n");
|
|
934
|
+
|
|
935
|
+
// Legend
|
|
936
|
+
process.stdout.write(
|
|
937
|
+
` ${dim("Confidence:")} ${green("[high]")} detected ${yellow("[med] ")} inferred ${red("[low] ")} guessed\n`,
|
|
938
|
+
);
|
|
939
|
+
process.stdout.write("\n");
|
|
940
|
+
|
|
941
|
+
// Why summary for non-high fields (or ALL fields when showSources is enabled)
|
|
942
|
+
const interestingFields = REVIEW_FIELDS.filter(
|
|
943
|
+
(fd) => (confidences[fd.key] !== "high" || showSources) && whys[fd.key],
|
|
944
|
+
);
|
|
945
|
+
if (interestingFields.length > 0) {
|
|
946
|
+
process.stdout.write(` ${bold("Notes:")}\n`);
|
|
947
|
+
for (const fd of interestingFields) {
|
|
948
|
+
const badge = confidenceBadge(confidences[fd.key]);
|
|
949
|
+
process.stdout.write(
|
|
950
|
+
` ${badge} ${bold(fd.label)}: ${dim(whys[fd.key])}\n`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
process.stdout.write("\n");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// TODO flag notice for low-confidence fields — derived live so it
|
|
957
|
+
// reflects any confidence promotions from interactive edits.
|
|
958
|
+
const liveLowKeys = REVIEW_FIELDS.filter(
|
|
959
|
+
(fd) => confidences[fd.key] === "low",
|
|
960
|
+
);
|
|
961
|
+
if (liveLowKeys.length > 0) {
|
|
962
|
+
const todoNames = liveLowKeys.map((fd) => fd.label).join(", ");
|
|
963
|
+
process.stdout.write(
|
|
964
|
+
` ${red("⚠")} ${bold("Low-confidence fields")} will be written with a ${cyan("# TODO(forgedock:<field>)")} comment:\n`,
|
|
965
|
+
);
|
|
966
|
+
process.stdout.write(` ${dim(todoNames)}\n\n`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
process.stdout.write(
|
|
970
|
+
` ${dim("Press")} ${bold("Enter")} ${dim("to accept all values, or enter a field number to edit it.")}\n\n`,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ── Inline editor for a single field ─────────────────────────────────────
|
|
975
|
+
async function editField(index) {
|
|
976
|
+
const fd = REVIEW_FIELDS[index];
|
|
977
|
+
const current = values[fd.key];
|
|
978
|
+
|
|
979
|
+
// We need to temporarily use readline (cooked mode input).
|
|
980
|
+
// Exit raw mode first if it was active.
|
|
981
|
+
let wasRaw = false;
|
|
982
|
+
if (process.stdin.isRaw) {
|
|
983
|
+
try {
|
|
984
|
+
process.stdin.setRawMode(false);
|
|
985
|
+
wasRaw = true;
|
|
986
|
+
} catch {
|
|
987
|
+
/* ignore */
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
process.stdin.resume();
|
|
991
|
+
process.stdin.setEncoding("utf-8");
|
|
992
|
+
|
|
993
|
+
const rl = readline.createInterface({
|
|
994
|
+
input: process.stdin,
|
|
995
|
+
output: process.stdout,
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const hint = current ? ` ${dim(`(current: ${current})`)}` : "";
|
|
999
|
+
const newValue = await new Promise((resolve) => {
|
|
1000
|
+
rl.question(
|
|
1001
|
+
` ${cyan("?")} ${bold(fd.label)}${hint}: `,
|
|
1002
|
+
(answer) => {
|
|
1003
|
+
rl.close();
|
|
1004
|
+
resolve(answer.trim() === "" ? current : answer.trim());
|
|
1005
|
+
},
|
|
1006
|
+
);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
// Restore raw mode if it was active.
|
|
1010
|
+
if (wasRaw) {
|
|
1011
|
+
try {
|
|
1012
|
+
process.stdin.setRawMode(true);
|
|
1013
|
+
} catch {
|
|
1014
|
+
/* ignore */
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Mark as user-edited (medium confidence).
|
|
1019
|
+
values[fd.key] = newValue;
|
|
1020
|
+
if (confidences[fd.key] !== "high") {
|
|
1021
|
+
confidences[fd.key] = "medium";
|
|
1022
|
+
sources[fd.key] = "user input";
|
|
1023
|
+
whys[fd.key] = "Edited interactively during review";
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ── Interaction loop ──────────────────────────────────────────────────────
|
|
1028
|
+
renderScreen();
|
|
1029
|
+
|
|
1030
|
+
// Set up raw mode for single-keypress interaction.
|
|
1031
|
+
const wasRaw = process.stdin.isRaw;
|
|
1032
|
+
try {
|
|
1033
|
+
process.stdin.setRawMode(true);
|
|
1034
|
+
} catch {
|
|
1035
|
+
// Terminal doesn't support raw mode — fall back to accept-all.
|
|
1036
|
+
return {
|
|
1037
|
+
...values,
|
|
1038
|
+
lowConfidenceKeys: REVIEW_FIELDS.filter(
|
|
1039
|
+
(fd) => confidences[fd.key] === "low",
|
|
1040
|
+
).map((fd) => fd.key),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
process.stdin.resume();
|
|
1044
|
+
process.stdin.setEncoding("utf-8");
|
|
1045
|
+
|
|
1046
|
+
let accepted = false;
|
|
1047
|
+
|
|
1048
|
+
// Buffer for multi-character field number input (1–8 fields).
|
|
1049
|
+
let inputBuffer = "";
|
|
1050
|
+
|
|
1051
|
+
const sigintHandler = () => {
|
|
1052
|
+
try {
|
|
1053
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
1054
|
+
} catch {
|
|
1055
|
+
/* ignore */
|
|
1056
|
+
}
|
|
1057
|
+
process.stdin.pause();
|
|
1058
|
+
process.stdin.removeAllListeners("data");
|
|
1059
|
+
if (process.stdout.isTTY) process.stdout.write("\x1b[?25h");
|
|
1060
|
+
process.exit(130);
|
|
1061
|
+
};
|
|
1062
|
+
process.once("SIGINT", sigintHandler);
|
|
1063
|
+
|
|
1064
|
+
await new Promise((resolve) => {
|
|
1065
|
+
function onData(key) {
|
|
1066
|
+
if (key === "") {
|
|
1067
|
+
// Ctrl+C
|
|
1068
|
+
sigintHandler();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (key === "\r" || key === "\n") {
|
|
1073
|
+
// Enter with no pending number → accept all
|
|
1074
|
+
if (inputBuffer === "") {
|
|
1075
|
+
accepted = true;
|
|
1076
|
+
cleanup();
|
|
1077
|
+
resolve();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
// Enter after typing a number → edit that field
|
|
1081
|
+
const num = parseInt(inputBuffer, 10);
|
|
1082
|
+
inputBuffer = "";
|
|
1083
|
+
if (num >= 1 && num <= REVIEW_FIELDS.length) {
|
|
1084
|
+
cleanup();
|
|
1085
|
+
editField(num - 1).then(() => {
|
|
1086
|
+
// Re-render and re-enter interaction loop
|
|
1087
|
+
renderScreen();
|
|
1088
|
+
try {
|
|
1089
|
+
process.stdin.setRawMode(true);
|
|
1090
|
+
} catch {
|
|
1091
|
+
accepted = true;
|
|
1092
|
+
resolve();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
process.stdin.resume();
|
|
1096
|
+
process.stdin.setEncoding("utf-8");
|
|
1097
|
+
process.stdin.on("data", onData);
|
|
1098
|
+
});
|
|
1099
|
+
} else {
|
|
1100
|
+
// Invalid number — ignore
|
|
1101
|
+
process.stdout.write(
|
|
1102
|
+
` ${red("Invalid field number")} — enter 1–${REVIEW_FIELDS.length} or press Enter to accept.\n`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Digit key — accumulate field number
|
|
1109
|
+
if (/^[0-9]$/.test(key)) {
|
|
1110
|
+
inputBuffer += key;
|
|
1111
|
+
process.stdout.write(key); // echo digit
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Backspace
|
|
1116
|
+
if (key === "" || key === "") {
|
|
1117
|
+
if (inputBuffer.length > 0) {
|
|
1118
|
+
inputBuffer = inputBuffer.slice(0, -1);
|
|
1119
|
+
process.stdout.write(" "); // erase char
|
|
1120
|
+
}
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Any other key — ignore
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function cleanup() {
|
|
1128
|
+
try {
|
|
1129
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
1130
|
+
} catch {
|
|
1131
|
+
/* ignore */
|
|
1132
|
+
}
|
|
1133
|
+
process.stdin.pause();
|
|
1134
|
+
process.stdin.removeListener("data", onData);
|
|
1135
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
process.stdin.on("data", onData);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
if (accepted) {
|
|
1142
|
+
process.stdout.write(
|
|
1143
|
+
`\n ${green("✔")} ${bold("All values accepted.")}\n\n`,
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
...values,
|
|
1149
|
+
lowConfidenceKeys: REVIEW_FIELDS.filter(
|
|
1150
|
+
(fd) => confidences[fd.key] === "low",
|
|
1151
|
+
).map((fd) => fd.key),
|
|
1152
|
+
};
|
|
1153
|
+
}
|