@lazycatcloud/lzc-cli 1.1.0 → 1.1.3

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.
Files changed (60) hide show
  1. package/lib/api.js +34 -36
  2. package/lib/archiver.js +50 -31
  3. package/lib/box/hportal.js +113 -0
  4. package/lib/box/index.js +135 -0
  5. package/lib/box/qemu_vm_mgr.js +553 -0
  6. package/lib/box/schemes/vm_box_system_debian.json +47 -0
  7. package/lib/builder.js +154 -35
  8. package/lib/dev.js +39 -31
  9. package/lib/env.js +276 -58
  10. package/lib/generator.js +31 -0
  11. package/lib/git/git-commit.sh +7 -0
  12. package/lib/git/git-reset.sh +15 -0
  13. package/lib/key.js +14 -25
  14. package/lib/sdk.js +7 -10
  15. package/lib/utils.js +149 -52
  16. package/package.json +14 -2
  17. package/scripts/auto-completion.sh +46 -0
  18. package/scripts/cli.js +134 -70
  19. package/template/_lazycat/app-config +1 -0
  20. package/template/_lazycat/docker-compose.yml.in +3 -5
  21. package/template/golang/README.md +3 -4
  22. package/template/golang/assets/css/bootstrap-responsive.css +26 -23
  23. package/template/golang/assets/css/bootstrap-responsive.min.css +1065 -1
  24. package/template/golang/assets/css/bootstrap.css +733 -362
  25. package/template/golang/assets/css/bootstrap.min.css +5299 -1
  26. package/template/golang/assets/css/rego.css +17 -17
  27. package/template/golang/assets/js/bootstrap.js +1340 -1311
  28. package/template/golang/assets/js/bootstrap.min.js +1240 -5
  29. package/template/golang/assets/js/rego.js +80 -69
  30. package/template/golang/index.html +61 -59
  31. package/template/ionic_vue3/README.md +46 -0
  32. package/template/ionic_vue3/_eslintrc.cjs +24 -0
  33. package/template/ionic_vue3/_gitignore +29 -0
  34. package/template/ionic_vue3/_vscode/extensions.json +6 -0
  35. package/template/ionic_vue3/capacitor.config.ts +10 -0
  36. package/template/ionic_vue3/env.d.ts +1 -0
  37. package/template/ionic_vue3/index.html +13 -0
  38. package/template/ionic_vue3/ionic.config.json +7 -0
  39. package/template/ionic_vue3/package.json +52 -0
  40. package/template/ionic_vue3/postcss.config.js +6 -0
  41. package/template/ionic_vue3/public/favicon.ico +0 -0
  42. package/template/ionic_vue3/src/App.vue +11 -0
  43. package/template/ionic_vue3/src/assets/logo.svg +1 -0
  44. package/template/ionic_vue3/src/index.css +3 -0
  45. package/template/ionic_vue3/src/main.ts +35 -0
  46. package/template/ionic_vue3/src/router/index.ts +15 -0
  47. package/template/ionic_vue3/src/theme/variables.css +231 -0
  48. package/template/ionic_vue3/src/views/Home.vue +38 -0
  49. package/template/ionic_vue3/tailwind.config.js +7 -0
  50. package/template/ionic_vue3/tsconfig.json +16 -0
  51. package/template/ionic_vue3/tsconfig.vite-config.json +8 -0
  52. package/template/ionic_vue3/vite.config.ts +28 -0
  53. package/template/release/golang/build.sh +1 -2
  54. package/template/release/ionic_vue3/Dockerfile +10 -0
  55. package/template/release/ionic_vue3/build.sh +9 -0
  56. package/template/release/ionic_vue3/docker-compose.yml.in +8 -0
  57. package/template/release/vue/Dockerfile +3 -2
  58. package/template/release/vue/build.sh +4 -2
  59. package/template/vue/README.md +5 -0
  60. package/template/vue/babel.config.js +2 -4
@@ -0,0 +1,553 @@
1
+ #!/bin/node
2
+ import fs from "node:fs";
3
+ import { spawn, spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import inquirer from "inquirer";
6
+ import https from "node:https";
7
+ import http from "node:http";
8
+ import lz4 from "lz4";
9
+ import process from "node:process";
10
+ import net from "node:net";
11
+
12
+ async function getFreePort() {
13
+ return new Promise((resolve, reject) => {
14
+ const server = net.createServer();
15
+ server.listen(() => {
16
+ let addr = server.address();
17
+ resolve(addr.port);
18
+ server.close();
19
+ });
20
+ server.on("error", reject);
21
+ });
22
+ }
23
+
24
+ function parseVmPID(vmDir) {
25
+ let p = spawnSync("pgrep", ["-f", path.join(vmDir, "disk-system.qcow2")]);
26
+ if (p.status == 0) {
27
+ return p.stdout.toString();
28
+ } else {
29
+ // 没有找到任何记录
30
+ return "";
31
+ }
32
+ }
33
+
34
+ export class QemuResource {
35
+ /**
36
+ * diskDir qemu 磁盘存储路径,默认 ~/.local/share/lazycat/box-emulator
37
+ * downloadUrl system-disk 下载路径
38
+ */
39
+ constructor(
40
+ diskDir = "~/.local/share/lazycat/box-emulator",
41
+ downloadUrl = "https://dl.lazycat.cloud/sdk/vm"
42
+ ) {
43
+ this.diskDir = diskDir;
44
+ this.downloadUrl = downloadUrl;
45
+ }
46
+
47
+ async init() {
48
+ fs.mkdirSync(this.diskDir, { recursive: true });
49
+ await this.ensureSystemDisk();
50
+ }
51
+
52
+ /**
53
+ * 确保 qemu 系统盘的镜像存在,如果不存在,将会从 downloadUrl 中下载
54
+ */
55
+ async ensureSystemDisk() {
56
+ let systemDiskFile = path.join(this.diskDir, "disk-system.qcow2");
57
+ try {
58
+ fs.accessSync(systemDiskFile);
59
+ return;
60
+ } catch {}
61
+
62
+ await this.downloadSystemDisk();
63
+ await this.downloadPrivateKey();
64
+ }
65
+
66
+ async downloadSystemDisk() {
67
+ let savePath = path.join(this.diskDir, "disk-system.qcow2");
68
+ let url = `${this.downloadUrl}/disk-system.qcow2.lz4`;
69
+ await this.download(url, savePath, true);
70
+ }
71
+
72
+ async downloadPrivateKey() {
73
+ let savePath = path.join(this.diskDir, "id_rsa");
74
+ let url = `${this.downloadUrl}/id_rsa`;
75
+ await this.download(url, savePath);
76
+ fs.chmodSync(savePath, 0o400);
77
+ return;
78
+ }
79
+
80
+ showDownloadingProgress(received, total) {
81
+ var percentage = ((received * 100) / total).toFixed(2);
82
+ process.stdout.write("\r");
83
+ process.stdout.write(
84
+ percentage +
85
+ "% | " +
86
+ received +
87
+ " bytes downloaded out of " +
88
+ total +
89
+ " bytes."
90
+ );
91
+ }
92
+
93
+ download(url, savePath, enableLz4 = false) {
94
+ let tmpPath = savePath + ".tmp";
95
+ const options = new URL(url);
96
+ let request = url.startsWith("https") ? https.request : http.request;
97
+
98
+ return new Promise((resolve, reject) => {
99
+ const req = request(options, (res) => {
100
+ if (res.statusCode != 200) {
101
+ reject(`下载 ${url} 失败`);
102
+ return;
103
+ }
104
+
105
+ let total = parseInt(res.headers["content-length"]);
106
+ let recive = 0;
107
+ res.on("data", (chunk) => {
108
+ recive += chunk.length;
109
+ this.showDownloadingProgress(recive, total);
110
+ });
111
+
112
+ let outputFile = fs.createWriteStream(tmpPath, { flags: "w+" });
113
+ if (enableLz4) {
114
+ let decoder = lz4.createDecoderStream();
115
+ res.pipe(decoder).pipe(outputFile);
116
+ } else {
117
+ res.pipe(outputFile);
118
+ }
119
+
120
+ outputFile.on("error", reject);
121
+ outputFile.on("finish", () => {
122
+ fs.renameSync(tmpPath, savePath);
123
+ resolve();
124
+ });
125
+ });
126
+ req.on("error", reject);
127
+ req.end();
128
+ });
129
+ }
130
+ }
131
+
132
+ export class QemuVM {
133
+ /**
134
+ * @param {Object} scheme - 各个操作系统的配置文件.
135
+ */
136
+ constructor(scheme) {
137
+ this.scheme = scheme;
138
+ }
139
+
140
+ /**
141
+ * 运行虚拟盒子,并等待 boxid 的出现,确保盒子启动正常
142
+ */
143
+ async startVM(name, vmDir) {
144
+ let args = await this.buildQemuArgs(name, vmDir);
145
+ let p = spawn("qemu-system-x86_64", args, {
146
+ detached: true,
147
+ stdio: ["ignore", "ignore", "inherit"],
148
+ });
149
+ console.log("启动中...");
150
+
151
+ // 需要等待 boxid 的出现
152
+ return new Promise((resolve, reject) => {
153
+ let count = 0;
154
+ let id = setInterval(() => {
155
+ if (count == 15) {
156
+ count = 0;
157
+ }
158
+ process.stdout.write("\r");
159
+ process.stdout.write("等待中" + ".".repeat(count));
160
+ count++;
161
+
162
+ if (p.exitCode) {
163
+ reject(`qemu 已经退出 Code: ${p.exitCode}`);
164
+ clearInterval(id);
165
+ return;
166
+ }
167
+
168
+ let boxid = this.readBoxid(name);
169
+ if (boxid) {
170
+ p.unref();
171
+ resolve(boxid);
172
+ clearInterval(id);
173
+ console.log("启动成功!");
174
+ }
175
+ }, 1000);
176
+ });
177
+ }
178
+
179
+ /**
180
+ * 运行一个指定的虚拟机,如果已经运行,将直接退出。如果盒子不存在,将报错退出
181
+ * @param {string} name - 盒子的名称
182
+ * @return {string} id - 盒子的id
183
+ **/
184
+ async runVM(name) {
185
+ let boxid = this.readBoxid(name);
186
+ if (!boxid) {
187
+ console.log(`${name} 盒子不存在,请通过 lzc-cli box create 创建`);
188
+ return;
189
+ }
190
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
191
+ let pid = parseVmPID(vmDir);
192
+ if (pid) {
193
+ console.log(`${name} 盒子已经启动`);
194
+ return boxid;
195
+ }
196
+ await this.startVM(name, vmDir);
197
+ console.log(`${name} 盒子启动成功!`);
198
+ }
199
+
200
+ /**
201
+ * 创建一个指定的虚拟机,并启动等待注册成功。
202
+ **/
203
+ async createVM() {
204
+ let answer = await this.askBoxInfo();
205
+
206
+ let vmDir = this.ensureVmDir(answer.boxName);
207
+ this.ensureVolumeDir(vmDir);
208
+ await this.buildDisks(vmDir);
209
+ let boxId = await this.startVM(answer.boxName, vmDir);
210
+ console.log("盒子ID: ", boxId);
211
+
212
+ let boxName = await this.registerVM(boxId, answer);
213
+ return {
214
+ boxName,
215
+ boxId,
216
+ user: answer.adminName,
217
+ password: answer.adminPass,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * 请求用户输入盒子的基本信息
223
+ **/
224
+ async askBoxInfo() {
225
+ const noEmpty = (value) => value != "";
226
+
227
+ return inquirer.prompt([
228
+ {
229
+ type: "input",
230
+ name: "boxName",
231
+ message: "请输入盒子名称:",
232
+ validate: noEmpty,
233
+ },
234
+ {
235
+ type: "input",
236
+ name: "adminName",
237
+ message: "请输入盒子管理员名称:",
238
+ default: "admin",
239
+ },
240
+ {
241
+ type: "password",
242
+ name: "adminPass",
243
+ message: "请输入盒子管理员密码:",
244
+ mask: "*",
245
+ validate: noEmpty,
246
+ },
247
+ {
248
+ type: "input",
249
+ name: "origin",
250
+ message: "注册服务器地址",
251
+ default: "origin.lazycat.cloud",
252
+ },
253
+ ]);
254
+ }
255
+
256
+ /**
257
+ * 调用 hportal client 进行注册
258
+ **/
259
+ async registerVM(boxid, { boxName, adminName, adminPass, origin }) {
260
+ // prettier-ignore
261
+ spawnSync("sudo",[
262
+ "-S",
263
+ "home-portal-client",
264
+ "-setup", "-boxid", boxid,
265
+ "-boxname", boxName,
266
+ "-user", adminName,
267
+ "-password", adminPass,
268
+ "-origin", origin,
269
+ ], {stdio: ["inherit", "inherit", "inherit"]})
270
+ return boxName;
271
+ }
272
+
273
+ /**
274
+ * 根据虚拟机名称确保缓存文件夹存在,返回对应的虚拟机缓存文件夹
275
+ */
276
+ ensureVmDir(name) {
277
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
278
+ fs.mkdirSync(vmDir, { recursive: true });
279
+ return vmDir;
280
+ }
281
+
282
+ ensureVolumeDir(vmDir) {
283
+ for (let key in this.scheme.volume) {
284
+ let bindPath = `${vmDir}/volume-${key}`;
285
+ fs.mkdirSync(bindPath, { recursive: true });
286
+ }
287
+ }
288
+
289
+ /**
290
+ * 构建硬盘,从 scheme 中读取 disk 的配置信息,并构建。
291
+ */
292
+ async buildDisks(vmDir) {
293
+ for (const diskInfo of this.scheme.disks) {
294
+ let name = `disk-${diskInfo.id}.qcow2`;
295
+ let diskPath = path.join(vmDir, name);
296
+
297
+ try {
298
+ fs.accessSync(diskPath, fs.constants.F_OK);
299
+ continue;
300
+ } catch {}
301
+
302
+ if (diskInfo.system) {
303
+ await this.buildSystemDisk(name, diskPath, diskInfo);
304
+ } else {
305
+ await this.buildDataDisk(name, diskPath, diskInfo);
306
+ }
307
+ }
308
+ }
309
+
310
+ async buildSystemDisk(name, diskPath, diskInfo) {
311
+ console.log(`构建系统盘快照:${diskPath}`);
312
+ let baseImage = path.join(this.scheme.path, name);
313
+ return spawnSync(
314
+ "qemu-img",
315
+ ["create", "-f", "qcow2", "-b", baseImage, "-F", "qcow2", diskPath],
316
+ { stdio: "inherit" }
317
+ );
318
+ }
319
+
320
+ async buildDataDisk(name, diskPath, diskInfo) {
321
+ console.log(`构建数据盘:${diskPath}`);
322
+ return spawnSync(
323
+ "qemu-img",
324
+ ["create", "-f", "qcow2", diskPath, diskInfo.size],
325
+ { stdio: "inherit" }
326
+ );
327
+ }
328
+
329
+ /**
330
+ * 寻找默认可用的 bios 文件
331
+ */
332
+ findBIOS() {
333
+ let includes = ["/usr/share/ovmf/x64/OVMF.fd", "/usr/share/ovmf/OVMF.fd"];
334
+ for (let file of includes) {
335
+ try {
336
+ fs.accessSync(file, fs.constants.F_OK);
337
+ return file;
338
+ } catch {}
339
+ }
340
+
341
+ throw "找不到 ovmf 文件";
342
+ }
343
+
344
+ /**
345
+ * 构建 qemu 启动的参数
346
+ */
347
+ async buildQemuArgs(name, vmDir) {
348
+ let sshPort = await getFreePort();
349
+ let bios = this.findBIOS();
350
+ // prettier-ignore
351
+ let args = [
352
+ "-name", `${this.scheme.name}-${this.scheme.uuid}-vm-${name}`,
353
+ "-machine", "pc,accel=kvm",
354
+ "-m", `${this.scheme.memory}`,
355
+ "-smp", "4,sockets=4,cores=1,threads=1",
356
+ "-bios", bios,
357
+ "-device", "piix3-usb-uhci",
358
+ "-vnc", `unix:${vmDir}/.vnc`,
359
+ "-monitor", `unix:${vmDir}/.monitor,server,nowait`,
360
+ "-serial", `unix:${vmDir}/.serial,server,nowait`,
361
+ "-parallel", `unix:${vmDir}/.parallel,server,nowait`,
362
+ "-netdev", `user,hostfwd=tcp::${sshPort}-:22,id=eth`,
363
+ "-device", "virtio-net-pci,netdev=eth",
364
+ "-chardev", "stdio,id=s1,signal=off",
365
+ "-device", "isa-serial,chardev=s1",
366
+ ]
367
+
368
+ this.scheme.disks.forEach((diskInfo, index) => {
369
+ let file = `file=${vmDir}/disk-${diskInfo.id}.qcow2,format=qcow2,index=${index},media=disk,if=virtio`;
370
+ args.push("-drive", file);
371
+ });
372
+
373
+ Object.keys(this.scheme.volume).forEach((key) => {
374
+ let volumePath = this.scheme.volume[key];
375
+ let bindPath = `${vmDir}/volume-${key}`;
376
+ args.push(
377
+ "-virtfs",
378
+ `local,path=${bindPath},mount_tag=id_2245023265ae4cf87d02c8b6,security_model=mapped-xattr,id=id_2245023265ae4cf87d02c8b6`
379
+ );
380
+ });
381
+
382
+ return args;
383
+ }
384
+
385
+ /**
386
+ * 停止虚拟盒子
387
+ */
388
+ async stopVM(name) {
389
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
390
+ let pid = parseVmPID(vmDir);
391
+ if (pid) {
392
+ process.kill(pid);
393
+ }
394
+ console.log(`${name} 盒子已停止`);
395
+ }
396
+
397
+ /**
398
+ * 删除虚拟盒子
399
+ */
400
+ async deleteVM(name) {
401
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
402
+
403
+ try {
404
+ fs.accessSync(vmDir);
405
+ } catch {
406
+ console.log(`${name} 盒子不存在或者该盒子为一个真实盒子`);
407
+ return;
408
+ }
409
+
410
+ let pid = parseVmPID(vmDir);
411
+ if (pid) {
412
+ await inquirer
413
+ .prompt([
414
+ {
415
+ type: "confirm",
416
+ name: "yesorno",
417
+ message: `${name} 正在运行中,是否停止?`,
418
+ default: false,
419
+ },
420
+ ])
421
+ .then(async (answer) => {
422
+ if (!answer.yesorno) {
423
+ return;
424
+ }
425
+ process.kill(pid);
426
+ console.log(`${name} 盒子已停止`);
427
+ });
428
+ }
429
+
430
+ return inquirer
431
+ .prompt([
432
+ {
433
+ type: "confirm",
434
+ name: "yesorno",
435
+ message: `确定要删除 ${name} 盒子吗?`,
436
+ default: false,
437
+ },
438
+ ])
439
+ .then(async (answer) => {
440
+ if (!answer.yesorno) {
441
+ return;
442
+ }
443
+ fs.rmSync(vmDir, { recursive: true, force: true });
444
+ });
445
+ }
446
+
447
+ /**
448
+ * 从缓存路径中列出指定盒子信息
449
+ */
450
+ sigleVM(name, defaultBoxName = "") {
451
+ let volumeDir = path.join(this.scheme.path, `vm-${name}`, `volume-config`);
452
+
453
+ try {
454
+ fs.accessSync(volumeDir);
455
+ } catch {
456
+ return;
457
+ }
458
+
459
+ const read = (name) => {
460
+ try {
461
+ return fs.readFileSync(path.join(volumeDir, name), {
462
+ encoding: "utf-8",
463
+ });
464
+ } catch {
465
+ return "not found";
466
+ }
467
+ };
468
+
469
+ let boxName = read("box.name");
470
+
471
+ return {
472
+ default: boxName == defaultBoxName ? "yes" : "no",
473
+ boxName,
474
+ boxId: read("box.id"),
475
+ };
476
+ }
477
+
478
+ /**
479
+ * 从缓存路径中列出所有的盒子信息
480
+ */
481
+ infoAllVM(defaultBoxName = "") {
482
+ let entries = fs.readdirSync(this.scheme.path, { withFileTypes: true });
483
+ let result = [];
484
+
485
+ entries.forEach((entry) => {
486
+ if (!entry.isDirectory()) {
487
+ return;
488
+ }
489
+
490
+ if (!entry.name.startsWith("vm-")) {
491
+ return;
492
+ }
493
+
494
+ let name = entry.name.substr(3);
495
+ let info = this.sigleVM(name, defaultBoxName);
496
+ if (info) {
497
+ let pid = parseVmPID(path.join(this.scheme.path, entry.name));
498
+ info["vm status"] = pid ? "running" : "stop";
499
+ result.push(info);
500
+ }
501
+ });
502
+
503
+ return result;
504
+ }
505
+
506
+ /**
507
+ * 返回指定盒子信息
508
+ */
509
+ async infoSigleVM(name, defaultBoxName = "") {
510
+ let baseInfo = this.sigleVM(name, defaultBoxName);
511
+ if (!baseInfo) {
512
+ return [];
513
+ }
514
+
515
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
516
+ baseInfo["vm status"] = parseVmPID(vmDir) ? "running" : "stop";
517
+ return [baseInfo];
518
+ }
519
+
520
+ /**
521
+ * 根据 scheme 中的 path 和 uuid 和 name 获取盒子的信息
522
+ */
523
+ async infoVM(name, defaultBoxName = "") {
524
+ let infos;
525
+ if (!name) {
526
+ infos = await this.infoAllVM(defaultBoxName);
527
+ } else {
528
+ infos = await this.infoSigleVM(name, defaultBoxName);
529
+ }
530
+ return infos;
531
+ }
532
+
533
+ /**
534
+ * 根据 scheme 中的 path 和 uuid 和 name 获取盒子的boxid
535
+ */
536
+ readBoxid(name) {
537
+ if (!this.scheme.volume["config"]) {
538
+ return "";
539
+ }
540
+
541
+ let vmDir = path.join(this.scheme.path, `vm-${name}`);
542
+ let volumeDir = path.join(vmDir, `volume-config`);
543
+
544
+ try {
545
+ let boxid = fs.readFileSync(path.join(volumeDir, "box.id"), {
546
+ encoding: "utf8",
547
+ });
548
+ return boxid;
549
+ } catch {
550
+ return "";
551
+ }
552
+ }
553
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "home-cloud",
3
+ "uuid": "047dac31-b75b-46ed-aedb-a31fa46a6503",
4
+ "memory": 4096,
5
+ "kernel": "debian",
6
+ "uefi": true,
7
+ "path": "$HOME/.cache/box-emulator",
8
+ "volume": {
9
+ "config": "/etc/home-cloud"
10
+ },
11
+ "disks": [
12
+ {
13
+ "id": "system",
14
+ "size": "8G",
15
+ "system": true,
16
+ "partitions": [
17
+ {
18
+ "fs_type": "vfat",
19
+ "size": "100M",
20
+ "mount_point": "/boot/ESP"
21
+ },
22
+ {
23
+ "fs_type": "ext4",
24
+ "size": "4G",
25
+ "mount_point": "/"
26
+ },
27
+ {
28
+ "fs_type": "ext4",
29
+ "size": "full",
30
+ "mount_point": "/mnt"
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "id": "data1",
36
+ "size": "64G",
37
+ "system": false,
38
+ "partitions": []
39
+ },
40
+ {
41
+ "id": "data2",
42
+ "size": "64G",
43
+ "rebuild": false,
44
+ "partitions": []
45
+ }
46
+ ]
47
+ }