auto-deploy-sh 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/LICENSE +15 -0
- package/README.md +58 -0
- package/dist/bin/config.d.ts +1 -0
- package/dist/bin/config.js +69 -0
- package/dist/bin/deploy.d.ts +31 -0
- package/dist/bin/deploy.js +232 -0
- package/dist/bin/index.d.ts +1 -0
- package/dist/bin/index.js +50 -0
- package/dist/bin/type/inquirer.d.ts +47 -0
- package/dist/bin/type/inquirer.js +7 -0
- package/dist/bin/type/types.d.ts +32 -0
- package/dist/bin/type/types.js +1 -0
- package/dist/bin/utils/chalk.d.ts +9 -0
- package/dist/bin/utils/chalk.js +10 -0
- package/dist/bin/utils/fse.d.ts +20 -0
- package/dist/bin/utils/fse.js +117 -0
- package/dist/bin/utils/index.d.ts +6 -0
- package/dist/bin/utils/index.js +6 -0
- package/dist/bin/utils/inquirer.d.ts +11 -0
- package/dist/bin/utils/inquirer.js +61 -0
- package/dist/bin/utils/logger.d.ts +10 -0
- package/dist/bin/utils/logger.js +46 -0
- package/dist/bin/utils/ora.d.ts +12 -0
- package/dist/bin/utils/ora.js +73 -0
- package/dist/bin/utils/process.d.ts +42 -0
- package/dist/bin/utils/process.js +31 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 yotomum_ml
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Auto-deploy-sh
|
|
2
|
+
|
|
3
|
+
English|[简体中文](README_zh_CN.md)
|
|
4
|
+
|
|
5
|
+
Cloud service deployment is a crucial aspect of project engineering development and an important way to reach more users with applications. Currently, there are various methods for running projects on the cloud, among which **containerized deployment** has become the mainstream method for delivering cloud-native applications. The technology stack represented by **Docker + Kubernetes** can effectively address core issues such as **environment consistency, resource utilization, and portability** in the software development and deployment process ([Docker Official Documentation](https://docs.docker.com/)).
|
|
6
|
+
|
|
7
|
+
In enterprise-level development, project deployment often relies on tools such as GitLab to establish a comprehensive **CI/CD** (Continuous Integration/Continuous Delivery) workflow. However, for individual developers or small teams, setting up and maintaining a complete CI/CD system is not only complex but also costly, making it difficult to implement quickly.
|
|
8
|
+
|
|
9
|
+
So, how can individuals or small teams efficiently deploy projects? A common practice nowadays is to directly integrate Docker for deployment through an IDE (such as IntelliJ IDEA). However, this approach typically relies on **paid IDE versions**, requires cumbersome local configuration, and demands the installation of Docker CLI in the development environment, imposing certain technical thresholds and hardware and software requirements on users.
|
|
10
|
+
|
|
11
|
+
This project aims to **reduce the deployment threshold**, achieve an automated Docker deployment process with **minimal configuration and minimal dependencies**, and help developers quickly deploy applications to cloud servers without requiring complex environments.
|
|
12
|
+
|
|
13
|
+
## Explanation of configuration file attributes
|
|
14
|
+
|
|
15
|
+
The configuration file name is fixed as `deploy-config.json` and stored in the root directory. Since it contains private information, if added manually, please add it to `.gitignore`.
|
|
16
|
+
If the file is absent, the `CLI` will guide the addition process and update the content of `.gitignore` under the added location, requiring no additional operations.
|
|
17
|
+
|
|
18
|
+
```javaScript
|
|
19
|
+
{
|
|
20
|
+
input: 'Remote server IP or domain name',
|
|
21
|
+
port: 'SSH port',
|
|
22
|
+
user: 'Username',
|
|
23
|
+
password: 'password',
|
|
24
|
+
beforLaunch:[ // Preparation operations before project release
|
|
25
|
+
'npm run build'
|
|
26
|
+
],
|
|
27
|
+
dockerBuildFiles: [
|
|
28
|
+
// The files involved in the construction, relative to the path where the script runs (usually the root directory of the project)
|
|
29
|
+
'Dockerfile',
|
|
30
|
+
'dist'
|
|
31
|
+
],
|
|
32
|
+
imageTag: '镜像标识', // Format: [Warehouse Address/] [Username/Project Name]: [Label]
|
|
33
|
+
containerName: '容器名', // Unique identifier of Docker running instance
|
|
34
|
+
BindPorts: '端口映射' // Format: `<Host Port>:<Container Port>`
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Package release
|
|
39
|
+
|
|
40
|
+
### Partial configuration of the project
|
|
41
|
+
|
|
42
|
+
Download: `npm install auto-deploy-sh -D `
|
|
43
|
+
Run:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
// package.json configuration
|
|
47
|
+
"scripts": {
|
|
48
|
+
"deploy": "auto-deploy-sh"
|
|
49
|
+
}
|
|
50
|
+
// Run
|
|
51
|
+
npm run deploy
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 全局配置
|
|
55
|
+
|
|
56
|
+
Download: `npm install auto-deploy-sh -g`
|
|
57
|
+
Run in the root directory of the project that needs to be deployed: `auto-deploy-sh`
|
|
58
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createConfig(rootPath: string): Promise<void>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fs, inquirer } from "./utils/index.js";
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const configMethod = {
|
|
4
|
+
host: {
|
|
5
|
+
type: 'input',
|
|
6
|
+
message: '远程服务器 IP 或域名: ',
|
|
7
|
+
},
|
|
8
|
+
port: {
|
|
9
|
+
type: 'number',
|
|
10
|
+
message: 'SSH 端口: ',
|
|
11
|
+
},
|
|
12
|
+
user: {
|
|
13
|
+
type: 'input',
|
|
14
|
+
message: '用户名: ',
|
|
15
|
+
},
|
|
16
|
+
password: {
|
|
17
|
+
type: 'input',
|
|
18
|
+
message: '密码: ',
|
|
19
|
+
},
|
|
20
|
+
beforLaunch: {
|
|
21
|
+
type: 'input',
|
|
22
|
+
message: '项目发布前的前置命令(befor launch) 多个以 , 隔开: ',
|
|
23
|
+
handleFn: (value) => value.split(','),
|
|
24
|
+
},
|
|
25
|
+
dockerBuildFiles: {
|
|
26
|
+
type: 'input',
|
|
27
|
+
message: 'docker构建所需的文件相对根目录的路径(多个 , 隔开): ',
|
|
28
|
+
handleFn: (value) => value.split(','),
|
|
29
|
+
},
|
|
30
|
+
imageTag: {
|
|
31
|
+
type: 'input',
|
|
32
|
+
message: 'Image tag: ',
|
|
33
|
+
},
|
|
34
|
+
containerName: {
|
|
35
|
+
type: 'input',
|
|
36
|
+
message: 'Container name: ',
|
|
37
|
+
},
|
|
38
|
+
BindPorts: {
|
|
39
|
+
type: 'input',
|
|
40
|
+
message: 'Bind Ports(such as 8080:8080): ',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
export async function createConfig(rootPath) {
|
|
44
|
+
const config = {};
|
|
45
|
+
const keys = Object.keys(configMethod);
|
|
46
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
47
|
+
const key = keys[i];
|
|
48
|
+
const fn = Object.hasOwn(configMethod[key], 'handleFn')
|
|
49
|
+
? configMethod[key].handleFn
|
|
50
|
+
: (value) => {
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return value.trim();
|
|
53
|
+
else
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
const value = (await inquirer.invoke(configMethod[key]));
|
|
57
|
+
config[key] = fn(value);
|
|
58
|
+
}
|
|
59
|
+
await fs.writeFileSync(path.resolve(rootPath, 'deploy-config.json'), JSON.stringify(config, null, 2));
|
|
60
|
+
const gitIgnorePath = path.resolve(rootPath, '.gitignore');
|
|
61
|
+
let contain = '';
|
|
62
|
+
if (await fs.exist(gitIgnorePath)) {
|
|
63
|
+
contain = (await fs.readDirOrFile(gitIgnorePath));
|
|
64
|
+
}
|
|
65
|
+
if (contain.includes('deploy-config.json'))
|
|
66
|
+
return;
|
|
67
|
+
contain += '\ndeploy-config.json\n';
|
|
68
|
+
fs.writeFileSync(gitIgnorePath, contain);
|
|
69
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { sshConfig } from './type/types';
|
|
2
|
+
import type { Options } from 'execa';
|
|
3
|
+
import type { NodeSSH } from 'node-ssh';
|
|
4
|
+
export declare class Deploy {
|
|
5
|
+
config: sshConfig;
|
|
6
|
+
context: string;
|
|
7
|
+
uploadFileName: string;
|
|
8
|
+
remotePath: string;
|
|
9
|
+
REMOTEAPPPATH: string;
|
|
10
|
+
constructor(config: sshConfig, context: string);
|
|
11
|
+
runCommon(_options: {
|
|
12
|
+
command: string;
|
|
13
|
+
context?: string;
|
|
14
|
+
options?: Options;
|
|
15
|
+
args?: string[];
|
|
16
|
+
}, std: {
|
|
17
|
+
startMsg: string;
|
|
18
|
+
succMsg: string;
|
|
19
|
+
errMsg: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
preBeforeRelease(): Promise<void>;
|
|
22
|
+
compressFiles(): Promise<void>;
|
|
23
|
+
uploadSSH(ssh: NodeSSH): Promise<void>;
|
|
24
|
+
execCommand(ssh: NodeSSH, std: {
|
|
25
|
+
startMsg: string;
|
|
26
|
+
succMsg: string;
|
|
27
|
+
errMsg?: string;
|
|
28
|
+
}, command: string): Promise<void>;
|
|
29
|
+
dockcerImageBuild(ssh: NodeSSH): Promise<void>;
|
|
30
|
+
clear(ssh: NodeSSH): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { log, ora, run, fs, exit, chalk } from "./utils/index.js";
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import archiver from 'archiver';
|
|
4
|
+
export class Deploy {
|
|
5
|
+
config;
|
|
6
|
+
context;
|
|
7
|
+
uploadFileName;
|
|
8
|
+
remotePath;
|
|
9
|
+
REMOTEAPPPATH = '/tmp/www/app';
|
|
10
|
+
constructor(config, context) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.context = context;
|
|
13
|
+
this.uploadFileName = `${this.config.containerName}.tar.gz`;
|
|
14
|
+
this.remotePath = `/tmp/${this.uploadFileName}`;
|
|
15
|
+
}
|
|
16
|
+
async runCommon(_options, std) {
|
|
17
|
+
ora.start(std.startMsg);
|
|
18
|
+
const { command, options, args } = _options;
|
|
19
|
+
try {
|
|
20
|
+
const { stdout, stderr, failed, signal, exitCode } = await run(command, this.context, options, args);
|
|
21
|
+
const isFailure = failed || signal || exitCode !== 0;
|
|
22
|
+
if (isFailure) {
|
|
23
|
+
throw new Error(`${stderr}\nexecution ${command} failed`);
|
|
24
|
+
}
|
|
25
|
+
ora.stop('succeed', std.succMsg);
|
|
26
|
+
if (stdout) {
|
|
27
|
+
log.info(stdout);
|
|
28
|
+
}
|
|
29
|
+
if (stderr) {
|
|
30
|
+
log.warn(stderr);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
ora.stop('fail', std.errMsg);
|
|
35
|
+
log.error(err.message ?? '');
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async preBeforeRelease() {
|
|
40
|
+
if (this.config.beforLaunch) {
|
|
41
|
+
if (Array.isArray(this.config.beforLaunch)) {
|
|
42
|
+
for (let i = 0; i < this.config.beforLaunch.length; ++i) {
|
|
43
|
+
const command = this.config.beforLaunch[i];
|
|
44
|
+
command &&
|
|
45
|
+
(await this.runCommon({ command, options: { reject: false } }, {
|
|
46
|
+
startMsg: `start execution ${command}....`,
|
|
47
|
+
succMsg: `success execution ${command}`,
|
|
48
|
+
errMsg: `error execution ${command}`,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
log.error(`beforLaunch should be an array, but the type you set is ${typeof this.config.beforLaunch}`);
|
|
54
|
+
throw new Error();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async compressFiles() {
|
|
59
|
+
const files = [];
|
|
60
|
+
if (this.config.dockerBuildFiles && this.config.dockerBuildFiles.length > 0) {
|
|
61
|
+
let dockerfileFlag = false, dockerignoreFlag = false;
|
|
62
|
+
for (let i = 0; i < this.config.dockerBuildFiles.length; ++i) {
|
|
63
|
+
const file = this.config.dockerBuildFiles[i];
|
|
64
|
+
if (/\bDockerfile\b/.test(file)) {
|
|
65
|
+
dockerfileFlag = true;
|
|
66
|
+
}
|
|
67
|
+
if (/\bDockerfile\b/.test(file)) {
|
|
68
|
+
dockerignoreFlag = true;
|
|
69
|
+
}
|
|
70
|
+
if (dockerfileFlag && dockerignoreFlag)
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
if (!dockerfileFlag) {
|
|
74
|
+
this.config.dockerBuildFiles.push('Dockerfile');
|
|
75
|
+
}
|
|
76
|
+
if (!dockerignoreFlag && fs._.existsSync(path.resolve(this.context, '.dockerignore'))) {
|
|
77
|
+
this.config.dockerBuildFiles.push('.dockerignore');
|
|
78
|
+
}
|
|
79
|
+
this.config.dockerBuildFiles.forEach(async (file) => {
|
|
80
|
+
const fullPath = path.resolve(this.context, file);
|
|
81
|
+
if (fs._.existsSync(fullPath)) {
|
|
82
|
+
files.push(fullPath);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
log.error(`The file name is ${file}, and the file does not exist`);
|
|
86
|
+
throw new Error();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
log.error(`The file to build the image cannot be empty. Please check whether dockerBuildFiles is set correctly`);
|
|
92
|
+
throw new Error();
|
|
93
|
+
}
|
|
94
|
+
const output = fs.createWriteStream(path.resolve(this.context, this.uploadFileName));
|
|
95
|
+
const archive = archiver('tar', { gzip: true, gzipOptions: { level: 9 } });
|
|
96
|
+
archive.pipe(output);
|
|
97
|
+
const waitForArchiveEnd = new Promise((resolve, rejects) => {
|
|
98
|
+
archive.on('error', function (err) {
|
|
99
|
+
log.error(`Error in compressed file`);
|
|
100
|
+
rejects(new Error(err.message));
|
|
101
|
+
});
|
|
102
|
+
archive.on('end', () => {
|
|
103
|
+
log.done(`✔ Success in compressed file`);
|
|
104
|
+
resolve('');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
const fileNames = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const fileName = path.basename(file);
|
|
110
|
+
fileNames.push(fileName);
|
|
111
|
+
if (await fs.isDirectory(file)) {
|
|
112
|
+
archive.directory(file, fileName);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
archive.file(file, { name: fileName });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await archive.finalize();
|
|
119
|
+
await waitForArchiveEnd;
|
|
120
|
+
}
|
|
121
|
+
async uploadSSH(ssh) {
|
|
122
|
+
await this.compressFiles();
|
|
123
|
+
const uploadPath = path.resolve(this.context, this.uploadFileName);
|
|
124
|
+
if (await fs.exist(uploadPath)) {
|
|
125
|
+
const state = await fs.getFileStat(uploadPath);
|
|
126
|
+
const localSize = state.size;
|
|
127
|
+
await ora.start('📤 开始上传...', async () => {
|
|
128
|
+
await ssh.putFile(uploadPath, this.remotePath);
|
|
129
|
+
}, status => {
|
|
130
|
+
if (status)
|
|
131
|
+
return `文件上传完成`;
|
|
132
|
+
else {
|
|
133
|
+
exit(1);
|
|
134
|
+
return chalk.error(`exit code: 1`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const exists = await ssh.execCommand(`test -f "${this.remotePath}" && echo "yes" || echo "no"`);
|
|
138
|
+
if (exists.stdout.trim() !== 'yes') {
|
|
139
|
+
log.error('文件上传检测失败');
|
|
140
|
+
throw new Error();
|
|
141
|
+
}
|
|
142
|
+
const sizeResult = await ssh.execCommand(`stat -c %s "${this.remotePath}"`);
|
|
143
|
+
const remoteSize = parseInt(sizeResult.stdout.trim(), 10);
|
|
144
|
+
if (isNaN(remoteSize)) {
|
|
145
|
+
log.error('无法获取远程文件大小');
|
|
146
|
+
throw new Error();
|
|
147
|
+
}
|
|
148
|
+
if (remoteSize !== localSize && remoteSize < localSize - 10) {
|
|
149
|
+
log.error(`文件大小不匹配: 本地=${localSize}, 远程=${remoteSize}`);
|
|
150
|
+
throw new Error();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
log.error(`The file to be uploaded is not found.`);
|
|
155
|
+
throw new Error();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async execCommand(ssh, std, command) {
|
|
159
|
+
if (!command || !std.startMsg || !std.succMsg || !ssh)
|
|
160
|
+
throw new Error();
|
|
161
|
+
ora.start(std.startMsg);
|
|
162
|
+
const { stderr, code } = await ssh.execCommand(command);
|
|
163
|
+
if (code !== 0 || (stderr && code !== 0)) {
|
|
164
|
+
ora.stop('fail', stderr);
|
|
165
|
+
throw new Error();
|
|
166
|
+
}
|
|
167
|
+
ora.stop('succeed', std.succMsg);
|
|
168
|
+
stderr && log.info(stderr);
|
|
169
|
+
}
|
|
170
|
+
async dockcerImageBuild(ssh) {
|
|
171
|
+
await this.uploadSSH(ssh);
|
|
172
|
+
try {
|
|
173
|
+
const REMOTEAPPPATH = this.REMOTEAPPPATH;
|
|
174
|
+
const CONTAINER_NAME = this.config.containerName;
|
|
175
|
+
const REMOTE_PATH = this.remotePath;
|
|
176
|
+
const IMAGE_TAG = this.config.imageTag;
|
|
177
|
+
const TARGET_DIR = `./${CONTAINER_NAME}`;
|
|
178
|
+
await this.execCommand(ssh, {
|
|
179
|
+
startMsg: 'Start decompress project....',
|
|
180
|
+
succMsg: 'Decompress project completed.',
|
|
181
|
+
}, `
|
|
182
|
+
if ! cd "${REMOTEAPPPATH}" &>/dev/null; then
|
|
183
|
+
mkdir -p "${REMOTEAPPPATH}";
|
|
184
|
+
cd "${REMOTEAPPPATH}";
|
|
185
|
+
fi
|
|
186
|
+
mkdir -p "${TARGET_DIR}"
|
|
187
|
+
|
|
188
|
+
if [ ! -f "${REMOTE_PATH}" ]; then
|
|
189
|
+
echo "❌ 错误:压缩包不存在: ${REMOTE_PATH}";
|
|
190
|
+
exit 1
|
|
191
|
+
fi
|
|
192
|
+
tar -xzf "${REMOTE_PATH}" -C "${TARGET_DIR}";
|
|
193
|
+
`);
|
|
194
|
+
await this.execCommand(ssh, {
|
|
195
|
+
startMsg: `🐳 构建镜像: ${IMAGE_TAG}...`,
|
|
196
|
+
succMsg: `🐳 构建镜像: ${IMAGE_TAG}成功.`,
|
|
197
|
+
}, `
|
|
198
|
+
cd "${REMOTEAPPPATH}"
|
|
199
|
+
docker build -t ${IMAGE_TAG} ${TARGET_DIR}
|
|
200
|
+
`);
|
|
201
|
+
await this.execCommand(ssh, {
|
|
202
|
+
startMsg: `🔄 停止并清理旧容器: ${CONTAINER_NAME}...`,
|
|
203
|
+
succMsg: `旧容器已经清理成功.`,
|
|
204
|
+
}, `
|
|
205
|
+
if docker inspect "${CONTAINER_NAME}" >/dev/null 2>&1; then
|
|
206
|
+
docker stop "${CONTAINER_NAME}";
|
|
207
|
+
docker rm "${CONTAINER_NAME}";
|
|
208
|
+
else
|
|
209
|
+
echo "ℹ️ 容器 ${CONTAINER_NAME} 不存在,跳过清理";
|
|
210
|
+
fi
|
|
211
|
+
`);
|
|
212
|
+
await this.execCommand(ssh, {
|
|
213
|
+
startMsg: `🐳 启动容器: ${CONTAINER_NAME}...`,
|
|
214
|
+
succMsg: `🐳 启动容器成功.`,
|
|
215
|
+
}, `
|
|
216
|
+
docker run -d --name ${CONTAINER_NAME} -p ${this.config.BindPorts} ${IMAGE_TAG}
|
|
217
|
+
`);
|
|
218
|
+
log.done('✔ 部署完成');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
log.error('✖ 部署失败');
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await this.clear(ssh);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async clear(ssh) {
|
|
228
|
+
await ssh.execCommand(`rm -f ${this.remotePath}`);
|
|
229
|
+
await ssh.execCommand(`rm -rf ${this.REMOTEAPPPATH}/${this.config.containerName}`);
|
|
230
|
+
await ssh.execCommand(`docker image prune -f`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { NodeSSH } from 'node-ssh';
|
|
4
|
+
import { log, ora, exit, chalk, fs } from "./utils/index.js";
|
|
5
|
+
import { Deploy } from "./deploy.js";
|
|
6
|
+
import { createConfig } from "./config.js";
|
|
7
|
+
const ssh = new NodeSSH();
|
|
8
|
+
const CONFIG_FILE = 'deploy-config.json';
|
|
9
|
+
const CONFIG_FILE_PATH = path.resolve(process.cwd(), `./${CONFIG_FILE}`);
|
|
10
|
+
async function deploySSH() {
|
|
11
|
+
if (await fs.exist(CONFIG_FILE_PATH)) {
|
|
12
|
+
let config = JSON.parse((await fs.readDirOrFile(CONFIG_FILE_PATH, { encoding: 'utf-8' })));
|
|
13
|
+
if (!config.BindPorts.includes(':')) {
|
|
14
|
+
log.error('Config BindPorts do not meet the standard');
|
|
15
|
+
exit(1);
|
|
16
|
+
}
|
|
17
|
+
await ora.start('conneting ssh...', async () => await ssh.connect({
|
|
18
|
+
host: config.host,
|
|
19
|
+
port: config.port,
|
|
20
|
+
username: config.user,
|
|
21
|
+
password: config.password,
|
|
22
|
+
}), status => {
|
|
23
|
+
if (status)
|
|
24
|
+
return `SSH连接成功!`;
|
|
25
|
+
else {
|
|
26
|
+
log.error('SSH连接失败!');
|
|
27
|
+
exit(1);
|
|
28
|
+
return chalk.error(`exit code: 1`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const deploy = new Deploy(config, process.cwd());
|
|
32
|
+
try {
|
|
33
|
+
await deploy.preBeforeRelease();
|
|
34
|
+
await deploy.dockcerImageBuild(ssh);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
exit(1);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.removeSync(path.resolve(process.cwd(), deploy.uploadFileName));
|
|
41
|
+
ssh.dispose();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
log.error(`⚠️ 在根目录下未找到配置文件 ${CONFIG_FILE},将引导创建... `);
|
|
46
|
+
await createConfig(process.cwd());
|
|
47
|
+
deploySSH();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
deploySSH();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export declare const inquirerTypes: {
|
|
2
|
+
readonly INPUT: "input";
|
|
3
|
+
readonly SELECT: "select";
|
|
4
|
+
readonly CHECKBOX: "checkbox";
|
|
5
|
+
readonly CONFIRM: "confirm";
|
|
6
|
+
readonly NUMBER: "number";
|
|
7
|
+
};
|
|
8
|
+
export type inquirerType = (typeof inquirerTypes)[keyof typeof inquirerTypes];
|
|
9
|
+
export type Choice<Value> = {
|
|
10
|
+
value: Value;
|
|
11
|
+
name?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
short?: string;
|
|
14
|
+
disabled?: boolean | string;
|
|
15
|
+
};
|
|
16
|
+
type theme = {
|
|
17
|
+
prefix?: string | {
|
|
18
|
+
idle: string;
|
|
19
|
+
done: string;
|
|
20
|
+
};
|
|
21
|
+
spinner?: {
|
|
22
|
+
interval: number;
|
|
23
|
+
frames: string[];
|
|
24
|
+
};
|
|
25
|
+
style?: {
|
|
26
|
+
answer?: (_text: string) => string;
|
|
27
|
+
message?: (_text: string, _status: 'idle' | 'done' | 'loading') => string;
|
|
28
|
+
error?: (_text: string) => string;
|
|
29
|
+
defaultAnswer?: (_text: string) => string;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
};
|
|
32
|
+
icon?: {
|
|
33
|
+
checked?: string;
|
|
34
|
+
unchecked?: string;
|
|
35
|
+
cursor?: string;
|
|
36
|
+
};
|
|
37
|
+
helpMode?: 'always' | 'never' | 'auto';
|
|
38
|
+
};
|
|
39
|
+
export interface inquirerOptions<T = any> {
|
|
40
|
+
type: inquirerType;
|
|
41
|
+
message: string;
|
|
42
|
+
default?: string | boolean | T;
|
|
43
|
+
choices?: readonly (Choice<T> | string | any)[];
|
|
44
|
+
theme?: theme;
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Error = NodeJS.ErrnoException | null;
|
|
2
|
+
export type sshConfig = {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number | string;
|
|
5
|
+
user: string;
|
|
6
|
+
password: string;
|
|
7
|
+
beforLaunch: string[];
|
|
8
|
+
dockerBuildFiles: string[];
|
|
9
|
+
imageTag: string;
|
|
10
|
+
containerName: string;
|
|
11
|
+
BindPorts: string;
|
|
12
|
+
};
|
|
13
|
+
export type fileData = string | NodeJS.ArrayBufferView;
|
|
14
|
+
import type { promises } from 'fs';
|
|
15
|
+
interface StreamOptions {
|
|
16
|
+
flags?: string | undefined;
|
|
17
|
+
encoding?: BufferEncoding | undefined;
|
|
18
|
+
fd?: number | promises.FileHandle | undefined;
|
|
19
|
+
mode?: number | undefined;
|
|
20
|
+
autoClose?: boolean | undefined;
|
|
21
|
+
emitClose?: boolean | undefined;
|
|
22
|
+
start?: number | undefined;
|
|
23
|
+
signal?: AbortSignal | null | undefined;
|
|
24
|
+
highWaterMark?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
interface streamOptions extends StreamOptions {
|
|
27
|
+
fs?: any | null | undefined;
|
|
28
|
+
end?: number | undefined;
|
|
29
|
+
flush?: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
export type ReadStreamOptions = BufferEncoding | streamOptions;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const chalk: {
|
|
2
|
+
_: import("chalk").ChalkInstance;
|
|
3
|
+
warn: import("chalk").ChalkInstance;
|
|
4
|
+
error: import("chalk").ChalkInstance;
|
|
5
|
+
info: import("chalk").ChalkInstance;
|
|
6
|
+
done: import("chalk").ChalkInstance;
|
|
7
|
+
primary: import("chalk").ChalkInstance;
|
|
8
|
+
stress: import("chalk").ChalkInstance;
|
|
9
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import _fs from 'fs-extra';
|
|
2
|
+
import type { Error, fileData, ReadStreamOptions } from '../type/types.ts';
|
|
3
|
+
import type { PathOrFileDescriptor, WriteFileOptions, MakeDirectoryOptions } from 'fs';
|
|
4
|
+
export declare class fs {
|
|
5
|
+
static _: typeof _fs;
|
|
6
|
+
static exist(path: string, callbak?: (_err: Error, _exists: boolean) => void | Promise<boolean>): Promise<boolean>;
|
|
7
|
+
static getFileStat(path: string): Promise<_fs.Stats>;
|
|
8
|
+
static isDirectory(path: string): Promise<Boolean>;
|
|
9
|
+
static removeSync(path: string): void;
|
|
10
|
+
static remove(path: string, callbak?: (_err: Error, _status: boolean) => Promise<boolean>): Promise<boolean>;
|
|
11
|
+
static writeFileSync(file: PathOrFileDescriptor, data: fileData, options?: WriteFileOptions): Promise<void>;
|
|
12
|
+
static mkdirSync(path: string, options?: MakeDirectoryOptions): string | undefined;
|
|
13
|
+
static readDirOrFile(path: string, options?: {
|
|
14
|
+
encoding?: BufferEncoding | null | undefined;
|
|
15
|
+
flag?: string | null;
|
|
16
|
+
signal?: any;
|
|
17
|
+
}, callbak?: (_err: Error, _data: Buffer | Buffer[] | string | string[]) => void): Promise<string | string[] | Buffer<ArrayBufferLike> | Buffer<ArrayBufferLike>[]>;
|
|
18
|
+
static createReadStream(path: string, options?: ReadStreamOptions): Promise<_fs.ReadStream>;
|
|
19
|
+
static createWriteStream(path: string, options?: ReadStreamOptions): _fs.WriteStream;
|
|
20
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import _fs from 'fs-extra';
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
import _path from 'path';
|
|
4
|
+
export class fs {
|
|
5
|
+
static _ = _fs;
|
|
6
|
+
static async exist(path, callbak) {
|
|
7
|
+
const _callbak = (err, exists) => {
|
|
8
|
+
if (callbak) {
|
|
9
|
+
callbak(err, exists);
|
|
10
|
+
}
|
|
11
|
+
else if (err) {
|
|
12
|
+
log.error(`${err}`);
|
|
13
|
+
return Promise.reject(false);
|
|
14
|
+
}
|
|
15
|
+
return Promise.resolve(exists);
|
|
16
|
+
};
|
|
17
|
+
let _exists = false, _err = null;
|
|
18
|
+
await _fs.pathExists(path).then((exists) => (_exists = exists), (err) => (_err = err));
|
|
19
|
+
return _callbak(_err, _exists);
|
|
20
|
+
}
|
|
21
|
+
static async getFileStat(path) {
|
|
22
|
+
const exist = await fs.exist(path);
|
|
23
|
+
if (!exist) {
|
|
24
|
+
log.error(`The file does not exist in ${path}`);
|
|
25
|
+
return Promise.reject(null);
|
|
26
|
+
}
|
|
27
|
+
return Promise.resolve(_fs.statSync(path));
|
|
28
|
+
}
|
|
29
|
+
static async isDirectory(path) {
|
|
30
|
+
const stat = await fs.getFileStat(path);
|
|
31
|
+
return Promise.resolve(stat?.isDirectory() ?? false);
|
|
32
|
+
}
|
|
33
|
+
static removeSync(path) {
|
|
34
|
+
_fs.removeSync(path);
|
|
35
|
+
}
|
|
36
|
+
static async remove(path, callbak) {
|
|
37
|
+
const _callbak = (err, status) => {
|
|
38
|
+
if (callbak) {
|
|
39
|
+
callbak(err, status);
|
|
40
|
+
}
|
|
41
|
+
else if (err) {
|
|
42
|
+
log.error(`${err}`);
|
|
43
|
+
return Promise.reject(err);
|
|
44
|
+
}
|
|
45
|
+
return Promise.resolve(status);
|
|
46
|
+
};
|
|
47
|
+
let status = false, err = null;
|
|
48
|
+
const exist = await fs.exist(path);
|
|
49
|
+
if (!exist) {
|
|
50
|
+
status = true;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await _fs.remove(path).then(() => (status = true), _err => {
|
|
54
|
+
status = false;
|
|
55
|
+
err = _err;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return _callbak(err, status);
|
|
59
|
+
}
|
|
60
|
+
static async writeFileSync(file, data, options) {
|
|
61
|
+
_fs.writeFileSync(file, data, options);
|
|
62
|
+
}
|
|
63
|
+
static mkdirSync(path, options) {
|
|
64
|
+
const _options = Object.assign({
|
|
65
|
+
recursive: true,
|
|
66
|
+
}, options);
|
|
67
|
+
return _fs.mkdirSync(path, _options);
|
|
68
|
+
}
|
|
69
|
+
static async readDirOrFile(path, options, callbak) {
|
|
70
|
+
let _options = Object.assign({
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
flag: 'r',
|
|
73
|
+
}, options);
|
|
74
|
+
const _callbak = (err, data) => {
|
|
75
|
+
if (callbak) {
|
|
76
|
+
callbak(err, data);
|
|
77
|
+
}
|
|
78
|
+
else if (err) {
|
|
79
|
+
log.error(`${err}`);
|
|
80
|
+
return Promise.reject(err);
|
|
81
|
+
}
|
|
82
|
+
return Promise.resolve(data);
|
|
83
|
+
};
|
|
84
|
+
let _err = null, _data = Buffer.from('');
|
|
85
|
+
const isDirectory = (await fs.getFileStat(path))?.isDirectory();
|
|
86
|
+
try {
|
|
87
|
+
if (isDirectory) {
|
|
88
|
+
_data = await _fs.readdir(path, _options);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
_data = await _fs.readFile(path, _options);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
_err = err;
|
|
96
|
+
}
|
|
97
|
+
return _callbak(_err, _data);
|
|
98
|
+
}
|
|
99
|
+
static async createReadStream(path, options) {
|
|
100
|
+
const exist = await fs.exist(path);
|
|
101
|
+
if (!exist) {
|
|
102
|
+
log.error(`The file does not exist in ${path}`);
|
|
103
|
+
return Promise.reject(`The file does not exist in ${path}`);
|
|
104
|
+
}
|
|
105
|
+
return Promise.resolve(_fs.createReadStream(path, options).on('error', err => log.error(`${err}`)));
|
|
106
|
+
}
|
|
107
|
+
static createWriteStream(path, options) {
|
|
108
|
+
const dir = _path.dirname(path);
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(dir);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
log.error(`${err}`);
|
|
114
|
+
}
|
|
115
|
+
return _fs.createWriteStream(path, options).on('error', err => log.error(`${err}`));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { inquirerOptions } from '../type/inquirer.ts';
|
|
2
|
+
export declare class inquirer {
|
|
3
|
+
static enumMethod: {
|
|
4
|
+
readonly INPUT: "input";
|
|
5
|
+
readonly SELECT: "select";
|
|
6
|
+
readonly CHECKBOX: "checkbox";
|
|
7
|
+
readonly CONFIRM: "confirm";
|
|
8
|
+
};
|
|
9
|
+
private static _enumMethod;
|
|
10
|
+
static invoke(options: inquirerOptions): any;
|
|
11
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { input, select, checkbox, confirm, number } from '@inquirer/prompts';
|
|
2
|
+
import { chalk } from "./chalk.js";
|
|
3
|
+
export class inquirer {
|
|
4
|
+
static enumMethod = {
|
|
5
|
+
INPUT: 'input',
|
|
6
|
+
SELECT: 'select',
|
|
7
|
+
CHECKBOX: 'checkbox',
|
|
8
|
+
CONFIRM: 'confirm',
|
|
9
|
+
};
|
|
10
|
+
static _enumMethod = {
|
|
11
|
+
input: input,
|
|
12
|
+
select: select,
|
|
13
|
+
checkbox: checkbox,
|
|
14
|
+
confirm: confirm,
|
|
15
|
+
number: number,
|
|
16
|
+
};
|
|
17
|
+
static invoke(options) {
|
|
18
|
+
const { type, ...args } = options;
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'checkbox':
|
|
21
|
+
args.message += ` Select by ${chalk.stress('Space bar check/cancel')}. Submit by ${chalk.stress('Enter')}`;
|
|
22
|
+
args.theme = {
|
|
23
|
+
style: {
|
|
24
|
+
description: (des) => chalk.stress(des),
|
|
25
|
+
},
|
|
26
|
+
icon: {
|
|
27
|
+
unchecked: '○',
|
|
28
|
+
},
|
|
29
|
+
...options.theme,
|
|
30
|
+
};
|
|
31
|
+
break;
|
|
32
|
+
case 'confirm':
|
|
33
|
+
args.theme = {
|
|
34
|
+
prefix: chalk.warn('✔'),
|
|
35
|
+
style: {
|
|
36
|
+
answer: (text) => chalk.done(text),
|
|
37
|
+
error: (text) => chalk.error(text),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
break;
|
|
41
|
+
case 'select':
|
|
42
|
+
if (!args.theme || !args.theme.spinner) {
|
|
43
|
+
args.theme = {
|
|
44
|
+
spinner: {
|
|
45
|
+
interval: 80,
|
|
46
|
+
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
47
|
+
},
|
|
48
|
+
...args.theme,
|
|
49
|
+
style: {
|
|
50
|
+
description: (des) => chalk.stress(des),
|
|
51
|
+
...options.theme?.style,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
return inquirer._enumMethod[type](args);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare class LogClass {
|
|
2
|
+
log(msg: string, label?: string): void;
|
|
3
|
+
warn(msg: string, label?: string): void;
|
|
4
|
+
error(msg: string, label?: string): void;
|
|
5
|
+
info(msg: string, label?: string): void;
|
|
6
|
+
done(msg: string, label?: string): void;
|
|
7
|
+
primary(msg: string, label?: string): void;
|
|
8
|
+
}
|
|
9
|
+
export declare const log: LogClass;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { chalk } from "./chalk.js";
|
|
2
|
+
function format(msg, label = '') {
|
|
3
|
+
return label
|
|
4
|
+
? `[${label}]:${msg
|
|
5
|
+
.split('\n')
|
|
6
|
+
.map((line, i) => line.padStart((i !== 0 ? label.length : 0) + line.length + 1))
|
|
7
|
+
.join('\n')}`
|
|
8
|
+
: msg;
|
|
9
|
+
}
|
|
10
|
+
class LogClass {
|
|
11
|
+
log(msg, label = '') {
|
|
12
|
+
console.log(format(msg, label));
|
|
13
|
+
}
|
|
14
|
+
warn(msg, label = '') {
|
|
15
|
+
if (!label) {
|
|
16
|
+
label = 'Warning';
|
|
17
|
+
msg.startsWith('Warning:') && (msg = msg.replace('Warning:', '').trimStart());
|
|
18
|
+
}
|
|
19
|
+
console.warn(chalk.warn(format(msg, label)));
|
|
20
|
+
}
|
|
21
|
+
error(msg, label = '') {
|
|
22
|
+
if (!label) {
|
|
23
|
+
label = 'Error';
|
|
24
|
+
msg.startsWith('Error:') && (msg = msg.replace('Error:', '').trimStart());
|
|
25
|
+
}
|
|
26
|
+
console.error(chalk.error(format(msg, label)));
|
|
27
|
+
}
|
|
28
|
+
info(msg, label = '') {
|
|
29
|
+
if (!label) {
|
|
30
|
+
label = 'Info';
|
|
31
|
+
msg.startsWith('Info:') && (msg = msg.replace('Info:', '').trimStart());
|
|
32
|
+
}
|
|
33
|
+
console.error(chalk.info(format(msg, label)));
|
|
34
|
+
}
|
|
35
|
+
done(msg, label = '') {
|
|
36
|
+
if (!label) {
|
|
37
|
+
label = 'Done';
|
|
38
|
+
msg.startsWith('Done:') && (msg = msg.replace('Done:', '').trimStart());
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.done(format(msg, label)));
|
|
41
|
+
}
|
|
42
|
+
primary(msg, label = '') {
|
|
43
|
+
console.log(chalk.primary(format(msg, label)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const log = new LogClass();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import _ora from 'ora';
|
|
2
|
+
import type { Ora, Options, PersistOptions } from 'ora';
|
|
3
|
+
import type { Error } from '../type/types.ts';
|
|
4
|
+
export declare class ora {
|
|
5
|
+
private static spinner?;
|
|
6
|
+
static _: typeof _ora;
|
|
7
|
+
static start(options: string | Options, fn?: Function, callbak?: (_status: boolean, _err?: Error | any) => string): Promise<void>;
|
|
8
|
+
static text(text: string): void;
|
|
9
|
+
static stop(type?: 'info' | 'warn' | 'fail' | 'succeed', text?: string): void;
|
|
10
|
+
static stopAndPersist(options?: PersistOptions): void;
|
|
11
|
+
static hasSpinner(): Ora | null | undefined;
|
|
12
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import _ora from 'ora';
|
|
2
|
+
import { chalk } from "./chalk.js";
|
|
3
|
+
export class ora {
|
|
4
|
+
static spinner;
|
|
5
|
+
static _ = _ora;
|
|
6
|
+
static async start(options, fn, callbak) {
|
|
7
|
+
let _options = {
|
|
8
|
+
spinner: 'dots',
|
|
9
|
+
color: 'blue',
|
|
10
|
+
text: '...',
|
|
11
|
+
};
|
|
12
|
+
const isText = typeof options == 'string';
|
|
13
|
+
if (isText) {
|
|
14
|
+
_options['text'] = options;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
_options = {
|
|
18
|
+
..._options,
|
|
19
|
+
...options,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (ora.spinner)
|
|
23
|
+
ora.spinner?.stop();
|
|
24
|
+
ora.spinner = _ora(_options);
|
|
25
|
+
ora.spinner.start();
|
|
26
|
+
if (fn) {
|
|
27
|
+
let _err = null;
|
|
28
|
+
let status = false;
|
|
29
|
+
try {
|
|
30
|
+
if (fn.constructor.name === 'AsyncFunction') {
|
|
31
|
+
await fn().then(() => (status = true), () => (status = false));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
fn();
|
|
35
|
+
status = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
_err = err;
|
|
40
|
+
status = false;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (status) {
|
|
44
|
+
ora.stop('succeed', callbak ? callbak(status) : chalk.done(`Succeed ${isText ? options : 'done'}`));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
ora.stop('fail', callbak ? callbak(status, _err) : chalk.error(`Error: ${_err}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
static text(text) {
|
|
53
|
+
ora.spinner && (ora.spinner.text = text);
|
|
54
|
+
}
|
|
55
|
+
static stop(type, text) {
|
|
56
|
+
if (!type)
|
|
57
|
+
ora.spinner?.stop();
|
|
58
|
+
else
|
|
59
|
+
ora.spinner && ora.spinner[type](text);
|
|
60
|
+
ora.spinner = null;
|
|
61
|
+
}
|
|
62
|
+
static stopAndPersist(options) {
|
|
63
|
+
ora.spinner &&
|
|
64
|
+
ora.spinner.stopAndPersist({
|
|
65
|
+
symbol: chalk.done('✔'),
|
|
66
|
+
...options,
|
|
67
|
+
});
|
|
68
|
+
ora.spinner = null;
|
|
69
|
+
}
|
|
70
|
+
static hasSpinner() {
|
|
71
|
+
return ora.spinner;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Options } from 'execa';
|
|
2
|
+
export declare function exit(code: number): void;
|
|
3
|
+
export declare function run(command: string, context?: string, options?: Options, args?: string[]): Promise<never> | import("execa").ResultPromise<{
|
|
4
|
+
preferLocal?: boolean;
|
|
5
|
+
localDir?: string | URL;
|
|
6
|
+
node?: boolean;
|
|
7
|
+
nodeOptions?: readonly string[];
|
|
8
|
+
nodePath?: string | URL;
|
|
9
|
+
shell?: boolean | string | URL;
|
|
10
|
+
cwd: string | URL;
|
|
11
|
+
env?: Readonly<Partial<Record<string, string>>>;
|
|
12
|
+
extendEnv?: boolean;
|
|
13
|
+
input?: string | Uint8Array | import("stream").Readable;
|
|
14
|
+
inputFile?: string | URL;
|
|
15
|
+
stdin?: import("execa/types/stdio/type").StdinOptionCommon<false>;
|
|
16
|
+
stdout?: import("execa/types/stdio/type").StdoutStderrOptionCommon<false>;
|
|
17
|
+
stderr?: import("execa/types/stdio/type").StdoutStderrOptionCommon<false>;
|
|
18
|
+
stdio?: import("execa/types/stdio/type").StdioOptionsProperty<false>;
|
|
19
|
+
all?: boolean;
|
|
20
|
+
encoding?: import("execa/types/arguments/encoding-option").EncodingOption;
|
|
21
|
+
lines?: import("execa/types/arguments/specific").FdGenericOption<boolean>;
|
|
22
|
+
stripFinalNewline?: import("execa/types/arguments/specific").FdGenericOption<boolean>;
|
|
23
|
+
maxBuffer?: import("execa/types/arguments/specific").FdGenericOption<number>;
|
|
24
|
+
buffer?: import("execa/types/arguments/specific").FdGenericOption<boolean>;
|
|
25
|
+
ipc?: boolean;
|
|
26
|
+
serialization?: "json" | "advanced";
|
|
27
|
+
ipcInput?: import("execa").Message;
|
|
28
|
+
verbose?: import("execa/types/verbose").VerboseOption;
|
|
29
|
+
reject?: boolean;
|
|
30
|
+
timeout?: number;
|
|
31
|
+
cancelSignal?: AbortSignal;
|
|
32
|
+
gracefulCancel?: boolean;
|
|
33
|
+
forceKillAfterDelay?: number | boolean;
|
|
34
|
+
killSignal?: keyof import("os").SignalConstants | number;
|
|
35
|
+
detached?: boolean;
|
|
36
|
+
cleanup?: boolean;
|
|
37
|
+
uid?: number;
|
|
38
|
+
gid?: number;
|
|
39
|
+
argv0?: string;
|
|
40
|
+
windowsHide?: boolean;
|
|
41
|
+
windowsVerbatimArguments?: boolean;
|
|
42
|
+
}>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
export function exit(code) {
|
|
4
|
+
try {
|
|
5
|
+
if (code > 0) {
|
|
6
|
+
process.exit(code);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
throw Error('The code for process exit must be greater than zero');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
log.error(`${err}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function run(command, context, options, args) {
|
|
17
|
+
if (command === '' || !command) {
|
|
18
|
+
log.error('command cannot be empty');
|
|
19
|
+
return Promise.reject();
|
|
20
|
+
}
|
|
21
|
+
const _context = context ?? process.cwd();
|
|
22
|
+
let cmd = command;
|
|
23
|
+
if (!args) {
|
|
24
|
+
const parts = command.trim().split(/\s+/);
|
|
25
|
+
cmd = parts[0];
|
|
26
|
+
if (parts.length > 1) {
|
|
27
|
+
args = parts.slice(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return execa(cmd, args, { cwd: _context, ...options });
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auto-deploy-sh",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automated Docker deployment tool",
|
|
5
|
+
"bin": {
|
|
6
|
+
"auto-deploy-sh": "./dist/bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"package.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/bin/index.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "npm run clean && tsc -p tsconfig.bulid.json",
|
|
17
|
+
"clean": "rimraf dist build",
|
|
18
|
+
"deploy": "node ./ts-node bin/index.ts",
|
|
19
|
+
"lint:eslint": "eslint --cache --ext .ts,.vue,.js,.jsx,.tsx,.json",
|
|
20
|
+
"lint:eslint:fix": "eslint --cache --ext .ts,.vue,.js,.jsx,.tsx,.json --fix",
|
|
21
|
+
"prepare": "husky",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"postbuild": "node ./postbuild.js"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"Automated",
|
|
27
|
+
"Lower deployment threshold",
|
|
28
|
+
"deployment",
|
|
29
|
+
"Minimalist configuration"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/yotomum-ml/auto-deploy-sh.git"
|
|
34
|
+
},
|
|
35
|
+
"author": "yotomum_ml",
|
|
36
|
+
"license": "ISC",
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"*.{js,ts,jsx,tsx,vue,css,json}": [
|
|
39
|
+
"eslint"
|
|
40
|
+
],
|
|
41
|
+
"*.md": [
|
|
42
|
+
"prettier --check"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"archiver": "^7.0.1",
|
|
47
|
+
"chalk": "^5.6.2",
|
|
48
|
+
"execa": "^9.6.0",
|
|
49
|
+
"fs-extra": "^11.3.1",
|
|
50
|
+
"inquirer": "^12.9.6",
|
|
51
|
+
"node-ssh": "^13.2.1",
|
|
52
|
+
"ora": "^8.2.0",
|
|
53
|
+
"readline-sync": "^1.4.10",
|
|
54
|
+
"ssh2": "^1.17.0",
|
|
55
|
+
"ts-node": "^10.9.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@commitlint/cli": "^19.8.1",
|
|
62
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
63
|
+
"@eslint/css": "^0.8.1",
|
|
64
|
+
"@eslint/js": "^9.29.0",
|
|
65
|
+
"@eslint/json": "^0.12.0",
|
|
66
|
+
"@stylistic/eslint-plugin": "^4.4.1",
|
|
67
|
+
"@types/archiver": "^6.0.3",
|
|
68
|
+
"@types/fs-extra": "^11.0.4",
|
|
69
|
+
"auto-deploy-sh": "file:auto-deploy-sh-1.0.0.tgz",
|
|
70
|
+
"eslint": "^9.27.0",
|
|
71
|
+
"eslint-config-prettier": "^10.1.5",
|
|
72
|
+
"eslint-plugin-prettier": "^5.4.1",
|
|
73
|
+
"eslint-plugin-vue": "^10.2.0",
|
|
74
|
+
"husky": "^9.1.7",
|
|
75
|
+
"lint-staged": "^15.5.2",
|
|
76
|
+
"node-notifier": "^10.0.1",
|
|
77
|
+
"rimraf": "^5.0.10",
|
|
78
|
+
"typescript": "^5.8.3",
|
|
79
|
+
"typescript-eslint": "^8.34.1"
|
|
80
|
+
}
|
|
81
|
+
}
|