forgedock 1.0.7 → 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 +2054 -496
- 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/autopilot.md +6 -0
- package/commands/deploy-info.md +8 -8
- package/commands/forgedock-init.md +305 -384
- package/commands/orchestrate.md +3 -3
- package/commands/review-pr-staging.md +21 -21
- package/commands/review-pr.md +17 -17
- package/commands/rollback.md +2 -2
- package/commands/work-on/build/validate.md +4 -4
- package/commands/work-on-monolithic.md +18 -1
- package/commands/work-on.md +4 -4
- 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 +50 -19
- package/scripts/verify-sops-chain.sh +4 -2
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
|
+
}
|
package/commands/autopilot.md
CHANGED
|
@@ -13,6 +13,12 @@ argument-hint: [--fix | --recon-only | --fix --limit 5 | --dry-run]
|
|
|
13
13
|
- `{EMEMO_PATH}` ← `services.ememo_path` (optional) — path on production server to open eMemo files
|
|
14
14
|
- `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data collection in the Analytics Snapshot agent
|
|
15
15
|
|
|
16
|
+
Resolve `{BILLING_ENABLED}` from `forge.yaml` before executing any phase:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
BILLING_ENABLED=$(yq '.billing.enabled // false' "$(git rev-parse --show-toplevel)/forge.yaml" 2>/dev/null || echo 'false')
|
|
20
|
+
```
|
|
21
|
+
|
|
16
22
|
You are an autonomous improvement engine for this project. Your job is to **find what's wrong, create trackable issues, and optionally fix the highest-impact ones** — all in a single cycle. Every cycle leaves the platform measurably better than before.
|
|
17
23
|
|
|
18
24
|
**This is designed to run repeatedly.** Each cycle builds on the last — issues created in cycle N get fixed in cycle N+1. The platform compounds improvements over time.
|
package/commands/deploy-info.md
CHANGED
|
@@ -156,9 +156,9 @@ done
|
|
|
156
156
|
git diff --stat origin/$TARGET..origin/$SOURCE
|
|
157
157
|
|
|
158
158
|
# Read layout paths from forge.yaml, fall back to AlterLab defaults
|
|
159
|
-
LAYOUT_API=$(yq '.review.layout.api // "services/api"'
|
|
160
|
-
LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"'
|
|
161
|
-
LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"'
|
|
159
|
+
LAYOUT_API=$(yq '.review.layout.api // "services/api"' forge.yaml 2>/dev/null || echo "services/api")
|
|
160
|
+
LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"' forge.yaml 2>/dev/null || echo "services/worker")
|
|
161
|
+
LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"' forge.yaml 2>/dev/null || echo "web/src/app")
|
|
162
162
|
# Derive web root from pages root (strip trailing /src/app or similar suffix if present)
|
|
163
163
|
LAYOUT_WEB=$(echo "$LAYOUT_PAGES" | sed 's|/src/app$||; s|/src$||')
|
|
164
164
|
|
|
@@ -180,7 +180,7 @@ done | sort
|
|
|
180
180
|
|
|
181
181
|
```bash
|
|
182
182
|
# Read migrations path from forge.yaml, fall back to infra/migrations
|
|
183
|
-
LAYOUT_MIGRATIONS=$(yq '.review.layout.migrations // "infra/migrations"'
|
|
183
|
+
LAYOUT_MIGRATIONS=$(yq '.review.layout.migrations // "infra/migrations"' forge.yaml 2>/dev/null || echo "infra/migrations")
|
|
184
184
|
|
|
185
185
|
# Are there new migrations?
|
|
186
186
|
MIGRATIONS=$(git diff --name-only origin/$TARGET..origin/$SOURCE | grep "^${LAYOUT_MIGRATIONS}/" | sort)
|
|
@@ -220,18 +220,18 @@ Analyze the deploy payload and flag risks:
|
|
|
220
220
|
# Check for new env vars that might not be in the configured secrets backend
|
|
221
221
|
NEW_ENV=$(git diff origin/$TARGET..origin/$SOURCE -- '.env.example' | grep "^+" | grep -v "^+++" | grep -v "^#")
|
|
222
222
|
if [ -n "$NEW_ENV" ]; then
|
|
223
|
-
echo "⚠️ NEW ENVIRONMENT VARIABLES — verify they exist in your secrets backend ({
|
|
223
|
+
echo "⚠️ NEW ENVIRONMENT VARIABLES — verify they exist in your secrets backend ({DEPLOY_SECRETS_BACKEND}):"
|
|
224
224
|
echo "$NEW_ENV"
|
|
225
225
|
# Cross-check with SOPS chain (only when deploy.secrets_backend == "sops")
|
|
226
226
|
# If your project uses a different backend, skip this block and verify manually.
|
|
227
|
-
if [ "{
|
|
227
|
+
if [ "{DEPLOY_SECRETS_BACKEND}" = "sops" ]; then
|
|
228
228
|
for var in $(echo "$NEW_ENV" | grep -oP '^\+\K[A-Z_]+'); do
|
|
229
229
|
if ! grep -q "$var" scripts/decrypt-secrets.sh 2>/dev/null; then
|
|
230
230
|
echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
|
|
231
231
|
fi
|
|
232
232
|
done
|
|
233
233
|
else
|
|
234
|
-
echo " ℹ️ SOPS chain check skipped — deploy.secrets_backend is '{
|
|
234
|
+
echo " ℹ️ SOPS chain check skipped — deploy.secrets_backend is '{DEPLOY_SECRETS_BACKEND}'. Verify new vars in your configured backend."
|
|
235
235
|
fi
|
|
236
236
|
fi
|
|
237
237
|
```
|
|
@@ -249,7 +249,7 @@ Generate a checklist based on what's in the deploy:
|
|
|
249
249
|
- [ ] All PRs in this deploy have been reviewed
|
|
250
250
|
- [ ] No open `needs-human` issues blocking deploy
|
|
251
251
|
- [ ] {If migrations} Database backup verified
|
|
252
|
-
- [ ] {If new env vars} Secrets backend ({
|
|
252
|
+
- [ ] {If new env vars} Secrets backend ({DEPLOY_SECRETS_BACKEND}) updated with new vars; verify via your configured secrets chain
|
|
253
253
|
- [ ] {If traefik changes} Routing tested locally
|
|
254
254
|
|
|
255
255
|
### Deploy Command
|