Skip to content

Commit 50ed2bd

Browse files
committed
feat: add CSV examples and enhance sync command with file selection prompt
1 parent 4b0443f commit 50ed2bd

7 files changed

Lines changed: 157 additions & 24 deletions

File tree

input/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
*
22
!.gitignore
3-
!example.csv
3+
!*-example.csv

input/batch-create-example.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
email,quota
2+
john.doe@example.com,1024
3+
jane.smith@example.com,2048
4+
support@example.com,512
5+
info@example.com,1024

src/commands/cpanel/create.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import {Command, Flags} from '@oclif/core'
2-
import { CpanelService } from '../../services/cpanel/auth.service.js'
3-
import { DomainService, setDebugLevel as setDomainDebugLevel } from '../../services/cpanel/domain.service.js'
4-
import { EmailService, setDebugLevel as setEmailDebugLevel } from '../../services/cpanel/email.service.js'
5-
import { UtilService } from '../../services/shared/util.service.js'
6-
import { setDebugLevel as setCpanelDebugLevel } from '../../services/cpanel/auth.service.js'
7-
import { createObjectCsvWriter } from 'csv-writer'
1+
import { Command, Flags } from '@oclif/core'
2+
import chalk from 'chalk'
83
import csvParser from 'csv-parser'
4+
import { createObjectCsvWriter } from 'csv-writer'
5+
import fs from 'fs'
96
import inquirer from 'inquirer'
10-
import chalk from 'chalk'
117
import ora from 'ora'
12-
import fs from 'fs'
8+
import { CpanelService, setDebugLevel as setCpanelDebugLevel } from '../../services/cpanel/auth.service.js'
9+
import { DomainService, setDebugLevel as setDomainDebugLevel } from '../../services/cpanel/domain.service.js'
10+
import { EmailService, setDebugLevel as setEmailDebugLevel } from '../../services/cpanel/email.service.js'
11+
import { UtilService } from '../../services/shared/util.service.js'
1312

1413
export default class CpanelCreate extends Command {
1514
static description = 'Create cPanel email accounts in bulk'

src/commands/sync.js

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import {Command, Flags} from '@oclif/core'
1+
import { Command, Flags } from '@oclif/core'
2+
import fs from 'fs-extra'
3+
import inquirer from 'inquirer'
4+
import path from 'path'
25
import { ImapService } from '../services/imap/sync.service.js'
36

47
export default class Sync extends Command {
@@ -8,14 +11,13 @@ export default class Sync extends Command {
811
'<%= config.bin %> <%= command.id %>',
912
'<%= config.bin %> <%= command.id %> --csv input/my-migration.csv',
1013
'<%= config.bin %> <%= command.id %> --dry-run --jobs 4',
11-
'<%= config.bin %> <%= command.id %> --docker --log-dir ./logs/sync',
14+
'<%= config.bin %> <%= command.id %> --docker --log-dir ./results/sync-log',
1215
]
1316

1417
static flags = {
1518
csv: Flags.string({
1619
char: 'c',
17-
description: 'CSV file containing sync configurations',
18-
default: 'input/example.csv',
20+
description: 'CSV file containing sync configurations (will prompt if not provided)',
1921
}),
2022
jobs: Flags.string({
2123
char: 'j',
@@ -27,22 +29,130 @@ export default class Sync extends Command {
2729
}),
2830
'log-dir': Flags.string({
2931
description: 'Log directory',
30-
default: './results',
32+
default: './results/sync-log',
3133
}),
3234
'dry-run': Flags.boolean({
3335
description: 'Show what would be synced without actually syncing',
3436
}),
3537
}
3638

39+
async promptForCsvFile() {
40+
// Get available CSV files from input/sync directory
41+
const syncDir = path.join(process.cwd(), 'input', 'sync')
42+
const inputDir = path.join(process.cwd(), 'input')
43+
const choices = []
44+
45+
try {
46+
// Add files from input/sync/ directory
47+
if (await fs.pathExists(syncDir)) {
48+
const syncFiles = await fs.readdir(syncDir)
49+
const csvFiles = syncFiles.filter(file => file.endsWith('.csv'))
50+
csvFiles.forEach(file => {
51+
choices.push({
52+
name: `input/sync/${file}`,
53+
value: `input/sync/${file}`,
54+
short: file
55+
})
56+
})
57+
}
58+
59+
// Add some files from input/ directory that are sync-related
60+
if (await fs.pathExists(inputDir)) {
61+
const inputFiles = await fs.readdir(inputDir)
62+
const syncRelatedFiles = inputFiles.filter(file =>
63+
file.endsWith('.csv') &&
64+
(file.includes('sync') || file.includes('imap'))
65+
)
66+
syncRelatedFiles.forEach(file => {
67+
choices.push({
68+
name: `input/${file}`,
69+
value: `input/${file}`,
70+
short: file
71+
})
72+
})
73+
}
74+
75+
// Add option to specify custom path
76+
choices.push({
77+
name: 'Enter custom file path',
78+
value: 'custom',
79+
short: 'custom'
80+
})
81+
82+
if (choices.length === 1) {
83+
// Only custom option available
84+
const { customPath } = await inquirer.prompt([
85+
{
86+
type: 'input',
87+
name: 'customPath',
88+
message: 'Enter CSV file path:',
89+
default: 'input/sync/example.csv',
90+
validate: input => input.trim().length > 0 ? true : 'Please enter a file path'
91+
}
92+
])
93+
return customPath
94+
}
95+
96+
const { selectedFile } = await inquirer.prompt([
97+
{
98+
type: 'list',
99+
name: 'selectedFile',
100+
message: 'Select CSV file for sync configuration:',
101+
choices: choices,
102+
pageSize: 10
103+
}
104+
])
105+
106+
if (selectedFile === 'custom') {
107+
const { customPath } = await inquirer.prompt([
108+
{
109+
type: 'input',
110+
name: 'customPath',
111+
message: 'Enter CSV file path:',
112+
default: 'input/sync/example.csv',
113+
validate: input => input.trim().length > 0 ? true : 'Please enter a file path'
114+
}
115+
])
116+
return customPath
117+
}
118+
119+
return selectedFile
120+
} catch (error) {
121+
this.log('Error reading input directories, please enter file path manually:')
122+
const { customPath } = await inquirer.prompt([
123+
{
124+
type: 'input',
125+
name: 'customPath',
126+
message: 'Enter CSV file path:',
127+
default: 'input/sync/example.csv',
128+
validate: input => input.trim().length > 0 ? true : 'Please enter a file path'
129+
}
130+
])
131+
return customPath
132+
}
133+
}
134+
37135
async run() {
38136
const {flags} = await this.parse(Sync)
39137

40138
try {
139+
// If CSV flag is not provided, prompt user for CSV file
140+
let csvFile = flags.csv
141+
if (!csvFile) {
142+
this.log('No CSV file specified, please select one:')
143+
csvFile = await this.promptForCsvFile()
144+
}
145+
146+
// Validate that the file exists
147+
if (!await fs.pathExists(csvFile)) {
148+
this.error(`CSV file not found: ${csvFile}`)
149+
}
150+
41151
const imapService = new ImapService()
42152

43153
// Map flags to options format expected by the service
44154
const options = {
45-
csv: flags.csv,
155+
csv: csvFile,
46156
jobs: flags.jobs,
47157
docker: flags.docker,
48158
logDir: flags['log-dir'],

src/services/cpanel/email.service.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ export class EmailService {
5050
}
5151
}
5252

53-
async createAccount(username, domain, password, quota = 1024) {
53+
async createAccount(username, domain, password, quota = 2048) {
5454
const fullEmail = `${username}@${domain}`;
5555

5656
debugLog(2, `Creating email account: ${fullEmail}`);
5757
debugLog(3, `Email creation parameters:`, { email: fullEmail, domain, quota });
5858

5959
try {
6060
// addpop creates a new email account
61-
const response = await this.cpanel.postRequest('Email/addpop', {
61+
const response = await this.cpanel.makeRequest('Email/add_pop', {
6262
email: username,
6363
password: password,
6464
domain: domain,

src/services/imap/sync.service.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { spawn } from 'child_process';
33
import csv from 'csv-parser';
44
import { createReadStream } from 'fs';
55
import fs from 'fs-extra';
6-
import path from 'path';
76
import ora from 'ora';
7+
import path from 'path';
88

99
/**
1010
* IMAP synchronization service
@@ -59,7 +59,7 @@ export class ImapService {
5959
/**
6060
* Build imapsync command arguments from configuration
6161
*/
62-
buildImapsyncArgs(config) {
62+
buildImapsyncArgs(config, options = {}) {
6363
const {
6464
src_host: shost,
6565
src_user: suser,
@@ -76,7 +76,22 @@ export class ImapService {
7676
} = config;
7777

7878
// Base flags
79-
const flags = ['--dry', '--justfolders'];
79+
const flags = [];
80+
81+
// Add dry run flag only if explicitly requested
82+
if (options.dryRun) {
83+
flags.push('--dry');
84+
}
85+
86+
// Add justfolders flag for folder structure preview (optional)
87+
if (options.justFolders) {
88+
flags.push('--justfolders');
89+
}
90+
91+
// Add log file if provided
92+
if (options.logFile) {
93+
flags.push('--logfile', options.logFile);
94+
}
8095

8196
// Add ports if specified
8297
if (src_port) flags.push('--port1', src_port);
@@ -114,7 +129,11 @@ export class ImapService {
114129
const { src_user: suser, dst_user: duser } = config;
115130
const sanitizedSrc = suser.replace(/@/g, '_');
116131
const sanitizedDst = duser.replace(/@/g, '_');
117-
return path.join(logDir, `${sanitizedSrc}__to__${sanitizedDst}.log`);
132+
133+
// Add timestamp to log file name
134+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
135+
136+
return path.join(logDir, `${sanitizedSrc}__to__${sanitizedDst}_${timestamp}.log`);
118137
}
119138

120139
/**
@@ -134,11 +153,11 @@ export class ImapService {
134153

135154
console.log(chalk.blue(`Syncing: ${suser} -> ${duser}`));
136155

137-
const logDir = options.logDir || './results';
156+
const logDir = options.logDir || './results/sync-log';
138157
await fs.ensureDir(logDir);
139158

140159
const logFile = this.generateLogFilePath(config, logDir);
141-
const imapsyncArgs = this.buildImapsyncArgs(config);
160+
const imapsyncArgs = this.buildImapsyncArgs(config, { ...options, logFile });
142161

143162
if (options.dryRun) {
144163
console.log(chalk.yellow('DRY RUN - Command that would be executed:'));

0 commit comments

Comments
 (0)