mudhost 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ ## Quick Start
2
+
3
+ #### Login to your Hosting instance:
4
+
5
+ ```bash
6
+ npx mudhost login --api https://your-domain.com
7
+ ```
8
+
9
+ Alternatively, you can provide username and password directly:
10
+ ```
11
+ mudhost login -u admin -p admin123 --api https://your-domain.com
12
+ ```
13
+
14
+ #### Deploy your project:
15
+
16
+ ```bash
17
+ npx mudhost deploy my-project ./dist
18
+ ```
19
+
20
+ #### Access your preview:
21
+
22
+ ```text
23
+ https://my-project-main-12345.your-domain.com
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ Login
29
+ ```bash
30
+ npx mudhost login [options]
31
+ ```
32
+ Deploy
33
+
34
+ ```bash
35
+ npx mudhost deploy <project-id> [dist-dir] [options]
36
+ ```
37
+ List Deployments
38
+ ```bash
39
+ npx mudhost list [options]
40
+ ```
41
+
42
+ Delete Deployment
43
+
44
+ ```bash
45
+ npx mudhost delete <deployment-id>
46
+ ```
47
+ Whoami
48
+
49
+ ```bash
50
+ npx mudhost whoami
51
+ ```
52
+
53
+ Logout
54
+ ```bash
55
+ npx mudhost logout
56
+ ```
57
+
58
+
59
+ ### Examples
60
+
61
+ ```bash
62
+ # Deploy with custom branch
63
+ npx mudhost deploy my-app ./build --branch feature/new-ui
64
+
65
+ # Deploy to custom API
66
+ npx mudhost deploy my-app ./dist --api https://staging.example.com
67
+
68
+ # List deployments for a project
69
+ npx mudhost list --project my-app
70
+ ```
71
+
72
+
73
+ ## Configuration
74
+
75
+ The CLI automatically saves your authentication token and API URL in ~/.mudhost/config.json.
76
+
77
+ You can also create a mudhost.json file in your project:
78
+
79
+ ```json
80
+ {
81
+ "apiUrl": "https://your-domain.com",
82
+ "defaultProject": "my-app",
83
+ "defaultBranch": "main"
84
+ }
85
+ ```
package/bin/hosting.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { CLI } = require('../src/cli');
4
+
5
+ const cli = new CLI();
6
+ cli.run().catch(error => {
7
+ console.error('❌ Error:', error.message);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "mudhost",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Hosting - deploy preview environments with one command",
5
+ "main": "src/cli.js",
6
+ "bin": {
7
+ "mudhost": "./bin/hosting.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/cli.js",
11
+ "test": "echo \"No tests yet\" && exit 0"
12
+ },
13
+ "keywords": [
14
+ "hosting",
15
+ "preview",
16
+ "deployment",
17
+ "cli",
18
+ "mudhost"
19
+ ],
20
+ "author": "Irustm",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/irustm/mudhost"
25
+ },
26
+ "dependencies": {
27
+ "axios": "^1.6.0",
28
+ "chalk": "^4.1.2",
29
+ "commander": "^11.1.0",
30
+ "inquirer": "^8.2.6",
31
+ "fs-extra": "^11.1.1",
32
+ "glob": "^10.3.0",
33
+ "mime-types": "^2.1.35",
34
+ "ora": "^5.4.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ }
42
+ }
package/src/auth.js ADDED
@@ -0,0 +1,98 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const inquirer = require('inquirer');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const { ConfigManager } = require('./utils');
7
+
8
+ class AuthManager {
9
+ constructor() {
10
+ this.config = new ConfigManager();
11
+ }
12
+
13
+ async login(apiUrl, options) {
14
+ let { username, password } = options;
15
+
16
+ // Если credentials не предоставлены, запросим их
17
+ if (!username || !password) {
18
+ const answers = await inquirer.prompt([
19
+ {
20
+ type: 'input',
21
+ name: 'username',
22
+ message: 'Username:',
23
+ when: !username
24
+ },
25
+ {
26
+ type: 'password',
27
+ name: 'password',
28
+ message: 'Password:',
29
+ when: !password
30
+ }
31
+ ]);
32
+
33
+ username = username || answers.username;
34
+ password = password || answers.password;
35
+ }
36
+
37
+ if (!username || !password) {
38
+ throw new Error('Username and password are required');
39
+ }
40
+
41
+ try {
42
+ const response = await axios.post(`${apiUrl}/api/auth/login`, {
43
+ username,
44
+ password
45
+ });
46
+
47
+ const { token, user } = response.data;
48
+
49
+ // Сохраняем токен и настройки
50
+ await this.config.set('token', token);
51
+ await this.config.set('apiUrl', apiUrl);
52
+ await this.config.set('user', user);
53
+
54
+ console.log(chalk.green(`✅ Logged in as ${user.username} (${user.role})`));
55
+ return token;
56
+ } catch (error) {
57
+ if (error.response?.status === 401) {
58
+ throw new Error('Invalid credentials');
59
+ }
60
+ throw new Error(`Login failed: ${error.message}`);
61
+ }
62
+ }
63
+
64
+ async getAuthHeaders() {
65
+ const token = await this.config.get('token');
66
+ if (!token) {
67
+ throw new Error('Not authenticated. Please run "mudhost login" first.');
68
+ }
69
+
70
+ return {
71
+ 'Authorization': `Bearer ${token}`,
72
+ 'Content-Type': 'application/json'
73
+ };
74
+ }
75
+
76
+ async whoami(apiUrl) {
77
+ try {
78
+ const headers = await this.getAuthHeaders();
79
+ const response = await axios.get(`${apiUrl}/api/auth/me`, { headers });
80
+
81
+ const user = response.data.user;
82
+ const config = await this.config.getAll();
83
+
84
+ console.log(chalk.blue('👤 Current User:'));
85
+ console.log(` Username: ${chalk.bold(user.username)}`);
86
+ console.log(` Role: ${chalk.bold(user.role)}`);
87
+ console.log(` API: ${chalk.bold(config.apiUrl)}`);
88
+ } catch (error) {
89
+ throw new Error(`Failed to get user info: ${error.message}`);
90
+ }
91
+ }
92
+
93
+ logout() {
94
+ this.config.clear();
95
+ }
96
+ }
97
+
98
+ module.exports = { AuthManager };
package/src/cli.js ADDED
@@ -0,0 +1,183 @@
1
+ const { Command } = require('commander');
2
+ const chalk = require('chalk');
3
+ const { AuthManager } = require('./auth');
4
+ const { DeployManager } = require('./deploy');
5
+ const { ConfigManager } = require('./utils');
6
+
7
+ class CLI {
8
+ constructor() {
9
+ this.program = new Command();
10
+ this.auth = new AuthManager();
11
+ this.deploy = new DeployManager();
12
+ this.config = new ConfigManager();
13
+ this.setupCommands();
14
+ }
15
+
16
+ setupCommands() {
17
+ this.program
18
+ .name('mudhost')
19
+ .description('CLI for Mud Hosting - Deploy preview environments')
20
+ .version('1.0.0');
21
+
22
+ // Login command
23
+ this.program
24
+ .command('login')
25
+ .description('Login to Mud Hosting')
26
+ .option('-u, --username <username>', 'Username')
27
+ .option('-p, --password <password>', 'Password')
28
+ .option('--api <url>', 'API URL', 'http://localhost:8000')
29
+ .action(async (options) => {
30
+ await this.handleLogin(options);
31
+ });
32
+
33
+ // Deploy command
34
+ this.program
35
+ .command('deploy')
36
+ .description('Deploy a project')
37
+ .argument('<project-id>', 'Project ID')
38
+ .argument('[dist-dir]', 'Distribution directory', './dist')
39
+ .option('-b, --branch <branch>', 'Git branch', 'main')
40
+ .option('-c, --commit <commit>', 'Git commit', 'latest')
41
+ .option('--api <url>', 'API URL')
42
+ .option('--config <file>', 'Config file')
43
+ .action(async (projectId, distDir, options) => {
44
+ await this.handleDeploy(projectId, distDir, options);
45
+ });
46
+
47
+ // List command
48
+ this.program
49
+ .command('list')
50
+ .description('List deployments')
51
+ .option('--api <url>', 'API URL')
52
+ .option('-p, --project <projectId>', 'Filter by project')
53
+ .action(async (options) => {
54
+ await this.handleList(options);
55
+ });
56
+
57
+ // Delete command
58
+ this.program
59
+ .command('delete')
60
+ .description('Delete a deployment')
61
+ .argument('<deployment-id>', 'Deployment ID')
62
+ .option('--api <url>', 'API URL')
63
+ .action(async (deploymentId, options) => {
64
+ await this.handleDelete(deploymentId, options);
65
+ });
66
+
67
+ // Whoami command
68
+ this.program
69
+ .command('whoami')
70
+ .description('Show current user info')
71
+ .option('--api <url>', 'API URL')
72
+ .action(async (options) => {
73
+ await this.handleWhoami(options);
74
+ });
75
+
76
+ // Logout command
77
+ this.program
78
+ .command('logout')
79
+ .description('Logout and clear saved token')
80
+ .action(() => {
81
+ this.handleLogout();
82
+ });
83
+
84
+ // Init command
85
+ this.program
86
+ .command('init')
87
+ .description('Create config file')
88
+ .action(() => {
89
+ this.handleInit();
90
+ });
91
+ }
92
+
93
+ async handleLogin(options) {
94
+ const apiUrl = options.api || await this.config.get('apiUrl') || 'http://localhost:8000';
95
+
96
+ try {
97
+ await this.auth.login(apiUrl, options);
98
+ console.log(chalk.green('✅ Login successful!'));
99
+ } catch (error) {
100
+ console.error(chalk.red('❌ Login failed:'), error.message);
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ async handleDeploy(projectId, distDir, options) {
106
+ const apiUrl = options.api || await this.config.get('apiUrl');
107
+
108
+ if (!apiUrl) {
109
+ console.error(chalk.red('❌ API URL not configured. Please login first or use --api option.'));
110
+ process.exit(1);
111
+ }
112
+
113
+ try {
114
+ await this.deploy.deploy(projectId, distDir, options, apiUrl);
115
+ } catch (error) {
116
+ console.error(chalk.red('❌ Deployment failed:'), error.message);
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ async handleList(options) {
122
+ const apiUrl = options.api || await this.config.get('apiUrl');
123
+
124
+ if (!apiUrl) {
125
+ console.error(chalk.red('❌ API URL not configured. Please login first or use --api option.'));
126
+ process.exit(1);
127
+ }
128
+
129
+ try {
130
+ await this.deploy.listDeployments(apiUrl, options.project);
131
+ } catch (error) {
132
+ console.error(chalk.red('❌ Failed to list deployments:'), error.message);
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ async handleDelete(deploymentId, options) {
138
+ const apiUrl = options.api || await this.config.get('apiUrl');
139
+
140
+ if (!apiUrl) {
141
+ console.error(chalk.red('❌ API URL not configured. Please login first or use --api option.'));
142
+ process.exit(1);
143
+ }
144
+
145
+ try {
146
+ await this.deploy.deleteDeployment(deploymentId, apiUrl);
147
+ } catch (error) {
148
+ console.error(chalk.red('❌ Failed to delete deployment:'), error.message);
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ async handleWhoami(options) {
154
+ const apiUrl = options.api || await this.config.get('apiUrl');
155
+
156
+ if (!apiUrl) {
157
+ console.error(chalk.red('❌ API URL not configured. Please login first or use --api option.'));
158
+ process.exit(1);
159
+ }
160
+
161
+ try {
162
+ await this.auth.whoami(apiUrl);
163
+ } catch (error) {
164
+ console.error(chalk.red('❌ Failed to get user info:'), error.message);
165
+ process.exit(1);
166
+ }
167
+ }
168
+
169
+ handleLogout() {
170
+ this.auth.logout();
171
+ console.log(chalk.green('✅ Logged out successfully!'));
172
+ }
173
+
174
+ handleInit() {
175
+ this.config.createConfig();
176
+ }
177
+
178
+ async run() {
179
+ await this.program.parseAsync(process.argv);
180
+ }
181
+ }
182
+
183
+ module.exports = { CLI };
package/src/deploy.js ADDED
@@ -0,0 +1,177 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const glob = require('glob');
6
+ const mime = require('mime-types');
7
+ const ora = require('ora');
8
+ const { AuthManager } = require('./auth');
9
+
10
+ class DeployManager {
11
+ constructor() {
12
+ this.auth = new AuthManager();
13
+ }
14
+
15
+ async collectFiles(distDir) {
16
+ const spinner = ora('📁 Scanning files...').start();
17
+
18
+ if (!await fs.pathExists(distDir)) {
19
+ spinner.fail();
20
+ throw new Error(`Directory not found: ${distDir}`);
21
+ }
22
+
23
+ try {
24
+ const files = {};
25
+ const allFiles = glob.sync('**/*', {
26
+ cwd: distDir,
27
+ nodir: true,
28
+ dot: true
29
+ });
30
+
31
+ for (const filePath of allFiles) {
32
+ const fullPath = path.join(distDir, filePath);
33
+ const content = await fs.readFile(fullPath, 'utf8');
34
+ files[filePath] = content;
35
+ }
36
+
37
+ spinner.succeed(`Found ${allFiles.length} files`);
38
+ return files;
39
+ } catch (error) {
40
+ spinner.fail();
41
+ throw new Error(`Failed to read files: ${error.message}`);
42
+ }
43
+ }
44
+
45
+ async deploy(projectId, distDir, options, apiUrl) {
46
+ const spinner = ora('🚀 Deploying...').start();
47
+
48
+ try {
49
+ // Собираем файлы
50
+ const files = await this.collectFiles(distDir);
51
+
52
+ if (Object.keys(files).length === 0) {
53
+ spinner.fail();
54
+ throw new Error(`No files found in ${distDir}`);
55
+ }
56
+
57
+ // Получаем заголовки аутентификации
58
+ const headers = await this.auth.getAuthHeaders();
59
+
60
+ // Отправляем деплой
61
+ spinner.text = '📦 Uploading deployment...';
62
+
63
+ const response = await axios.post(`${apiUrl}/api/deployments`, {
64
+ projectId,
65
+ branch: options.branch,
66
+ commit: options.commit,
67
+ files
68
+ }, { headers });
69
+
70
+ const deployment = response.data;
71
+
72
+ spinner.succeed('✅ Deployment successful!');
73
+
74
+ console.log(chalk.green('\n📊 Deployment Info:'));
75
+ console.log(` Project: ${chalk.bold(deployment.projectId)}`);
76
+ console.log(` Branch: ${chalk.bold(deployment.branch)}`);
77
+ console.log(` ID: ${chalk.bold(deployment.id)}`);
78
+ console.log(` Files: ${chalk.bold(deployment.files.length)}`);
79
+
80
+ console.log(chalk.blue('\n🌐 Preview URLs:'));
81
+ deployment.urls?.forEach(url => {
82
+ console.log(` ${chalk.cyan('→')} ${chalk.underline(url)}`);
83
+ });
84
+
85
+ if (deployment.url) {
86
+ console.log(` ${chalk.cyan('→')} ${chalk.underline(deployment.url)}`);
87
+ }
88
+
89
+ return deployment;
90
+ } catch (error) {
91
+ spinner.fail();
92
+
93
+ if (error.response) {
94
+ const status = error.response.status;
95
+ const message = error.response.data?.error || error.message;
96
+
97
+ switch (status) {
98
+ case 401:
99
+ throw new Error('Authentication failed. Please login again.');
100
+ case 413:
101
+ throw new Error('Deployment too large. Reduce file size.');
102
+ default:
103
+ throw new Error(`HTTP ${status}: ${message}`);
104
+ }
105
+ }
106
+
107
+ throw new Error(`Deployment failed: ${error.message}`);
108
+ }
109
+ }
110
+
111
+ async listDeployments(apiUrl, projectFilter) {
112
+ const spinner = ora('📋 Loading deployments...').start();
113
+
114
+ try {
115
+ const headers = await this.auth.getAuthHeaders();
116
+ const response = await axios.get(`${apiUrl}/api/deployments`, { headers });
117
+
118
+ let deployments = response.data;
119
+
120
+ // Фильтруем по проекту если нужно
121
+ if (projectFilter) {
122
+ deployments = deployments.filter(d => d.projectId === projectFilter);
123
+ }
124
+
125
+ spinner.succeed(`Found ${deployments.length} deployments`);
126
+
127
+ if (deployments.length === 0) {
128
+ console.log(chalk.yellow('No deployments found'));
129
+ return;
130
+ }
131
+
132
+ console.log('\n' + chalk.blue.bold('📦 Deployments:'));
133
+ console.log('=' .repeat(80));
134
+
135
+ deployments.forEach(deployment => {
136
+ const status = deployment.isExpired ? chalk.red('❌ EXPIRED') : chalk.green('✅ ACTIVE');
137
+ const daysLeft = deployment.isExpired ?
138
+ 'EXPIRED' :
139
+ `${Math.ceil(deployment.expiresIn / (1000 * 60 * 60 * 24))} days`;
140
+
141
+ console.log(`🆔 ${chalk.bold(deployment.id)}`);
142
+ console.log(` 📁 ${chalk.gray('Project:')} ${deployment.projectId}`);
143
+ console.log(` 🌿 ${chalk.gray('Branch:')} ${deployment.branch}`);
144
+ console.log(` 🔗 ${chalk.gray('URL:')} ${chalk.cyan(deployment.url)}`);
145
+ console.log(` 📅 ${chalk.gray('Created:')} ${new Date(deployment.createdAt).toLocaleString()}`);
146
+ console.log(` ⏰ ${chalk.gray('Status:')} ${status} (${daysLeft} left)`);
147
+ console.log(` 📊 ${chalk.gray('Files:')} ${deployment.files.length}`);
148
+ console.log('-'.repeat(80));
149
+ });
150
+
151
+ } catch (error) {
152
+ spinner.fail();
153
+ throw new Error(`Failed to list deployments: ${error.message}`);
154
+ }
155
+ }
156
+
157
+ async deleteDeployment(deploymentId, apiUrl) {
158
+ const spinner = ora('🗑️ Deleting deployment...').start();
159
+
160
+ try {
161
+ const headers = await this.auth.getAuthHeaders();
162
+ await axios.delete(`${apiUrl}/api/deployments/${deploymentId}`, { headers });
163
+
164
+ spinner.succeed(`✅ Deployment ${deploymentId} deleted successfully`);
165
+ } catch (error) {
166
+ spinner.fail();
167
+
168
+ if (error.response?.status === 404) {
169
+ throw new Error(`Deployment not found: ${deploymentId}`);
170
+ }
171
+
172
+ throw new Error(`Failed to delete deployment: ${error.message}`);
173
+ }
174
+ }
175
+ }
176
+
177
+ module.exports = { DeployManager };
package/src/utils.js ADDED
@@ -0,0 +1,70 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+
6
+ class ConfigManager {
7
+ constructor() {
8
+ this.configDir = path.join(os.homedir(), '.mudhost');
9
+ this.configFile = path.join(this.configDir, 'config.json');
10
+ this.ensureConfigDir();
11
+ }
12
+
13
+ ensureConfigDir() {
14
+ if (!fs.existsSync(this.configDir)) {
15
+ fs.mkdirSync(this.configDir, { recursive: true });
16
+ }
17
+ }
18
+
19
+ async get(key) {
20
+ try {
21
+ const config = await this.getAll();
22
+ return config[key];
23
+ } catch (error) {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ async getAll() {
29
+ try {
30
+ if (await fs.pathExists(this.configFile)) {
31
+ return await fs.readJson(this.configFile);
32
+ }
33
+ return {};
34
+ } catch (error) {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ async set(key, value) {
40
+ const config = await this.getAll();
41
+ config[key] = value;
42
+ await fs.writeJson(this.configFile, config, { spaces: 2 });
43
+ }
44
+
45
+ async clear() {
46
+ if (await fs.pathExists(this.configFile)) {
47
+ await fs.remove(this.configFile);
48
+ }
49
+ }
50
+
51
+ createConfig() {
52
+ const configTemplate = {
53
+ apiUrl: 'http://localhost:8000',
54
+ defaultProject: 'my-app',
55
+ defaultBranch: 'main'
56
+ };
57
+
58
+ const configPath = path.join(process.cwd(), 'mudhost.json');
59
+
60
+ if (fs.existsSync(configPath)) {
61
+ console.log(chalk.yellow('⚠️ Config file already exists'));
62
+ return;
63
+ }
64
+
65
+ fs.writeJsonSync(configPath, configTemplate, { spaces: 2 });
66
+ console.log(chalk.green('✅ Created mudhost.json config file'));
67
+ }
68
+ }
69
+
70
+ module.exports = { ConfigManager };