@lazycatcloud/lzc-cli 1.1.2 → 1.1.5
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 +124 -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 +29 -10
- package/lib/box/qemu_vm_mgr.js +131 -46
- package/lib/builder.js +14 -3
- package/lib/dev.js +14 -4
- package/lib/env.js +1 -1
- package/lib/git/git-commit.sh +3 -3
- package/lib/sdk.js +25 -0
- package/package.json +7 -6
- package/scripts/cli.js +2 -2
- package/template/vue/vue.config.js +15 -0
- package/scripts/auto-completion.sh +0 -46
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,13 @@ async function initQemuVM() {
|
|
|
24
25
|
scheme.path = path.join(contextDirname(), "box-emulator", scheme.path);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
fs.mkdirSync(scheme.path, { recursive: true });
|
|
29
|
+
|
|
30
|
+
if (ensureResources) {
|
|
31
|
+
// 使用本地网络 api 调试 new QemuResource(scheme.path, "http://127.0.0.1");
|
|
32
|
+
let resource = new QemuResource(scheme.path);
|
|
33
|
+
await resource.init();
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
let m = new QemuVM(scheme);
|
|
32
37
|
return m;
|
|
@@ -44,21 +49,35 @@ export function boxCommand(box) {
|
|
|
44
49
|
command: "create",
|
|
45
50
|
desc: "创建一个虚拟盒子,并注册运行",
|
|
46
51
|
handler: async () => {
|
|
47
|
-
let
|
|
48
|
-
|
|
52
|
+
let cq = new CheckQemu();
|
|
53
|
+
await cq.init();
|
|
54
|
+
|
|
55
|
+
let boxId;
|
|
56
|
+
let m = await initQemuVM(true);
|
|
57
|
+
let answer = await m.askBoxInfo();
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// 创建盒子阶段,如果出错,将直接删除所有的资源。
|
|
61
|
+
boxId = await m.createVM(answer);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(error);
|
|
64
|
+
await m.cleanVM(answer.boxName);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
// 如果没有指定默认的盒子名称将会将第一个盒子作为默认的盒子
|
|
50
69
|
let defaultBoxName = env.get("DEFAULT_BOXNAME");
|
|
51
70
|
if (!defaultBoxName) {
|
|
52
71
|
// 新创建的盒子在初始化的时候,盒子里面需要安装hc的组件,需要一点时间,
|
|
53
72
|
// 所以这里并不对sdk url进行校验是否有效
|
|
54
|
-
await sdkEnv.setDefaultBoxName(boxName, false);
|
|
73
|
+
await sdkEnv.setDefaultBoxName(answer.boxName, false);
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
let h = await initHportal();
|
|
58
77
|
console.log("添加盒子到 Hportal ......");
|
|
59
|
-
await h.addBox(boxName);
|
|
78
|
+
await h.addBox(answer.boxName);
|
|
60
79
|
console.log("正在使用管理员帐号密码登录中......");
|
|
61
|
-
await h.loginBox(boxId,
|
|
80
|
+
await h.loginBox(boxId, answer.adminName, answer.adminPass);
|
|
62
81
|
console.log("登录成功!");
|
|
63
82
|
},
|
|
64
83
|
},
|
|
@@ -107,7 +126,7 @@ export function boxCommand(box) {
|
|
|
107
126
|
let rmInfos = await rm.boxs(boxName, defaultBoxName);
|
|
108
127
|
|
|
109
128
|
// 过滤条件不满足
|
|
110
|
-
if (vmInfos.length == 0 && rmInfos == 0 && boxName
|
|
129
|
+
if (vmInfos.length == 0 && rmInfos == 0 && boxName) {
|
|
111
130
|
console.log(`${boxName} 盒子不存在`);
|
|
112
131
|
let allVmInfos = await m.infoVM("", defaultBoxName);
|
|
113
132
|
let allRmInfos = await rm.boxs("", defaultBoxName);
|
package/lib/box/qemu_vm_mgr.js
CHANGED
|
@@ -5,9 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import https from "node:https";
|
|
7
7
|
import http from "node:http";
|
|
8
|
-
import lz4 from "lz4";
|
|
9
8
|
import process from "node:process";
|
|
10
9
|
import net from "node:net";
|
|
10
|
+
import fetch from "node-fetch";
|
|
11
|
+
import zlib from "node:zlib";
|
|
12
|
+
import os from "node:os";
|
|
11
13
|
|
|
12
14
|
async function getFreePort() {
|
|
13
15
|
return new Promise((resolve, reject) => {
|
|
@@ -37,8 +39,8 @@ export class QemuResource {
|
|
|
37
39
|
* downloadUrl system-disk 下载路径
|
|
38
40
|
*/
|
|
39
41
|
constructor(
|
|
40
|
-
diskDir = "~/.
|
|
41
|
-
downloadUrl = "https://dl.lazycat.cloud/sdk
|
|
42
|
+
diskDir = "~/.cache/box-emulator",
|
|
43
|
+
downloadUrl = "https://dl.lazycat.cloud/sdk"
|
|
42
44
|
) {
|
|
43
45
|
this.diskDir = diskDir;
|
|
44
46
|
this.downloadUrl = downloadUrl;
|
|
@@ -46,32 +48,96 @@ export class QemuResource {
|
|
|
46
48
|
|
|
47
49
|
async init() {
|
|
48
50
|
fs.mkdirSync(this.diskDir, { recursive: true });
|
|
49
|
-
await this.
|
|
51
|
+
let shouldUpdate = await this.shouldUpdate();
|
|
52
|
+
if (shouldUpdate) {
|
|
53
|
+
let answer = await inquirer.prompt([
|
|
54
|
+
{
|
|
55
|
+
type: "confirm",
|
|
56
|
+
name: "yesorno",
|
|
57
|
+
message: `检测到盒子系统具有更新,是否需要重新下载盒子系统镜像?`,
|
|
58
|
+
default: true,
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
if (!answer.yesorno) {
|
|
62
|
+
shouldUpdate = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await this.ensureSystemDisk(shouldUpdate);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 根据下载到的 md5 和本地缓存的 md5 进行比较,如果不一样,则代表具有更新,
|
|
70
|
+
* 然后提示用户是否更新。
|
|
71
|
+
* 如果在获取 md5 的过程中,发生网络错误,则跳过更新检查。
|
|
72
|
+
* 目前的更新,只是使用 md5 来判断是否需要重新下载 md5, 并不会使用 md5 来判断所下载的数据是否正确。
|
|
73
|
+
*/
|
|
74
|
+
async shouldUpdate() {
|
|
75
|
+
let oldMd5Path = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
76
|
+
|
|
77
|
+
// 如果不存在 md5 文件,直接返回需要更新
|
|
78
|
+
try {
|
|
79
|
+
fs.accessSync(oldMd5Path);
|
|
80
|
+
} catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 拉取最新的 md5
|
|
85
|
+
let newMd5;
|
|
86
|
+
try {
|
|
87
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
88
|
+
let res = await fetch(url);
|
|
89
|
+
if (res.status !== 200) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
newMd5 = await res.text();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let oldMd5 = fs.readFileSync(oldMd5Path, { encoding: "utf-8" });
|
|
98
|
+
return oldMd5 !== newMd5;
|
|
50
99
|
}
|
|
51
100
|
|
|
52
101
|
/**
|
|
53
102
|
* 确保 qemu 系统盘的镜像存在,如果不存在,将会从 downloadUrl 中下载
|
|
54
103
|
*/
|
|
55
|
-
async ensureSystemDisk() {
|
|
104
|
+
async ensureSystemDisk(update = false) {
|
|
56
105
|
let systemDiskFile = path.join(this.diskDir, "disk-system.qcow2");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
if (!update) {
|
|
107
|
+
try {
|
|
108
|
+
fs.accessSync(systemDiskFile);
|
|
109
|
+
return;
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
61
112
|
|
|
113
|
+
await this.downloadOVMF();
|
|
62
114
|
await this.downloadSystemDisk();
|
|
63
115
|
await this.downloadPrivateKey();
|
|
116
|
+
await this.downloadSystemDiskMd5();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async downloadOVMF() {
|
|
120
|
+
let name = `${os.arch()}.OVMF.fd`;
|
|
121
|
+
let savePath = path.join(this.diskDir, name);
|
|
122
|
+
let url = `${this.downloadUrl}/${name}`;
|
|
123
|
+
await this.download(url, savePath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async downloadSystemDiskMd5() {
|
|
127
|
+
let savePath = path.join(this.diskDir, "disk-system.qcow2.md5");
|
|
128
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.md5`;
|
|
129
|
+
await this.download(url, savePath);
|
|
64
130
|
}
|
|
65
131
|
|
|
66
132
|
async downloadSystemDisk() {
|
|
67
133
|
let savePath = path.join(this.diskDir, "disk-system.qcow2");
|
|
68
|
-
let url = `${this.downloadUrl}/disk-system.qcow2.
|
|
134
|
+
let url = `${this.downloadUrl}/vm/disk-system.qcow2.gz`;
|
|
69
135
|
await this.download(url, savePath, true);
|
|
70
136
|
}
|
|
71
137
|
|
|
72
138
|
async downloadPrivateKey() {
|
|
73
139
|
let savePath = path.join(this.diskDir, "id_rsa");
|
|
74
|
-
let url = `${this.downloadUrl}/id_rsa`;
|
|
140
|
+
let url = `${this.downloadUrl}/vm/id_rsa`;
|
|
75
141
|
await this.download(url, savePath);
|
|
76
142
|
fs.chmodSync(savePath, 0o400);
|
|
77
143
|
return;
|
|
@@ -90,7 +156,7 @@ export class QemuResource {
|
|
|
90
156
|
);
|
|
91
157
|
}
|
|
92
158
|
|
|
93
|
-
download(url, savePath,
|
|
159
|
+
download(url, savePath, enableGzip = false) {
|
|
94
160
|
let tmpPath = savePath + ".tmp";
|
|
95
161
|
const options = new URL(url);
|
|
96
162
|
let request = url.startsWith("https") ? https.request : http.request;
|
|
@@ -110,8 +176,8 @@ export class QemuResource {
|
|
|
110
176
|
});
|
|
111
177
|
|
|
112
178
|
let outputFile = fs.createWriteStream(tmpPath, { flags: "w+" });
|
|
113
|
-
if (
|
|
114
|
-
let decoder =
|
|
179
|
+
if (enableGzip) {
|
|
180
|
+
let decoder = zlib.createUnzip();
|
|
115
181
|
res.pipe(decoder).pipe(outputFile);
|
|
116
182
|
} else {
|
|
117
183
|
res.pipe(outputFile);
|
|
@@ -144,7 +210,10 @@ export class QemuVM {
|
|
|
144
210
|
let args = await this.buildQemuArgs(name, vmDir);
|
|
145
211
|
let p = spawn("qemu-system-x86_64", args, {
|
|
146
212
|
detached: true,
|
|
147
|
-
stdio: ["
|
|
213
|
+
stdio: ["pipe", "ignore", "inherit"],
|
|
214
|
+
});
|
|
215
|
+
p.on("error", (e) => {
|
|
216
|
+
throw e;
|
|
148
217
|
});
|
|
149
218
|
console.log("启动中...");
|
|
150
219
|
|
|
@@ -165,14 +234,21 @@ export class QemuVM {
|
|
|
165
234
|
return;
|
|
166
235
|
}
|
|
167
236
|
|
|
168
|
-
let
|
|
169
|
-
if (
|
|
170
|
-
p.unref();
|
|
171
|
-
resolve(boxid);
|
|
237
|
+
let boxId = this.readBoxid(name);
|
|
238
|
+
if (boxId) {
|
|
172
239
|
clearInterval(id);
|
|
240
|
+
p.unref();
|
|
241
|
+
resolve(boxId);
|
|
173
242
|
console.log("启动成功!");
|
|
174
243
|
}
|
|
175
244
|
}, 1000);
|
|
245
|
+
|
|
246
|
+
// 当在等待期间,ctrl-c 退出
|
|
247
|
+
process.on("SIGINT", () => {
|
|
248
|
+
p.kill();
|
|
249
|
+
clearInterval(id);
|
|
250
|
+
reject("exit");
|
|
251
|
+
});
|
|
176
252
|
});
|
|
177
253
|
}
|
|
178
254
|
|
|
@@ -200,22 +276,19 @@ export class QemuVM {
|
|
|
200
276
|
/**
|
|
201
277
|
* 创建一个指定的虚拟机,并启动等待注册成功。
|
|
202
278
|
**/
|
|
203
|
-
async createVM() {
|
|
204
|
-
let
|
|
205
|
-
|
|
206
|
-
let vmDir = this.ensureVmDir(answer.boxName);
|
|
279
|
+
async createVM({ boxName, adminName, adminPass }) {
|
|
280
|
+
let vmDir = this.ensureVmDir(boxName);
|
|
207
281
|
this.ensureVolumeDir(vmDir);
|
|
208
282
|
await this.buildDisks(vmDir);
|
|
209
|
-
let boxId = await this.startVM(
|
|
283
|
+
let boxId = await this.startVM(boxName, vmDir);
|
|
210
284
|
console.log("盒子ID: ", boxId);
|
|
211
285
|
|
|
212
|
-
|
|
213
|
-
return {
|
|
286
|
+
await this.registerVM(boxId, {
|
|
214
287
|
boxName,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
288
|
+
adminName,
|
|
289
|
+
adminPass,
|
|
290
|
+
});
|
|
291
|
+
return boxId;
|
|
219
292
|
}
|
|
220
293
|
|
|
221
294
|
/**
|
|
@@ -244,29 +317,20 @@ export class QemuVM {
|
|
|
244
317
|
mask: "*",
|
|
245
318
|
validate: noEmpty,
|
|
246
319
|
},
|
|
247
|
-
{
|
|
248
|
-
type: "input",
|
|
249
|
-
name: "origin",
|
|
250
|
-
message: "注册服务器地址",
|
|
251
|
-
default: "origin.lazycat.cloud",
|
|
252
|
-
},
|
|
253
320
|
]);
|
|
254
321
|
}
|
|
255
322
|
|
|
256
323
|
/**
|
|
257
324
|
* 调用 hportal client 进行注册
|
|
258
325
|
**/
|
|
259
|
-
async registerVM(boxid, { boxName, adminName, adminPass
|
|
326
|
+
async registerVM(boxid, { boxName, adminName, adminPass }) {
|
|
260
327
|
// prettier-ignore
|
|
261
|
-
spawnSync("
|
|
262
|
-
"-S",
|
|
263
|
-
"home-portal-client",
|
|
328
|
+
spawnSync("home-portal-client",[
|
|
264
329
|
"-setup", "-boxid", boxid,
|
|
265
330
|
"-boxname", boxName,
|
|
266
331
|
"-user", adminName,
|
|
267
332
|
"-password", adminPass,
|
|
268
|
-
|
|
269
|
-
], {stdio: ["inherit", "inherit", "inherit"]})
|
|
333
|
+
], {stdio: "inherit"})
|
|
270
334
|
return boxName;
|
|
271
335
|
}
|
|
272
336
|
|
|
@@ -330,7 +394,11 @@ export class QemuVM {
|
|
|
330
394
|
* 寻找默认可用的 bios 文件
|
|
331
395
|
*/
|
|
332
396
|
findBIOS() {
|
|
333
|
-
let includes = [
|
|
397
|
+
let includes = [
|
|
398
|
+
"/usr/share/ovmf/x64/OVMF.fd",
|
|
399
|
+
"/usr/share/ovmf/OVMF.fd",
|
|
400
|
+
path.join(this.scheme.path, `${os.arch()}.OVMF.fd`),
|
|
401
|
+
];
|
|
334
402
|
for (let file of includes) {
|
|
335
403
|
try {
|
|
336
404
|
fs.accessSync(file, fs.constants.F_OK);
|
|
@@ -350,7 +418,7 @@ export class QemuVM {
|
|
|
350
418
|
// prettier-ignore
|
|
351
419
|
let args = [
|
|
352
420
|
"-name", `${this.scheme.name}-${this.scheme.uuid}-vm-${name}`,
|
|
353
|
-
"-machine", "pc,accel=kvm",
|
|
421
|
+
"-machine", "pc,accel=kvm,accel=kvf,accel=xen,accel=hax,accel=nvmm,accel=whpx,accel=tcg",
|
|
354
422
|
"-m", `${this.scheme.memory}`,
|
|
355
423
|
"-smp", "4,sockets=4,cores=1,threads=1",
|
|
356
424
|
"-bios", bios,
|
|
@@ -394,16 +462,33 @@ export class QemuVM {
|
|
|
394
462
|
console.log(`${name} 盒子已停止`);
|
|
395
463
|
}
|
|
396
464
|
|
|
465
|
+
/**
|
|
466
|
+
* 清理盒子,用于注册失败或者在注册过程中用户手动中止的情况
|
|
467
|
+
*/
|
|
468
|
+
async cleanVM(name) {
|
|
469
|
+
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
fs.accessSync(vmDir);
|
|
473
|
+
} catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
fs.rmSync(vmDir, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
|
|
397
479
|
/**
|
|
398
480
|
* 删除虚拟盒子
|
|
481
|
+
* @param {boolean} silence - 是否提示错误,如果被删除的盒子不存在
|
|
399
482
|
*/
|
|
400
|
-
async deleteVM(name) {
|
|
483
|
+
async deleteVM(name, silence = false) {
|
|
401
484
|
let vmDir = path.join(this.scheme.path, `vm-${name}`);
|
|
402
485
|
|
|
403
486
|
try {
|
|
404
487
|
fs.accessSync(vmDir);
|
|
405
488
|
} catch {
|
|
406
|
-
|
|
489
|
+
if (!silence) {
|
|
490
|
+
console.log(`${name} 盒子不存在或者该盒子为一个真实盒子`);
|
|
491
|
+
}
|
|
407
492
|
return;
|
|
408
493
|
}
|
|
409
494
|
|
package/lib/builder.js
CHANGED
|
@@ -4,12 +4,12 @@ import path from "path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import execa from "execa";
|
|
6
6
|
// import { archiveFolder } from "./utils.js";
|
|
7
|
-
import { DockerClient } from "./sdk.js";
|
|
7
|
+
import { dockerPullLzcAppsImage, DockerClient } from "./sdk.js";
|
|
8
8
|
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,10 +209,21 @@ 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 {
|
|
216
|
+
// TODO, 使用 docker.pull 方法会失败, 原因未知, 使用 workaround 方案
|
|
217
|
+
// docker.pull(
|
|
218
|
+
// "hello-world",
|
|
219
|
+
// // "registry.lazycat.cloud/lzc/lzcapp:0.1",
|
|
220
|
+
// // { authconfig: { auth: "bG5rczpsbmtzMjAyMA==" } },
|
|
221
|
+
// (err, stream) => {
|
|
222
|
+
// console.log("====", err, stream);
|
|
223
|
+
// }
|
|
224
|
+
// );
|
|
225
|
+
await dockerPullLzcAppsImage(host);
|
|
226
|
+
|
|
216
227
|
return new Promise(async (resolve, reject) => {
|
|
217
228
|
let refresher = new StatusRefresher();
|
|
218
229
|
|
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,16 +10,17 @@ 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";
|
|
16
17
|
import chokidar from "chokidar";
|
|
18
|
+
import commandExists from "command-exists";
|
|
17
19
|
|
|
18
20
|
import Archiver from "../lib/archiver.js";
|
|
19
21
|
|
|
20
22
|
async function sdkSSHAddr(env) {
|
|
21
|
-
let url =
|
|
23
|
+
let url = sdkEnv.sdkUrl;
|
|
22
24
|
return `${APP_SDK_HOSTNAME}@${urlHostname(url)}:2222`;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -48,7 +50,7 @@ class DevModule {
|
|
|
48
50
|
...env.all,
|
|
49
51
|
APP_IMAGE_NAME: env.get("APP_ID"),
|
|
50
52
|
};
|
|
51
|
-
await execPreBuild(buildDir, allEnv);
|
|
53
|
+
await execPreBuild(buildDir, buildDir, allEnv);
|
|
52
54
|
let arch = new Archiver(this.tempDir);
|
|
53
55
|
await arch.load(this.cwd, allEnv);
|
|
54
56
|
await arch.add(buildDir);
|
|
@@ -136,6 +138,13 @@ class DevModule {
|
|
|
136
138
|
"-i",
|
|
137
139
|
keyFile,
|
|
138
140
|
].join(" ");
|
|
141
|
+
// 检查rsync工具是否存在:提示用户
|
|
142
|
+
const rsyncExisted = commandExists.sync('rsync')
|
|
143
|
+
if (!rsyncExisted) {
|
|
144
|
+
console.log(chalk.red("请检查 rsync 是否安装,路径是否正确!"));
|
|
145
|
+
process.exit();
|
|
146
|
+
}
|
|
147
|
+
// FIXME: 下方执行命令不确定是否有兼容性问题
|
|
139
148
|
execSync(
|
|
140
149
|
`rsync --rsh='${rsh}' -czrPR . root@${appAddr}:/root/${this.appId} --filter=':- .gitignore' --exclude='node_modules' --exclude='.git' --delete`,
|
|
141
150
|
{ stdio: ["ignore", "ignore", "inherit"] }
|
|
@@ -196,6 +205,7 @@ class DevModule {
|
|
|
196
205
|
ignoreInitial: true,
|
|
197
206
|
})
|
|
198
207
|
.on("all", (event, path) => {
|
|
208
|
+
// console.log(event, path);
|
|
199
209
|
this.syncProject(keyFile, appAddr);
|
|
200
210
|
});
|
|
201
211
|
}
|
|
@@ -253,7 +263,7 @@ class DevModule {
|
|
|
253
263
|
|
|
254
264
|
try {
|
|
255
265
|
// 确保 sdk 能通过公钥正常访问
|
|
256
|
-
await new Key().ensure(
|
|
266
|
+
await new Key().ensure(sdkEnv.sdkUrl);
|
|
257
267
|
|
|
258
268
|
keyFile = path.join(this.tempDir, "debug/ssh/id_ed25519");
|
|
259
269
|
appAddr = this.getAddress();
|
package/lib/env.js
CHANGED
package/lib/git/git-commit.sh
CHANGED
package/lib/sdk.js
CHANGED
|
@@ -124,3 +124,28 @@ export class SSHClient {
|
|
|
124
124
|
);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
export async function dockerPullLzcAppsImage(host) {
|
|
129
|
+
console.log(chalk.yellow("* 更新lzcapp镜像, 未来可能会移除此操作"));
|
|
130
|
+
const opts = await connectOptions(host);
|
|
131
|
+
const client = new SSHClient(opts);
|
|
132
|
+
await client.connect();
|
|
133
|
+
try {
|
|
134
|
+
const stream = await client.exec(
|
|
135
|
+
"docker pull registry.lazycat.cloud/lzc/lzcapp:0.1"
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
stream.stdout.pipe(process.stdout);
|
|
140
|
+
stream.stderr.pipe(process.stdout);
|
|
141
|
+
stream.on("close", () => {
|
|
142
|
+
client.close();
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
stream.on("error", (e) => reject(e));
|
|
146
|
+
});
|
|
147
|
+
} catch (e) {
|
|
148
|
+
client.close();
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|