litai-spex 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.
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_CONFIG = {
5
+ connection: {
6
+ host: '',
7
+ username: '',
8
+ password: '',
9
+ privateKeyPath: '',
10
+ targetDirectory: '/var/www/app'
11
+ },
12
+ deploy: {
13
+ excludeDirectories: ['node_modules', 'logs', '.git', '.idea', '.vscode'],
14
+ excludeFiles: ['package-lock.json', '.env.local', '.DS_Store'],
15
+ excludePatterns: ['*.log', '*.tmp']
16
+ },
17
+ scripts: {
18
+ afterDeploy: 'deploy.sh'
19
+ },
20
+ project: {
21
+ repositoryUrl: '',
22
+ targetDirectory: '.'
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Load configuration from file
28
+ * @param {string} configPath - Path to config file
29
+ * @returns {object} Configuration object
30
+ */
31
+ function loadConfig(configPath) {
32
+ const fullPath = path.resolve(process.cwd(), configPath);
33
+
34
+ if (!fs.existsSync(fullPath)) {
35
+ return { ...DEFAULT_CONFIG };
36
+ }
37
+
38
+ try {
39
+ const configContent = fs.readFileSync(fullPath, 'utf8');
40
+ const userConfig = JSON.parse(configContent);
41
+
42
+ // Deep merge with defaults
43
+ return {
44
+ connection: { ...DEFAULT_CONFIG.connection, ...userConfig.connection },
45
+ deploy: {
46
+ excludeDirectories: userConfig.deploy?.excludeDirectories || DEFAULT_CONFIG.deploy.excludeDirectories,
47
+ excludeFiles: userConfig.deploy?.excludeFiles || DEFAULT_CONFIG.deploy.excludeFiles,
48
+ excludePatterns: userConfig.deploy?.excludePatterns || DEFAULT_CONFIG.deploy.excludePatterns
49
+ },
50
+ scripts: { ...DEFAULT_CONFIG.scripts, ...userConfig.scripts },
51
+ project: { ...DEFAULT_CONFIG.project, ...userConfig.project }
52
+ };
53
+ } catch (error) {
54
+ throw new Error(`Failed to parse config file: ${error.message}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Merge CLI options with config file settings (CLI takes precedence)
60
+ * @param {object} config - Config from file
61
+ * @param {object} options - CLI options
62
+ * @returns {object} Merged configuration
63
+ */
64
+ function mergeWithCliOptions(config, options) {
65
+ return {
66
+ ...config,
67
+ connection: {
68
+ ...config.connection,
69
+ host: options.ip || config.connection.host,
70
+ username: options.user || config.connection.username,
71
+ password: options.password || config.connection.password,
72
+ privateKeyPath: options.key || config.connection.privateKeyPath,
73
+ targetDirectory: options.directory || config.connection.targetDirectory
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Validate that required connection settings are present
80
+ * @param {object} config - Configuration object
81
+ * @returns {object} Object with isValid boolean and missing fields array
82
+ */
83
+ function validateConfig(config) {
84
+ const missing = [];
85
+
86
+ if (!config.connection.host) missing.push('host (use -ip or set in config.json)');
87
+ if (!config.connection.username) missing.push('username (use -u or set in config.json)');
88
+ if (!config.connection.password && !config.connection.privateKeyPath) {
89
+ missing.push('password or privateKeyPath (use -p/-k or set in config.json)');
90
+ }
91
+ if (!config.connection.targetDirectory) missing.push('targetDirectory (use -dir or set in config.json)');
92
+
93
+ return {
94
+ isValid: missing.length === 0,
95
+ missing
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Create default config file
101
+ * @param {string} configPath - Path to create config file
102
+ */
103
+ function createDefaultConfig(configPath) {
104
+ const fullPath = path.resolve(process.cwd(), configPath);
105
+ fs.writeFileSync(fullPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
106
+ return fullPath;
107
+ }
108
+
109
+ module.exports = {
110
+ DEFAULT_CONFIG,
111
+ loadConfig,
112
+ mergeWithCliOptions,
113
+ validateConfig,
114
+ createDefaultConfig
115
+ };
@@ -0,0 +1,101 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Check if a filename matches any pattern (supports * wildcard)
6
+ * @param {string} filename - File name to check
7
+ * @param {string[]} patterns - Array of patterns
8
+ * @returns {boolean}
9
+ */
10
+ function matchesPattern(filename, patterns) {
11
+ return patterns.some(pattern => {
12
+ if (pattern.includes('*')) {
13
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
14
+ return regex.test(filename);
15
+ }
16
+ return filename === pattern;
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Recursively scan directory and collect files to deploy
22
+ * @param {string} dir - Directory to scan
23
+ * @param {object} excludeConfig - Exclusion configuration
24
+ * @param {string} baseDir - Base directory for relative paths
25
+ * @returns {Array<{local: string, remote: string}>} Array of file mappings
26
+ */
27
+ function scanDirectory(dir, excludeConfig, baseDir = dir) {
28
+ const { excludeDirectories, excludeFiles, excludePatterns } = excludeConfig;
29
+ const files = [];
30
+
31
+ const items = fs.readdirSync(dir);
32
+
33
+ for (const item of items) {
34
+ const fullPath = path.join(dir, item);
35
+ const relativePath = path.relative(baseDir, fullPath);
36
+ const stat = fs.statSync(fullPath);
37
+
38
+ if (stat.isDirectory()) {
39
+ // Check if directory should be excluded
40
+ if (excludeDirectories.includes(item)) {
41
+ continue;
42
+ }
43
+
44
+ // Recursively scan subdirectory
45
+ const subFiles = scanDirectory(fullPath, excludeConfig, baseDir);
46
+ files.push(...subFiles);
47
+ } else {
48
+ // Check if file should be excluded
49
+ if (excludeFiles.includes(item)) {
50
+ continue;
51
+ }
52
+
53
+ // Check if file matches any exclude pattern
54
+ if (matchesPattern(item, excludePatterns)) {
55
+ continue;
56
+ }
57
+
58
+ files.push({
59
+ local: fullPath,
60
+ remote: relativePath.replace(/\\/g, '/') // Normalize to forward slashes for remote
61
+ });
62
+ }
63
+ }
64
+
65
+ return files;
66
+ }
67
+
68
+ /**
69
+ * Get all directories that need to be created on remote
70
+ * @param {Array<{local: string, remote: string}>} files - File mappings
71
+ * @returns {string[]} Array of directory paths
72
+ */
73
+ function getRemoteDirectories(files) {
74
+ const dirs = new Set();
75
+
76
+ for (const file of files) {
77
+ const dir = path.dirname(file.remote);
78
+ if (dir && dir !== '.') {
79
+ // Add all parent directories as well
80
+ const parts = dir.split('/');
81
+ let current = '';
82
+ for (const part of parts) {
83
+ current = current ? `${current}/${part}` : part;
84
+ dirs.add(current);
85
+ }
86
+ }
87
+ }
88
+
89
+ // Sort by depth (shorter paths first) to ensure parent dirs are created first
90
+ return Array.from(dirs).sort((a, b) => {
91
+ const depthA = a.split('/').length;
92
+ const depthB = b.split('/').length;
93
+ return depthA - depthB;
94
+ });
95
+ }
96
+
97
+ module.exports = {
98
+ scanDirectory,
99
+ getRemoteDirectories,
100
+ matchesPattern
101
+ };
@@ -0,0 +1,138 @@
1
+ const { NodeSSH } = require('node-ssh');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ class SSHDeployer {
6
+ constructor() {
7
+ this.ssh = new NodeSSH();
8
+ this.connected = false;
9
+ }
10
+
11
+ /**
12
+ * Connect to remote server
13
+ * @param {object} connectionConfig - Connection configuration
14
+ */
15
+ async connect(connectionConfig) {
16
+ const config = {
17
+ host: connectionConfig.host,
18
+ username: connectionConfig.username
19
+ };
20
+
21
+ if (connectionConfig.privateKeyPath) {
22
+ const keyPath = path.resolve(process.cwd(), connectionConfig.privateKeyPath);
23
+ if (!fs.existsSync(keyPath)) {
24
+ throw new Error(`Private key file not found: ${keyPath}`);
25
+ }
26
+ config.privateKey = fs.readFileSync(keyPath, 'utf8');
27
+ } else if (connectionConfig.password) {
28
+ config.password = connectionConfig.password;
29
+ } else {
30
+ throw new Error('No authentication method provided (password or private key)');
31
+ }
32
+
33
+ await this.ssh.connect(config);
34
+ this.connected = true;
35
+ }
36
+
37
+ /**
38
+ * Disconnect from remote server
39
+ */
40
+ disconnect() {
41
+ if (this.connected) {
42
+ this.ssh.dispose();
43
+ this.connected = false;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Create directories on remote server
49
+ * @param {string} baseDir - Base directory on remote
50
+ * @param {string[]} directories - Array of directory paths relative to baseDir
51
+ * @param {function} onProgress - Progress callback
52
+ */
53
+ async createDirectories(baseDir, directories, onProgress) {
54
+ // First ensure base directory exists
55
+ await this.ssh.execCommand(`mkdir -p "${baseDir}"`);
56
+
57
+ for (const dir of directories) {
58
+ const fullPath = `${baseDir}/${dir}`;
59
+ await this.ssh.execCommand(`mkdir -p "${fullPath}"`);
60
+ if (onProgress) onProgress(dir);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Upload files to remote server
66
+ * @param {string} baseDir - Base directory on remote
67
+ * @param {Array<{local: string, remote: string}>} files - File mappings
68
+ * @param {function} onProgress - Progress callback (receives current file index and total)
69
+ */
70
+ async uploadFiles(baseDir, files, onProgress) {
71
+ const total = files.length;
72
+
73
+ for (let i = 0; i < files.length; i++) {
74
+ const file = files[i];
75
+ const remotePath = `${baseDir}/${file.remote}`;
76
+
77
+ await this.ssh.putFile(file.local, remotePath);
78
+
79
+ if (onProgress) {
80
+ onProgress(i + 1, total, file.remote);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Execute a script on remote server
87
+ * @param {string} baseDir - Base directory on remote
88
+ * @param {string} scriptName - Script filename
89
+ * @returns {object} Execution result with stdout and stderr
90
+ */
91
+ async executeScript(baseDir, scriptName) {
92
+ const scriptPath = `${baseDir}/${scriptName}`;
93
+
94
+ // First check if script exists
95
+ const checkResult = await this.ssh.execCommand(`test -f "${scriptPath}" && echo "exists"`);
96
+ if (!checkResult.stdout.includes('exists')) {
97
+ throw new Error(`Script not found: ${scriptPath}`);
98
+ }
99
+
100
+ // Make script executable
101
+ await this.ssh.execCommand(`chmod +x "${scriptPath}"`);
102
+
103
+ // Execute script
104
+ const result = await this.ssh.execCommand(`cd "${baseDir}" && bash "${scriptPath}"`, {
105
+ cwd: baseDir
106
+ });
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Check if remote directory exists
113
+ * @param {string} dir - Directory path
114
+ * @returns {boolean}
115
+ */
116
+ async directoryExists(dir) {
117
+ const result = await this.ssh.execCommand(`test -d "${dir}" && echo "exists"`);
118
+ return result.stdout.includes('exists');
119
+ }
120
+
121
+ /**
122
+ * Get remote server info
123
+ * @returns {object} Server info
124
+ */
125
+ async getServerInfo() {
126
+ const hostname = await this.ssh.execCommand('hostname');
127
+ const os = await this.ssh.execCommand('cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\'');
128
+ const uptime = await this.ssh.execCommand('uptime -p 2>/dev/null || uptime');
129
+
130
+ return {
131
+ hostname: hostname.stdout.trim(),
132
+ os: os.stdout.trim() || 'Unknown',
133
+ uptime: uptime.stdout.trim()
134
+ };
135
+ }
136
+ }
137
+
138
+ module.exports = { SSHDeployer };