@nostrbox/cli 1.6.3 → 1.7.1

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.
@@ -39,10 +39,186 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  const core_1 = require("@nostrbox/core");
42
+ const crypto_1 = require("crypto");
42
43
  const argParser_js_1 = require("../../utils/argParser.js");
44
+ const cliHelpers_js_1 = require("../../utils/cliHelpers.js");
43
45
  const tabtab = __importStar(require("tabtab"));
44
46
  // Valid zodiac signs (converted to lowercase for CLI compatibility)
45
47
  const VALID_ZODIAC_SIGNS = core_1.ZODIAC_SIGNS.map((sign) => sign.toLowerCase());
48
+ /**
49
+ * Derives encryption password from user-memorable parameters
50
+ *
51
+ * @param color - Color selection (blue, green, purple, etc.)
52
+ * @param birthday - Birthday day number (1-31)
53
+ * @param zodiacSign - Zodiac sign (Aries, Taurus, etc.)
54
+ * @param operation - Operation type (add or subtract)
55
+ * @returns Derived password string with format:
56
+ * - ADD: UPPERCASE_COLOR + "+" + birthday + "+" + lowercase_zodiac (e.g., "BLUE+17+aries")
57
+ * - SUBTRACT: lowercase_color + "-" + birthday + "-" + UPPERCASE_ZODIAC (e.g., "blue-17-ARIES")
58
+ */
59
+ function deriveMemoryKeys(color, birthday, zodiacSign, operation) {
60
+ if (operation === 'add') {
61
+ return `${color.toUpperCase()}+${birthday}+${zodiacSign.toLowerCase()}`;
62
+ }
63
+ else {
64
+ return `${color.toLowerCase()}-${birthday}-${zodiacSign.toUpperCase()}`;
65
+ }
66
+ }
67
+ /**
68
+ * Generates random Base64 string of specified length
69
+ */
70
+ function generateRandomBase64(length) {
71
+ const bytesNeeded = Math.ceil((length * 3) / 4);
72
+ const randomBytesBuffer = (0, crypto_1.randomBytes)(bytesNeeded);
73
+ const base64String = randomBytesBuffer.toString('base64');
74
+ return base64String.substring(0, length);
75
+ }
76
+ /**
77
+ * Splits an encrypted zodiac password into two parts based on birthday and color
78
+ *
79
+ * @param encryptedPassword - The encrypted zodiac password
80
+ * @param birthday - Birthday value (1-31)
81
+ * @param colorValue - Color value (1-7)
82
+ * @returns Two parts of same length as original, each containing partial original data
83
+ */
84
+ function splitZodiacPassword(encryptedPassword, birthday, colorValue) {
85
+ const length = encryptedPassword.length;
86
+ const splitPoint = birthday + colorValue;
87
+ if (splitPoint >= length) {
88
+ throw new Error(`Split point (${splitPoint}) must be less than password length (${length})`);
89
+ }
90
+ // Part 1: First splitPoint chars from original + random garbage for rest
91
+ const part1Original = encryptedPassword.substring(0, splitPoint);
92
+ const part1Random = generateRandomBase64(length - splitPoint);
93
+ const part1 = part1Original + part1Random;
94
+ // Part 2: Random garbage for first splitPoint chars + remaining original chars
95
+ const part2Random = generateRandomBase64(splitPoint);
96
+ const part2Original = encryptedPassword.substring(splitPoint);
97
+ const part2 = part2Random + part2Original;
98
+ return {
99
+ part1,
100
+ part2,
101
+ splitPoint
102
+ };
103
+ }
104
+ /**
105
+ * Combines two split parts to recover the original encrypted zodiac password
106
+ *
107
+ * @param part1 - First split part
108
+ * @param part2 - Second split part
109
+ * @param birthday - Birthday value (1-31)
110
+ * @param colorValue - Color value (1-7)
111
+ * @returns Original encrypted zodiac password
112
+ */
113
+ function combineZodiacPassword(part1, part2, birthday, colorValue) {
114
+ const splitPoint = birthday + colorValue;
115
+ if (part1.length !== part2.length) {
116
+ throw new Error(`Split parts must have same length (part1: ${part1.length}, part2: ${part2.length})`);
117
+ }
118
+ // Extract first splitPoint chars from part1
119
+ const firstPart = part1.substring(0, splitPoint);
120
+ // Extract remaining chars from part2
121
+ const secondPart = part2.substring(splitPoint);
122
+ return firstPart + secondPart;
123
+ }
124
+ // Color values mapping
125
+ const COLOR_VALUES = {
126
+ blue: 1,
127
+ green: 2,
128
+ purple: 3,
129
+ orange: 4,
130
+ yellow: 5,
131
+ pink: 6,
132
+ brown: 7
133
+ };
134
+ const VALID_COLORS = Object.keys(COLOR_VALUES);
135
+ /**
136
+ * Generates export JSON with encrypted split parts and all chain addresses
137
+ * Derives public addresses from the seed phrase for Bitcoin, Ethereum, Solana, TRON, and Nostr
138
+ */
139
+ function generateExportData(seedPhrase, splitPart1, splitPart2, passphrase = '') {
140
+ const mnemonic = seedPhrase.join(' ');
141
+ // Derive Bitcoin native (bc1q...) address
142
+ const bitcoinResult = (0, core_1.deriveKeyFromMnemonic)({
143
+ mnemonic,
144
+ chain: 'bitcoin',
145
+ format: 'native',
146
+ passphrase,
147
+ accountIndex: 0,
148
+ changeIndex: 0,
149
+ addressIndex: 0
150
+ });
151
+ // Derive Nostr public key (npub1...)
152
+ const nostrResult = (0, core_1.deriveKeyFromMnemonic)({
153
+ mnemonic,
154
+ chain: 'nostr',
155
+ format: 'bech32',
156
+ passphrase,
157
+ accountIndex: 0,
158
+ changeIndex: 0,
159
+ addressIndex: 0
160
+ });
161
+ // Derive Ethereum address (0x...)
162
+ const ethereumResult = (0, core_1.deriveKeyFromMnemonic)({
163
+ mnemonic,
164
+ chain: 'ethereum',
165
+ format: 'native',
166
+ passphrase,
167
+ accountIndex: 0,
168
+ changeIndex: 0,
169
+ addressIndex: 0
170
+ });
171
+ // Derive Solana address
172
+ const solanaResult = (0, core_1.deriveKeyFromMnemonic)({
173
+ mnemonic,
174
+ chain: 'solana',
175
+ format: 'native',
176
+ passphrase,
177
+ accountIndex: 0
178
+ });
179
+ // Derive TRON address (T...)
180
+ const tronResult = (0, core_1.deriveKeyFromMnemonic)({
181
+ mnemonic,
182
+ chain: 'tron',
183
+ format: 'native',
184
+ passphrase,
185
+ accountIndex: 0,
186
+ changeIndex: 0,
187
+ addressIndex: 0
188
+ });
189
+ // Validate that all addresses were derived successfully
190
+ if (!bitcoinResult.address) {
191
+ throw new Error('Failed to derive Bitcoin address');
192
+ }
193
+ if (!nostrResult.publicKey) {
194
+ throw new Error('Failed to derive Nostr public key');
195
+ }
196
+ if (!ethereumResult.address) {
197
+ throw new Error('Failed to derive Ethereum address');
198
+ }
199
+ if (!solanaResult.address) {
200
+ throw new Error('Failed to derive Solana address');
201
+ }
202
+ if (!tronResult.address) {
203
+ throw new Error('Failed to derive TRON address');
204
+ }
205
+ return {
206
+ encZodiacPwdSplit1: splitPart1,
207
+ encZodiacPwdSplit2: splitPart2,
208
+ nostr: nostrResult.publicKey,
209
+ bitcoin: bitcoinResult.address,
210
+ ethereum: ethereumResult.address,
211
+ solana: solanaResult.address,
212
+ tron: tronResult.address
213
+ };
214
+ }
215
+ // Constants for format detection and validation
216
+ const BASE58_PATTERN = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
217
+ const MAX_ZODIAC_PASSWORD_LENGTH = 30;
218
+ /**
219
+ * Creates and configures the argument parser for the CLI
220
+ * Defines all available flags, options, and their validation rules
221
+ */
46
222
  function createParser() {
47
223
  return new argParser_js_1.ArgumentParser({
48
224
  positional: {
@@ -53,14 +229,11 @@ function createParser() {
53
229
  options: [
54
230
  // Core mandatory flags
55
231
  {
56
- name: 'red',
57
- type: 'flag',
58
- description: 'Red: add birthday offset (+) (mandatory: choose --red or --black)'
59
- },
60
- {
61
- name: 'black',
62
- type: 'flag',
63
- description: 'Black: subtract birthday offset (-) (mandatory: choose --red or --black)'
232
+ name: 'operation',
233
+ alias: 'o',
234
+ type: 'string',
235
+ description: 'Operation type: add or subtract (mandatory)',
236
+ choices: ['add', 'subtract']
64
237
  },
65
238
  {
66
239
  name: 'birthday',
@@ -75,6 +248,13 @@ function createParser() {
75
248
  description: 'Zodiac sign for shuffling (mandatory)',
76
249
  choices: [...VALID_ZODIAC_SIGNS]
77
250
  },
251
+ {
252
+ name: 'color',
253
+ alias: 'c',
254
+ type: 'string',
255
+ description: 'Color for additional offset: blue(1), green(2), purple(3), orange(4), yellow(5), pink(6), brown(7) (mandatory)',
256
+ choices: [...VALID_COLORS]
257
+ },
78
258
  // Main use cases
79
259
  {
80
260
  name: 'restore',
@@ -94,15 +274,47 @@ function createParser() {
94
274
  },
95
275
  {
96
276
  name: 'load',
277
+ type: 'flag',
278
+ description: 'Modifier flag: when used, data input flags expect file paths instead of direct values'
279
+ },
280
+ // Old import methods (for backward compatibility)
281
+ {
282
+ name: 'codes',
283
+ type: 'string',
284
+ description: 'Secret codes: provide numbers directly, or file path if --load is used'
285
+ },
286
+ {
287
+ name: 'zodiacPwd',
288
+ type: 'string',
289
+ description: 'Plain zodiac password: provide Base58 directly, or file path if --load is used'
290
+ },
291
+ {
292
+ name: 'encZodiacPwd',
97
293
  type: 'string',
98
- description: 'Load from file (seed phrase or numbers depending on --restore flag)'
294
+ description: 'Encrypted zodiac password: provide string directly, or file path if --load is used'
295
+ },
296
+ {
297
+ name: 'encZodiacPwdSplit',
298
+ type: 'string',
299
+ description: 'Encrypted split parts: provide comma-separated directly, or file path if --load is used'
300
+ },
301
+ // Encryption options
302
+ {
303
+ name: 'passphrase',
304
+ type: 'string',
305
+ description: 'Custom user password for encryption/decryption (overrides auto-derived memory keys)'
99
306
  },
100
307
  // Output format options
101
308
  {
102
309
  name: 'zodiac-password',
103
310
  alias: 'p',
104
311
  type: 'flag',
105
- description: 'Output/input zodiac password (Base58) instead of 12 numbers'
312
+ description: 'Output/input zodiac password (Base58, always encrypted and split) instead of 12 numbers'
313
+ },
314
+ {
315
+ name: 'export',
316
+ type: 'string',
317
+ description: 'Export JSON with encrypted split parts and public addresses to specified file'
106
318
  },
107
319
  // File options
108
320
  {
@@ -114,18 +326,24 @@ function createParser() {
114
326
  ]
115
327
  });
116
328
  }
329
+ /**
330
+ * Sets up shell tab completion for the bip39zodiac command
331
+ * Provides intelligent autocomplete for flags, operations, zodiac signs, and colors
332
+ */
117
333
  async function setupCompletion(env) {
118
334
  // Handle completion request - return available options
119
335
  const flagCompletions = [
120
- '--red',
121
- '--black',
336
+ '--operation',
122
337
  '--birthday',
123
338
  '--zodiac',
339
+ '--color',
124
340
  '--new',
125
341
  '--seed',
126
342
  '--restore',
127
343
  '--load',
344
+ '--passphrase',
128
345
  '--zodiac-password',
346
+ '--export',
129
347
  '--save'
130
348
  ];
131
349
  const commandCompletions = ['install', 'uninstall'];
@@ -139,74 +357,84 @@ async function setupCompletion(env) {
139
357
  return tabtab.log(matches);
140
358
  }
141
359
  }
360
+ // Special case for operation values
361
+ const operationMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'operation', ['add', 'subtract']);
362
+ if (operationMatches) {
363
+ return tabtab.log(operationMatches);
364
+ }
142
365
  // Special case for zodiac values
143
- if (env.line && env.line.includes('--zodiac=')) {
144
- const zodiacPart = current.replace('--zodiac=', '');
145
- const matches = VALID_ZODIAC_SIGNS.filter((sign) => sign.startsWith(zodiacPart));
146
- return tabtab.log(matches);
366
+ const zodiacMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'zodiac', VALID_ZODIAC_SIGNS);
367
+ if (zodiacMatches) {
368
+ return tabtab.log(zodiacMatches);
369
+ }
370
+ // Special case for color values
371
+ const colorMatches = (0, cliHelpers_js_1.handleOptionValueCompletion)(env.line || '', current, 'color', VALID_COLORS);
372
+ if (colorMatches) {
373
+ return tabtab.log(colorMatches);
147
374
  }
148
375
  // Filter flag names based on current input
149
376
  const matches = flagCompletions.filter((comp) => comp.startsWith(current));
150
377
  return tabtab.log(matches);
151
378
  }
152
- async function installCompletion() {
153
- try {
154
- await tabtab.install({
155
- name: 'bip39zodiac',
156
- completer: 'bip39zodiac'
157
- });
158
- console.log('āœ… Shell completion installed successfully!');
159
- console.log('šŸ“ You may need to restart your shell or run: source ~/.bashrc (or equivalent)');
160
- }
161
- catch (error) {
162
- console.error('āŒ Failed to install completion:', error instanceof Error ? error.message : String(error));
163
- process.exit(1);
164
- }
165
- }
166
- async function uninstallCompletion() {
167
- try {
168
- await tabtab.uninstall({
169
- name: 'bip39zodiac'
170
- });
171
- console.log('āœ… Shell completion uninstalled successfully!');
172
- }
173
- catch (error) {
174
- console.error('āŒ Failed to uninstall completion:', error instanceof Error ? error.message : String(error));
175
- process.exit(1);
176
- }
177
- }
379
+ // Create shared completion installer and uninstaller using utility functions
380
+ const installCompletion = (0, cliHelpers_js_1.createCompletionInstaller)('bip39zodiac');
381
+ const uninstallCompletion = (0, cliHelpers_js_1.createCompletionUninstaller)('bip39zodiac');
178
382
  function showHelp() {
179
383
  console.log(`
180
384
  🌌 Seed Phrase CLI Tool - BIP39 Zodiac Operations
181
385
 
182
386
  USAGE:
183
- bip39zodiac [INPUT_METHOD] [OPTIONS] [CONVERSION_FLAGS]
387
+ # Generation
388
+ bip39zodiac [GENERATION_INPUT] [CONVERSION_FLAGS] [OPTIONS]
389
+
390
+ # Restoration
391
+ bip39zodiac [RESTORE_INPUT] --restore [CONVERSION_FLAGS] [OPTIONS]
392
+
393
+ # Commands
184
394
  bip39zodiac <install|uninstall>
185
395
 
186
- INPUT METHODS (choose one):
396
+ GENERATION INPUT METHODS (choose one):
187
397
  --new, -n Generate new random 12-word seed phrase
188
- --seed "content" Transform seed phrase or restore from codes
189
- --load <file> Load from file (seed phrase or codes)
398
+ --seed "12 words" Transform existing seed phrase (12 words directly)
399
+ --seed <file> ... or file path when used with --load flag
190
400
 
191
- OPTIONS:
192
- --restore Restore mode (reverses the transformation)
193
- --zodiac-password, -p Output/input zodiac password (Base58) instead of 12 numbers
194
- --save <file> Save results to file
401
+ RESTORE INPUT METHODS (choose one, use with --restore):
402
+ --codes "numbers" Secret codes: provide numbers directly
403
+ --codes <file> ... or file path when used with --load flag
404
+ --zodiacPwd "base58" Plain zodiac password: provide Base58 directly
405
+ --zodiacPwd <file> ... or file path when used with --load flag
406
+ --encZodiacPwd "encrypted" Encrypted password: provide string directly
407
+ --encZodiacPwd <file> ... or file path when used with --load flag
408
+ --encZodiacPwdSplit "p1,p2" Split parts: provide comma-separated directly
409
+ --encZodiacPwdSplit <file> ... or file path when used with --load flag (2 lines or 1 line with comma)
410
+ --load Modifier flag: makes above flags expect file paths
195
411
 
196
412
  CONVERSION FLAGS (all required):
197
- --red OR --black Card color (exactly one required):
198
- --red Add birthday offset (+)
199
- --black Subtract birthday offset (-)
200
-
201
- --birthday <1-31> Birthday day number (mandatory)
202
- --zodiac <sign> Zodiac sign for shuffling (mandatory)
413
+ --operation, -o <add|subtract> Operation type (mandatory):
414
+ add Add offsets (+)
415
+ subtract Subtract offsets (-)
416
+
417
+ --birthday, -b <1-31> Birthday day number (mandatory)
418
+ --zodiac, -z <sign> Zodiac sign for shuffling (mandatory)
419
+ --color, -c <color> Color for additional offset (mandatory):
420
+ blue(1), green(2), purple(3), orange(4), yellow(5), pink(6), brown(7)
421
+
422
+ ENCRYPTION OPTIONS:
423
+ --passphrase <password> Custom user password for encryption/decryption
424
+ (overrides auto-derived memory keys)
425
+ Requirements: min 8 chars, letters + numbers + special char,
426
+ no common weak passwords (e.g., "password", "123456")
427
+
428
+ OUTPUT OPTIONS:
429
+ --zodiac-password, -p Output zodiac password (Base58) format
430
+ --export <file> Export JSON with encrypted split parts and public addresses
431
+ to specified file (Bitcoin, Ethereum, Solana, TRON, Nostr)
432
+ --save <file> Save results to file
203
433
 
204
434
  COMMANDS:
205
435
  install Install shell completion for this command
206
436
  uninstall Uninstall shell completion for this command
207
437
 
208
- OPTIONS:
209
-
210
438
  ZODIAC SIGNS & ALGORITHMS:
211
439
  Original: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] - Original positions
212
440
 
@@ -223,43 +451,66 @@ ZODIAC SIGNS & ALGORITHMS:
223
451
  aquarius: [3, 4, 5, 0, 1, 2, 9, 10, 11, 6, 7, 8] - Swap segment pairs
224
452
  pisces: [6, 7, 8, 9, 10, 11, 5, 4, 3, 2, 1, 0] - Second half + mirrored first
225
453
 
226
- āš ļø WARNING: Use a family member's birthday, a celebritiy zodiac, or another memorable combination.
454
+ āš ļø WARNING: Use a family member's birthday, a celebrity zodiac, or another memorable combination.
227
455
  Not your own birthday and zodiac for maximum security.
228
456
 
229
457
  EXAMPLES:
458
+ # GENERATION
230
459
  # Generate new seed phrase and transform it
231
- bip39zodiac --new --red --birthday=17 --zodiac=aries
232
-
233
- # Transform existing seed phrase into codes and save
234
- bip39zodiac --seed "abandon ability able..." --black --birthday=25 --zodiac=leo --save codes.txt
235
-
236
- # Transform to zodiac password (compact Base58 format)
237
- bip39zodiac --seed "abandon ability able..." --red --birthday=10 --zodiac=virgo --zodiac-password
238
-
239
- # Load seed phrase from file and output zodiac password
240
- bip39zodiac --load seedphrase.txt --red --birthday=10 --zodiac=virgo --zodiac-password --save password.txt
241
-
242
- # Restore original from codes (same color, auto-reverses) and save
243
- bip39zodiac --seed "1234 567 890..." --restore --red --birthday=17 --zodiac=aries --save restored.txt
244
-
245
- # Restore from zodiac password
246
- bip39zodiac --seed "AoWJhqGkNxEe2D" --restore --zodiac-password --black --birthday=25 --zodiac=leo
247
-
248
- # Load zodiac password from file to restore
249
- bip39zodiac --load password.txt --restore --zodiac-password --red --birthday=10 --zodiac=virgo
250
-
251
- # Full workflow: transform to zodiac password and save, then restore
252
- bip39zodiac --seed "abandon ability able..." --red --birthday=15 --zodiac=gemini --zodiac-password --save secret.txt
253
- bip39zodiac --load secret.txt --restore --zodiac-password --red --birthday=15 --zodiac=gemini
460
+ bip39zodiac --new --operation=add --birthday=17 --zodiac=aries --color=blue
461
+
462
+ # Transform existing seed phrase and save
463
+ bip39zodiac --seed "abandon ability able..." --save codes.txt --operation=subtract --birthday=25 --zodiac=leo --color=green
464
+
465
+ # Load seed phrase from file and save with zodiac password format
466
+ bip39zodiac --load --seed seedphrase.txt --zodiac-password --save password.txt --operation=add --birthday=10 --zodiac=virgo --color=orange
467
+
468
+ # Generate with custom passphrase instead of memory keys
469
+ bip39zodiac --new --passphrase "MySecretPassword.123" --operation=add --birthday=17 --zodiac=aries --color=blue
470
+
471
+ # Export JSON with encrypted split parts and all chain addresses to file
472
+ bip39zodiac --new --export export.json --operation=add --birthday=17 --zodiac=aries --color=blue
473
+
474
+ # RESTORATION
475
+ # Restore from secret codes
476
+ bip39zodiac --restore --codes "22 23 24 25 26 27 28 29 30 19 20 21" --operation=add --birthday=17 --zodiac=aries --color=blue
477
+
478
+ # Restore from secret codes loaded from file
479
+ bip39zodiac --restore --load --codes codes.txt --operation=add --birthday=17 --zodiac=aries --color=blue
480
+
481
+ # Restore from plain zodiac password
482
+ bip39zodiac --restore --zodiacPwd "1P1Q1R1S1T1U1V1W1X1L1M1N" --operation=subtract --birthday=25 --zodiac=leo --color=yellow
483
+
484
+ # Restore from encrypted zodiac password (single string)
485
+ bip39zodiac --restore --encZodiacPwd "s2ubbiTDzq..." --operation=add --birthday=10 --zodiac=virgo --color=pink
486
+
487
+ # Restore from encrypted split parts
488
+ bip39zodiac --restore --encZodiacPwdSplit "part1...,part2..." --operation=add --birthday=15 --zodiac=gemini --color=brown
489
+
490
+ # Load codes from file and restore (--load makes --codes expect file path)
491
+ bip39zodiac --restore --load --codes codes.txt --operation=add --birthday=17 --zodiac=aries --color=blue
492
+
493
+ # Load encrypted split from file (2 lines or 1 line with comma)
494
+ bip39zodiac --restore --load --encZodiacPwdSplit password.txt --operation=add --birthday=10 --zodiac=virgo --color=orange
495
+
496
+ # Restore with custom passphrase
497
+ bip39zodiac --restore --encZodiacPwdSplit "part1...,part2..." --passphrase "MySecretPassword.123" --operation=add --birthday=15 --zodiac=gemini --color=brown
498
+
499
+ # INSTALL COMPLETION
500
+ bip39zodiac install
254
501
  `);
255
502
  }
503
+ /**
504
+ * Validates that all core mandatory flags are present and have valid values
505
+ * Throws descriptive errors if any validation fails
506
+ */
256
507
  function validateCoreFlags(args) {
257
- // Validate exactly one of red/black
258
- if (!args.red && !args.black) {
259
- throw new Error('Core flag required: exactly one of --red or --black must be specified');
508
+ // Validate operation
509
+ if (!args.operation) {
510
+ throw new Error('Core flag required: --operation <add|subtract> must be specified');
260
511
  }
261
- if (args.red && args.black) {
262
- throw new Error('Core flag conflict: cannot use both --red and --black');
512
+ if (args.operation !== 'add' && args.operation !== 'subtract') {
513
+ throw new Error('Operation must be either "add" or "subtract"');
263
514
  }
264
515
  // Validate birthday
265
516
  const birthdayArg = args.birthday;
@@ -274,125 +525,207 @@ function validateCoreFlags(args) {
274
525
  if (!args.zodiac) {
275
526
  throw new Error('Core flag required: --zodiac <sign> must be specified');
276
527
  }
528
+ // Validate color
529
+ if (!args.color) {
530
+ throw new Error('Core flag required: --color <blue|green|purple|orange|yellow|pink|brown> must be specified');
531
+ }
532
+ if (!VALID_COLORS.includes(args.color)) {
533
+ throw new Error(`Color must be one of: ${VALID_COLORS.join(', ')}`);
534
+ }
277
535
  }
536
+ /**
537
+ * Validates that exactly one input method is specified and required combinations are met
538
+ * For generation: use --new or --seed (seed phrase)
539
+ * For restore: use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit (optionally with --load)
540
+ */
278
541
  function validateUseCase(args) {
279
- // --restore requires either --seed or --load for input
280
- if (args.restore && !args.seed && !args.load) {
281
- throw new Error('--restore requires either --seed or --load to provide the numbers');
542
+ // For restore mode
543
+ if (args.restore) {
544
+ // --seed cannot be used with --restore
545
+ if (args.seed) {
546
+ throw new Error('--seed is only for seed phrases during generation. For restore, use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
547
+ }
548
+ if (args.new) {
549
+ throw new Error('--new cannot be used with --restore');
550
+ }
551
+ // Must have one of the restore input methods
552
+ if (!args.codes &&
553
+ !args.zodiacPwd &&
554
+ !args.encZodiacPwd &&
555
+ !args.encZodiacPwdSplit) {
556
+ throw new Error('--restore requires one of: --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit (optionally with --load <file>)');
557
+ }
558
+ }
559
+ else {
560
+ // For generation mode, cannot use restore-specific flags
561
+ if (args.codes ||
562
+ args.zodiacPwd ||
563
+ args.encZodiacPwd ||
564
+ args.encZodiacPwdSplit) {
565
+ throw new Error('Restore input methods (--codes, --zodiacPwd, --encZodiacPwd, --encZodiacPwdSplit) require --restore flag');
566
+ }
282
567
  }
283
568
  // Check that we have at least one input method
284
- const inputs = [args.new, args.seed, args.load].filter(Boolean);
569
+ const inputs = [
570
+ args.new,
571
+ args.seed,
572
+ args.codes,
573
+ args.zodiacPwd,
574
+ args.encZodiacPwd,
575
+ args.encZodiacPwdSplit
576
+ ].filter(Boolean);
285
577
  if (inputs.length === 0) {
286
- throw new Error('Input required: exactly one of --new, --seed, or --load must be specified');
578
+ throw new Error('Input required: for generation use --new or --seed; for restore use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
287
579
  }
580
+ // Only one data input method allowed (--load is separate as it's just the file path)
288
581
  if (inputs.length > 1) {
289
- throw new Error('Input conflict: only one of --new, --seed, or --load can be used');
582
+ throw new Error('Input conflict: only one data input method can be used at a time');
290
583
  }
291
584
  }
585
+ /**
586
+ * Gets seed phrase based on input method (new, seed, or load)
587
+ * Returns empty array for restore mode (handled separately in main)
588
+ */
292
589
  function getSeedPhrase(args) {
590
+ // Handle restore mode upfront - will be processed separately in main
591
+ if (args.restore) {
592
+ return [];
593
+ }
293
594
  if (args.new) {
294
595
  console.log('šŸŽ² Generating 12-word seed phrase...');
295
596
  return (0, core_1.generateSecureSeedPhrase)(12, 128);
296
597
  }
297
598
  if (args.seed) {
298
- console.log('šŸ“„ Processing existing seed phrase...');
299
- const words = args.seed
300
- .split(' ')
301
- .map((word) => word.trim())
302
- .filter((word) => word.length > 0);
303
- (0, core_1.validateBip39Words)(words);
304
- return words;
305
- }
306
- // For restore mode, return empty array - will be handled in main function
307
- if (args.restore) {
308
- return [];
309
- }
310
- if (args.load) {
311
- console.log(`šŸ“‚ Loading from file: ${args.load}`);
312
- try {
313
- const fileContent = (0, core_1.loadArrayFromFile)(args.load);
314
- // fileContent is always string[], join and split to handle both formats
315
- const content = fileContent
316
- .join(' ')
317
- .split(/\s+/)
318
- .filter((item) => item.length > 0);
319
- if (args.restore) {
320
- // For restore case - return empty array, will be handled in main function
321
- console.log('šŸ“„ Loading numbers for restoration');
322
- return [];
323
- }
324
- else {
325
- // For seed phrase transformation
326
- console.log('šŸ“„ Loading seed phrase for transformation');
599
+ if (args.load) {
600
+ // When --load is present, args.seed contains the file path
601
+ console.log(`šŸ“‚ Loading seed phrase from file: ${args.seed}`);
602
+ try {
603
+ const fileContent = (0, core_1.loadArrayFromFile)(args.seed);
604
+ // fileContent is always string[], join and split to handle both formats
605
+ const content = fileContent
606
+ .join(' ')
607
+ .split(/\s+/)
608
+ .filter((item) => item.length > 0);
609
+ console.log('šŸ“„ Processing seed phrase for transformation');
327
610
  (0, core_1.validateBip39Words)(content);
328
611
  return content;
329
612
  }
613
+ catch (error) {
614
+ throw new Error(`Failed to load file '${args.seed}': ${error instanceof Error ? error.message : String(error)}`);
615
+ }
330
616
  }
331
- catch (error) {
332
- throw new Error(`Failed to load file '${args.load}': ${error instanceof Error ? error.message : String(error)}`);
617
+ else {
618
+ // Direct input as string
619
+ console.log('šŸ“„ Processing existing seed phrase...');
620
+ const words = args.seed
621
+ .split(' ')
622
+ .map((word) => word.trim())
623
+ .filter((word) => word.length > 0);
624
+ (0, core_1.validateBip39Words)(words);
625
+ return words;
333
626
  }
334
627
  }
335
628
  throw new Error('No valid use case specified');
336
629
  }
630
+ /**
631
+ * Formats source information for error messages
632
+ */
633
+ function formatSource(source) {
634
+ return source ? ` from ${source}` : '';
635
+ }
636
+ /**
637
+ * Gets appropriate label for input source
638
+ */
639
+ function getInputLabel(source) {
640
+ return source ? 'File content' : 'Input';
641
+ }
642
+ /**
643
+ * Detects whether input appears to be a zodiac password or numbers
644
+ */
645
+ function detectInputFormat(input) {
646
+ const hasSpaces = input.includes(' ');
647
+ const hasOnlyBase58Chars = BASE58_PATTERN.test(input);
648
+ return !hasSpaces &&
649
+ hasOnlyBase58Chars &&
650
+ input.length < MAX_ZODIAC_PASSWORD_LENGTH
651
+ ? 'zodiac-password'
652
+ : 'numbers';
653
+ }
654
+ /**
655
+ * Parses space-separated numbers from input string
656
+ */
657
+ function parseNumbers(input) {
658
+ return input
659
+ .split(/\s+/)
660
+ .filter((item) => item.length > 0)
661
+ .map((code) => parseInt(code.trim()))
662
+ .filter((code) => !isNaN(code));
663
+ }
664
+ /**
665
+ * Decodes a zodiac password with explicit flag
666
+ */
667
+ function decodeZodiacPassword(input, source) {
668
+ console.log(`šŸ“„ Decoding zodiac password${formatSource(source)}...`);
669
+ try {
670
+ const secretCodes = (0, core_1.decodePassword)(input);
671
+ console.log(`šŸ”“ Zodiac password decoded to: ${secretCodes.join(' ')}`);
672
+ return secretCodes;
673
+ }
674
+ catch (error) {
675
+ throw new Error(`Failed to decode zodiac password${formatSource(source)}: ${error instanceof Error ? error.message : String(error)}`);
676
+ }
677
+ }
678
+ /**
679
+ * Attempts to auto-detect and decode zodiac password format
680
+ */
681
+ function decodeZodiacPasswordWithAutoDetect(input, source) {
682
+ console.log(`šŸ” Auto-detecting zodiac password format${formatSource(source)}...`);
683
+ try {
684
+ const secretCodes = (0, core_1.decodePassword)(input);
685
+ console.log(`šŸ”“ Zodiac password decoded to: ${secretCodes.join(' ')}`);
686
+ return secretCodes;
687
+ }
688
+ catch (error) {
689
+ throw new Error(`${getInputLabel(source)} appears to be zodiac password but failed to decode: ${error instanceof Error ? error.message : String(error)}. Use --zodiac-password flag or ${source ? 'ensure file contains numbers' : 'provide numbers instead'}.`);
690
+ }
691
+ }
337
692
  /**
338
693
  * Parses input string as either zodiac password or numbers based on flags and content
339
694
  */
340
695
  function parseSecretInput(input, useZodiacPassword, source) {
341
696
  if (useZodiacPassword) {
342
- console.log(`šŸ“„ Decoding zodiac password${source ? ` from ${source}` : ''}...`);
343
- try {
344
- const secretCodes = (0, core_1.decodePassword)(input);
345
- console.log(`šŸ”“ Zodiac password decoded to: ${secretCodes.join(' ')}`);
346
- return secretCodes;
347
- }
348
- catch (error) {
349
- throw new Error(`Failed to decode zodiac password${source ? ` from ${source}` : ''}: ${error instanceof Error ? error.message : String(error)}`);
350
- }
697
+ return decodeZodiacPassword(input, source);
351
698
  }
352
- else {
353
- // Try to detect if input looks like a zodiac password (Base58) or numbers
354
- const hasSpaces = input.includes(' ');
355
- const hasOnlyBase58Chars = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(input);
356
- if (!hasSpaces && hasOnlyBase58Chars && input.length < 30) {
357
- // Looks like a zodiac password, try to decode it
358
- console.log(`šŸ” Auto-detecting zodiac password format${source ? ` in ${source}` : ''}...`);
359
- try {
360
- const secretCodes = (0, core_1.decodePassword)(input);
361
- console.log(`šŸ”“ Zodiac password decoded to: ${secretCodes.join(' ')}`);
362
- return secretCodes;
363
- }
364
- catch (error) {
365
- throw new Error(`${source ? 'File content' : 'Input'} appears to be zodiac password but failed to decode: ${error instanceof Error ? error.message : String(error)}. Use --zodiac-password flag or ${source ? 'ensure file contains numbers' : 'provide numbers instead'}.`);
366
- }
367
- }
368
- else {
369
- // Parse as numbers
370
- const codes = input
371
- .split(/\s+/)
372
- .filter((item) => item.length > 0)
373
- .map((code) => parseInt(code.trim()))
374
- .filter((code) => !isNaN(code));
375
- return codes;
376
- }
699
+ const format = detectInputFormat(input);
700
+ if (format === 'zodiac-password') {
701
+ return decodeZodiacPasswordWithAutoDetect(input, source);
377
702
  }
703
+ return parseNumbers(input);
378
704
  }
705
+ /**
706
+ * Displays a formatted table showing the complete transformation process
707
+ * Shows original words through each transformation step to final shuffled codes
708
+ */
379
709
  function displayTable(originalWords, mappedNumbers, offsetNumbers, shuffledNumbers, base58Values, zodiacSign) {
380
710
  console.log('\nšŸ“Š SEED PHRASE TRANSFORMATION TABLE');
381
- console.log('====================================');
382
- console.log(`Original Words | Mapped Numbers | Birthday Offset | ${zodiacSign} Shuffled | Base58 directly mapped`);
383
- console.log('---------------|----------------|-----------------|-----------------|---------------------');
711
+ console.log('================================================================');
712
+ console.log(`# | Original Words | Mapped | Offset | ${zodiacSign} Shuffled | Base58`);
713
+ console.log('-----|------------------|--------|--------|----------------|--------');
384
714
  for (let i = 0; i < originalWords.length; i++) {
385
- const word = originalWords[i].padEnd(13);
386
- const mapped = String(mappedNumbers[i]).padEnd(15);
387
- const offset = String(offsetNumbers[i]).padEnd(16);
388
- const shuffled = String(shuffledNumbers[i]).padEnd(15);
389
- const base58 = base58Values[i].padEnd(2);
390
- console.log(`${String(i + 1).padStart(2)}. ${word} ${mapped} ${offset} ${shuffled} ${base58}`);
391
- }
392
- console.log('---------------|----------------|-----------------|-----------------|---------------------');
393
- console.log(`Final secret codes: ${shuffledNumbers.join(' ')}`);
394
- console.log(`Original seed phrase: ${originalWords.join(' ')}`);
715
+ const num = String(i + 1).padStart(4);
716
+ const word = originalWords[i].padEnd(16);
717
+ const mapped = String(mappedNumbers[i]).padStart(6);
718
+ const offset = String(offsetNumbers[i]).padStart(6);
719
+ const shuffled = String(shuffledNumbers[i]).padStart(14);
720
+ const base58 = base58Values[i].padStart(6);
721
+ console.log(`${num} | ${word} ${mapped} ${offset} ${shuffled} ${base58}`);
722
+ }
723
+ console.log('-----|------------------|--------|--------|----------------|--------');
395
724
  }
725
+ /**
726
+ * Main entry point for the BIP39 Zodiac CLI
727
+ * Handles argument parsing, validation, and orchestrates transformation or restoration
728
+ */
396
729
  async function main() {
397
730
  try {
398
731
  const parser = createParser();
@@ -425,33 +758,149 @@ async function main() {
425
758
  if (args.restore) {
426
759
  console.log('šŸ”„ RESTORING ORIGINAL SEED PHRASE FROM SECRET CODES');
427
760
  console.log('==================================================');
761
+ const colorValue = COLOR_VALUES[args.color];
762
+ const isAdd = args.operation === 'add';
428
763
  let secretCodes;
429
- if (args.seed) {
430
- const input = args.seed.trim();
431
- secretCodes = parseSecretInput(input, args['zodiac-password'] || false, '');
764
+ let inputString = '';
765
+ // Handle old import methods
766
+ if (args.codes) {
767
+ // --codes: Secret codes (numbers from string or file)
768
+ console.log('šŸ“„ Processing secret codes...');
769
+ if (args.load) {
770
+ // When --load is present, args.codes contains the file path
771
+ console.log(`šŸ“‚ Loading from file: ${args.codes}`);
772
+ const fileContent = (0, core_1.loadArrayFromFile)(args.codes);
773
+ inputString = fileContent.join(' ').trim();
774
+ secretCodes = parseNumbers(inputString);
775
+ }
776
+ else {
777
+ // Direct input as string
778
+ secretCodes = parseNumbers(args.codes);
779
+ }
780
+ (0, core_1.validateBip39Indices)(secretCodes);
432
781
  }
433
- else if (args.load) {
434
- const fileContent = (0, core_1.loadArrayFromFile)(args.load);
435
- const content = fileContent.join(' ').trim();
436
- secretCodes = parseSecretInput(content, args['zodiac-password'] || false, 'file');
782
+ else if (args.zodiacPwd) {
783
+ // --zodiacPwd: Plain unencrypted zodiac password (from string or file)
784
+ console.log('šŸ“„ Processing plain zodiac password...');
785
+ let passwordInput;
786
+ if (args.load) {
787
+ // When --load is present, args.zodiacPwd contains the file path
788
+ console.log(`šŸ“‚ Loading from file: ${args.zodiacPwd}`);
789
+ passwordInput = (0, core_1.loadArrayFromFile)(args.zodiacPwd).join('').trim();
790
+ }
791
+ else {
792
+ // Direct input as string
793
+ passwordInput = args.zodiacPwd;
794
+ }
795
+ secretCodes = (0, core_1.decodePassword)(passwordInput);
796
+ }
797
+ else if (args.encZodiacPwd) {
798
+ // --encZodiacPwd: Single encrypted password (from string or file)
799
+ console.log('šŸ“„ Processing encrypted zodiac password...');
800
+ let encryptedInput;
801
+ if (args.load) {
802
+ // When --load is present, args.encZodiacPwd contains the file path
803
+ console.log(`šŸ“‚ Loading from file: ${args.encZodiacPwd}`);
804
+ encryptedInput = (0, core_1.loadArrayFromFile)(args.encZodiacPwd).join('').trim();
805
+ }
806
+ else {
807
+ // Direct input as string
808
+ encryptedInput = args.encZodiacPwd;
809
+ }
810
+ // Use custom passphrase if provided, otherwise derive from memory keys
811
+ const memoryKeys = args.passphrase
812
+ ? args.passphrase
813
+ : deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
814
+ if (args.passphrase) {
815
+ console.log(` Using custom passphrase`);
816
+ }
817
+ else {
818
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
819
+ }
820
+ const decryptionResult = (0, core_1.decryptString)(encryptedInput, memoryKeys);
821
+ const decryptedPassword = decryptionResult.decrypted;
822
+ console.log(`āœ… Decryption successful!`);
823
+ console.log(`šŸ”“ Decrypted zodiac password: ${decryptedPassword}`);
824
+ secretCodes = (0, core_1.decodePassword)(decryptedPassword);
825
+ }
826
+ else if (args.encZodiacPwdSplit) {
827
+ // --encZodiacPwdSplit: Comma-separated split parts or from file (2 lines)
828
+ console.log('šŸ“„ Processing encrypted zodiac password (split parts)...');
829
+ let part1;
830
+ let part2;
831
+ if (args.load) {
832
+ // When --load is present, args.encZodiacPwdSplit contains the file path
833
+ console.log(`šŸ“‚ Loading from file: ${args.encZodiacPwdSplit}`);
834
+ const fileContent = (0, core_1.loadArrayFromFile)(args.encZodiacPwdSplit);
835
+ if (fileContent.length === 2) {
836
+ // Two lines: each line is a part
837
+ part1 = fileContent[0].trim();
838
+ part2 = fileContent[1].trim();
839
+ }
840
+ else if (fileContent.length === 1 && fileContent[0].includes(',')) {
841
+ // One line with comma: split by comma
842
+ const parts = fileContent[0].split(',').map((p) => p.trim());
843
+ if (parts.length !== 2) {
844
+ throw new Error(`Expected 2 comma-separated parts, got ${parts.length}`);
845
+ }
846
+ ;
847
+ [part1, part2] = parts;
848
+ }
849
+ else {
850
+ throw new Error(`Expected file to contain either 2 lines or 1 line with comma-separated parts, got ${fileContent.length} line(s)`);
851
+ }
852
+ }
853
+ else {
854
+ // Direct input as comma-separated string
855
+ const parts = args.encZodiacPwdSplit.split(',').map((p) => p.trim());
856
+ if (parts.length !== 2) {
857
+ throw new Error(`Expected 2 comma-separated parts, got ${parts.length}`);
858
+ }
859
+ ;
860
+ [part1, part2] = parts;
861
+ }
862
+ console.log(`šŸ”€ Combining split parts...`);
863
+ console.log(` Split point: ${birthday} + ${colorValue} = ${birthday + colorValue}`);
864
+ const combinedPassword = combineZodiacPassword(part1, part2, birthday, colorValue);
865
+ console.log(`āœ… Parts combined successfully!`);
866
+ // Use custom passphrase if provided, otherwise derive from memory keys
867
+ const memoryKeys = args.passphrase
868
+ ? args.passphrase
869
+ : deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
870
+ console.log(`\nšŸ”“ Decrypting zodiac password...`);
871
+ if (args.passphrase) {
872
+ console.log(` Using custom passphrase`);
873
+ }
874
+ else {
875
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
876
+ }
877
+ const decryptionResult = (0, core_1.decryptString)(combinedPassword, memoryKeys);
878
+ const decryptedPassword = decryptionResult.decrypted;
879
+ console.log(`āœ… Decryption successful!`);
880
+ console.log(`šŸ”“ Decrypted zodiac password: ${decryptedPassword}`);
881
+ secretCodes = (0, core_1.decodePassword)(decryptedPassword);
437
882
  }
438
883
  else {
439
- throw new Error('No secret codes provided for restoration');
884
+ throw new Error('No restore input method specified. Use --codes, --zodiacPwd, --encZodiacPwd, or --encZodiacPwdSplit');
440
885
  }
441
- (0, core_1.validateBip39Indices)(secretCodes);
442
886
  console.log(`šŸ“„ Input secret codes: ${secretCodes.join(' ')}`);
443
- // Reverse the process: unshuffle → remove offset → get original words
887
+ // Reverse the process: unshuffle → remove color offset → remove birthday offset → get original words
444
888
  console.log(`šŸ”„ Unshuffling with ${zodiacSign}...`);
445
889
  const unshuffleResult = (0, core_1.unshuffleByZodiac)(secretCodes, zodiacSign);
446
890
  if (!unshuffleResult.isValid) {
447
891
  throw new Error('Failed to unshuffle - invalid codes or wrong zodiac sign');
448
892
  }
449
- const offsetNumbers = unshuffleResult.restored;
450
- console.log(`šŸ”„ Removing birthday offset (${args.red ? '+' : '-'}${birthday})...`);
451
- // Apply the REVERSE offset operation for restoration
452
- const originalNumbers = args.red
453
- ? (0, core_1.subtractOffset)(offsetNumbers, birthday)
454
- : (0, core_1.addOffset)(offsetNumbers, birthday);
893
+ const afterUnshuffleNumbers = unshuffleResult.restored;
894
+ console.log(`šŸ”„ Removing color offset ${args.color} (${isAdd ? '+' : '-'}${colorValue})...`);
895
+ // Apply the REVERSE color offset operation for restoration
896
+ const afterColorRemovalNumbers = isAdd
897
+ ? (0, core_1.subtractOffset)(afterUnshuffleNumbers, colorValue)
898
+ : (0, core_1.addOffset)(afterUnshuffleNumbers, colorValue);
899
+ console.log(`šŸ”„ Removing birthday offset (${isAdd ? '+' : '-'}${birthday})...`);
900
+ // Apply the REVERSE birthday offset operation for restoration
901
+ const originalNumbers = isAdd
902
+ ? (0, core_1.subtractOffset)(afterColorRemovalNumbers, birthday)
903
+ : (0, core_1.addOffset)(afterColorRemovalNumbers, birthday);
455
904
  const originalWords = (0, core_1.indicesToWords)(originalNumbers);
456
905
  (0, core_1.validateBip39Words)(originalWords);
457
906
  console.log('\nāœ… RESTORATION COMPLETE!');
@@ -481,58 +930,138 @@ async function main() {
481
930
  console.log('===================================');
482
931
  console.log(`šŸ“„ Input seed phrase: ${originalWords.join(' ')}`);
483
932
  }
484
- // Process: words → numbers → offset → shuffle
485
- console.log(`\nšŸ”„ TRANSFORMATION PROCESS (${args.red ? 'RED (+)' : 'BLACK (-)'} ${birthday}, ${zodiacSign.toUpperCase()})`);
933
+ const colorValue = COLOR_VALUES[args.color];
934
+ const isAdd = args.operation === 'add';
935
+ // Process: words → numbers → birthday offset → color offset → shuffle
936
+ console.log(`\nšŸ”„ TRANSFORMATION PROCESS (${isAdd ? 'ADD (+)' : 'SUBTRACT (-)'} ${birthday}, ${args.color.toUpperCase()}(${isAdd ? '+' : '-'}${colorValue}), ${zodiacSign.toUpperCase()})`);
486
937
  console.log('='.repeat(70));
487
938
  // Step 1: Convert words to numbers
488
939
  const mappedNumbers = (0, core_1.wordsToIndices)(originalWords);
489
940
  console.log(`šŸ“ Step 1: Words → Numbers`);
490
941
  // Step 2: Apply birthday offset
491
- const offsetNumbers = args.red
942
+ const birthdayOffsetNumbers = isAdd
492
943
  ? (0, core_1.addOffset)(mappedNumbers, birthday)
493
944
  : (0, core_1.subtractOffset)(mappedNumbers, birthday);
494
- console.log(`šŸ“ Step 2: ${args.red ? 'Add' : 'Subtract'} birthday offset (${birthday})`);
495
- // Step 3: Apply zodiac shuffle
496
- const shuffleResult = (0, core_1.shuffleByZodiac)(offsetNumbers, zodiacSign);
945
+ console.log(`šŸ“ Step 2: ${isAdd ? 'Add' : 'Subtract'} birthday offset (${birthday})`);
946
+ // Step 3: Apply color offset
947
+ const colorOffsetNumbers = isAdd
948
+ ? (0, core_1.addOffset)(birthdayOffsetNumbers, colorValue)
949
+ : (0, core_1.subtractOffset)(birthdayOffsetNumbers, colorValue);
950
+ console.log(`šŸ“ Step 3: ${isAdd ? 'Add' : 'Subtract'} color offset ${args.color} (${colorValue})`);
951
+ // Step 4: Apply zodiac shuffle
952
+ const shuffleResult = (0, core_1.shuffleByZodiac)(colorOffsetNumbers, zodiacSign);
497
953
  const shuffledNumbers = shuffleResult.shuffled;
498
- console.log(`šŸ“ Step 3: Shuffle with ${zodiacSign}`);
954
+ console.log(`šŸ“ Step 4: Shuffle with ${zodiacSign}`);
499
955
  // Generate zodiac password and get individual Base58 values
500
- let zodiacPassword;
956
+ let unencryptedZodiacPassword;
501
957
  let base58Values;
502
958
  try {
503
959
  const passwordResult = (0, core_1.encodePassword)(shuffledNumbers);
504
- zodiacPassword = passwordResult.password;
960
+ unencryptedZodiacPassword = passwordResult.password;
505
961
  base58Values = passwordResult.individual;
506
962
  }
507
963
  catch (error) {
508
964
  throw new Error(`Failed to generate zodiac password: ${error instanceof Error ? error.message : String(error)}`);
509
965
  }
510
- // Display the 5-column table with Base58 values
511
- displayTable(originalWords, mappedNumbers, offsetNumbers, shuffledNumbers, base58Values, zodiacSign);
512
- // Display zodiac password section
513
- console.log('\nšŸ” ZODIAC PASSWORD');
514
- console.log('==================');
966
+ // Encrypt zodiac password (ALWAYS)
967
+ let zodiacPassword;
968
+ let splitResult;
969
+ let memoryKeys;
970
+ try {
971
+ // Use custom passphrase if provided, otherwise derive from memory keys
972
+ if (args.passphrase) {
973
+ memoryKeys = args.passphrase;
974
+ console.log(`\nšŸ”’ Encrypting zodiac password...`);
975
+ console.log(` Using custom passphrase`);
976
+ }
977
+ else {
978
+ memoryKeys = deriveMemoryKeys(args.color, birthday, zodiacSign, args.operation);
979
+ console.log(`\nšŸ”’ Encrypting zodiac password...`);
980
+ console.log(` Memory keys: ${memoryKeys} (${args.operation} operation)`);
981
+ }
982
+ const encryptionResult = (0, core_1.encryptString)(unencryptedZodiacPassword, memoryKeys);
983
+ zodiacPassword = encryptionResult.encrypted;
984
+ console.log(`āœ… Encryption complete!`);
985
+ // Split the encrypted password using birthday + color
986
+ console.log(`\nšŸ”€ Splitting encrypted password at position ${birthday} + ${colorValue} = ${birthday + colorValue}...`);
987
+ splitResult = splitZodiacPassword(zodiacPassword, birthday, colorValue);
988
+ console.log(`āœ… Split complete!`);
989
+ }
990
+ catch (error) {
991
+ throw new Error(`Failed to encrypt zodiac password: ${error instanceof Error ? error.message : String(error)}`);
992
+ }
993
+ // Handle export mode - output JSON and exit
994
+ if (args.export) {
995
+ console.log('\nšŸ“¦ Generating export data...');
996
+ const bip39Passphrase = args.passphrase || '';
997
+ const exportData = generateExportData(originalWords, splitResult.part1, splitResult.part2, bip39Passphrase);
998
+ const jsonOutput = JSON.stringify(exportData, null, 2);
999
+ try {
1000
+ (0, core_1.saveArrayToFile)([jsonOutput], args.export);
1001
+ console.log(`āœ… Export data saved to: ${args.export}`);
1002
+ }
1003
+ catch (error) {
1004
+ console.error(`āš ļø Failed to save to file '${args.export}': ${error instanceof Error ? error.message : String(error)}`);
1005
+ }
1006
+ return;
1007
+ }
1008
+ // Display the transformation table
1009
+ displayTable(originalWords, mappedNumbers, colorOffsetNumbers, // Combined offset (birthday + color applied)
1010
+ shuffledNumbers, base58Values, zodiacSign);
1011
+ // Display inline format below table
1012
+ console.log('\nOriginal Words:');
1013
+ console.log(originalWords.join(' '));
1014
+ console.log('\nMapped Numbers:');
1015
+ console.log(mappedNumbers.join(' '));
1016
+ console.log('\nOffset Applied:');
1017
+ console.log(colorOffsetNumbers.join(' '));
1018
+ console.log(`\n${zodiacSign} Shuffled:`);
1019
+ console.log(shuffledNumbers.join(' '));
1020
+ console.log('\nBase58:');
1021
+ console.log(base58Values.join(' '));
1022
+ // Display password information
1023
+ console.log('\nZodiac Password:');
1024
+ console.log(unencryptedZodiacPassword);
1025
+ console.log('\nEncrypted Zodiac Password:');
515
1026
  console.log(zodiacPassword);
516
- console.log('\nāœ… TRANSFORMATION COMPLETE!');
1027
+ console.log('\nEncrypted Zodiac Password (Split Parts):');
1028
+ console.log(`šŸ’” Each part contains a portion of the encrypted password padded with random data`);
1029
+ console.log(` Both parts are required to reconstruct the original encrypted password`);
1030
+ console.log(`Part 1: ${splitResult.part1}`);
1031
+ console.log(`Part 2: ${splitResult.part2}`);
1032
+ console.log(`Split point: ${birthday} + ${colorValue} = ${birthday + colorValue}`);
1033
+ const passphraseFlag = args.passphrase
1034
+ ? ` --passphrase "${args.passphrase}"`
1035
+ : '';
517
1036
  if (args['zodiac-password']) {
518
- console.log(`šŸ” Zodiac Password: ${zodiacPassword}`);
519
- console.log(`šŸ“Š Secret codes: ${shuffledNumbers.join(' ')} (for reference)`);
520
- console.log(`šŸ”„ To restore, use: bip39zodiac --seed "${zodiacPassword}" --restore --zodiac-password --${args.red ? 'red' : 'black'} --birthday=${birthday} --zodiac=${args.zodiac}`);
1037
+ console.log(`\nšŸ”„ To restore, use: bip39zodiac --encZodiacPwdSplit "${splitResult.part1},${splitResult.part2}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}${passphraseFlag}`);
1038
+ if (args.passphrase) {
1039
+ console.log(`\nšŸ’” Using custom passphrase for encryption`);
1040
+ }
1041
+ else {
1042
+ console.log(`\nšŸ’” Memory keys: ${memoryKeys} (${args.operation} operation - auto-derived from your parameters)`);
1043
+ }
521
1044
  }
522
1045
  else {
523
- console.log(`šŸ” Use these secret codes: ${shuffledNumbers.join(' ')}`);
524
- console.log(`šŸ”‘ Zodiac Password: ${zodiacPassword} (compact format)`);
525
- console.log(`šŸ”„ To restore, use: bip39zodiac --seed "${shuffledNumbers.join(' ')}" --restore --${args.red ? 'red' : 'black'} --birthday=${birthday} --zodiac=${args.zodiac}`);
526
- console.log(`šŸ”„ Or with zodiac password: bip39zodiac --seed "${zodiacPassword}" --restore --zodiac-password --${args.red ? 'red' : 'black'} --birthday=${birthday} --zodiac=${args.zodiac}`);
1046
+ console.log(`\nšŸ”„ To restore with codes: bip39zodiac --codes "${shuffledNumbers.join(' ')}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1047
+ console.log(`\nšŸ”„ To restore with zodiac password: bip39zodiac --zodiacPwd "${unencryptedZodiacPassword}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}`);
1048
+ console.log(`\nšŸ”„ To restore with encrypted split: bip39zodiac --encZodiacPwdSplit "${splitResult.part1},${splitResult.part2}" --restore --operation=${args.operation} --birthday=${birthday} --zodiac=${args.zodiac} --color=${args.color}${passphraseFlag}`);
527
1049
  }
528
1050
  // Save to file if --save option provided
529
1051
  if (args.save) {
530
1052
  try {
531
- const contentToSave = args['zodiac-password']
532
- ? zodiacPassword
533
- : shuffledNumbers.join(' ');
534
- (0, core_1.saveArrayToFile)([contentToSave], args.save);
535
- console.log(`šŸ’¾ Results saved to: ${args.save}`);
1053
+ if (args['zodiac-password']) {
1054
+ // Save both split parts (part1 on first line, part2 on second line)
1055
+ (0, core_1.saveArrayToFile)([splitResult.part1, splitResult.part2], args.save);
1056
+ console.log(`šŸ’¾ Split parts saved to: ${args.save}`);
1057
+ console.log(` Part 1: Line 1`);
1058
+ console.log(` Part 2: Line 2`);
1059
+ }
1060
+ else {
1061
+ // Save numeric codes
1062
+ (0, core_1.saveArrayToFile)([shuffledNumbers.join(' ')], args.save);
1063
+ console.log(`šŸ’¾ Results saved to: ${args.save}`);
1064
+ }
536
1065
  }
537
1066
  catch (error) {
538
1067
  console.error(`āš ļø Failed to save to file '${args.save}': ${error instanceof Error ? error.message : String(error)}`);