@lazycatcloud/lzc-cli 1.1.3 → 1.1.4
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/cmds/app.js +137 -0
- package/cmds/config.js +55 -0
- package/cmds/create.js +55 -0
- package/cmds/dev.js +122 -0
- package/cmds/init.js +125 -0
- package/cmds/log.js +103 -0
- package/cmds/publish.js +116 -0
- package/lib/box/check_qemu.js +27 -0
- package/lib/box/hportal.js +1 -0
- package/lib/box/index.js +27 -10
- package/lib/box/qemu_vm_mgr.js +110 -38
- package/lib/builder.js +2 -2
- package/lib/dev.js +14 -3
- package/lib/env.js +1 -1
- package/lib/git/git-commit.sh +3 -3
- package/package.json +6 -5
- package/scripts/cli.js +2 -2
- package/scripts/auto-completion.sh +0 -46
package/cmds/app.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import {
|
|
4
|
+
archiveFolder,
|
|
5
|
+
isValidApp,
|
|
6
|
+
getMetaInfo,
|
|
7
|
+
ensureDir,
|
|
8
|
+
} from "../lib/utils.js";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import Builder, { execPreBuild } from "../lib/builder.js";
|
|
12
|
+
import env, { sdkEnv } from "../lib/env.js";
|
|
13
|
+
import BoxAPI from "../lib/api.js";
|
|
14
|
+
import Archiver from "../lib/archiver.js";
|
|
15
|
+
import Key from "../lib/key.js";
|
|
16
|
+
|
|
17
|
+
import { debuglog } from "util";
|
|
18
|
+
|
|
19
|
+
const debug = debuglog("cmd/app");
|
|
20
|
+
|
|
21
|
+
// this set of api target box
|
|
22
|
+
class App {
|
|
23
|
+
constructor(context) {
|
|
24
|
+
this.context = context;
|
|
25
|
+
|
|
26
|
+
const { appDir, isTemplate } = isValidApp(process.cwd());
|
|
27
|
+
if (!appDir) {
|
|
28
|
+
console.log(chalk.red("未识别懒猫云应用"));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
this.appPath = appDir;
|
|
32
|
+
this.isTemplate = isTemplate;
|
|
33
|
+
env.load(process.cwd());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async deploy() {
|
|
37
|
+
try {
|
|
38
|
+
if (this.isTemplate) {
|
|
39
|
+
const outDir = await this.prepare();
|
|
40
|
+
|
|
41
|
+
await this.archiveAndInstall(outDir);
|
|
42
|
+
} else {
|
|
43
|
+
await sdkEnv.ensure();
|
|
44
|
+
|
|
45
|
+
await this.archiveAndInstall(this.appPath);
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async uninstall() {
|
|
53
|
+
let api;
|
|
54
|
+
const sdkUrl = sdkEnv.sdkUrl;
|
|
55
|
+
if (this.isTemplate) {
|
|
56
|
+
api = new BoxAPI(env.get("APP_ID"), sdkUrl);
|
|
57
|
+
} else {
|
|
58
|
+
const metaInfo = getMetaInfo(
|
|
59
|
+
path.join(this.appPath, "docker-compose.yml")
|
|
60
|
+
);
|
|
61
|
+
api = new BoxAPI(metaInfo.id, sdkUrl);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await api.uninstall();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async prepare() {
|
|
72
|
+
await this.prepareBuildEnv();
|
|
73
|
+
|
|
74
|
+
const outputDir = path.join(this.appPath, "output/release/");
|
|
75
|
+
const buildDir = path.join(process.cwd(), env.get("BUILD_CONTEXT"));
|
|
76
|
+
|
|
77
|
+
ensureDir(outputDir);
|
|
78
|
+
|
|
79
|
+
await new Key().ensure(sdkEnv.sdkUrl);
|
|
80
|
+
|
|
81
|
+
const envAll = {
|
|
82
|
+
...env.all,
|
|
83
|
+
APP_IMAGE_NAME: env.get("APP_ID"),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// exec pre build
|
|
87
|
+
await execPreBuild(buildDir, process.cwd(), envAll);
|
|
88
|
+
|
|
89
|
+
// do image build
|
|
90
|
+
await new Builder(envAll).buildImage(process.cwd(), buildDir);
|
|
91
|
+
|
|
92
|
+
// do archive
|
|
93
|
+
const archiver = new Archiver(outputDir);
|
|
94
|
+
console.log("appPath", this.appPath);
|
|
95
|
+
await archiver.load(this.appPath);
|
|
96
|
+
await archiver.add(buildDir);
|
|
97
|
+
|
|
98
|
+
await archiver.finalize();
|
|
99
|
+
|
|
100
|
+
return outputDir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async prepareBuildEnv() {
|
|
104
|
+
await sdkEnv.ensure();
|
|
105
|
+
await env.ensure([{ name: "HTTP_SERVICE_PORT" }]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// archive current dir and install
|
|
109
|
+
async archiveAndInstall(dir) {
|
|
110
|
+
const metaInfo = getMetaInfo(path.join(dir, "docker-compose.yml"));
|
|
111
|
+
const api = new BoxAPI(metaInfo.id, sdkEnv.sdkUrl);
|
|
112
|
+
let out;
|
|
113
|
+
try {
|
|
114
|
+
const out = await archiveFolder(dir);
|
|
115
|
+
console.log(chalk.green("开始部署应用"));
|
|
116
|
+
await api.install(out.path);
|
|
117
|
+
await api.checkStatus();
|
|
118
|
+
|
|
119
|
+
console.log(
|
|
120
|
+
`请在浏览器中访问 ${chalk.green(
|
|
121
|
+
sdkEnv.sdkUrl.replace(/sdk/, metaInfo.id)
|
|
122
|
+
)}`
|
|
123
|
+
);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
throw e;
|
|
126
|
+
} finally {
|
|
127
|
+
if (out) fs.rmSync(out.path, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const app = new App();
|
|
133
|
+
|
|
134
|
+
export default {
|
|
135
|
+
deploy: () => app.deploy(),
|
|
136
|
+
uninstall: () => app.uninstall(),
|
|
137
|
+
};
|
package/cmds/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { isValidApp } from "../lib/utils.js";
|
|
3
|
+
import env, { sdkEnv } from "../lib/env.js";
|
|
4
|
+
import { debuglog } from "util";
|
|
5
|
+
|
|
6
|
+
const debug = debuglog("cmd/config");
|
|
7
|
+
|
|
8
|
+
class Config {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
}
|
|
12
|
+
async exec() {
|
|
13
|
+
const [key, value, options] = this.context;
|
|
14
|
+
|
|
15
|
+
isValidApp(process.cwd());
|
|
16
|
+
env.load(process.cwd());
|
|
17
|
+
|
|
18
|
+
// // lzc-cli config
|
|
19
|
+
if (!key && !value) {
|
|
20
|
+
console.log(env.stringify());
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// lzc-cli config SDK_URL
|
|
25
|
+
if (key && !value) {
|
|
26
|
+
if (env.get(key)) {
|
|
27
|
+
console.log(env.get(key));
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (key && value) {
|
|
33
|
+
const pair = Object.fromEntries([[key, value]]);
|
|
34
|
+
// if (pair["SDK_URL"]) {
|
|
35
|
+
// pair["SDK_URL"] = pair["SDK_URL"].replace(/\/$/, "");
|
|
36
|
+
//
|
|
37
|
+
// try {
|
|
38
|
+
// // 这里只需要测试sdk的合法性即可, 不需要处理安装的逻辑
|
|
39
|
+
// await sdkEnv.checkConnection(pair["SDK_URL"]);
|
|
40
|
+
// } catch (e) {
|
|
41
|
+
// console.log(e.message);
|
|
42
|
+
// debug("got error when check connection %s", e);
|
|
43
|
+
// process.exit();
|
|
44
|
+
// }
|
|
45
|
+
// }
|
|
46
|
+
env.set(pair, options.global);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
config: async (context) => {
|
|
53
|
+
return await new Config(context).exec();
|
|
54
|
+
},
|
|
55
|
+
};
|
package/cmds/create.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { Init, chooseTemplate } from "./init.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import Generator, { TemplateConfig } from "../lib/generator.js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import { parse2CorrectName } from "../lib/utils.js";
|
|
8
|
+
const fsPromises = fs.promises;
|
|
9
|
+
class Create {
|
|
10
|
+
constructor({ name }) {
|
|
11
|
+
this.name = name;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async exec() {
|
|
15
|
+
const type = await chooseTemplate();
|
|
16
|
+
this.init = new Init({
|
|
17
|
+
cwd: path.join(process.cwd(), this.name),
|
|
18
|
+
type: type,
|
|
19
|
+
});
|
|
20
|
+
if (TemplateConfig[type]) {
|
|
21
|
+
const config = TemplateConfig[type];
|
|
22
|
+
const init = this.init;
|
|
23
|
+
const answers = await init.askQuestions();
|
|
24
|
+
console.log(chalk.green(`初始化项目 ${this.name}`));
|
|
25
|
+
await Generator().generate(type, this.name);
|
|
26
|
+
console.log(chalk.green("设置懒猫云应用"));
|
|
27
|
+
await init.createTemplates(answers);
|
|
28
|
+
await init.createReleaseFolder();
|
|
29
|
+
await config.after(this.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async (context) => {
|
|
35
|
+
context.name = parse2CorrectName(context.name);
|
|
36
|
+
await fsPromises.access(process.cwd() + "/" + context.name).then(
|
|
37
|
+
async () => {
|
|
38
|
+
const questions = [
|
|
39
|
+
{
|
|
40
|
+
name: "override",
|
|
41
|
+
type: "input",
|
|
42
|
+
default: "n",
|
|
43
|
+
message: "项目已存在,是否覆盖(y/n): ",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const answers = await inquirer.prompt(questions);
|
|
47
|
+
if (answers.override.toLowerCase() === "y") {
|
|
48
|
+
return new Create(context).exec();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
() => {
|
|
52
|
+
return new Create(context).exec();
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
};
|
package/cmds/dev.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import process from "process";
|
|
2
|
+
import { isValidApp } from "../lib/utils.js";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import Dev from "../lib/dev.js";
|
|
5
|
+
import env, { sdkEnv } from "../lib/env.js";
|
|
6
|
+
import BoxAPI from "../lib/api.js";
|
|
7
|
+
|
|
8
|
+
// this set of api target box
|
|
9
|
+
class Develop {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
|
|
13
|
+
const { appDir, isTemplate } = isValidApp(process.cwd());
|
|
14
|
+
if (!appDir) {
|
|
15
|
+
console.log(chalk.red("未识别懒猫云应用"));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.appPath = appDir;
|
|
20
|
+
this.isTemplate = isTemplate;
|
|
21
|
+
env.load(process.cwd());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async devShell({ build }) {
|
|
25
|
+
let appdev = new Dev(env.get("APP_ID"), this.appPath);
|
|
26
|
+
|
|
27
|
+
if (build) {
|
|
28
|
+
appdev.reset();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 当如果已经连接。直接连接ssh
|
|
32
|
+
if (appdev.hasShellStatus()) {
|
|
33
|
+
try {
|
|
34
|
+
await appdev.directShell();
|
|
35
|
+
return;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return Promise.reject(e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 重新部署dev app container
|
|
42
|
+
try {
|
|
43
|
+
await sdkEnv.ensure();
|
|
44
|
+
const { APP_ID: appId } = await env.ensure([{ name: "APP_ID" }]);
|
|
45
|
+
|
|
46
|
+
console.debug(sdkEnv.sdkUrl);
|
|
47
|
+
const api = new BoxAPI(appId, sdkEnv.sdkUrl);
|
|
48
|
+
|
|
49
|
+
let out = await appdev.fakeApp("shell");
|
|
50
|
+
|
|
51
|
+
console.debug("设置端口转发");
|
|
52
|
+
await api.install(out.path, { build: true });
|
|
53
|
+
await api.checkStatus();
|
|
54
|
+
|
|
55
|
+
await appdev.shell();
|
|
56
|
+
return;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return Promise.reject(e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async devPortForward(localaddr) {
|
|
63
|
+
console.log("逻辑需要重新梳理");
|
|
64
|
+
process.exit();
|
|
65
|
+
// try {
|
|
66
|
+
// await this.ensureEnv([
|
|
67
|
+
// {
|
|
68
|
+
// name: "DEV_PORT",
|
|
69
|
+
// type: "input",
|
|
70
|
+
// message: "开发暴露端口",
|
|
71
|
+
// default: "8080",
|
|
72
|
+
// },
|
|
73
|
+
// {
|
|
74
|
+
// name: "DEV_CMD",
|
|
75
|
+
// type: "input",
|
|
76
|
+
// default: "npm run serve",
|
|
77
|
+
// message: "开发运行命令",
|
|
78
|
+
// },
|
|
79
|
+
// ]);
|
|
80
|
+
//
|
|
81
|
+
// await this.ensureSDK();
|
|
82
|
+
//
|
|
83
|
+
// console.debug(env.get("SDK_URL"));
|
|
84
|
+
// const api = new BoxAPI(env.get("APP_ID"), env.get("SDK_URL"));
|
|
85
|
+
//
|
|
86
|
+
// let appdev = new Dev(env.get("APP_ID"), this.appPath);
|
|
87
|
+
//
|
|
88
|
+
// let out = await appdev.fakeApp("devforward");
|
|
89
|
+
//
|
|
90
|
+
// console.debug("设置端口转发");
|
|
91
|
+
// await api.install(out.path, { build: true });
|
|
92
|
+
// await api.checkStatus();
|
|
93
|
+
//
|
|
94
|
+
// console.log(
|
|
95
|
+
// `请在浏览器中访问 ${chalk.green(
|
|
96
|
+
// env.get("SDK_URL").replace(/sdk/, env.get("APP_ID"))
|
|
97
|
+
// )}`
|
|
98
|
+
// );
|
|
99
|
+
//
|
|
100
|
+
// await appdev.portForward(localaddr);
|
|
101
|
+
// } catch (e) {
|
|
102
|
+
// throw e;
|
|
103
|
+
// }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const dev = new Develop();
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
dev: (addr) => dev.devPortForward(addr),
|
|
111
|
+
devShell: async (options) => {
|
|
112
|
+
try {
|
|
113
|
+
await dev.devShell(options);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.log(chalk.red(e));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 需要显示的退出,因为当shell退出后,rsync和watch的进程还在执行
|
|
120
|
+
// process.exit();
|
|
121
|
+
},
|
|
122
|
+
};
|
package/cmds/init.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import generator, { TemplateConfig } from "../lib/generator.js";
|
|
4
|
+
import { APP_FOLDER } from "../lib/utils.js";
|
|
5
|
+
import env from "../lib/env.js";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
let fsPromises = fs.promises;
|
|
8
|
+
import { debuglog } from "util";
|
|
9
|
+
|
|
10
|
+
const debug = debuglog("cmd/init");
|
|
11
|
+
|
|
12
|
+
export class Init {
|
|
13
|
+
constructor(context) {
|
|
14
|
+
this.context = context;
|
|
15
|
+
this.type = context.type;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async askQuestions() {
|
|
19
|
+
const questions = [
|
|
20
|
+
{
|
|
21
|
+
name: "APP_ID",
|
|
22
|
+
type: "input",
|
|
23
|
+
default: () => {
|
|
24
|
+
const name = path.basename(this.context.cwd);
|
|
25
|
+
return name;
|
|
26
|
+
},
|
|
27
|
+
message: "应用名称",
|
|
28
|
+
validate: (input) => {
|
|
29
|
+
if (!/^([a-z]|[0-9]|[\-])+$/.test(input)) {
|
|
30
|
+
return "应用名称只能包含(减号,小写字母,数字),请重新输入";
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "APP_DESCRIPTION",
|
|
37
|
+
type: "input",
|
|
38
|
+
default: (answers) => {
|
|
39
|
+
return answers["APP_ID"];
|
|
40
|
+
},
|
|
41
|
+
message: "应用描述",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
return inquirer.prompt(questions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async createTemplates(answers) {
|
|
48
|
+
answers["APP_VERSION"] = "0.0.1";
|
|
49
|
+
env.load(path.join(this.context.cwd));
|
|
50
|
+
await generator(answers).generate(
|
|
51
|
+
APP_FOLDER.replace(".", "_"),
|
|
52
|
+
this.context.cwd,
|
|
53
|
+
{
|
|
54
|
+
prefix: APP_FOLDER,
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
debug(answers);
|
|
58
|
+
env.set(answers);
|
|
59
|
+
this.setDefaultEnvs(env);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async createReleaseFolder() {
|
|
63
|
+
await generator({}).generate(
|
|
64
|
+
"release/" + this.type,
|
|
65
|
+
path.join(this.context.cwd, APP_FOLDER),
|
|
66
|
+
{
|
|
67
|
+
prefix: "release",
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async setDefaultEnvs(env) {
|
|
73
|
+
debug(TemplateConfig[this.type].defaultEnvs);
|
|
74
|
+
env.set(TemplateConfig[this.type].defaultEnvs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async create() {
|
|
78
|
+
if (!this.type) {
|
|
79
|
+
this.type = await chooseTemplate();
|
|
80
|
+
}
|
|
81
|
+
const createConfig = async () => {
|
|
82
|
+
const answers = await this.askQuestions();
|
|
83
|
+
await this.createTemplates(answers);
|
|
84
|
+
await this.createReleaseFolder();
|
|
85
|
+
};
|
|
86
|
+
// .lazycat文件已存在时提示用户是否需要覆盖
|
|
87
|
+
await fsPromises
|
|
88
|
+
.access(
|
|
89
|
+
this.context.cwd + "/.lazycat/",
|
|
90
|
+
fs.constants.F_OK | fs.constants.W_OK
|
|
91
|
+
)
|
|
92
|
+
.then(
|
|
93
|
+
async () => {
|
|
94
|
+
const questions = [
|
|
95
|
+
{
|
|
96
|
+
name: "override",
|
|
97
|
+
type: "input",
|
|
98
|
+
default: "n",
|
|
99
|
+
message: "懒猫云已配置,是否覆盖(y/n): ",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
const answers = await inquirer.prompt(questions);
|
|
103
|
+
if (answers.override.toLowerCase() === "y") {
|
|
104
|
+
createConfig();
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async () => {
|
|
108
|
+
createConfig();
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function chooseTemplate() {
|
|
115
|
+
return (
|
|
116
|
+
await inquirer.prompt([
|
|
117
|
+
{
|
|
118
|
+
name: "type",
|
|
119
|
+
message: "选择项目构建模板",
|
|
120
|
+
type: "list",
|
|
121
|
+
choices: ["vue", "golang", "ionic_vue3"],
|
|
122
|
+
},
|
|
123
|
+
])
|
|
124
|
+
)["type"];
|
|
125
|
+
}
|
package/cmds/log.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { Client } from "ssh2";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import process from "process";
|
|
6
|
+
import { isValidApp, META_MARK } from "../lib/utils.js";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
import env, { sdkEnv } from "../lib/env.js";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { SSHClient, connectOptions } from "../lib/sdk.js";
|
|
11
|
+
|
|
12
|
+
function StreamPromise(stream) {
|
|
13
|
+
let data = "";
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
stream
|
|
16
|
+
.on("data", (d) => {
|
|
17
|
+
data = d;
|
|
18
|
+
})
|
|
19
|
+
.on("close", (code, signal) => {
|
|
20
|
+
resolve(String(data));
|
|
21
|
+
})
|
|
22
|
+
.stderr.on("data", (data) => {
|
|
23
|
+
reject(String(data));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function monitor(projectName, options = {}) {
|
|
29
|
+
env.load(process.cwd());
|
|
30
|
+
|
|
31
|
+
await sdkEnv.ensure();
|
|
32
|
+
|
|
33
|
+
const sdk_url = sdkEnv.sdkUrl;
|
|
34
|
+
|
|
35
|
+
if (!sdk_url) {
|
|
36
|
+
console.log(chalk.red("无法连接SDK, 请确保已设置 DEFAULT_BOXNAME"));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const host = new URL(sdk_url).hostname;
|
|
41
|
+
if (projectName) {
|
|
42
|
+
await new Log(host, projectName).start();
|
|
43
|
+
} else {
|
|
44
|
+
const name = tryGuessProject(options.user);
|
|
45
|
+
// console.log(chalk.green("guess project name " + name));
|
|
46
|
+
await new Log(host, name).start();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function tryGuessProject(user) {
|
|
51
|
+
const { appDir, isTemplate } = isValidApp(process.cwd());
|
|
52
|
+
if (!appDir) {
|
|
53
|
+
console.log(chalk.red("无法识别为懒猫云应用"));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let info;
|
|
58
|
+
let appId;
|
|
59
|
+
if (isTemplate) {
|
|
60
|
+
info = yaml.load(readFileSync(path.join(appDir, "docker-compose.yml.in")));
|
|
61
|
+
appId = env.get("APP_ID");
|
|
62
|
+
} else {
|
|
63
|
+
info = yaml.load(readFileSync(path.join(appDir, "docker-compose.yml")));
|
|
64
|
+
appId = info[META_MARK]["id"];
|
|
65
|
+
}
|
|
66
|
+
return `lzc-${user || ""}-${appId}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class Log {
|
|
70
|
+
constructor(host, project) {
|
|
71
|
+
this.project = project;
|
|
72
|
+
this.host = host;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async start() {
|
|
76
|
+
const opts = await connectOptions(this.host);
|
|
77
|
+
const client = new SSHClient(opts);
|
|
78
|
+
try {
|
|
79
|
+
await client.connect();
|
|
80
|
+
let stream = await client.exec(
|
|
81
|
+
`docker-compose ls | grep -wE '^${this.project}'`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const result = await StreamPromise(stream);
|
|
85
|
+
if (result != "") {
|
|
86
|
+
stream = await client.exec(
|
|
87
|
+
`docker-compose -p ${this.project} logs -f`,
|
|
88
|
+
{ pty: true }
|
|
89
|
+
);
|
|
90
|
+
stream.stdout.pipe(process.stdout);
|
|
91
|
+
stream.stderr.pipe(process.stdout);
|
|
92
|
+
} else {
|
|
93
|
+
client.close();
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
client.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default {
|
|
102
|
+
monitor,
|
|
103
|
+
};
|
package/cmds/publish.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import env from "../lib/env.js";
|
|
2
|
+
import Builder from "../lib/builder.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import Semver from "semver";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import Archiver from "../lib/archiver.js";
|
|
7
|
+
import { isValidApp, contextDirname as getLibDir } from "../lib/utils.js";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import execa from "execa";
|
|
10
|
+
import { GLOBAL_CONFIG_DIR } from "../lib/utils.js";
|
|
11
|
+
|
|
12
|
+
const REGISTRY_REPO = "registry.lazycat.cloud";
|
|
13
|
+
const APPDB_DIR = path.join(GLOBAL_CONFIG_DIR, "appdb");
|
|
14
|
+
|
|
15
|
+
export default class Publisher {
|
|
16
|
+
constructor() {
|
|
17
|
+
env.load(process.cwd());
|
|
18
|
+
}
|
|
19
|
+
async run() {
|
|
20
|
+
const { appDir, _ } = isValidApp(process.cwd());
|
|
21
|
+
const builder = new Builder();
|
|
22
|
+
const contextDir = process.cwd();
|
|
23
|
+
|
|
24
|
+
console.log(getLibDir());
|
|
25
|
+
//
|
|
26
|
+
const { tag, newVersion } = await this.generateTag();
|
|
27
|
+
|
|
28
|
+
const dockerfile = path.join(env.get("BUILD_CONTEXT"), "Dockerfile");
|
|
29
|
+
await builder.dockerLocalBuild(contextDir, dockerfile, tag);
|
|
30
|
+
|
|
31
|
+
// 该操作确保了 appdb 的存在
|
|
32
|
+
execa.sync(path.join(getLibDir(), "git/git-reset.sh"), {
|
|
33
|
+
cwd: appDir,
|
|
34
|
+
env: {
|
|
35
|
+
APPDB_DIR,
|
|
36
|
+
},
|
|
37
|
+
stdio: "inherit",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 生成新的应用
|
|
41
|
+
const outputDir = path.join(APPDB_DIR, env.get("APP_ID"));
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(outputDir)) {
|
|
44
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
const buildDir = path.join(process.cwd(), env.get("BUILD_CONTEXT"));
|
|
47
|
+
|
|
48
|
+
const archiver = new Archiver(outputDir);
|
|
49
|
+
await archiver.load(appDir);
|
|
50
|
+
await archiver.add(buildDir);
|
|
51
|
+
archiver.replace(
|
|
52
|
+
`
|
|
53
|
+
x-lazycat-app:
|
|
54
|
+
version: ${newVersion}
|
|
55
|
+
services:
|
|
56
|
+
${env.get("APP_ID")}:
|
|
57
|
+
image: ${tag}
|
|
58
|
+
`.trim()
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
execa.sync(path.join(getLibDir(), "git/git-commit.sh"), {
|
|
62
|
+
cwd: APPDB_DIR,
|
|
63
|
+
env: {
|
|
64
|
+
APP_ID: env.get("APP_ID"),
|
|
65
|
+
APP_VERSION: newVersion,
|
|
66
|
+
},
|
|
67
|
+
stdio: "inherit",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// confirm git push
|
|
71
|
+
const { push } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
name: "push",
|
|
74
|
+
message: `确定发布版本 ${newVersion}`,
|
|
75
|
+
type: "confirm",
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
if (push) {
|
|
80
|
+
execa.sync("git", ["push"], {
|
|
81
|
+
cwd: APPDB_DIR,
|
|
82
|
+
});
|
|
83
|
+
// 更新版本
|
|
84
|
+
env.set({ APP_VERSION: newVersion });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async generateTag() {
|
|
89
|
+
const currentVersion = Semver.parse(env.get("APP_VERSION"));
|
|
90
|
+
if (currentVersion == null) throw new Error("无法获取当前版本号");
|
|
91
|
+
|
|
92
|
+
const newVersion = Semver.inc(currentVersion, "patch");
|
|
93
|
+
const answer = await inquirer.prompt({
|
|
94
|
+
name: "version",
|
|
95
|
+
message: "请填写新的版本号",
|
|
96
|
+
type: "input",
|
|
97
|
+
default: () => newVersion,
|
|
98
|
+
validate: (input) => {
|
|
99
|
+
let v = Semver.parse(input);
|
|
100
|
+
if (v) {
|
|
101
|
+
if (Semver.lte(v, currentVersion)) {
|
|
102
|
+
return "该版本号小于当前版本";
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
} else {
|
|
106
|
+
return "请输入正确的版本号";
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
tag: `${REGISTRY_REPO}/${env.get("APP_ID")}:${answer.version}`,
|
|
113
|
+
newVersion: answer.version,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 检测当前系统是否安装 qemu 软件
|
|
2
|
+
import commandExist from "command-exists";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
export class CheckQemu {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.commands = ["qemu-img", "qemu-system-x86_64", "pgrep"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init() {
|
|
11
|
+
let ps = [];
|
|
12
|
+
this.commands.forEach((cmd) => {
|
|
13
|
+
let p = new Promise((resolve, reject) => {
|
|
14
|
+
commandExist(cmd)
|
|
15
|
+
.then(resolve)
|
|
16
|
+
.catch(() => {
|
|
17
|
+
console.error(
|
|
18
|
+
chalk.red(`${cmd} 命令没有发现,请通过系统包管理器安装对应的包`)
|
|
19
|
+
);
|
|
20
|
+
reject();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
ps.push(p);
|
|
24
|
+
});
|
|
25
|
+
return Promise.all(ps);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/box/hportal.js
CHANGED
package/lib/box/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import { QemuVM, QemuResource } from "./qemu_vm_mgr.js";
|
|
|
6
6
|
import { contextDirname } from "../utils.js";
|
|
7
7
|
import env, { sdkEnv } from "../env.js";
|
|
8
8
|
import { HportalManager, showBoxInfo } from "./hportal.js";
|
|
9
|
+
import { CheckQemu } from "./check_qemu.js";
|
|
9
10
|
|
|
10
|
-
async function initQemuVM() {
|
|
11
|
+
async function initQemuVM(ensureResources = false) {
|
|
11
12
|
let defaultSchemeFile = path.join(
|
|
12
13
|
contextDirname(),
|
|
13
14
|
"box",
|
|
@@ -24,9 +25,11 @@ async function initQemuVM() {
|
|
|
24
25
|
scheme.path = path.join(contextDirname(), "box-emulator", scheme.path);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (ensureResources) {
|
|
29
|
+
// 使用本地网络 api 调试 new QemuResource(scheme.path, "http://127.0.0.1");
|
|
30
|
+
let resource = new QemuResource(scheme.path);
|
|
31
|
+
await resource.init();
|
|
32
|
+
}
|
|
30
33
|
|
|
31
34
|
let m = new QemuVM(scheme);
|
|
32
35
|
return m;
|
|
@@ -44,21 +47,35 @@ export function boxCommand(box) {
|
|
|
44
47
|
command: "create",
|
|
45
48
|
desc: "创建一个虚拟盒子,并注册运行",
|
|
46
49
|
handler: async () => {
|
|
47
|
-
let
|
|
48
|
-
|
|
50
|
+
let cq = new CheckQemu();
|
|
51
|
+
await cq.init();
|
|
52
|
+
|
|
53
|
+
let boxId;
|
|
54
|
+
let m = await initQemuVM(true);
|
|
55
|
+
let answer = await m.askBoxInfo();
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// 创建盒子阶段,如果出错,将直接删除所有的资源。
|
|
59
|
+
boxId = await m.createVM(answer);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(error);
|
|
62
|
+
await m.cleanVM(answer.boxName);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
// 如果没有指定默认的盒子名称将会将第一个盒子作为默认的盒子
|
|
50
67
|
let defaultBoxName = env.get("DEFAULT_BOXNAME");
|
|
51
68
|
if (!defaultBoxName) {
|
|
52
69
|
// 新创建的盒子在初始化的时候,盒子里面需要安装hc的组件,需要一点时间,
|
|
53
70
|
// 所以这里并不对sdk url进行校验是否有效
|
|
54
|
-
await sdkEnv.setDefaultBoxName(boxName, false);
|
|
71
|
+
await sdkEnv.setDefaultBoxName(answer.boxName, false);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
let h = await initHportal();
|
|
58
75
|
console.log("添加盒子到 Hportal ......");
|
|
59
|
-
await h.addBox(boxName);
|
|
76
|
+
await h.addBox(answer.boxName);
|
|
60
77
|
console.log("正在使用管理员帐号密码登录中......");
|
|
61
|
-
await h.loginBox(boxId,
|
|
78
|
+
await h.loginBox(boxId, answer.adminName, answer.adminPass);
|
|
62
79
|
console.log("登录成功!");
|
|
63
80
|
},
|
|
64
81
|
},
|
|
@@ -107,7 +124,7 @@ export function boxCommand(box) {
|
|
|
107
124
|
let rmInfos = await rm.boxs(boxName, defaultBoxName);
|
|
108
125
|
|
|
109
126
|
// 过滤条件不满足
|
|
110
|
-
if (vmInfos.length == 0 && rmInfos == 0 && boxName
|
|
127
|
+
if (vmInfos.length == 0 && rmInfos == 0 && boxName) {
|
|
111
128
|
console.log(`${boxName} 盒子不存在`);
|
|
112
129
|
let allVmInfos = await m.infoVM("", defaultBoxName);
|
|
113
130
|
let allRmInfos = await rm.boxs("", defaultBoxName);
|
package/lib/box/qemu_vm_mgr.js
CHANGED
|
@@ -8,6 +8,7 @@ import http from "node:http";
|
|
|
8
8
|
import lz4 from "lz4";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import net from "node:net";
|
|
11
|
+
import fetch from "node-fetch";
|
|
11
12
|
|
|
12
13
|
async function getFreePort() {
|
|
13
14
|
return new Promise((resolve, reject) => {
|
|
@@ -37,7 +38,7 @@ export class QemuResource {
|
|
|
37
38
|
* downloadUrl system-disk 下载路径
|
|
38
39
|
*/
|
|
39
40
|
constructor(
|
|
40
|
-
diskDir = "~/.
|
|
41
|
+
diskDir = "~/.cache/box-emulator",
|
|
41
42
|
downloadUrl = "https://dl.lazycat.cloud/sdk/vm"
|
|
42
43
|
) {
|
|
43
44
|
this.diskDir = diskDir;
|
|
@@ -46,21 +47,77 @@ export class QemuResource {
|
|
|
46
47
|
|
|
47
48
|
async init() {
|
|
48
49
|
fs.mkdirSync(this.diskDir, { recursive: true });
|
|
49
|
-
await this.
|
|
50
|
+
let shouldUpdate = await this.shouldUpdate();
|
|
51
|
+
if (shouldUpdate) {
|
|
52
|
+
let answer = await inquirer.prompt([
|
|
53
|
+
{
|
|
54
|
+
type: "confirm",
|
|
55
|
+
name: "yesorno",
|
|
56
|
+
message: `检测到盒子系统具有更新,是否需要重新下载盒子系统镜像?`,
|
|
57
|
+
default: true,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
if (!answer.yesorno) {
|
|
61
|
+
shouldUpdate = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await this.ensureSystemDisk(shouldUpdate);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 根据下载到的 md5 和本地缓存的 md5 进行比较,如果不一样,则代表具有更新,
|
|
69
|
+
* 然后提示用户是否更新。
|
|
70
|
+
* 如果在获取 md5 的过程中,发生网络错误,则跳过更新检查。
|
|
71
|
+
* 目前的更新,只是使用 md5 来判断是否需要重新下载 md5, 并不会使用 md5 来判断所下载的数据是否正确。
|
|
72
|
+
*/
|
|
73
|
+
async shouldUpdate() {
|
|
74
|
+
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.lz4.md5");
|
|
75
|
+
|
|
76
|
+
// 如果不存在 md5 文件,直接返回需要更新
|
|
77
|
+
try {
|
|
78
|
+
fs.accessSync(oldMd5Path);
|
|
79
|
+
} catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 拉取最新的 md5
|
|
84
|
+
let newMd5;
|
|
85
|
+
try {
|
|
86
|
+
let url = `${this.downloadUrl}/disk-system.qcow2.lz4.md5`;
|
|
87
|
+
let res = await fetch(url);
|
|
88
|
+
if (res.status !== 200) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
newMd5 = await res.text();
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let oldMd5 = fs.readFileSync(oldMd5Path, { encoding: "utf-8" });
|
|
97
|
+
return oldMd5 !== newMd5;
|
|
50
98
|
}
|
|
51
99
|
|
|
52
100
|
/**
|
|
53
101
|
* 确保 qemu 系统盘的镜像存在,如果不存在,将会从 downloadUrl 中下载
|
|
54
102
|
*/
|
|
55
|
-
async ensureSystemDisk() {
|
|
103
|
+
async ensureSystemDisk(update = false) {
|
|
56
104
|
let systemDiskFile = path.join(this.diskDir, "disk-system.qcow2");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
if (!update) {
|
|
106
|
+
try {
|
|
107
|
+
fs.accessSync(systemDiskFile);
|
|
108
|
+
return;
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
61
111
|
|
|
62
112
|
await this.downloadSystemDisk();
|
|
63
113
|
await this.downloadPrivateKey();
|
|
114
|
+
await this.downloadSystemDiskMd5();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async downloadSystemDiskMd5() {
|
|
118
|
+
let savePath = path.join(this.diskDir, "disk-system.qcow2.lz4.md5");
|
|
119
|
+
let url = `${this.downloadUrl}/disk-system.qcow2.lz4.md5`;
|
|
120
|
+
await this.download(url, savePath);
|
|
64
121
|
}
|
|
65
122
|
|
|
66
123
|
async downloadSystemDisk() {
|
|
@@ -144,7 +201,10 @@ export class QemuVM {
|
|
|
144
201
|
let args = await this.buildQemuArgs(name, vmDir);
|
|
145
202
|
let p = spawn("qemu-system-x86_64", args, {
|
|
146
203
|
detached: true,
|
|
147
|
-
stdio: ["
|
|
204
|
+
stdio: ["pipe", "ignore", "inherit"],
|
|
205
|
+
});
|
|
206
|
+
p.on("error", (e) => {
|
|
207
|
+
throw e;
|
|
148
208
|
});
|
|
149
209
|
console.log("启动中...");
|
|
150
210
|
|
|
@@ -165,14 +225,21 @@ export class QemuVM {
|
|
|
165
225
|
return;
|
|
166
226
|
}
|
|
167
227
|
|
|
168
|
-
let
|
|
169
|
-
if (
|
|
170
|
-
p.unref();
|
|
171
|
-
resolve(boxid);
|
|
228
|
+
let boxId = this.readBoxid(name);
|
|
229
|
+
if (boxId) {
|
|
172
230
|
clearInterval(id);
|
|
231
|
+
p.unref();
|
|
232
|
+
resolve(boxId);
|
|
173
233
|
console.log("启动成功!");
|
|
174
234
|
}
|
|
175
235
|
}, 1000);
|
|
236
|
+
|
|
237
|
+
// 当在等待期间,ctrl-c 退出
|
|
238
|
+
process.on("SIGINT", () => {
|
|
239
|
+
p.kill();
|
|
240
|
+
clearInterval(id);
|
|
241
|
+
reject("exit");
|
|
242
|
+
});
|
|
176
243
|
});
|
|
177
244
|
}
|
|
178
245
|
|
|
@@ -200,22 +267,19 @@ export class QemuVM {
|
|
|
200
267
|
/**
|
|
201
268
|
* 创建一个指定的虚拟机,并启动等待注册成功。
|
|
202
269
|
**/
|
|
203
|
-
async createVM() {
|
|
204
|
-
let
|
|
205
|
-
|
|
206
|
-
let vmDir = this.ensureVmDir(answer.boxName);
|
|
270
|
+
async createVM({ boxName, adminName, adminPass }) {
|
|
271
|
+
let vmDir = this.ensureVmDir(boxName);
|
|
207
272
|
this.ensureVolumeDir(vmDir);
|
|
208
273
|
await this.buildDisks(vmDir);
|
|
209
|
-
let boxId = await this.startVM(
|
|
274
|
+
let boxId = await this.startVM(boxName, vmDir);
|
|
210
275
|
console.log("盒子ID: ", boxId);
|
|
211
276
|
|
|
212
|
-
|
|
213
|
-
return {
|
|
277
|
+
await this.registerVM(boxId, {
|
|
214
278
|
boxName,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
279
|
+
adminName,
|
|
280
|
+
adminPass,
|
|
281
|
+
});
|
|
282
|
+
return boxId;
|
|
219
283
|
}
|
|
220
284
|
|
|
221
285
|
/**
|
|
@@ -244,29 +308,20 @@ export class QemuVM {
|
|
|
244
308
|
mask: "*",
|
|
245
309
|
validate: noEmpty,
|
|
246
310
|
},
|
|
247
|
-
{
|
|
248
|
-
type: "input",
|
|
249
|
-
name: "origin",
|
|
250
|
-
message: "注册服务器地址",
|
|
251
|
-
default: "origin.lazycat.cloud",
|
|
252
|
-
},
|
|
253
311
|
]);
|
|
254
312
|
}
|
|
255
313
|
|
|
256
314
|
/**
|
|
257
315
|
* 调用 hportal client 进行注册
|
|
258
316
|
**/
|
|
259
|
-
async registerVM(boxid, { boxName, adminName, adminPass
|
|
317
|
+
async registerVM(boxid, { boxName, adminName, adminPass }) {
|
|
260
318
|
// prettier-ignore
|
|
261
|
-
spawnSync("
|
|
262
|
-
"-S",
|
|
263
|
-
"home-portal-client",
|
|
319
|
+
spawnSync("home-portal-client",[
|
|
264
320
|
"-setup", "-boxid", boxid,
|
|
265
321
|
"-boxname", boxName,
|
|
266
322
|
"-user", adminName,
|
|
267
323
|
"-password", adminPass,
|
|
268
|
-
|
|
269
|
-
], {stdio: ["inherit", "inherit", "inherit"]})
|
|
324
|
+
], {stdio: "inherit"})
|
|
270
325
|
return boxName;
|
|
271
326
|
}
|
|
272
327
|
|
|
@@ -350,7 +405,7 @@ export class QemuVM {
|
|
|
350
405
|
// prettier-ignore
|
|
351
406
|
let args = [
|
|
352
407
|
"-name", `${this.scheme.name}-${this.scheme.uuid}-vm-${name}`,
|
|
353
|
-
"-machine", "pc,accel=kvm",
|
|
408
|
+
"-machine", "pc,accel=kvm,accel=kvf,accel=xen,accel=hax,accel=nvmm,accel=whpx,accel=tcg",
|
|
354
409
|
"-m", `${this.scheme.memory}`,
|
|
355
410
|
"-smp", "4,sockets=4,cores=1,threads=1",
|
|
356
411
|
"-bios", bios,
|
|
@@ -394,16 +449,33 @@ export class QemuVM {
|
|
|
394
449
|
console.log(`${name} 盒子已停止`);
|
|
395
450
|
}
|
|
396
451
|
|
|
452
|
+
/**
|
|
453
|
+
* 清理盒子,用于注册失败或者在注册过程中用户手动中止的情况
|
|
454
|
+
*/
|
|
455
|
+
async cleanVM(name) {
|
|
456
|
+
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
fs.accessSync(vmDir);
|
|
460
|
+
} catch {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
464
|
+
}
|
|
465
|
+
|
|
397
466
|
/**
|
|
398
467
|
* 删除虚拟盒子
|
|
468
|
+
* @param {boolean} silence - 是否提示错误,如果被删除的盒子不存在
|
|
399
469
|
*/
|
|
400
|
-
async deleteVM(name) {
|
|
470
|
+
async deleteVM(name, silence = false) {
|
|
401
471
|
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
402
472
|
|
|
403
473
|
try {
|
|
404
474
|
fs.accessSync(vmDir);
|
|
405
475
|
} catch {
|
|
406
|
-
|
|
476
|
+
if (!silence) {
|
|
477
|
+
console.log(`${name} 盒子不存在或者该盒子为一个真实盒子`);
|
|
478
|
+
}
|
|
407
479
|
return;
|
|
408
480
|
}
|
|
409
481
|
|
package/lib/builder.js
CHANGED
|
@@ -9,7 +9,7 @@ import { DockerfileParser } from "dockerfile-ast";
|
|
|
9
9
|
import glob from "fast-glob";
|
|
10
10
|
import ignore from "@balena/dockerignore";
|
|
11
11
|
import { createLogUpdate } from "log-update";
|
|
12
|
-
import env from "./env.js";
|
|
12
|
+
import env, { sdkEnv } from "./env.js";
|
|
13
13
|
import Dockerode from "dockerode";
|
|
14
14
|
|
|
15
15
|
export const PRE_BUILD_FILE = "build.sh";
|
|
@@ -209,7 +209,7 @@ export default class Builder {
|
|
|
209
209
|
*/
|
|
210
210
|
async dockerRemoteBuildV2(contextDir, dockerfile, tag = env.get("APP_ID")) {
|
|
211
211
|
const src = await this.collectContextFromDockerFile(contextDir, dockerfile);
|
|
212
|
-
const host = new URL(
|
|
212
|
+
const host = new URL(sdkEnv.sdkUrl).host;
|
|
213
213
|
console.log(host);
|
|
214
214
|
const docker = await new DockerClient(host).init();
|
|
215
215
|
try {
|
package/lib/dev.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import execa from "execa";
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
3
|
import fs from "fs";
|
|
4
|
+
import chalk from "chalk";
|
|
4
5
|
import {
|
|
5
6
|
GitIgnore,
|
|
6
7
|
isDirSync,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
APP_SDK_HOSTNAME,
|
|
10
11
|
} from "./utils.js";
|
|
11
12
|
import path from "path";
|
|
12
|
-
import env from "./env.js";
|
|
13
|
+
import env, { sdkEnv } from "./env.js";
|
|
13
14
|
import _get from "lodash.get";
|
|
14
15
|
import { execPreBuild } from "./builder.js";
|
|
15
16
|
import Key from "./key.js";
|
|
@@ -18,7 +19,7 @@ import chokidar from "chokidar";
|
|
|
18
19
|
import Archiver from "../lib/archiver.js";
|
|
19
20
|
|
|
20
21
|
async function sdkSSHAddr(env) {
|
|
21
|
-
let url =
|
|
22
|
+
let url = sdkEnv.sdkUrl;
|
|
22
23
|
return `${APP_SDK_HOSTNAME}@${urlHostname(url)}:2222`;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -136,6 +137,15 @@ class DevModule {
|
|
|
136
137
|
"-i",
|
|
137
138
|
keyFile,
|
|
138
139
|
].join(" ");
|
|
140
|
+
// 检查rsync工具是否存在:提示用户
|
|
141
|
+
try {
|
|
142
|
+
execSync("rsync -V", {
|
|
143
|
+
stdio: "ignore",
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
console.log(chalk.red("请检查 rsync 是否安装,路径是否正确!"));
|
|
147
|
+
process.exit();
|
|
148
|
+
}
|
|
139
149
|
execSync(
|
|
140
150
|
`rsync --rsh='${rsh}' -czrPR . root@${appAddr}:/root/${this.appId} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete`,
|
|
141
151
|
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
@@ -196,6 +206,7 @@ class DevModule {
|
|
|
196
206
|
ignoreInitial: true,
|
|
197
207
|
})
|
|
198
208
|
.on("all", (event, path) => {
|
|
209
|
+
// console.log(event, path);
|
|
199
210
|
this.syncProject(keyFile, appAddr);
|
|
200
211
|
});
|
|
201
212
|
}
|
|
@@ -253,7 +264,7 @@ class DevModule {
|
|
|
253
264
|
|
|
254
265
|
try {
|
|
255
266
|
// 确保 sdk 能通过公钥正常访问
|
|
256
|
-
await new Key().ensure(
|
|
267
|
+
await new Key().ensure(sdkEnv.sdkUrl);
|
|
257
268
|
|
|
258
269
|
keyFile = path.join(this.tempDir, "debug/ssh/id_ed25519");
|
|
259
270
|
appAddr = this.getAddress();
|
package/lib/env.js
CHANGED
package/lib/git/git-commit.sh
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazycatcloud/lzc-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "lazycat cloud developer kit",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "mocha",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"template",
|
|
16
16
|
"scripts",
|
|
17
|
-
"lib"
|
|
17
|
+
"lib",
|
|
18
|
+
"cmds"
|
|
18
19
|
],
|
|
19
20
|
"bin": {
|
|
20
21
|
"lzc-cli": "./scripts/cli.js"
|
|
@@ -30,8 +31,7 @@
|
|
|
30
31
|
"archiver": "^5.3.0",
|
|
31
32
|
"chalk": "^4.1.2",
|
|
32
33
|
"chokidar": "^3.5.3",
|
|
33
|
-
"
|
|
34
|
-
"commander-completion": "^1.0.1",
|
|
34
|
+
"command-exists": "^1.2.9",
|
|
35
35
|
"dockerfile-ast": "^0.4.1",
|
|
36
36
|
"dockerode": "^3.3.1",
|
|
37
37
|
"ejs": "^3.1.6",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"ora": "^6.0.1",
|
|
56
56
|
"semver": "^7.3.5",
|
|
57
57
|
"ssh2": "^1.5.0",
|
|
58
|
-
"ssh2-promise": "^1.0.2"
|
|
58
|
+
"ssh2-promise": "^1.0.2",
|
|
59
|
+
"yargs": "^17.5.1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"chai": "^4.3.6",
|
package/scripts/cli.js
CHANGED
|
@@ -129,7 +129,7 @@ program.command({
|
|
|
129
129
|
args.option("u", {
|
|
130
130
|
alias: "user",
|
|
131
131
|
describe: "多实例对应的用户",
|
|
132
|
-
demandOption:
|
|
132
|
+
demandOption: false,
|
|
133
133
|
type: "string",
|
|
134
134
|
});
|
|
135
135
|
},
|
|
@@ -159,4 +159,4 @@ program.command({
|
|
|
159
159
|
|
|
160
160
|
boxCommand(program);
|
|
161
161
|
|
|
162
|
-
program.parse();
|
|
162
|
+
program.showHelpOnFail(false, "使用 lzc-cli help 查看更多帮助").parse();
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Forked from https://github.com/isaacs/npm/blob/v1.3.17/lib/utils/completion.sh
|
|
2
|
-
# Forked from https://github.com/twolfson/foundry/blob/4.3.3/bin/completion/foundry
|
|
3
|
-
#!/bin/bash
|
|
4
|
-
###-begin-lzc-cli-completion-###
|
|
5
|
-
#
|
|
6
|
-
# foundry command completion script
|
|
7
|
-
#
|
|
8
|
-
# Installation: lzc-cli completion >> ~/.bashrc (or ~/.zshrc)
|
|
9
|
-
# Or, maybe: lzc-cli completion > /usr/local/etc/bash_completion.d/lzc-cli
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
COMP_WORDBREAKS=${COMP_WORDBREAKS/=/}
|
|
13
|
-
COMP_WORDBREAKS=${COMP_WORDBREAKS/@/}
|
|
14
|
-
export COMP_WORDBREAKS
|
|
15
|
-
|
|
16
|
-
if type complete &>/dev/null; then
|
|
17
|
-
_lzc_cli_completion () {
|
|
18
|
-
local si="$IFS"
|
|
19
|
-
IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
|
|
20
|
-
COMP_LINE="$COMP_LINE" \
|
|
21
|
-
COMP_POINT="$COMP_POINT" \
|
|
22
|
-
lzc-cli completion -- "${COMP_WORDS[@]}" \
|
|
23
|
-
2>/dev/null)) || return $?
|
|
24
|
-
IFS="$si"
|
|
25
|
-
}
|
|
26
|
-
complete -F _lzc_cli_completion lzc-cli
|
|
27
|
-
# DEV: We removed `compdef` due to issues with `zsh` (zsh 5.0.0 (x86_64-unknown-linux-gnu))
|
|
28
|
-
elif type compctl &>/dev/null; then
|
|
29
|
-
_lzc_cli_completion () {
|
|
30
|
-
local cword line point words si
|
|
31
|
-
read -Ac words
|
|
32
|
-
read -cn cword
|
|
33
|
-
let cword-=1
|
|
34
|
-
read -l line
|
|
35
|
-
read -ln point
|
|
36
|
-
si="$IFS"
|
|
37
|
-
IFS=$'\n' reply=($(COMP_CWORD="$cword" \
|
|
38
|
-
COMP_LINE="$line" \
|
|
39
|
-
COMP_POINT="$point" \
|
|
40
|
-
lzc-cli completion -- "${words[@]}" \
|
|
41
|
-
2>/dev/null)) || return $?
|
|
42
|
-
IFS="$si"
|
|
43
|
-
}
|
|
44
|
-
compctl -K _lzc_cli_completion lzc-cli
|
|
45
|
-
fi
|
|
46
|
-
###-end-lzc-cli-completion-###
|