@lazycatcloud/lzc-cli 1.3.11 → 1.3.13

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.
@@ -1,331 +1,291 @@
1
- import spawn from "cross-spawn"
2
- import fs from "node:fs"
3
- import shellApi from "./shellapi.js"
4
- import {
5
- isDebugMode,
6
- isTraceMode,
7
- resolveDomain,
8
- sleep,
9
- findSshPublicKey,
10
- selectSshPublicKey,
11
- isWindows,
12
- contextDirname,
13
- compareVersions
14
- } from "./utils.js"
15
- import logger from "loglevel"
16
- import commandExists from "command-exists"
17
- import path from "node:path"
18
- import fetch from "node-fetch"
19
-
20
- const bannerfileContent = `˄=ᆽ=ᐟ \\`
1
+ import fs from 'node:fs';
2
+ import spawn from 'cross-spawn';
3
+ import logger from 'loglevel';
4
+ import fetch from 'node-fetch';
5
+
6
+ import shellApi from './shellapi.js';
7
+ import { _SYSTEM_ENV_PREFIX } from './config/env.js';
8
+ import { isDebugMode, isTraceMode, resolveDomain, sleep, findSshPublicKey, selectSshPublicKey, isWindows, contextDirname, compareVersions } from './utils.js';
9
+ import { t } from './i18n/index.js';
10
+
11
+ const bannerfileContent = `˄=ᆽ=ᐟ \\`;
21
12
 
22
13
  export function sshBinary() {
23
- if (isWindows) {
24
- return "ssh"
25
- }
26
- return "ssh"
14
+ if (isWindows) {
15
+ return 'ssh';
16
+ }
17
+ return 'ssh';
27
18
  }
28
19
 
29
20
  export function sshCmdArgs(...args) {
30
- const defaultOptions = [
31
- `-o "StrictHostKeyChecking=no"`,
32
- `-o "UserKnownHostsFile=/dev/null"`,
33
- `-o "ControlMaster=no"`,
34
- "-q",
35
- "-p 22222",
36
- `${isTraceMode() ? "-v" : ""}`
37
- ]
38
- return [...defaultOptions, ...args]
21
+ const defaultOptions = [`-o "StrictHostKeyChecking=no"`, `-o "UserKnownHostsFile=/dev/null"`, `-o "ControlMaster=no"`, '-q', '-p 22222', `${isTraceMode() ? '-v' : ''}`];
22
+ return [...defaultOptions, ...args];
39
23
  }
40
24
 
41
25
  export class DebugBridge {
42
- constructor() {
43
- this.uid = shellApi.uid
44
- this.boxname = shellApi.boxname
45
- this.domain = `dev.${this.boxname}.heiyu.space`
46
- }
47
-
48
- async init() {
49
- await this.checkDevTools()
50
- if (!(await this.canPublicKey())) {
51
- // 如果不能 ssh public key 登录则提示授权申请,否则后面可能会出现 rsync 询问密码的问题
52
- await this.sshApplyGrant()
53
- }
54
- }
55
-
56
- async checkDevTools() {
57
- const url = `https://dev.${this.boxname}.heiyu.space/bannerfile`
58
- return new Promise((resolve, reject) => {
59
- fetch(url, { redirect: "error" })
60
- .then(async (res) => {
61
- const content = await res.text()
62
- if (res.status == 200 && content == bannerfileContent) {
63
- resolve()
64
- return
65
- }
66
- logger.warn(
67
- `检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
68
- 点击直接跳转 https://appstore.${this.boxname}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
69
- 点击打开应用 https://dev.${this.boxname}.heiyu.space 查看应用状态
70
- `
71
- )
72
- reject()
73
- })
74
- .catch(() => {
75
- logger.error(
76
- `检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。`
77
- )
78
- reject()
79
- })
80
- })
81
- }
82
-
83
- async common(cmd, args) {
84
- const resolvedIp = await resolveDomain(this.domain)
85
- args = args.map((arg) => arg.replace(this.domain, resolvedIp))
86
- const ssh = spawn.sync(cmd, args, {
87
- shell: true,
88
- encoding: "utf-8",
89
- stdio: ["pipe", "pipe", "pipe"]
90
- })
91
- logger.debug(`执行命令 ${cmd} ${args.join(" ")}`)
92
- return new Promise((resolve, reject) => {
93
- ssh.status == 0
94
- ? resolve(ssh.stdout)
95
- : reject(
96
- `执行命令 ${cmd} ${args.join(" ")} 出错\n${ssh.stdout ?? ""}\n${ssh.stderr ?? ""}`
97
- )
98
- })
99
- }
100
-
101
- async install(lpkPath, pkgId) {
102
- const stream = fs.createReadStream(lpkPath)
103
- const resolvedIp = await resolveDomain(this.domain)
104
- const ssh = spawn(
105
- sshBinary(),
106
- [
107
- ...sshCmdArgs(`box@${resolvedIp}`),
108
- `install --uid ${this.uid}`,
109
- pkgId ? `--pkgId ${pkgId}` : ""
110
- ],
111
- {
112
- shell: true,
113
- stdio: ["pipe", "inherit", "inherit"]
114
- }
115
- )
116
- stream.pipe(ssh.stdin)
117
- return new Promise((resolve, reject) => {
118
- ssh.on("close", (code) => {
119
- code == 0 ? resolve() : reject("install 失败")
120
- })
121
- })
122
- }
123
-
124
- async canPublicKey() {
125
- try {
126
- await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)])
127
- return true
128
- } catch (err) {
129
- logger.debug("canPublicKey error: ", err)
130
- if (err?.code == "ETIMEOUT") {
131
- throw `域名解析失败,请检查代理软件是否对*.heiyu.space拦截`
132
- } else {
133
- return false
134
- }
135
- }
136
- }
137
-
138
- async sshApplyGrant() {
139
- const keys = await findSshPublicKey()
140
- logger.info(
141
- "检测到您当前的环境还没有添加 ssh 公钥到 ‘懒猫开发者工具’ 中,请选择您需要添加的公钥类型"
142
- )
143
- const sshInfo = await selectSshPublicKey(keys)
144
- logger.debug("ssh public key info", sshInfo)
145
-
146
- const pk = Buffer.from(sshInfo.content.trimLeft()).toString("base64")
147
-
148
- logger.warn(
149
- `您当前机器的公钥未添加到微服(${this.boxname})的信任列表中,请使用微服管理员账号在浏览器中访问以下地址,将您选择的公钥自动添加到信任列表中。(所有操作均只在您微服中进行,包括本开发机在内的任何数据不会泄漏到您的微服之外)
150
-
151
- -> https://${this.domain}/auth?key=${pk}
152
- `
153
-
154
- )
155
- throw "请在授权完成后重试!"
156
- }
157
-
158
- async status(appId) {
159
- return this.common(sshBinary(), [
160
- ...sshCmdArgs(`box@${this.domain}`),
161
- `status --uid ${this.uid}`,
162
- appId
163
- ])
164
- }
165
-
166
- async isDevshell(appId) {
167
- await this.backendVersion020()
168
- const stdout = await this.common(sshBinary(), [
169
- ...sshCmdArgs(`box@${this.domain}`),
170
- `isDevshellV2 --uid ${this.uid}`,
171
- appId
172
- ])
173
- return stdout == "true"
174
- }
175
-
176
- async resume(appId) {
177
- return this.common(sshBinary(), [
178
- ...sshCmdArgs(`box@${this.domain}`),
179
- `resume --uid ${this.uid}`,
180
- appId
181
- ])
182
- }
183
-
184
- async version() {
185
- const output = await this.common(sshBinary(), [
186
- ...sshCmdArgs(`box@${this.domain}`),
187
- `version`
188
- ])
189
- logger.debug(`backend version:\n${output}`)
190
- try {
191
- const data = JSON.parse(output)
192
- return data.version
193
- } catch {
194
- return "0.0.0"
195
- }
196
- }
197
-
198
- async uninstall(appId) {
199
- return this.common(sshBinary(), [
200
- ...sshCmdArgs(`box@${this.domain}`),
201
- `uninstall --uid ${this.uid}`,
202
- appId
203
- ])
204
- }
205
-
206
- async devshell(appId, isUserApp, onconnect = null) {
207
- await this.backendVersion020()
208
-
209
- let waiting = true
210
- while (waiting) {
211
- if (await this.isDevshell(appId)) {
212
- waiting = false
213
- break
214
- }
215
- logger.debug("wait app container to running...")
216
- await sleep(100)
217
- }
218
-
219
- const resolvedIp = await resolveDomain(this.domain)
220
-
221
- const stream = spawn(
222
- sshBinary(),
223
- [
224
- ...sshCmdArgs(`box@${resolvedIp}`),
225
- "-t",
226
- "devshell",
227
- `--uid ${this.uid}`,
228
- isUserApp ? "--userapp" : "",
229
- appId,
230
- "/bin/sh",
231
- "/lzcapp/pkg/content/devshell/exec.sh"
232
- ],
233
- {
234
- shell: true,
235
- stdio: "inherit"
236
- }
237
- )
238
- return new Promise((resolve, reject) => {
239
- stream.on("close", (code) => {
240
- code == 0 ? resolve() : reject()
241
- })
242
- // spawn 事件只是进程启动成功的事件,并不是ssh成功连接上的事件。所以这里使
243
- // 用前面的一个 isDevshell 判断是否已经启动这个容器,再使用 spawn
244
- stream.on("spawn", () => {
245
- onconnect && onconnect()
246
- })
247
- })
248
- }
249
-
250
- async buildImage(label, contextTar) {
251
- const backendVersion = await this.version()
252
-
253
- const tag = `debug.bridge/${label}`
254
- const resolvedIp = await resolveDomain(this.domain)
255
- const stream = fs.createReadStream(contextTar)
256
-
257
- const buildStream = spawn(
258
- sshBinary(),
259
- [...sshCmdArgs(`box@${resolvedIp}`), `build --tag ${tag}`],
260
- {
261
- shell: true,
262
- stdio: ["pipe", "inherit", "inherit"]
263
- }
264
- )
265
- stream.pipe(buildStream.stdin)
266
- return new Promise((resolve, reject) => {
267
- buildStream.on("close", (code) => {
268
- code == 0
269
- ? resolve(
270
- compareVersions("0.1.12", backendVersion) >= 0
271
- ? `127.0.0.1:5000/${tag}`
272
- : `dev.${this.boxname}.heiyu.space/${tag}`
273
- )
274
- : reject(`在盒子中构建 image 失败`)
275
- })
276
- }).finally(() => {
277
- fs.rmSync(contextTar)
278
- })
279
- }
280
-
281
- async lzcDocker(argv) {
282
- await this.backendVersion020()
283
-
284
- const resolvedIp = await resolveDomain(this.domain)
285
- const stream = spawn(
286
- sshBinary(),
287
- [...sshCmdArgs(`box@${resolvedIp}`), "-t", "lzc-docker", ...argv],
288
- {
289
- shell: true,
290
- stdio: "inherit"
291
- }
292
- )
293
- return new Promise((resolve, reject) => {
294
- stream.on("close", (code) => {
295
- code == 0 ? resolve() : reject()
296
- })
297
- })
298
- }
299
-
300
- async lzcDockerCompose(argv) {
301
- await this.backendVersion020()
302
-
303
- const resolvedIp = await resolveDomain(this.domain)
304
- const stream = spawn(
305
- sshBinary(),
306
- [...sshCmdArgs(`box@${resolvedIp}`), "-t", "lzc-docker-compose", ...argv],
307
- {
308
- shell: true,
309
- stdio: "inherit"
310
- }
311
- )
312
- return new Promise((resolve, reject) => {
313
- stream.on("close", (code) => {
314
- code == 0 ? resolve() : reject()
315
- })
316
- })
317
- }
318
-
319
- async backendVersion020() {
320
- const backendVersion = await this.version()
321
- if (compareVersions("0.2.0", backendVersion) < 0) {
322
- logger.warn(`
26
+ constructor() {
27
+ this.uid = shellApi.uid;
28
+ this.boxname = shellApi.boxname;
29
+ this.domain = `dev.${this.boxname}.heiyu.space`;
30
+ this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
31
+ }
32
+
33
+ async init() {
34
+ await this.checkDevTools();
35
+ if (!(await this.canPublicKey())) {
36
+ // 如果不能 ssh public key 登录则提示授权申请,否则后面可能会出现 rsync 询问密码的问题
37
+ await this.sshApplyGrant();
38
+ }
39
+ }
40
+
41
+ async checkDevTools() {
42
+ let domain = this.domain;
43
+ if (this.checkUseResolve) {
44
+ try {
45
+ const _ipv6 = await resolveDomain(this.domain, true);
46
+ domain = `[${_ipv6}]`;
47
+ } catch {}
48
+ }
49
+ const url = `https://${domain}/bannerfile`;
50
+ return new Promise((resolve, reject) => {
51
+ fetch(url, { redirect: 'error' })
52
+ .then(async (res) => {
53
+ const content = await res.text();
54
+ if (res.status == 200 && content == bannerfileContent) {
55
+ resolve();
56
+ return;
57
+ }
58
+ logger.warn(
59
+ t(
60
+ 'lzc_cli.lib.debug_bridge.check_dev_tools_not_exist_tips',
61
+ `检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
62
+ 点击直接跳转 https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
63
+ 点击打开应用 https://dev.{{boxname}}.heiyu.space 查看应用状态
64
+ `,
65
+ { boxname: this.boxname },
66
+ ),
67
+ );
68
+ reject();
69
+ })
70
+ .catch((err) => {
71
+ logger.error(t('lzc_cli.lib.debug_bridge.check_dev_tools_fail_tips', `检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。`), err);
72
+ reject();
73
+ });
74
+ });
75
+ }
76
+
77
+ async common(cmd, args) {
78
+ const resolvedIp = await resolveDomain(this.domain);
79
+ args = args.map((arg) => arg.replace(this.domain, resolvedIp));
80
+ const ssh = spawn.sync(cmd, args, {
81
+ shell: true,
82
+ encoding: 'utf-8',
83
+ stdio: ['pipe', 'pipe', 'pipe'],
84
+ });
85
+ logger.debug(t('lzc_cli.lib.debug_bridge.common_start_log', `执行命令 {{cmd}} {{args}}`, { cmd, args: args.join(' ') }));
86
+ return new Promise((resolve, reject) => {
87
+ ssh.status == 0
88
+ ? resolve(ssh.stdout)
89
+ : reject(
90
+ t('lzc_cli.lib.debug_bridge.common_exec_fail', `执行命令 {{cmd}} {{args}} 出错\n{{stdout}}\n{{stderr}}`, {
91
+ cmd,
92
+ args: args.join(' '),
93
+ stdout: ssh.stdout ?? '',
94
+ stdout: ssh.stderr ?? '',
95
+ }),
96
+ );
97
+ });
98
+ }
99
+
100
+ async install(lpkPath, pkgId) {
101
+ const stream = fs.createReadStream(lpkPath);
102
+ const resolvedIp = await resolveDomain(this.domain);
103
+ const ssh = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `install --uid ${this.uid}`, pkgId ? `--pkgId ${pkgId}` : ''], {
104
+ shell: true,
105
+ stdio: ['pipe', 'inherit', 'inherit'],
106
+ });
107
+ stream.pipe(ssh.stdin);
108
+ return new Promise((resolve, reject) => {
109
+ ssh.on('close', (code) => {
110
+ code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
111
+ });
112
+ });
113
+ }
114
+
115
+ async canPublicKey() {
116
+ try {
117
+ await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)]);
118
+ return true;
119
+ } catch (err) {
120
+ logger.debug('canPublicKey error: ', err);
121
+ if (err?.code == 'ETIMEOUT') {
122
+ throw t('lzc_cli.lib.debug_bridge.can_public_key_resolve_fail', `域名解析失败,请检查代理软件是否对 *.heiyu.space 拦截`);
123
+ } else {
124
+ return false;
125
+ }
126
+ }
127
+ }
128
+
129
+ async sshApplyGrant() {
130
+ const keys = await findSshPublicKey();
131
+ logger.info(t('lzc_cli.lib.debug_bridge.ssh_apply_grant_not_exist_tips', '检测到您当前的环境还没有添加 ssh 公钥到 ‘懒猫开发者工具’ 中,请选择您需要添加的公钥类型'));
132
+ const sshInfo = await selectSshPublicKey(keys);
133
+ logger.debug('ssh public key info', sshInfo);
134
+
135
+ const pk = Buffer.from(sshInfo.content.trimLeft()).toString('base64');
136
+
137
+ logger.warn(
138
+ t(
139
+ 'lzc_cli.lib.debug_bridge.ssh_apply_grant_not_credible_tips',
140
+ `您当前机器的公钥未添加到微服({{boxname}})的信任列表中,请使用微服管理员账号在浏览器中访问以下地址,将您选择的公钥自动添加到信任列表中。(所有操作均只在您微服中进行,包括本开发机在内的任何数据不会泄漏到您的微服之外)
141
+
142
+ -> https://{{domain}}/auth?key={{pk}}
143
+ `,
144
+ {
145
+ boxname: this.boxname,
146
+ domain: this.domain,
147
+ kp,
148
+ },
149
+ ),
150
+ );
151
+ throw '请在授权完成后重试!';
152
+ }
153
+
154
+ async status(appId) {
155
+ return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `status --uid ${this.uid}`, appId]);
156
+ }
157
+
158
+ async isDevshell(appId) {
159
+ await this.backendVersion020();
160
+ const stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `isDevshellV2 --uid ${this.uid}`, appId]);
161
+ return stdout == 'true';
162
+ }
163
+
164
+ async resume(appId) {
165
+ return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `resume --uid ${this.uid}`, appId]);
166
+ }
167
+
168
+ async version() {
169
+ const output = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
170
+ logger.debug(`backend version:\n${output}`);
171
+ try {
172
+ const data = JSON.parse(output);
173
+ return data.version;
174
+ } catch {
175
+ return '0.0.0';
176
+ }
177
+ }
178
+
179
+ async uninstall(appId, deleteAppData = false) {
180
+ return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `uninstall --uid ${this.uid}`, deleteAppData ? '--delete-data' : '', appId]);
181
+ }
182
+
183
+ async devshell(appId, isUserApp, onconnect = null) {
184
+ await this.backendVersion020();
185
+
186
+ let waiting = true;
187
+ while (waiting) {
188
+ if (await this.isDevshell(appId)) {
189
+ waiting = false;
190
+ break;
191
+ }
192
+ logger.debug('wait app container to running...');
193
+ await sleep(100);
194
+ }
195
+
196
+ const resolvedIp = await resolveDomain(this.domain);
197
+
198
+ const stream = spawn(
199
+ sshBinary(),
200
+ [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'devshell', `--uid ${this.uid}`, isUserApp ? '--userapp' : '', appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh'],
201
+ {
202
+ shell: true,
203
+ stdio: 'inherit',
204
+ },
205
+ );
206
+ return new Promise((resolve, reject) => {
207
+ stream.on('close', (code) => {
208
+ code == 0 ? resolve() : reject();
209
+ });
210
+ // spawn 事件只是进程启动成功的事件,并不是ssh成功连接上的事件。所以这里使
211
+ // 用前面的一个 isDevshell 判断是否已经启动这个容器,再使用 spawn
212
+ stream.on('spawn', () => {
213
+ onconnect && onconnect();
214
+ });
215
+ });
216
+ }
217
+
218
+ async buildImage(label, contextTar) {
219
+ const backendVersion = await this.version();
220
+
221
+ const tag = `debug.bridge/${label}`;
222
+ const resolvedIp = await resolveDomain(this.domain);
223
+ const stream = fs.createReadStream(contextTar);
224
+
225
+ const buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `build --tag ${tag}`], {
226
+ shell: true,
227
+ stdio: ['pipe', 'inherit', 'inherit'],
228
+ });
229
+ stream.pipe(buildStream.stdin);
230
+ return new Promise((resolve, reject) => {
231
+ buildStream.on('close', (code) => {
232
+ code == 0
233
+ ? resolve(compareVersions('0.1.12', backendVersion) >= 0 ? `127.0.0.1:5000/${tag}` : `dev.${this.boxname}.heiyu.space/${tag}`)
234
+ : reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
235
+ });
236
+ }).finally(() => {
237
+ fs.rmSync(contextTar);
238
+ });
239
+ }
240
+
241
+ async lzcDocker(argv) {
242
+ await this.backendVersion020();
243
+
244
+ const resolvedIp = await resolveDomain(this.domain);
245
+ const stream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'lzc-docker', ...argv], {
246
+ shell: true,
247
+ stdio: 'inherit',
248
+ });
249
+ return new Promise((resolve, reject) => {
250
+ stream.on('close', (code) => {
251
+ code == 0 ? resolve() : reject();
252
+ });
253
+ });
254
+ }
255
+
256
+ async lzcDockerCompose(argv) {
257
+ await this.backendVersion020();
258
+
259
+ const resolvedIp = await resolveDomain(this.domain);
260
+ const stream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'lzc-docker-compose', ...argv], {
261
+ shell: true,
262
+ stdio: 'inherit',
263
+ });
264
+ return new Promise((resolve, reject) => {
265
+ stream.on('close', (code) => {
266
+ code == 0 ? resolve() : reject();
267
+ });
268
+ });
269
+ }
270
+
271
+ async backendVersion020() {
272
+ const backendVersion = await this.version();
273
+ if (compareVersions('0.2.0', backendVersion) < 0) {
274
+ logger.warn(
275
+ t(
276
+ 'lzc_cli.lib.debug_bridge.backend_version_020_no_match_tips',
277
+ `
323
278
  检测到您当前的 lzc-cli 版本较新,而 '懒猫开发者工具' 比较旧,请先到微服的商店升级 '懒猫开发者工具',点击下面的连接跳转:
324
279
 
325
- -> https://appstore.${this.boxname}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
326
- `)
327
- process.exit(1)
328
- return
329
- }
330
- }
280
+ -> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
281
+ `,
282
+ {
283
+ boxname: this.boxname,
284
+ },
285
+ ),
286
+ );
287
+ process.exit(1);
288
+ return;
289
+ }
290
+ }
331
291
  }