@sendblue/cli 0.6.0 → 0.6.2
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/commands/add-contact.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/setup.d.ts +8 -1
- package/dist/commands/setup.js +138 -68
- package/dist/index.js +4 -0
- package/dist/lib/format.js +91 -36
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@ export async function addContactCommand(number) {
|
|
|
25
25
|
spinner.succeed(`Contact ${formatPhoneNumber(normalized)} is already verified!`);
|
|
26
26
|
console.log();
|
|
27
27
|
console.log(chalk.bold(' You can now send messages:'));
|
|
28
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
28
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello!'`));
|
|
29
29
|
console.log();
|
|
30
30
|
return;
|
|
31
31
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -34,7 +34,7 @@ export async function loginCommand() {
|
|
|
34
34
|
type: 'text',
|
|
35
35
|
name: 'email',
|
|
36
36
|
message: 'Email',
|
|
37
|
-
validate: (v) => /^[^\s@]+@[^\s@]+\.[
|
|
37
|
+
validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
|
|
38
38
|
}, { onCancel });
|
|
39
39
|
// Step 2: Send verification code
|
|
40
40
|
const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -17,7 +17,8 @@ const onCancel = () => {
|
|
|
17
17
|
printError('Setup cancelled.');
|
|
18
18
|
process.exit(0);
|
|
19
19
|
};
|
|
20
|
-
export async function setupCommand() {
|
|
20
|
+
export async function setupCommand(opts) {
|
|
21
|
+
const nonInteractive = !!(opts.email && opts.code && opts.company);
|
|
21
22
|
console.log();
|
|
22
23
|
printLogo();
|
|
23
24
|
console.log(chalk.bold(' sendblue setup'));
|
|
@@ -26,57 +27,113 @@ export async function setupCommand() {
|
|
|
26
27
|
// Check for existing credentials
|
|
27
28
|
const existing = getCredentials();
|
|
28
29
|
if (existing) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
if (nonInteractive) {
|
|
31
|
+
console.log(chalk.dim(` Overwriting existing account (${existing.email})`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const { overwrite } = await prompts({
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'overwrite',
|
|
37
|
+
message: `You already have an account configured (${existing.email}). Overwrite?`,
|
|
38
|
+
initial: false
|
|
39
|
+
}, { onCancel });
|
|
40
|
+
if (!overwrite) {
|
|
41
|
+
console.log(chalk.dim(' Setup cancelled.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
message: 'Email',
|
|
45
|
-
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Enter a valid email'
|
|
46
|
-
}, { onCancel });
|
|
47
|
-
// Step 2: Send verification code
|
|
48
|
-
const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
|
|
49
|
-
try {
|
|
50
|
-
await sendCode(email);
|
|
51
|
-
sendSpinner.succeed(`Code sent to ${email}`);
|
|
46
|
+
// Validate flags upfront
|
|
47
|
+
if (opts.email && !/^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(opts.email)) {
|
|
48
|
+
printError('Invalid email address.');
|
|
49
|
+
process.exit(1);
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
if (opts.code && !/^\d{8}$/.test(opts.code)) {
|
|
52
|
+
printError('Verification code must be 8 digits.');
|
|
55
53
|
process.exit(1);
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
if (opts.company) {
|
|
56
|
+
if (!/^[a-z0-9_-]+$/.test(opts.company)) {
|
|
57
|
+
printError('Company name: only lowercase letters, numbers, hyphens, and underscores.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (opts.company.length < 3 || opts.company.length > 64) {
|
|
61
|
+
printError('Company name must be 3-64 characters.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Step 1: Collect email
|
|
66
|
+
let email;
|
|
67
|
+
if (opts.email) {
|
|
68
|
+
email = opts.email;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const response = await prompts({
|
|
72
|
+
type: 'text',
|
|
73
|
+
name: 'email',
|
|
74
|
+
message: 'Email',
|
|
75
|
+
validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
|
|
76
|
+
}, { onCancel });
|
|
77
|
+
email = response.email;
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Send verification code (skip if code already provided)
|
|
80
|
+
if (!opts.code) {
|
|
81
|
+
const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
|
|
82
|
+
try {
|
|
83
|
+
await sendCode(email);
|
|
84
|
+
sendSpinner.succeed(`Code sent to ${email}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
sendSpinner.fail(`Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Non-interactive: just send the code and exit so the user can
|
|
91
|
+
// re-run with --code once they have it
|
|
92
|
+
if (opts.email) {
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(chalk.dim(' Once you have the code, run:'));
|
|
95
|
+
console.log(chalk.cyan(` sendblue setup --email ${email} --code <CODE> --company <NAME>`));
|
|
96
|
+
console.log();
|
|
97
|
+
return;
|
|
78
98
|
}
|
|
79
|
-
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
// Step 3: Enter code
|
|
102
|
+
let code;
|
|
103
|
+
if (opts.code) {
|
|
104
|
+
code = opts.code;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const response = await prompts({
|
|
108
|
+
type: 'text',
|
|
109
|
+
name: 'code',
|
|
110
|
+
message: 'Verification code',
|
|
111
|
+
validate: (v) => /^\d{8}$/.test(v) || 'Enter the 8-digit code from your email'
|
|
112
|
+
}, { onCancel });
|
|
113
|
+
code = response.code;
|
|
114
|
+
}
|
|
115
|
+
// Step 4: Company name
|
|
116
|
+
let companyName;
|
|
117
|
+
if (opts.company) {
|
|
118
|
+
companyName = opts.company;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const response = await prompts({
|
|
122
|
+
type: 'text',
|
|
123
|
+
name: 'companyName',
|
|
124
|
+
message: 'Company name (lowercase, hyphens/underscores only)',
|
|
125
|
+
validate: (v) => {
|
|
126
|
+
if (!v)
|
|
127
|
+
return 'Company name is required';
|
|
128
|
+
if (!/^[a-z0-9_-]+$/.test(v))
|
|
129
|
+
return 'Only lowercase letters, numbers, hyphens, and underscores';
|
|
130
|
+
if (v.length < 3 || v.length > 64)
|
|
131
|
+
return 'Must be 3-64 characters';
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}, { onCancel });
|
|
135
|
+
companyName = response.companyName;
|
|
136
|
+
}
|
|
80
137
|
// Step 5: Verify code + create account
|
|
81
138
|
const setupSpinner = ora({ text: 'Setting up your account...', indent: 2 }).start();
|
|
82
139
|
let result;
|
|
@@ -100,18 +157,29 @@ export async function setupCommand() {
|
|
|
100
157
|
process.exit(1);
|
|
101
158
|
}
|
|
102
159
|
// Step 6: Add first contact
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
160
|
+
let contactNumber;
|
|
161
|
+
if (opts.contact) {
|
|
162
|
+
contactNumber = opts.contact;
|
|
163
|
+
}
|
|
164
|
+
else if (!nonInteractive) {
|
|
165
|
+
console.log(chalk.bold(' Add your first contact'));
|
|
166
|
+
console.log(chalk.dim(' Enter the phone number you want to message via iMessage.'));
|
|
167
|
+
console.log();
|
|
168
|
+
const response = await prompts({
|
|
169
|
+
type: 'text',
|
|
170
|
+
name: 'contactNumber',
|
|
171
|
+
message: 'Contact phone number',
|
|
172
|
+
validate: (v) => {
|
|
173
|
+
const n = normalizeNumber(v);
|
|
174
|
+
return /^\+\d{10,15}$/.test(n) || 'Enter a valid phone number (e.g. +15551234567)';
|
|
175
|
+
}
|
|
176
|
+
}, { onCancel });
|
|
177
|
+
contactNumber = response.contactNumber;
|
|
178
|
+
}
|
|
179
|
+
if (!contactNumber) {
|
|
180
|
+
console.log();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
115
183
|
const normalized = normalizeNumber(contactNumber);
|
|
116
184
|
const contactSpinner = ora({ text: `Adding contact ${formatPhoneNumber(normalized)}...`, indent: 2 }).start();
|
|
117
185
|
try {
|
|
@@ -120,7 +188,7 @@ export async function setupCommand() {
|
|
|
120
188
|
contactSpinner.succeed(`Contact ${formatPhoneNumber(normalized)} is already verified!`);
|
|
121
189
|
console.log();
|
|
122
190
|
console.log(chalk.bold(' You\'re all set! Send a message:'));
|
|
123
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
191
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello from Sendblue!'`));
|
|
124
192
|
console.log();
|
|
125
193
|
return;
|
|
126
194
|
}
|
|
@@ -136,17 +204,19 @@ export async function setupCommand() {
|
|
|
136
204
|
console.log();
|
|
137
205
|
console.log(chalk.cyan.bold(` ${formatPhoneNumber(sharedNumber)}`));
|
|
138
206
|
console.log();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
if (!nonInteractive) {
|
|
208
|
+
// Show QR code for easy texting
|
|
209
|
+
const qr = await generateSmsQr(sharedNumber);
|
|
210
|
+
console.log(chalk.dim(' Or scan this QR code to open a text:'));
|
|
211
|
+
console.log();
|
|
212
|
+
// Indent each line of the QR code
|
|
213
|
+
for (const line of qr.split('\n')) {
|
|
214
|
+
console.log(` ${line}`);
|
|
215
|
+
}
|
|
216
|
+
console.log();
|
|
146
217
|
}
|
|
147
|
-
console.log();
|
|
148
218
|
console.log(chalk.dim(' Once they text in, run:'));
|
|
149
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
219
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello from Sendblue!'`));
|
|
150
220
|
}
|
|
151
221
|
console.log();
|
|
152
222
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,10 @@ program
|
|
|
19
19
|
program
|
|
20
20
|
.command('setup')
|
|
21
21
|
.description('Create a new Sendblue account and get an iMessage number')
|
|
22
|
+
.option('--email <email>', 'Email address (skip prompt)')
|
|
23
|
+
.option('--code <code>', 'Verification code (skip prompt, requires --email)')
|
|
24
|
+
.option('--company <name>', 'Company name (skip prompt)')
|
|
25
|
+
.option('--contact <number>', 'First contact phone number (skip prompt)')
|
|
22
26
|
.action(setupCommand);
|
|
23
27
|
program
|
|
24
28
|
.command('login')
|
package/dist/lib/format.js
CHANGED
|
@@ -1,50 +1,105 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
const blue = chalk.hex('#0088FF');
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
3
|
+
/*
|
|
4
|
+
export function printLogo(): void {
|
|
5
|
+
// Chat bubble with small satellite circle, matching Sendblue logo
|
|
6
|
+
const b = blue('█')
|
|
7
|
+
const h = blue('▀')
|
|
8
|
+
const l = blue('▄')
|
|
8
9
|
const logo = [
|
|
9
|
-
`
|
|
10
|
-
`
|
|
11
|
-
`
|
|
12
|
-
`
|
|
13
|
-
`
|
|
14
|
-
`
|
|
15
|
-
`
|
|
16
|
-
`
|
|
17
|
-
`
|
|
18
|
-
`
|
|
19
|
-
|
|
10
|
+
` ${l}${l}`,
|
|
11
|
+
` ${l}${l}${l}${l}${l}${l}${l} ${l}${l}${l}`,
|
|
12
|
+
` ${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
|
|
13
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
|
|
14
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${h}${h}${h}`,
|
|
15
|
+
` ${l}${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
16
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
17
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
18
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
19
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
20
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
21
|
+
` ${h}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${h}`,
|
|
22
|
+
` ${h}${h}${h} ${h}${h}${h}${h}${h}${h}${h} ${h}${h}`,
|
|
23
|
+
` ${h}${h}${h}${h}${h}`,
|
|
24
|
+
]
|
|
20
25
|
for (const line of logo) {
|
|
21
|
-
console.log(` ${line}`)
|
|
26
|
+
console.log(` ${line}`)
|
|
27
|
+
}
|
|
28
|
+
console.log()
|
|
29
|
+
console.log(blue.bold(' sendblue'))
|
|
30
|
+
console.log(chalk.dim(' iMessage for agents'))
|
|
31
|
+
console.log()
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
34
|
+
export function printLogo() {
|
|
35
|
+
const lines = [
|
|
36
|
+
'███████╗███████╗███╗ ██╗██████╗',
|
|
37
|
+
'██╔════╝██╔════╝████╗ ██║██╔══██╗',
|
|
38
|
+
'███████╗█████╗ ██╔██╗ ██║██║ ██║',
|
|
39
|
+
'╚════██║██╔══╝ ██║╚██╗██║██║ ██║',
|
|
40
|
+
'███████║███████╗██║ ╚████║██████╔╝',
|
|
41
|
+
'╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝',
|
|
42
|
+
'',
|
|
43
|
+
'██████╗ ██╗ ██╗ ██╗███████╗',
|
|
44
|
+
'██╔══██╗██║ ██║ ██║██╔════╝',
|
|
45
|
+
'██████╔╝██║ ██║ ██║█████╗',
|
|
46
|
+
'██╔══██╗██║ ██║ ██║██╔══╝',
|
|
47
|
+
'██████╔╝███████╗╚██████╔╝███████╗',
|
|
48
|
+
'╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝',
|
|
49
|
+
];
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
console.log(blue(line));
|
|
22
52
|
}
|
|
23
53
|
console.log();
|
|
24
|
-
console.log(
|
|
25
|
-
console.log(chalk.dim(' iMessage for agents'));
|
|
54
|
+
console.log(chalk.dim(' iMessage for agents'));
|
|
26
55
|
console.log();
|
|
27
56
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
57
|
+
/*
|
|
58
|
+
export function getLogo(): string {
|
|
59
|
+
const b = blue('█')
|
|
60
|
+
const h = blue('▀')
|
|
61
|
+
const l = blue('▄')
|
|
32
62
|
const lines = [
|
|
33
|
-
`
|
|
34
|
-
`
|
|
35
|
-
`
|
|
36
|
-
`
|
|
37
|
-
`
|
|
38
|
-
`
|
|
39
|
-
`
|
|
40
|
-
`
|
|
41
|
-
`
|
|
42
|
-
`
|
|
63
|
+
` ${l}${l}`,
|
|
64
|
+
` ${l}${l}${l}${l}${l}${l}${l} ${l}${l}${l}`,
|
|
65
|
+
` ${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
|
|
66
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
|
|
67
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${h}${h}${h}`,
|
|
68
|
+
` ${l}${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
69
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
70
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
71
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
72
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
73
|
+
` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
|
|
74
|
+
` ${h}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${h}`,
|
|
75
|
+
` ${h}${h}${h} ${h}${h}${h}${h}${h}${h}${h} ${h}${h}`,
|
|
76
|
+
` ${h}${h}${h}${h}${h}`,
|
|
43
77
|
``,
|
|
44
|
-
blue.bold('
|
|
45
|
-
chalk.dim('
|
|
78
|
+
blue.bold(' Sendblue'),
|
|
79
|
+
chalk.dim(' iMessage for agents'),
|
|
80
|
+
]
|
|
81
|
+
return '\n' + lines.map(l => ` ${l}`).join('\n') + '\n'
|
|
82
|
+
}
|
|
83
|
+
*/
|
|
84
|
+
export function getLogo() {
|
|
85
|
+
const lines = [
|
|
86
|
+
'███████╗███████╗███╗ ██╗██████╗',
|
|
87
|
+
'██╔════╝██╔════╝████╗ ██║██╔══██╗',
|
|
88
|
+
'███████╗█████╗ ██╔██╗ ██║██║ ██║',
|
|
89
|
+
'╚════██║██╔══╝ ██║╚██╗██║██║ ██║',
|
|
90
|
+
'███████║███████╗██║ ╚████║██████╔╝',
|
|
91
|
+
'╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝',
|
|
92
|
+
'',
|
|
93
|
+
'██████╗ ██╗ ██╗ ██╗███████╗',
|
|
94
|
+
'██╔══██╗██║ ██║ ██║██╔════╝',
|
|
95
|
+
'██████╔╝██║ ██║ ██║█████╗',
|
|
96
|
+
'██╔══██╗██║ ██║ ██║██╔══╝',
|
|
97
|
+
'██████╔╝███████╗╚██████╔╝███████╗',
|
|
98
|
+
'╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝',
|
|
99
|
+
'',
|
|
100
|
+
chalk.dim(' iMessage for agents'),
|
|
46
101
|
];
|
|
47
|
-
return '\n' + lines.map(l =>
|
|
102
|
+
return '\n' + lines.map(l => blue(l)).join('\n') + '\n';
|
|
48
103
|
}
|
|
49
104
|
export function normalizeNumber(input) {
|
|
50
105
|
// Strip non-digit chars except leading +
|