@sundaeswap/sprinkles 0.8.2 → 0.9.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.
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +19 -7
- package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +3 -0
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +109 -98
- package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +474 -177
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/cjs/Sprinkle/types.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js +19 -7
- package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +3 -0
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js +110 -99
- package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +477 -180
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/types.js.map +1 -1
- package/dist/types/Sprinkle/index.d.ts +29 -0
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/Sprinkle/types.d.ts +6 -0
- package/dist/types/Sprinkle/types.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/enhancements.test.ts +20 -7
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +1 -0
- package/src/Sprinkle/__tests__/show-menu.test.ts +132 -116
- package/src/Sprinkle/index.ts +546 -186
- package/src/Sprinkle/types.ts +6 -0
|
@@ -617,9 +617,133 @@ class Sprinkle {
|
|
|
617
617
|
}
|
|
618
618
|
}
|
|
619
619
|
}
|
|
620
|
+
|
|
621
|
+
// --- Importing profiles from external files ---
|
|
622
|
+
|
|
623
|
+
static globToRegex(pattern) {
|
|
624
|
+
const escaped = pattern.split("*").map(p => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
625
|
+
return new RegExp(`^${escaped}$`);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Scan the current working directory for files matching `importPattern`.
|
|
630
|
+
* Returns absolute paths. Designed to surface state files committed to a
|
|
631
|
+
* project repo (e.g. `state.preview.json`) that the consumer might want
|
|
632
|
+
* to restore from.
|
|
633
|
+
*/
|
|
634
|
+
findImportableProfiles(cwd = process.cwd()) {
|
|
635
|
+
if (this.options.importPattern === null) return [];
|
|
636
|
+
const pattern = this.options.importPattern ?? "state.*.json";
|
|
637
|
+
const regex = Sprinkle.globToRegex(pattern);
|
|
638
|
+
try {
|
|
639
|
+
return fs.readdirSync(cwd).filter(f => regex.test(f)).map(f => path.join(cwd, f));
|
|
640
|
+
} catch {
|
|
641
|
+
return [];
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Read a profile or raw-settings JSON file and save it as a new local
|
|
647
|
+
* profile. Prompts for missing sensitive fields before writing.
|
|
648
|
+
*/
|
|
649
|
+
async importProfileFromFile(filePath) {
|
|
650
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
651
|
+
const parsed = JSON.parse(content, Sprinkle.bigIntReviver);
|
|
652
|
+
let importedSettings;
|
|
653
|
+
let importedMeta;
|
|
654
|
+
let importedDefaults = {};
|
|
655
|
+
if (parsed && typeof parsed === "object" && "settings" in parsed && "meta" in parsed) {
|
|
656
|
+
const obj = parsed;
|
|
657
|
+
importedMeta = obj.meta;
|
|
658
|
+
importedSettings = obj.settings;
|
|
659
|
+
importedDefaults = obj.defaults ?? {};
|
|
660
|
+
} else {
|
|
661
|
+
importedSettings = parsed;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// If the file came from this same Sprinkle install (same encryption),
|
|
665
|
+
// decrypt sensitive fields. Otherwise we'll re-prompt for missing ones below.
|
|
666
|
+
try {
|
|
667
|
+
importedSettings = await this.decryptSettings(importedSettings);
|
|
668
|
+
} catch {
|
|
669
|
+
// ignore - encrypted with a different key, will re-prompt
|
|
670
|
+
}
|
|
671
|
+
const baseName = path.basename(filePath).replace(/\.json$/, "");
|
|
672
|
+
const meta = await this.promptProfileMeta(importedMeta?.name ?? baseName, importedMeta?.description);
|
|
673
|
+
if (meta === null) return; // user cancelled
|
|
674
|
+
const {
|
|
675
|
+
name,
|
|
676
|
+
description
|
|
677
|
+
} = meta;
|
|
678
|
+
|
|
679
|
+
// Re-prompt any sensitive fields that are missing or look encrypted
|
|
680
|
+
const sensitivePaths = (0, _encryption.collectSensitivePaths)(this.type);
|
|
681
|
+
for (const p of sensitivePaths) {
|
|
682
|
+
const value = (0, _encryption.getNestedValue)(importedSettings, p);
|
|
683
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
684
|
+
const entered = await (0, _prompts.passwordCancellable)({
|
|
685
|
+
message: `Enter value for sensitive field "${p}":`
|
|
686
|
+
});
|
|
687
|
+
if (entered === null) return; // user cancelled
|
|
688
|
+
(0, _encryption.setNestedValue)(importedSettings, p, entered);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const profilesDir = Sprinkle.profilesDir(this.storagePath);
|
|
692
|
+
if (!fs.existsSync(profilesDir)) {
|
|
693
|
+
fs.mkdirSync(profilesDir, {
|
|
694
|
+
recursive: true
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const id = Sprinkle.findAvailableId(profilesDir, Sprinkle.sanitizeProfileId(name));
|
|
698
|
+
const now = new Date().toISOString();
|
|
699
|
+
this.profileId = id;
|
|
700
|
+
this.profileMeta = {
|
|
701
|
+
name,
|
|
702
|
+
description,
|
|
703
|
+
createdAt: now,
|
|
704
|
+
updatedAt: now
|
|
705
|
+
};
|
|
706
|
+
this.settings = importedSettings;
|
|
707
|
+
this.defaults = importedDefaults;
|
|
708
|
+
this.saveProfile();
|
|
709
|
+
fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), id, "utf-8");
|
|
710
|
+
}
|
|
711
|
+
async promptImportFromCwd() {
|
|
712
|
+
const importable = this.findImportableProfiles();
|
|
713
|
+
if (importable.length === 0) return false;
|
|
714
|
+
const cancelToken = "__cancel__";
|
|
715
|
+
const choices = [...importable.map(f => ({
|
|
716
|
+
name: `Import ${path.basename(f)}`,
|
|
717
|
+
value: f
|
|
718
|
+
})), {
|
|
719
|
+
name: "Enter a different path...",
|
|
720
|
+
value: "__custom__"
|
|
721
|
+
}, {
|
|
722
|
+
name: "Skip import",
|
|
723
|
+
value: cancelToken
|
|
724
|
+
}];
|
|
725
|
+
const choice = await (0, _prompts.select)({
|
|
726
|
+
message: "Found importable state file(s) in this directory. Restore from one?",
|
|
727
|
+
choices
|
|
728
|
+
});
|
|
729
|
+
if (choice === null || choice === cancelToken) return false;
|
|
730
|
+
let filePath = choice;
|
|
731
|
+
if (choice === "__custom__") {
|
|
732
|
+
const entered = await (0, _prompts.inputCancellable)({
|
|
733
|
+
message: "Path to state file:",
|
|
734
|
+
validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
|
|
735
|
+
});
|
|
736
|
+
if (entered === null) return false;
|
|
737
|
+
filePath = entered;
|
|
738
|
+
}
|
|
739
|
+
await this.importProfileFromFile(filePath);
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
620
742
|
async selectOrCreateProfile() {
|
|
621
743
|
const profiles = this.scanProfiles();
|
|
622
744
|
if (profiles.length === 0) {
|
|
745
|
+
const imported = await this.promptImportFromCwd();
|
|
746
|
+
if (imported) return;
|
|
623
747
|
await this.createProfile();
|
|
624
748
|
return;
|
|
625
749
|
}
|
|
@@ -742,8 +866,318 @@ class Sprinkle {
|
|
|
742
866
|
|
|
743
867
|
// --- Menu ---
|
|
744
868
|
|
|
869
|
+
async importProfileInteractive() {
|
|
870
|
+
const importable = this.findImportableProfiles();
|
|
871
|
+
const customToken = "__custom__";
|
|
872
|
+
let filePath;
|
|
873
|
+
if (importable.length > 0) {
|
|
874
|
+
const choice = await (0, _prompts.select)({
|
|
875
|
+
message: "Select a file to import:",
|
|
876
|
+
choices: [...importable.map(f => ({
|
|
877
|
+
name: path.basename(f),
|
|
878
|
+
value: f
|
|
879
|
+
})), {
|
|
880
|
+
name: "Enter a different path...",
|
|
881
|
+
value: customToken
|
|
882
|
+
}]
|
|
883
|
+
});
|
|
884
|
+
if (choice === null) return; // user cancelled
|
|
885
|
+
if (choice === customToken) {
|
|
886
|
+
const entered = await (0, _prompts.inputCancellable)({
|
|
887
|
+
message: "Path to state file:",
|
|
888
|
+
validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
|
|
889
|
+
});
|
|
890
|
+
if (entered === null) return;
|
|
891
|
+
filePath = entered;
|
|
892
|
+
} else {
|
|
893
|
+
filePath = choice;
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
const entered = await (0, _prompts.inputCancellable)({
|
|
897
|
+
message: "Path to state file:",
|
|
898
|
+
validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
|
|
899
|
+
});
|
|
900
|
+
if (entered === null) return;
|
|
901
|
+
filePath = entered;
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
await this.importProfileFromFile(filePath);
|
|
905
|
+
console.log(`Imported profile "${this.profileMeta.name}".`);
|
|
906
|
+
} catch (error) {
|
|
907
|
+
console.error(`Import failed: ${error.message}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
buildSettingsMenu() {
|
|
911
|
+
return {
|
|
912
|
+
title: "Settings & Profiles",
|
|
913
|
+
items: [{
|
|
914
|
+
title: "View settings",
|
|
915
|
+
action: async () => {
|
|
916
|
+
const jsonStr = JSON.stringify(this.getDisplaySettings(), _encryption.bigIntReplacer, 2);
|
|
917
|
+
const jsonLines = jsonStr.split("\n").length;
|
|
918
|
+
console.log(jsonStr);
|
|
919
|
+
|
|
920
|
+
// Wait for user to press Enter
|
|
921
|
+
await (0, _prompts.selectWithClear)({
|
|
922
|
+
message: "Press Enter to continue...",
|
|
923
|
+
choices: [{
|
|
924
|
+
name: "Continue",
|
|
925
|
+
value: "continue"
|
|
926
|
+
}]
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Clear the JSON output
|
|
930
|
+
process.stdout.write("\x1b[1A\x1b[2K".repeat(jsonLines) + "\x1b[G");
|
|
931
|
+
}
|
|
932
|
+
}, {
|
|
933
|
+
title: "Edit settings",
|
|
934
|
+
action: async () => {
|
|
935
|
+
try {
|
|
936
|
+
this.settings = await this.EditStruct(this.type, this.settings);
|
|
937
|
+
this.saveSettings();
|
|
938
|
+
} catch (e) {
|
|
939
|
+
if (e instanceof _types.UserCancelledError) {
|
|
940
|
+
return; // User cancelled, return to menu
|
|
941
|
+
}
|
|
942
|
+
throw e;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}, {
|
|
946
|
+
title: "Switch profile",
|
|
947
|
+
action: async () => {
|
|
948
|
+
this.saveSettings();
|
|
949
|
+
const profiles = this.scanProfiles();
|
|
950
|
+
if (profiles.length <= 1) {
|
|
951
|
+
console.log("No other profiles to switch to. Create a new one first.");
|
|
952
|
+
} else {
|
|
953
|
+
await this.selectProfile(profiles);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}, {
|
|
957
|
+
title: "Create new profile",
|
|
958
|
+
action: async () => {
|
|
959
|
+
await this.createProfile();
|
|
960
|
+
}
|
|
961
|
+
}, {
|
|
962
|
+
title: "Duplicate current profile",
|
|
963
|
+
action: async () => {
|
|
964
|
+
await this.duplicateProfile();
|
|
965
|
+
}
|
|
966
|
+
}, {
|
|
967
|
+
title: "Rename current profile",
|
|
968
|
+
action: async () => {
|
|
969
|
+
await this.renameProfile();
|
|
970
|
+
}
|
|
971
|
+
}, {
|
|
972
|
+
title: "Delete a profile",
|
|
973
|
+
action: async () => {
|
|
974
|
+
await this.deleteProfile();
|
|
975
|
+
}
|
|
976
|
+
}, {
|
|
977
|
+
title: "Import profile from file",
|
|
978
|
+
action: async () => {
|
|
979
|
+
await this.importProfileInteractive();
|
|
980
|
+
}
|
|
981
|
+
}, {
|
|
982
|
+
title: "Addressbook",
|
|
983
|
+
items: [{
|
|
984
|
+
title: "View entries",
|
|
985
|
+
action: async () => {
|
|
986
|
+
const entries = Object.entries(this.addressbook);
|
|
987
|
+
if (entries.length === 0) {
|
|
988
|
+
console.log("Addressbook is empty.");
|
|
989
|
+
} else {
|
|
990
|
+
for (const [name, ms] of entries) {
|
|
991
|
+
const json = JSON.stringify(ms, _encryption.bigIntReplacer, 2);
|
|
992
|
+
let hashStr = "unknown";
|
|
993
|
+
try {
|
|
994
|
+
hashStr = (0, _index2.toNativeScript)(ms).hash();
|
|
995
|
+
} catch {/* skip */}
|
|
996
|
+
console.log(_yoctocolorsCjs.default.bold(name) + " " + _yoctocolorsCjs.default.dim(hashStr));
|
|
997
|
+
console.log(json);
|
|
998
|
+
console.log();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
await (0, _prompts.selectWithClear)({
|
|
1002
|
+
message: "Press Enter to continue...",
|
|
1003
|
+
choices: [{
|
|
1004
|
+
name: "Continue",
|
|
1005
|
+
value: "continue"
|
|
1006
|
+
}]
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}, {
|
|
1010
|
+
title: "Add entry",
|
|
1011
|
+
action: async () => {
|
|
1012
|
+
const name = await (0, _prompts.inputCancellable)({
|
|
1013
|
+
message: "Entry name:",
|
|
1014
|
+
validate: v => v.trim().length > 0 ? true : "Name cannot be empty"
|
|
1015
|
+
});
|
|
1016
|
+
if (name === null) return;
|
|
1017
|
+
if (this.addressbook[name]) {
|
|
1018
|
+
const overwrite = await (0, _prompts.confirmCancellable)({
|
|
1019
|
+
message: `Entry "${name}" already exists. Overwrite?`,
|
|
1020
|
+
default: false
|
|
1021
|
+
});
|
|
1022
|
+
if (!overwrite) return;
|
|
1023
|
+
}
|
|
1024
|
+
try {
|
|
1025
|
+
const script = await this.FillInStruct(_schemas.MultisigScript);
|
|
1026
|
+
this.addressbook[name] = script;
|
|
1027
|
+
this.saveSettings();
|
|
1028
|
+
console.log(`Added "${name}" to addressbook.`);
|
|
1029
|
+
} catch (e) {
|
|
1030
|
+
if (e instanceof _types.UserCancelledError) return;
|
|
1031
|
+
throw e;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}, {
|
|
1035
|
+
title: "Edit entry",
|
|
1036
|
+
action: async () => {
|
|
1037
|
+
const entries = Object.keys(this.addressbook);
|
|
1038
|
+
if (entries.length === 0) {
|
|
1039
|
+
console.log("Addressbook is empty.");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const selected = await (0, _prompts.selectCancellable)({
|
|
1043
|
+
message: "Select entry to edit:",
|
|
1044
|
+
choices: entries.map(n => ({
|
|
1045
|
+
name: n,
|
|
1046
|
+
value: n
|
|
1047
|
+
}))
|
|
1048
|
+
});
|
|
1049
|
+
if (selected === null) return;
|
|
1050
|
+
const editName = selected;
|
|
1051
|
+
try {
|
|
1052
|
+
const updated = await this.EditStruct(_schemas.MultisigScript, this.addressbook[editName]);
|
|
1053
|
+
this.addressbook[editName] = updated;
|
|
1054
|
+
this.saveSettings();
|
|
1055
|
+
console.log(`Updated "${editName}".`);
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
if (e instanceof _types.UserCancelledError) return;
|
|
1058
|
+
throw e;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}, {
|
|
1062
|
+
title: "Delete entry",
|
|
1063
|
+
action: async () => {
|
|
1064
|
+
const entries = Object.keys(this.addressbook);
|
|
1065
|
+
if (entries.length === 0) {
|
|
1066
|
+
console.log("Addressbook is empty.");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const delSelected = await (0, _prompts.selectCancellable)({
|
|
1070
|
+
message: "Select entry to delete:",
|
|
1071
|
+
choices: entries.map(n => ({
|
|
1072
|
+
name: n,
|
|
1073
|
+
value: n
|
|
1074
|
+
}))
|
|
1075
|
+
});
|
|
1076
|
+
if (delSelected === null) return;
|
|
1077
|
+
const delName = delSelected;
|
|
1078
|
+
const confirmed = await (0, _prompts.confirmCancellable)({
|
|
1079
|
+
message: `Delete "${delName}"?`,
|
|
1080
|
+
default: false
|
|
1081
|
+
});
|
|
1082
|
+
if (!confirmed) return;
|
|
1083
|
+
delete this.addressbook[delName];
|
|
1084
|
+
this.saveSettings();
|
|
1085
|
+
console.log(`Deleted "${delName}".`);
|
|
1086
|
+
}
|
|
1087
|
+
}]
|
|
1088
|
+
}]
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
745
1091
|
async showMenu(menu) {
|
|
746
|
-
return this.
|
|
1092
|
+
return this._searchMenu(menu);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// --- Flat search-based top-level menu ---
|
|
1096
|
+
|
|
1097
|
+
_flattenLeaves(menu, prefix, beforeShowChain, isRoot) {
|
|
1098
|
+
// Root's title is omitted from breadcrumbs (no "Main Menu › " prefix on
|
|
1099
|
+
// every leaf). Root's beforeShow is fired by the search loop, not as part
|
|
1100
|
+
// of the leaf chain (otherwise it would fire twice).
|
|
1101
|
+
const chain = !isRoot && menu.beforeShow ? [...beforeShowChain, menu.beforeShow] : beforeShowChain;
|
|
1102
|
+
const breadcrumbPrefix = isRoot ? prefix : [...prefix, menu.title];
|
|
1103
|
+
const leaves = [];
|
|
1104
|
+
for (const item of menu.items) {
|
|
1105
|
+
if ("action" in item) {
|
|
1106
|
+
leaves.push({
|
|
1107
|
+
breadcrumb: [...breadcrumbPrefix, item.title].join(" › "),
|
|
1108
|
+
leafTitle: item.title,
|
|
1109
|
+
action: item.action,
|
|
1110
|
+
beforeShowChain: chain
|
|
1111
|
+
});
|
|
1112
|
+
} else {
|
|
1113
|
+
leaves.push(...this._flattenLeaves(item, breadcrumbPrefix, chain, false));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return leaves;
|
|
1117
|
+
}
|
|
1118
|
+
static _scoreLeaf(leaf, tokens) {
|
|
1119
|
+
if (tokens.length === 0) return 1;
|
|
1120
|
+
const crumbLower = leaf.breadcrumb.toLowerCase();
|
|
1121
|
+
const titleLower = leaf.leafTitle.toLowerCase();
|
|
1122
|
+
let score = 0;
|
|
1123
|
+
for (const t of tokens) {
|
|
1124
|
+
if (!crumbLower.includes(t)) return 0; // require every token
|
|
1125
|
+
if (titleLower.includes(t)) {
|
|
1126
|
+
score += 3; // matches in the leaf title outrank ancestor matches
|
|
1127
|
+
} else {
|
|
1128
|
+
score += 1;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return score;
|
|
1132
|
+
}
|
|
1133
|
+
_filterLeaves(leaves, term) {
|
|
1134
|
+
const trimmed = term.trim();
|
|
1135
|
+
if (!trimmed) return leaves;
|
|
1136
|
+
const tokens = trimmed.toLowerCase().split(/\s+/);
|
|
1137
|
+
return leaves.map(leaf => ({
|
|
1138
|
+
leaf,
|
|
1139
|
+
score: Sprinkle._scoreLeaf(leaf, tokens)
|
|
1140
|
+
})).filter(x => x.score > 0).sort((a, b) => b.score - a.score).map(x => x.leaf);
|
|
1141
|
+
}
|
|
1142
|
+
async _searchMenu(menu) {
|
|
1143
|
+
const EXIT_TITLE = "Exit";
|
|
1144
|
+
while (true) {
|
|
1145
|
+
// Root beforeShow fires once per iteration so consumers can refresh any
|
|
1146
|
+
// dynamic state before the search prompt is built.
|
|
1147
|
+
if (menu.beforeShow) {
|
|
1148
|
+
await menu.beforeShow(this);
|
|
1149
|
+
}
|
|
1150
|
+
const userLeaves = this._flattenLeaves(menu, [], [], true);
|
|
1151
|
+
const settingsLeaves = this._flattenLeaves(this.buildSettingsMenu(), [], [], false);
|
|
1152
|
+
const exitLeaf = {
|
|
1153
|
+
breadcrumb: EXIT_TITLE,
|
|
1154
|
+
leafTitle: EXIT_TITLE,
|
|
1155
|
+
action: async () => {},
|
|
1156
|
+
beforeShowChain: [],
|
|
1157
|
+
isExit: true
|
|
1158
|
+
};
|
|
1159
|
+
const allLeaves = [...userLeaves, ...settingsLeaves, exitLeaf];
|
|
1160
|
+
const selected = await (0, _prompts.searchCancellable)({
|
|
1161
|
+
message: "What would you like to do?",
|
|
1162
|
+
source: async term => {
|
|
1163
|
+
return this._filterLeaves(allLeaves, term ?? "").map(leaf => ({
|
|
1164
|
+
name: leaf.breadcrumb,
|
|
1165
|
+
value: leaf
|
|
1166
|
+
}));
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
if (selected === null || selected.isExit) return;
|
|
1170
|
+
|
|
1171
|
+
// Fire submenu beforeShow hooks along the path to the selected leaf
|
|
1172
|
+
for (const beforeShow of selected.beforeShowChain) {
|
|
1173
|
+
await beforeShow(this);
|
|
1174
|
+
}
|
|
1175
|
+
const result = await selected.action(this);
|
|
1176
|
+
if (result instanceof Sprinkle) {
|
|
1177
|
+
this.settings = result.settings;
|
|
1178
|
+
this.saveSettings();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
747
1181
|
}
|
|
748
1182
|
async _showMenu(menu, main, path, clearPrevious = false) {
|
|
749
1183
|
// Clear previous breadcrumb if coming back from action/submenu
|
|
@@ -852,181 +1286,7 @@ class Sprinkle {
|
|
|
852
1286
|
return;
|
|
853
1287
|
}
|
|
854
1288
|
if (selection === -5) {
|
|
855
|
-
|
|
856
|
-
title: "Settings & Profiles",
|
|
857
|
-
items: [{
|
|
858
|
-
title: "View settings",
|
|
859
|
-
action: async () => {
|
|
860
|
-
const jsonStr = JSON.stringify(this.getDisplaySettings(), _encryption.bigIntReplacer, 2);
|
|
861
|
-
const jsonLines = jsonStr.split("\n").length;
|
|
862
|
-
console.log(jsonStr);
|
|
863
|
-
|
|
864
|
-
// Wait for user to press Enter
|
|
865
|
-
await (0, _prompts.selectWithClear)({
|
|
866
|
-
message: "Press Enter to continue...",
|
|
867
|
-
choices: [{
|
|
868
|
-
name: "Continue",
|
|
869
|
-
value: "continue"
|
|
870
|
-
}]
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
// Clear the JSON output
|
|
874
|
-
process.stdout.write("\x1b[1A\x1b[2K".repeat(jsonLines) + "\x1b[G");
|
|
875
|
-
}
|
|
876
|
-
}, {
|
|
877
|
-
title: "Edit settings",
|
|
878
|
-
action: async () => {
|
|
879
|
-
try {
|
|
880
|
-
this.settings = await this.EditStruct(this.type, this.settings);
|
|
881
|
-
this.saveSettings();
|
|
882
|
-
} catch (e) {
|
|
883
|
-
if (e instanceof _types.UserCancelledError) {
|
|
884
|
-
return; // User cancelled, return to menu
|
|
885
|
-
}
|
|
886
|
-
throw e;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}, {
|
|
890
|
-
title: "Switch profile",
|
|
891
|
-
action: async () => {
|
|
892
|
-
this.saveSettings();
|
|
893
|
-
const profiles = this.scanProfiles();
|
|
894
|
-
if (profiles.length <= 1) {
|
|
895
|
-
console.log("No other profiles to switch to. Create a new one first.");
|
|
896
|
-
} else {
|
|
897
|
-
await this.selectProfile(profiles);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}, {
|
|
901
|
-
title: "Create new profile",
|
|
902
|
-
action: async () => {
|
|
903
|
-
await this.createProfile();
|
|
904
|
-
}
|
|
905
|
-
}, {
|
|
906
|
-
title: "Duplicate current profile",
|
|
907
|
-
action: async () => {
|
|
908
|
-
await this.duplicateProfile();
|
|
909
|
-
}
|
|
910
|
-
}, {
|
|
911
|
-
title: "Rename current profile",
|
|
912
|
-
action: async () => {
|
|
913
|
-
await this.renameProfile();
|
|
914
|
-
}
|
|
915
|
-
}, {
|
|
916
|
-
title: "Delete a profile",
|
|
917
|
-
action: async () => {
|
|
918
|
-
await this.deleteProfile();
|
|
919
|
-
}
|
|
920
|
-
}, {
|
|
921
|
-
title: "Addressbook",
|
|
922
|
-
items: [{
|
|
923
|
-
title: "View entries",
|
|
924
|
-
action: async () => {
|
|
925
|
-
const entries = Object.entries(this.addressbook);
|
|
926
|
-
if (entries.length === 0) {
|
|
927
|
-
console.log("Addressbook is empty.");
|
|
928
|
-
} else {
|
|
929
|
-
for (const [name, ms] of entries) {
|
|
930
|
-
const json = JSON.stringify(ms, _encryption.bigIntReplacer, 2);
|
|
931
|
-
let hashStr = "unknown";
|
|
932
|
-
try {
|
|
933
|
-
hashStr = (0, _index2.toNativeScript)(ms).hash();
|
|
934
|
-
} catch {/* skip */}
|
|
935
|
-
console.log(_yoctocolorsCjs.default.bold(name) + " " + _yoctocolorsCjs.default.dim(hashStr));
|
|
936
|
-
console.log(json);
|
|
937
|
-
console.log();
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
await (0, _prompts.selectWithClear)({
|
|
941
|
-
message: "Press Enter to continue...",
|
|
942
|
-
choices: [{
|
|
943
|
-
name: "Continue",
|
|
944
|
-
value: "continue"
|
|
945
|
-
}]
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
}, {
|
|
949
|
-
title: "Add entry",
|
|
950
|
-
action: async () => {
|
|
951
|
-
const name = await (0, _prompts.inputCancellable)({
|
|
952
|
-
message: "Entry name:",
|
|
953
|
-
validate: v => v.trim().length > 0 ? true : "Name cannot be empty"
|
|
954
|
-
});
|
|
955
|
-
if (name === null) return;
|
|
956
|
-
if (this.addressbook[name]) {
|
|
957
|
-
const overwrite = await (0, _prompts.confirmCancellable)({
|
|
958
|
-
message: `Entry "${name}" already exists. Overwrite?`,
|
|
959
|
-
default: false
|
|
960
|
-
});
|
|
961
|
-
if (!overwrite) return;
|
|
962
|
-
}
|
|
963
|
-
try {
|
|
964
|
-
const script = await this.FillInStruct(_schemas.MultisigScript);
|
|
965
|
-
this.addressbook[name] = script;
|
|
966
|
-
this.saveSettings();
|
|
967
|
-
console.log(`Added "${name}" to addressbook.`);
|
|
968
|
-
} catch (e) {
|
|
969
|
-
if (e instanceof _types.UserCancelledError) return;
|
|
970
|
-
throw e;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}, {
|
|
974
|
-
title: "Edit entry",
|
|
975
|
-
action: async () => {
|
|
976
|
-
const entries = Object.keys(this.addressbook);
|
|
977
|
-
if (entries.length === 0) {
|
|
978
|
-
console.log("Addressbook is empty.");
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
const selected = await (0, _prompts.selectCancellable)({
|
|
982
|
-
message: "Select entry to edit:",
|
|
983
|
-
choices: entries.map(n => ({
|
|
984
|
-
name: n,
|
|
985
|
-
value: n
|
|
986
|
-
}))
|
|
987
|
-
});
|
|
988
|
-
if (selected === null) return;
|
|
989
|
-
const editName = selected;
|
|
990
|
-
try {
|
|
991
|
-
const updated = await this.EditStruct(_schemas.MultisigScript, this.addressbook[editName]);
|
|
992
|
-
this.addressbook[editName] = updated;
|
|
993
|
-
this.saveSettings();
|
|
994
|
-
console.log(`Updated "${editName}".`);
|
|
995
|
-
} catch (e) {
|
|
996
|
-
if (e instanceof _types.UserCancelledError) return;
|
|
997
|
-
throw e;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}, {
|
|
1001
|
-
title: "Delete entry",
|
|
1002
|
-
action: async () => {
|
|
1003
|
-
const entries = Object.keys(this.addressbook);
|
|
1004
|
-
if (entries.length === 0) {
|
|
1005
|
-
console.log("Addressbook is empty.");
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
const delSelected = await (0, _prompts.selectCancellable)({
|
|
1009
|
-
message: "Select entry to delete:",
|
|
1010
|
-
choices: entries.map(n => ({
|
|
1011
|
-
name: n,
|
|
1012
|
-
value: n
|
|
1013
|
-
}))
|
|
1014
|
-
});
|
|
1015
|
-
if (delSelected === null) return;
|
|
1016
|
-
const delName = delSelected;
|
|
1017
|
-
const confirmed = await (0, _prompts.confirmCancellable)({
|
|
1018
|
-
message: `Delete "${delName}"?`,
|
|
1019
|
-
default: false
|
|
1020
|
-
});
|
|
1021
|
-
if (!confirmed) return;
|
|
1022
|
-
delete this.addressbook[delName];
|
|
1023
|
-
this.saveSettings();
|
|
1024
|
-
console.log(`Deleted "${delName}".`);
|
|
1025
|
-
}
|
|
1026
|
-
}]
|
|
1027
|
-
}]
|
|
1028
|
-
};
|
|
1029
|
-
await this._showMenu(settingsMenu, false, [...path, "Settings & Profiles"], true);
|
|
1289
|
+
await this._showMenu(this.buildSettingsMenu(), false, [...path, "Settings & Profiles"], true);
|
|
1030
1290
|
await this._showMenu(menu, main, path, true);
|
|
1031
1291
|
return;
|
|
1032
1292
|
}
|
|
@@ -1068,6 +1328,37 @@ class Sprinkle {
|
|
|
1068
1328
|
return (0, _wallet.GetBlaze)(network, providerSettings, walletSettings);
|
|
1069
1329
|
}
|
|
1070
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* Derive a Bip32 private key hex from a BIP39 mnemonic phrase.
|
|
1333
|
+
* Accepts 12, 15, or 24 word phrases.
|
|
1334
|
+
*/
|
|
1335
|
+
static privateKeyFromMnemonic(phrase) {
|
|
1336
|
+
const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
|
|
1337
|
+
const words = normalized.split(" ");
|
|
1338
|
+
if (![12, 15, 24].includes(words.length)) {
|
|
1339
|
+
throw new Error(`Expected 12, 15, or 24 words but got ${words.length}.`);
|
|
1340
|
+
}
|
|
1341
|
+
const entropy = _sdk.Core.mnemonicToEntropy(normalized, _core.wordlist);
|
|
1342
|
+
const masterKey = _sdk.Core.Bip32PrivateKey.fromBip39Entropy(Buffer.from(entropy), "");
|
|
1343
|
+
return masterKey.hex();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Prompt the user for a recovery phrase and derive the Bip32 private key hex.
|
|
1348
|
+
*/
|
|
1349
|
+
static async importWalletFromMnemonic() {
|
|
1350
|
+
const phrase = await (0, _prompts.passwordCancellable)({
|
|
1351
|
+
message: "Enter your recovery phrase (12, 15, or 24 words):"
|
|
1352
|
+
});
|
|
1353
|
+
if (phrase === null) {
|
|
1354
|
+
throw new _types.UserCancelledError();
|
|
1355
|
+
}
|
|
1356
|
+
if (phrase.trim().length === 0) {
|
|
1357
|
+
throw new Error("Recovery phrase cannot be empty");
|
|
1358
|
+
}
|
|
1359
|
+
return Sprinkle.privateKeyFromMnemonic(phrase);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1071
1362
|
/**
|
|
1072
1363
|
* Generates a new wallet from a BIP39 mnemonic phrase.
|
|
1073
1364
|
* Displays the 24-word recovery phrase and requires user confirmation.
|
|
@@ -1648,13 +1939,16 @@ class Sprinkle {
|
|
|
1648
1939
|
return this._fillInStruct(selection, path, defs, matchedDef);
|
|
1649
1940
|
}
|
|
1650
1941
|
if ((0, _typeGuards.isString)(type)) {
|
|
1651
|
-
// Special handling for hot wallet private key - offer
|
|
1942
|
+
// Special handling for hot wallet private key - offer multiple sources
|
|
1652
1943
|
if (type.title === "Hot Wallet Private Key") {
|
|
1653
1944
|
const choice = await (0, _prompts.selectWithClear)({
|
|
1654
1945
|
message: "Hot wallet setup:",
|
|
1655
1946
|
choices: [{
|
|
1656
1947
|
name: "Enter existing private key",
|
|
1657
1948
|
value: "existing"
|
|
1949
|
+
}, {
|
|
1950
|
+
name: "Import from recovery phrase",
|
|
1951
|
+
value: "mnemonic"
|
|
1658
1952
|
}, {
|
|
1659
1953
|
name: "Generate new wallet",
|
|
1660
1954
|
value: "generate"
|
|
@@ -1666,6 +1960,9 @@ class Sprinkle {
|
|
|
1666
1960
|
if (choice === "generate") {
|
|
1667
1961
|
return Sprinkle.generateWalletFromMnemonic();
|
|
1668
1962
|
}
|
|
1963
|
+
if (choice === "mnemonic") {
|
|
1964
|
+
return Sprinkle.importWalletFromMnemonic();
|
|
1965
|
+
}
|
|
1669
1966
|
// Fall through to password prompt for "existing" choice
|
|
1670
1967
|
const answer = await (0, _prompts.passwordWithClear)({
|
|
1671
1968
|
message: "Enter your private key:"
|