create-neomutt-gmail 0.1.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.
- package/LICENSE +0 -0
- package/README.md +265 -0
- package/__tests__/prerequisites.test.js +0 -0
- package/bin/cli.js +48 -0
- package/package.json +49 -0
- package/src/display.js +115 -0
- package/src/download.js +59 -0
- package/src/index.js +167 -0
- package/src/prerequisites.js +98 -0
- package/src/questions.js +68 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# create-neomutt-gmail
|
|
2
|
+
|
|
3
|
+
Interactive setup wizard for Neomutt + Gmail + OAuth2.0
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/create-neomutt-gmail)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ✅ **Interactive wizard** - Answer a few questions, get a working config
|
|
11
|
+
- ✅ **Prerequisites check** - Verifies all dependencies before starting
|
|
12
|
+
- ✅ **Safe operations** - Automatic backups, confirmation prompts
|
|
13
|
+
- ✅ **Dry-run mode** - Preview changes without modifying files
|
|
14
|
+
- ✅ **Automatic downloads** - Fetches OAuth2 script from official source
|
|
15
|
+
- ✅ **Helpful guidance** - Clear next steps after installation
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx create-neomutt-gmail
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That's it! Answer the questions and follow the on-screen instructions.
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
The wizard checks for these automatically, but you'll need:
|
|
28
|
+
|
|
29
|
+
- **neomutt** - Email client
|
|
30
|
+
- **python3** - For OAuth2 script
|
|
31
|
+
- **gpg** - For token encryption
|
|
32
|
+
|
|
33
|
+
On macOS:
|
|
34
|
+
```bash
|
|
35
|
+
brew install neomutt python3 gnupg
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
On Linux:
|
|
39
|
+
```bash
|
|
40
|
+
# Debian/Ubuntu
|
|
41
|
+
sudo apt install neomutt python3 gnupg
|
|
42
|
+
|
|
43
|
+
# Arch
|
|
44
|
+
sudo pacman -S neomutt python gnupg
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### GPG Key Setup
|
|
48
|
+
|
|
49
|
+
If you don't have a GPG key yet:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
gpg --full-generate-key
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Choose:
|
|
56
|
+
- RSA and RSA
|
|
57
|
+
- 4096 bits
|
|
58
|
+
- No expiration (or as preferred)
|
|
59
|
+
- Enter your name and email
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Standard Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx create-neomutt-gmail
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Dry Run (Preview Only)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx create-neomutt-gmail --dry-run
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Shows what would be created without actually creating any files.
|
|
76
|
+
|
|
77
|
+
### Skip Prerequisites Check
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx create-neomutt-gmail --skip-prereqs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Skips the dependency verification (not recommended).
|
|
84
|
+
|
|
85
|
+
### Help
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx create-neomutt-gmail --help
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## What It Does
|
|
92
|
+
|
|
93
|
+
The wizard will:
|
|
94
|
+
|
|
95
|
+
1. **Check prerequisites** - Verify neomutt, python3, gpg are installed
|
|
96
|
+
2. **Ask configuration questions**:
|
|
97
|
+
- Gmail address
|
|
98
|
+
- Your name
|
|
99
|
+
- Preferred editor (nvim, vim, nano, emacs)
|
|
100
|
+
- Gmail language (Japanese or English)
|
|
101
|
+
- GPG key ID (optional)
|
|
102
|
+
3. **Create directories**:
|
|
103
|
+
- `~/.config/mutt/`
|
|
104
|
+
- `~/.config/mutt/accounts/`
|
|
105
|
+
- `~/.local/etc/oauth-tokens/`
|
|
106
|
+
- `~/.cache/mutt/`
|
|
107
|
+
4. **Generate config files**:
|
|
108
|
+
- `~/.config/mutt/muttrc`
|
|
109
|
+
- `~/.config/mutt/accounts/YOUR_EMAIL.muttrc`
|
|
110
|
+
5. **Download OAuth script**:
|
|
111
|
+
- `~/.config/mutt/mutt_oauth2.py`
|
|
112
|
+
6. **Display next steps** for Google Cloud Console setup
|
|
113
|
+
|
|
114
|
+
## After Installation
|
|
115
|
+
|
|
116
|
+
### Step 1: Google Cloud Console
|
|
117
|
+
|
|
118
|
+
1. Visit https://console.cloud.google.com/
|
|
119
|
+
2. Create a new project
|
|
120
|
+
3. Enable Gmail API
|
|
121
|
+
4. Configure OAuth consent screen (External)
|
|
122
|
+
5. Add scope: `https://mail.google.com/`
|
|
123
|
+
6. Add your email as test user
|
|
124
|
+
7. Create OAuth client ID (Desktop application)
|
|
125
|
+
8. Note Client ID and Client Secret
|
|
126
|
+
|
|
127
|
+
### Step 2: Obtain OAuth Token
|
|
128
|
+
|
|
129
|
+
Run the command shown by the wizard (it will look like this):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python3 ~/.config/mutt/mutt_oauth2.py \
|
|
133
|
+
--authorize \
|
|
134
|
+
--authflow localhostauthcode \
|
|
135
|
+
--encryption-pipe 'gpg --encrypt --recipient your@email.com' \
|
|
136
|
+
--client-id YOUR_CLIENT_ID.apps.googleusercontent.com \
|
|
137
|
+
--client-secret YOUR_CLIENT_SECRET \
|
|
138
|
+
--provider google \
|
|
139
|
+
--email your@email.com \
|
|
140
|
+
~/.local/etc/oauth-tokens/gmail.tokens
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Step 3: Launch Neomutt
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
neomutt
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Keyboard Shortcuts
|
|
150
|
+
|
|
151
|
+
Once Neomutt is running:
|
|
152
|
+
|
|
153
|
+
| Key | Action |
|
|
154
|
+
|-----|--------|
|
|
155
|
+
| `gi` | Go to inbox |
|
|
156
|
+
| `gt` | Go to trash |
|
|
157
|
+
| `gs` | Go to sent mail |
|
|
158
|
+
| `gd` | Go to drafts |
|
|
159
|
+
| `m` | Compose new email |
|
|
160
|
+
| `r` | Reply |
|
|
161
|
+
| `q` | Quit |
|
|
162
|
+
|
|
163
|
+
## Safety Features
|
|
164
|
+
|
|
165
|
+
### Automatic Backups
|
|
166
|
+
|
|
167
|
+
Existing config files are backed up with timestamps:
|
|
168
|
+
```
|
|
169
|
+
~/.config/mutt/muttrc.backup-2026-01-26T12-34-56-789Z
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Confirmation Prompts
|
|
173
|
+
|
|
174
|
+
The wizard asks for confirmation before:
|
|
175
|
+
- Overwriting existing configurations
|
|
176
|
+
- Creating files
|
|
177
|
+
- Proceeding with installation
|
|
178
|
+
|
|
179
|
+
### Dry-Run Mode
|
|
180
|
+
|
|
181
|
+
Preview all changes before applying them:
|
|
182
|
+
```bash
|
|
183
|
+
npx create-neomutt-gmail --dry-run
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## File Permissions
|
|
187
|
+
|
|
188
|
+
Created files have appropriate permissions:
|
|
189
|
+
- Config files: `0o600` (owner read/write only)
|
|
190
|
+
- OAuth script: `0o755` (owner rwx, others rx)
|
|
191
|
+
|
|
192
|
+
## Troubleshooting
|
|
193
|
+
|
|
194
|
+
### "Command not found: neomutt"
|
|
195
|
+
|
|
196
|
+
Install neomutt:
|
|
197
|
+
```bash
|
|
198
|
+
brew install neomutt # macOS
|
|
199
|
+
sudo apt install neomutt # Linux
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### "No GPG keys found"
|
|
203
|
+
|
|
204
|
+
Create a GPG key:
|
|
205
|
+
```bash
|
|
206
|
+
gpg --full-generate-key
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### "Failed to download mutt_oauth2.py"
|
|
210
|
+
|
|
211
|
+
Download manually:
|
|
212
|
+
```bash
|
|
213
|
+
curl -o ~/.config/mutt/mutt_oauth2.py \
|
|
214
|
+
https://raw.githubusercontent.com/neomutt/neomutt/main/contrib/oauth2/mutt_oauth2.py
|
|
215
|
+
chmod +x ~/.config/mutt/mutt_oauth2.py
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### OAuth Token Issues
|
|
219
|
+
|
|
220
|
+
See the [detailed troubleshooting guide](https://github.com/a-lost-social-misfit/terminal-blog) for OAuth-specific issues.
|
|
221
|
+
|
|
222
|
+
## Powered By
|
|
223
|
+
|
|
224
|
+
This wizard uses:
|
|
225
|
+
- [mutt-config-core](https://www.npmjs.com/package/mutt-config-core) - Configuration generation
|
|
226
|
+
- [config-fs-utils](https://www.npmjs.com/package/config-fs-utils) - File system operations
|
|
227
|
+
|
|
228
|
+
## Related Projects
|
|
229
|
+
|
|
230
|
+
- [mutt-config-core](https://github.com/a-lost-social-misfit/mutt-config-core) - Core config generator
|
|
231
|
+
- [config-fs-utils](https://github.com/a-lost-social-misfit/config-fs-utils) - FS utilities
|
|
232
|
+
- [terminal-blog](https://github.com/a-lost-social-misfit/terminal-blog) - Detailed setup guide
|
|
233
|
+
|
|
234
|
+
## Contributing
|
|
235
|
+
|
|
236
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
237
|
+
|
|
238
|
+
## ⚠️ Important Notice
|
|
239
|
+
|
|
240
|
+
This tool modifies files in your home directory:
|
|
241
|
+
- `~/.config/mutt/`
|
|
242
|
+
- `~/.local/etc/oauth-tokens/`
|
|
243
|
+
- `~/.cache/mutt/`
|
|
244
|
+
|
|
245
|
+
Before running:
|
|
246
|
+
1. ✅ Backup existing Neomutt configuration (if any)
|
|
247
|
+
2. ✅ Review the code on GitHub
|
|
248
|
+
3. ✅ Use `--dry-run` to preview changes
|
|
249
|
+
|
|
250
|
+
The tool creates automatic backups, but it's your responsibility to:
|
|
251
|
+
- Verify the configuration before use
|
|
252
|
+
- Keep OAuth tokens secure
|
|
253
|
+
- Review file permissions
|
|
254
|
+
|
|
255
|
+
**No warranty is provided. Use at your own risk.**
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT © a-lost-social-misfit
|
|
260
|
+
|
|
261
|
+
## Author
|
|
262
|
+
|
|
263
|
+
Created by [a-lost-social-misfit](https://github.com/a-lost-social-misfit)
|
|
264
|
+
|
|
265
|
+
Based on practical experience and lessons learned from setting up Neomutt with Gmail OAuth2.0 on macOS.
|
|
File without changes
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* create-neomutt-gmail
|
|
5
|
+
* Interactive setup wizard for Neomutt + Gmail + OAuth2.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { run } from "../src/index.js";
|
|
9
|
+
|
|
10
|
+
// Parse command line arguments
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const options = {
|
|
13
|
+
dryRun: args.includes("--dry-run"),
|
|
14
|
+
skipPrereqs: args.includes("--skip-prereqs"),
|
|
15
|
+
help: args.includes("--help") || args.includes("-h"),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Show help
|
|
19
|
+
if (options.help) {
|
|
20
|
+
console.log(`
|
|
21
|
+
create-neomutt-gmail - Interactive setup wizard for Neomutt + Gmail
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
npx create-neomutt-gmail [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--dry-run Preview changes without creating files
|
|
28
|
+
--skip-prereqs Skip prerequisites check
|
|
29
|
+
--help, -h Show this help message
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
npx create-neomutt-gmail
|
|
33
|
+
npx create-neomutt-gmail --dry-run
|
|
34
|
+
|
|
35
|
+
For more information, visit:
|
|
36
|
+
https://github.com/a-lost-social-misfit/create-neomutt-gmail
|
|
37
|
+
`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run the wizard
|
|
42
|
+
run(options).catch((error) => {
|
|
43
|
+
console.error("\n❌ Error:", error.message);
|
|
44
|
+
if (process.env.DEBUG) {
|
|
45
|
+
console.error(error.stack);
|
|
46
|
+
}
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-neomutt-gmail",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Interactive setup wizard for Neomutt + Gmail + OAuth2.0",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-neomutt-gmail": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"dev": "node bin/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"neomutt",
|
|
16
|
+
"gmail",
|
|
17
|
+
"oauth2",
|
|
18
|
+
"email",
|
|
19
|
+
"cli",
|
|
20
|
+
"setup",
|
|
21
|
+
"wizard",
|
|
22
|
+
"mutt"
|
|
23
|
+
],
|
|
24
|
+
"author": "a-lost-social-misfit",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/a-lost-social-misfit/create-neomutt-gmail.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/a-lost-social-misfit/create-neomutt-gmail/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/a-lost-social-misfit/create-neomutt-gmail#readme",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"mutt-config-core": "^0.1.0",
|
|
36
|
+
"config-fs-utils": "^0.1.0",
|
|
37
|
+
"inquirer": "^13.2.1",
|
|
38
|
+
"chalk": "^5.6.2",
|
|
39
|
+
"ora": "^9.1.0",
|
|
40
|
+
"node-fetch": "^3.3.2"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"jest": "^30.2.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"type": "module"
|
|
49
|
+
}
|
package/src/display.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display functions for user output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import os from "os";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Display next steps after installation
|
|
10
|
+
* @param {Object} answers - User configuration
|
|
11
|
+
*/
|
|
12
|
+
export function displayNextSteps(answers) {
|
|
13
|
+
console.log(chalk.bold("📖 Next Steps:\n"));
|
|
14
|
+
|
|
15
|
+
// Step 1: Google Cloud Console
|
|
16
|
+
console.log(chalk.bold("1. Setup Google Cloud Console"));
|
|
17
|
+
console.log(
|
|
18
|
+
" Visit: " + chalk.cyan("https://console.cloud.google.com/") + "\n"
|
|
19
|
+
);
|
|
20
|
+
console.log(
|
|
21
|
+
" " + chalk.dim("a.") + ' Create a new project (e.g., "neomutt-gmail")'
|
|
22
|
+
);
|
|
23
|
+
console.log(" " + chalk.dim("b.") + " Enable Gmail API");
|
|
24
|
+
console.log(" " + chalk.dim("c.") + " Configure OAuth consent screen:");
|
|
25
|
+
console.log(" • User Type: External");
|
|
26
|
+
console.log(" • Add scope: https://mail.google.com/");
|
|
27
|
+
console.log(` • Add test user: ${chalk.cyan(answers.email)}`);
|
|
28
|
+
console.log(
|
|
29
|
+
" " + chalk.dim("d.") + " Create OAuth client ID (Desktop application)"
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
" " + chalk.dim("e.") + " Note down Client ID and Client Secret\n"
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Step 2: Obtain OAuth token
|
|
36
|
+
console.log(chalk.bold("2. Obtain OAuth 2.0 token"));
|
|
37
|
+
console.log(" Run the following command:\n");
|
|
38
|
+
|
|
39
|
+
const command = `python3 ~/.config/mutt/mutt_oauth2.py \\
|
|
40
|
+
--authorize \\
|
|
41
|
+
--authflow localhostauthcode \\
|
|
42
|
+
--encryption-pipe 'gpg --encrypt --recipient ${answers.gpgKeyId || answers.email}' \\
|
|
43
|
+
--client-id YOUR_CLIENT_ID.apps.googleusercontent.com \\
|
|
44
|
+
--client-secret YOUR_CLIENT_SECRET \\
|
|
45
|
+
--provider google \\
|
|
46
|
+
--email ${answers.email} \\
|
|
47
|
+
~/.local/etc/oauth-tokens/gmail.tokens`;
|
|
48
|
+
|
|
49
|
+
console.log(chalk.cyan(command) + "\n");
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.dim(
|
|
52
|
+
" Replace YOUR_CLIENT_ID and YOUR_CLIENT_SECRET with actual values"
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
console.log(chalk.dim(" You will be prompted for your GPG passphrase\n"));
|
|
56
|
+
|
|
57
|
+
// Step 3: Launch Neomutt
|
|
58
|
+
console.log(chalk.bold("3. Launch Neomutt"));
|
|
59
|
+
console.log(" " + chalk.cyan("neomutt") + "\n");
|
|
60
|
+
|
|
61
|
+
// Additional resources
|
|
62
|
+
console.log(chalk.dim("───────────────────────────────────\n"));
|
|
63
|
+
console.log("📚 For detailed instructions, visit:");
|
|
64
|
+
console.log(" https://github.com/a-lost-social-misfit/terminal-blog\n");
|
|
65
|
+
console.log("💬 Need help? Open an issue:");
|
|
66
|
+
console.log(
|
|
67
|
+
" https://github.com/a-lost-social-misfit/create-neomutt-gmail/issues\n"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Keyboard shortcuts reminder
|
|
71
|
+
console.log(chalk.bold("🎹 Neomutt Keyboard Shortcuts:\n"));
|
|
72
|
+
console.log(" " + chalk.cyan("gi") + " - Go to inbox");
|
|
73
|
+
console.log(" " + chalk.cyan("gt") + " - Go to trash");
|
|
74
|
+
console.log(" " + chalk.cyan("gs") + " - Go to sent mail");
|
|
75
|
+
console.log(" " + chalk.cyan("gd") + " - Go to drafts");
|
|
76
|
+
console.log(" " + chalk.cyan("m") + " - Compose new email");
|
|
77
|
+
console.log(" " + chalk.cyan("r") + " - Reply");
|
|
78
|
+
console.log(" " + chalk.cyan("q") + " - Quit");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Display dry-run preview
|
|
83
|
+
* @param {Object} answers - User configuration
|
|
84
|
+
* @param {Object} paths - Configuration paths
|
|
85
|
+
* @param {Object} configs - Generated configurations
|
|
86
|
+
*/
|
|
87
|
+
export function displayDryRun(answers, paths, configs) {
|
|
88
|
+
const homeDir = os.homedir();
|
|
89
|
+
|
|
90
|
+
console.log(chalk.bold("📁 Directories to be created:\n"));
|
|
91
|
+
console.log(` ${homeDir}/.config/mutt/`);
|
|
92
|
+
console.log(` ${homeDir}/.config/mutt/accounts/`);
|
|
93
|
+
console.log(` ${homeDir}/.local/etc/oauth-tokens/`);
|
|
94
|
+
console.log(` ${homeDir}/.cache/mutt/headers/`);
|
|
95
|
+
console.log(` ${homeDir}/.cache/mutt/bodies/`);
|
|
96
|
+
|
|
97
|
+
console.log(chalk.bold("\n📄 Files to be created:\n"));
|
|
98
|
+
console.log(` ${homeDir}/${paths.accountMuttrc}`);
|
|
99
|
+
console.log(` ${homeDir}/${paths.mainMuttrc}`);
|
|
100
|
+
console.log(` ${homeDir}/.config/mutt/mutt_oauth2.py`);
|
|
101
|
+
|
|
102
|
+
console.log(chalk.bold("\n📝 Configuration preview:\n"));
|
|
103
|
+
console.log(chalk.dim("Account config (first 10 lines):"));
|
|
104
|
+
console.log(
|
|
105
|
+
chalk.dim(configs.accountMuttrc.split("\n").slice(0, 10).join("\n"))
|
|
106
|
+
);
|
|
107
|
+
console.log(chalk.dim(" ..."));
|
|
108
|
+
|
|
109
|
+
console.log(chalk.bold("\n⚙️ Settings:\n"));
|
|
110
|
+
console.log(` Email: ${chalk.cyan(answers.email)}`);
|
|
111
|
+
console.log(` Name: ${chalk.cyan(answers.realName)}`);
|
|
112
|
+
console.log(` Editor: ${chalk.cyan(answers.editor)}`);
|
|
113
|
+
console.log(` Locale: ${chalk.cyan(answers.locale)}`);
|
|
114
|
+
console.log(` GPG Key: ${chalk.cyan(answers.gpgKeyId || answers.email)}`);
|
|
115
|
+
}
|
package/src/download.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download mutt_oauth2.py script
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "fs/promises";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import fetch from "node-fetch";
|
|
9
|
+
|
|
10
|
+
const MUTT_OAUTH2_URL =
|
|
11
|
+
"https://raw.githubusercontent.com/neomutt/neomutt/main/contrib/oauth2/mutt_oauth2.py";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Download and install mutt_oauth2.py
|
|
15
|
+
* @returns {Promise<string>} Path to installed script
|
|
16
|
+
*/
|
|
17
|
+
export async function downloadMuttOAuth2() {
|
|
18
|
+
const targetPath = path.join(
|
|
19
|
+
os.homedir(),
|
|
20
|
+
".config",
|
|
21
|
+
"mutt",
|
|
22
|
+
"mutt_oauth2.py"
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Check if already exists
|
|
26
|
+
try {
|
|
27
|
+
await fs.access(targetPath);
|
|
28
|
+
// File exists, skip download
|
|
29
|
+
return targetPath;
|
|
30
|
+
} catch {
|
|
31
|
+
// File doesn't exist, proceed with download
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Download
|
|
35
|
+
const response = await fetch(MUTT_OAUTH2_URL);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Failed to download: ${response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = await response.text();
|
|
41
|
+
|
|
42
|
+
// Basic validation - check if it looks like a Python script
|
|
43
|
+
if (
|
|
44
|
+
!content.includes("#!/usr/bin/env python") &&
|
|
45
|
+
!content.includes("import")
|
|
46
|
+
) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Downloaded file does not appear to be a valid Python script"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Write file
|
|
53
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
54
|
+
|
|
55
|
+
// Make executable
|
|
56
|
+
await fs.chmod(targetPath, 0o755);
|
|
57
|
+
|
|
58
|
+
return targetPath;
|
|
59
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main application logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { generateMuttConfigs, getConfigPaths } from "mutt-config-core";
|
|
6
|
+
import {
|
|
7
|
+
setupStandardMuttDirs,
|
|
8
|
+
writeConfigFiles,
|
|
9
|
+
exists,
|
|
10
|
+
} from "config-fs-utils";
|
|
11
|
+
import inquirer from "inquirer";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
|
|
15
|
+
import { checkPrerequisites } from "./prerequisites.js";
|
|
16
|
+
import { askQuestions } from "./questions.js";
|
|
17
|
+
import { downloadMuttOAuth2 } from "./download.js";
|
|
18
|
+
import { displayNextSteps, displayDryRun } from "./display.js";
|
|
19
|
+
|
|
20
|
+
export async function run(options = {}) {
|
|
21
|
+
console.log(chalk.bold("\n🔧 Neomutt + Gmail Setup Wizard\n"));
|
|
22
|
+
|
|
23
|
+
// Prerequisites check
|
|
24
|
+
if (!options.skipPrereqs) {
|
|
25
|
+
console.log(chalk.blue("📋 Prerequisites Check"));
|
|
26
|
+
const prereqCheck = await checkPrerequisites();
|
|
27
|
+
|
|
28
|
+
if (!prereqCheck.allPassed) {
|
|
29
|
+
console.log(chalk.red("\n❌ Missing required dependencies."));
|
|
30
|
+
console.log("Please install them first and try again.\n");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.dim("\n───────────────────────────────────\n"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Ask user questions
|
|
38
|
+
console.log(chalk.blue("📝 Configuration\n"));
|
|
39
|
+
const answers = await askQuestions();
|
|
40
|
+
|
|
41
|
+
console.log(chalk.dim("\n───────────────────────────────────\n"));
|
|
42
|
+
|
|
43
|
+
// Check for existing config and warn
|
|
44
|
+
const muttrcPath = `${process.env.HOME}/.config/mutt/muttrc`;
|
|
45
|
+
if (await exists(muttrcPath)) {
|
|
46
|
+
console.log(chalk.yellow("⚠️ Existing Neomutt configuration found."));
|
|
47
|
+
console.log(
|
|
48
|
+
chalk.yellow(" A backup will be created before overwriting.\n")
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { proceed } = await inquirer.prompt([
|
|
52
|
+
{
|
|
53
|
+
type: "confirm",
|
|
54
|
+
name: "proceed",
|
|
55
|
+
message: "Continue with setup?",
|
|
56
|
+
default: true,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
if (!proceed) {
|
|
61
|
+
console.log(chalk.dim("\nSetup cancelled."));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Dry run mode - show what would happen
|
|
69
|
+
if (options.dryRun) {
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.bold.cyan("🔍 DRY RUN MODE - No files will be created\n")
|
|
72
|
+
);
|
|
73
|
+
const configs = generateMuttConfigs(answers);
|
|
74
|
+
const paths = getConfigPaths(answers.email);
|
|
75
|
+
displayDryRun(answers, paths, configs);
|
|
76
|
+
console.log(chalk.dim("\nRun without --dry-run to actually create files."));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Final confirmation
|
|
81
|
+
console.log(chalk.bold("📦 Ready to create:\n"));
|
|
82
|
+
console.log(" • Configuration directories");
|
|
83
|
+
console.log(" • Neomutt config files");
|
|
84
|
+
console.log(" • OAuth2 script\n");
|
|
85
|
+
|
|
86
|
+
const { finalConfirm } = await inquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: "confirm",
|
|
89
|
+
name: "finalConfirm",
|
|
90
|
+
message: "Proceed with installation?",
|
|
91
|
+
default: true,
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
if (!finalConfirm) {
|
|
96
|
+
console.log(chalk.dim("\nSetup cancelled."));
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log();
|
|
101
|
+
|
|
102
|
+
// Create directories
|
|
103
|
+
const spinner = ora("Creating directories...").start();
|
|
104
|
+
try {
|
|
105
|
+
const dirs = await setupStandardMuttDirs();
|
|
106
|
+
spinner.succeed("Directories created");
|
|
107
|
+
|
|
108
|
+
for (const [name, dirPath] of Object.entries(dirs)) {
|
|
109
|
+
console.log(chalk.green(` ✓ ${dirPath}`));
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
spinner.fail("Failed to create directories");
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// Generate and write config files
|
|
119
|
+
spinner.text = "Generating configuration files...";
|
|
120
|
+
spinner.start();
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const configs = generateMuttConfigs(answers);
|
|
124
|
+
const paths = getConfigPaths(answers.email);
|
|
125
|
+
|
|
126
|
+
const results = await writeConfigFiles({
|
|
127
|
+
[`~/${paths.accountMuttrc}`]: configs.accountMuttrc,
|
|
128
|
+
[`~/${paths.mainMuttrc}`]: configs.mainMuttrc,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
spinner.succeed("Configuration files created");
|
|
132
|
+
|
|
133
|
+
for (const result of results) {
|
|
134
|
+
console.log(chalk.green(` ✓ ${result.path}`));
|
|
135
|
+
if (result.backup) {
|
|
136
|
+
console.log(chalk.dim(` (backup: ${result.backup})`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
spinner.fail("Failed to create config files");
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log();
|
|
145
|
+
|
|
146
|
+
// Download mutt_oauth2.py
|
|
147
|
+
spinner.text = "Downloading mutt_oauth2.py...";
|
|
148
|
+
spinner.start();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const scriptPath = await downloadMuttOAuth2();
|
|
152
|
+
spinner.succeed("mutt_oauth2.py downloaded");
|
|
153
|
+
console.log(chalk.green(` ✓ ${scriptPath}`));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
spinner.fail("Failed to download mutt_oauth2.py");
|
|
156
|
+
console.log(chalk.yellow("\n⚠️ You can download it manually from:"));
|
|
157
|
+
console.log(
|
|
158
|
+
" https://raw.githubusercontent.com/neomutt/neomutt/main/contrib/oauth2/mutt_oauth2.py"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.dim("\n───────────────────────────────────\n"));
|
|
163
|
+
|
|
164
|
+
// Display next steps
|
|
165
|
+
console.log(chalk.bold.green("🎉 Configuration complete!\n"));
|
|
166
|
+
displayNextSteps(answers);
|
|
167
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prerequisites checking
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
const REQUIRED_COMMANDS = [
|
|
12
|
+
{
|
|
13
|
+
cmd: "neomutt",
|
|
14
|
+
name: "neomutt",
|
|
15
|
+
required: true,
|
|
16
|
+
install: "brew install neomutt",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
cmd: "python3",
|
|
20
|
+
name: "python3",
|
|
21
|
+
required: true,
|
|
22
|
+
install: "brew install python3",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
cmd: "gpg",
|
|
26
|
+
name: "gpg",
|
|
27
|
+
required: true,
|
|
28
|
+
install: "brew install gnupg",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if a command exists in PATH
|
|
34
|
+
* @param {string} command - Command to check
|
|
35
|
+
* @returns {Promise<boolean>}
|
|
36
|
+
*/
|
|
37
|
+
async function checkCommand(command) {
|
|
38
|
+
try {
|
|
39
|
+
await execAsync(`which ${command}`);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if GPG has any keys configured
|
|
48
|
+
* @returns {Promise<boolean>}
|
|
49
|
+
*/
|
|
50
|
+
async function checkGPGKeys() {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execAsync("gpg --list-secret-keys");
|
|
53
|
+
return stdout.trim().length > 0;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check all prerequisites
|
|
61
|
+
* @returns {Promise<Object>} Check results
|
|
62
|
+
*/
|
|
63
|
+
export async function checkPrerequisites() {
|
|
64
|
+
const results = [];
|
|
65
|
+
let allPassed = true;
|
|
66
|
+
|
|
67
|
+
// Check required commands
|
|
68
|
+
for (const { cmd, name, required, install } of REQUIRED_COMMANDS) {
|
|
69
|
+
const found = await checkCommand(cmd);
|
|
70
|
+
results.push({ name, found, required, install });
|
|
71
|
+
|
|
72
|
+
if (found) {
|
|
73
|
+
console.log(chalk.green(`✓ ${name} found`));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(chalk.red(`✗ ${name} not found`));
|
|
76
|
+
if (required) {
|
|
77
|
+
console.log(chalk.dim(` Install: ${install}`));
|
|
78
|
+
allPassed = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check GPG keys
|
|
84
|
+
const hasGPGKeys = await checkGPGKeys();
|
|
85
|
+
if (hasGPGKeys) {
|
|
86
|
+
console.log(chalk.green("✓ GPG keys configured"));
|
|
87
|
+
} else {
|
|
88
|
+
console.log(chalk.yellow("⚠ No GPG keys found"));
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.dim(
|
|
91
|
+
" You will need to create a GPG key for OAuth token encryption"
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
console.log(chalk.dim(" Run: gpg --full-generate-key"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { results, allPassed, hasGPGKeys };
|
|
98
|
+
}
|
package/src/questions.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User interaction prompts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ask user for configuration details
|
|
9
|
+
* @returns {Promise<Object>} User answers
|
|
10
|
+
*/
|
|
11
|
+
export async function askQuestions() {
|
|
12
|
+
const questions = [
|
|
13
|
+
{
|
|
14
|
+
type: "input",
|
|
15
|
+
name: "email",
|
|
16
|
+
message: "Gmail address:",
|
|
17
|
+
validate: (input) => {
|
|
18
|
+
const emailRegex = /^[^\s@]+@gmail\.com$/;
|
|
19
|
+
if (!emailRegex.test(input)) {
|
|
20
|
+
return "Please enter a valid Gmail address (e.g., user@gmail.com)";
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "input",
|
|
27
|
+
name: "realName",
|
|
28
|
+
message: "Your name (for email headers):",
|
|
29
|
+
validate: (input) => {
|
|
30
|
+
if (input.trim().length === 0) {
|
|
31
|
+
return "Name is required";
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "list",
|
|
38
|
+
name: "editor",
|
|
39
|
+
message: "Preferred text editor:",
|
|
40
|
+
choices: [
|
|
41
|
+
{ name: "Neovim (nvim)", value: "nvim" },
|
|
42
|
+
{ name: "Vim", value: "vim" },
|
|
43
|
+
{ name: "Nano", value: "nano" },
|
|
44
|
+
{ name: "Emacs", value: "emacs" },
|
|
45
|
+
],
|
|
46
|
+
default: "nvim",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "list",
|
|
50
|
+
name: "locale",
|
|
51
|
+
message: "Gmail language setting:",
|
|
52
|
+
choices: [
|
|
53
|
+
{ name: "日本語 (Japanese)", value: "ja" },
|
|
54
|
+
{ name: "English", value: "en" },
|
|
55
|
+
],
|
|
56
|
+
default: "ja",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "input",
|
|
60
|
+
name: "gpgKeyId",
|
|
61
|
+
message: "GPG key ID (press Enter to use email address):",
|
|
62
|
+
default: (answers) => answers.email,
|
|
63
|
+
when: () => true,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
return inquirer.prompt(questions);
|
|
68
|
+
}
|