@lazycatcloud/lzc-cli 1.3.14 → 2.0.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +30 -5
  2. package/changelog.md +4 -0
  3. package/lib/app/index.js +174 -58
  4. package/lib/app/lpk_build.js +192 -17
  5. package/lib/app/lpk_build_images.js +728 -0
  6. package/lib/app/lpk_create.js +93 -21
  7. package/lib/app/lpk_create_generator.js +144 -9
  8. package/lib/app/lpk_devshell.js +33 -19
  9. package/lib/app/lpk_embed_images.js +257 -0
  10. package/lib/app/lpk_installer.js +14 -7
  11. package/lib/app/project_cp.js +64 -0
  12. package/lib/app/project_deploy.js +33 -0
  13. package/lib/app/project_exec.js +45 -0
  14. package/lib/app/project_info.js +106 -0
  15. package/lib/app/project_log.js +67 -0
  16. package/lib/app/project_runtime.js +261 -0
  17. package/lib/app/project_start.js +100 -0
  18. package/lib/box/index.js +101 -4
  19. package/lib/box/ssh_remote.js +259 -0
  20. package/lib/build_remote.js +22 -0
  21. package/lib/config/index.js +1 -1
  22. package/lib/debug_bridge.js +837 -46
  23. package/lib/docker/index.js +30 -10
  24. package/lib/i18n/index.js +1 -0
  25. package/lib/i18n/locales/en/translation.json +17 -5
  26. package/lib/i18n/locales/zh/translation.json +16 -4
  27. package/lib/lpk/core.js +487 -0
  28. package/lib/lpk/index.js +210 -0
  29. package/lib/sig/core.js +254 -0
  30. package/lib/sig/index.js +88 -0
  31. package/lib/utils.js +3 -1
  32. package/package.json +2 -1
  33. package/scripts/cli.js +4 -0
  34. package/template/_lpk/README.md +11 -3
  35. package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
  36. package/template/_lpk/manifest.yml.in +4 -2
  37. package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
  38. package/template/_lpk/todolist-java.manifest.yml.in +15 -0
  39. package/template/_lpk/todolist-python.manifest.yml.in +15 -0
  40. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  41. package/template/blank/_gitignore +1 -0
  42. package/template/blank/lzc-build.yml +25 -40
  43. package/template/blank/lzc-manifest.yml +14 -7
  44. package/template/golang/Dockerfile +19 -0
  45. package/template/golang/README.md +33 -0
  46. package/template/golang/_gitignore +3 -0
  47. package/template/golang/go.mod +3 -0
  48. package/template/golang/lzc-build.yml +21 -0
  49. package/template/golang/lzc-icon.png +0 -0
  50. package/template/golang/main.go +252 -0
  51. package/template/golang/run.sh +3 -0
  52. package/template/golang/web/index.html +238 -0
  53. package/template/gui-vnc/README.md +19 -0
  54. package/template/gui-vnc/_gitignore +2 -0
  55. package/template/gui-vnc/images/Dockerfile +30 -0
  56. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  57. package/template/gui-vnc/images/startup-script.desktop +9 -0
  58. package/template/gui-vnc/images/startup-script.sh +6 -0
  59. package/template/gui-vnc/lzc-build.yml +23 -0
  60. package/template/gui-vnc/lzc-icon.png +0 -0
  61. package/template/python/Dockerfile +15 -0
  62. package/template/python/README.md +33 -0
  63. package/template/python/_gitignore +3 -0
  64. package/template/python/app.py +110 -0
  65. package/template/python/lzc-build.yml +21 -0
  66. package/template/python/lzc-icon.png +0 -0
  67. package/template/python/requirements.txt +1 -0
  68. package/template/python/run.sh +3 -0
  69. package/template/python/web/index.html +238 -0
  70. package/template/springboot/Dockerfile +20 -0
  71. package/template/springboot/README.md +33 -0
  72. package/template/springboot/_gitignore +3 -0
  73. package/template/springboot/lzc-build.yml +21 -0
  74. package/template/springboot/lzc-icon.png +0 -0
  75. package/template/springboot/pom.xml +38 -0
  76. package/template/springboot/run.sh +3 -0
  77. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  78. package/template/springboot/src/main/resources/application.properties +1 -0
  79. package/template/springboot/src/main/resources/static/index.html +238 -0
  80. package/template/vue/README.md +17 -7
  81. package/template/vue/_gitignore +1 -0
  82. package/template/vue/lzc-build.yml +31 -42
  83. package/template/vue/src/App.vue +36 -25
  84. package/template/vue/src/style.css +106 -49
  85. package/template/vue-minidb/README.md +34 -0
  86. package/template/vue-minidb/_gitignore +26 -0
  87. package/template/vue-minidb/index.html +13 -0
  88. package/template/vue-minidb/lzc-build.yml +48 -0
  89. package/template/vue-minidb/lzc-icon.png +0 -0
  90. package/template/vue-minidb/package.json +21 -0
  91. package/template/vue-minidb/public/vite.svg +1 -0
  92. package/template/vue-minidb/src/App.vue +206 -0
  93. package/template/vue-minidb/src/assets/vue.svg +1 -0
  94. package/template/vue-minidb/src/main.ts +5 -0
  95. package/template/vue-minidb/src/style.css +136 -0
  96. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  97. package/template/vue-minidb/tsconfig.app.json +24 -0
  98. package/template/vue-minidb/tsconfig.json +7 -0
  99. package/template/vue-minidb/tsconfig.node.json +22 -0
  100. package/template/vue-minidb/vite.config.ts +10 -0
  101. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
@@ -5,10 +5,14 @@ import fetch from 'node-fetch';
5
5
 
6
6
  import shellApi from './shellapi.js';
7
7
  import { _SYSTEM_ENV_PREFIX } from './config/env.js';
8
- import { isDebugMode, isTraceMode, resolveDomain, sleep, findSshPublicKey, selectSshPublicKey, isWindows, contextDirname, compareVersions } from './utils.js';
8
+ import { isTraceMode, resolveDomain, sleep, findSshPublicKey, selectSshPublicKey, isWindows, compareVersions } from './utils.js';
9
9
  import { t } from './i18n/index.js';
10
+ import { resolveBuildRemoteFromFile } from './build_remote.js';
10
11
 
11
12
  const bannerfileContent = `˄=ᆽ=ᐟ \\`;
13
+ const DEBUG_BRIDGE_CONTAINER = 'cloudlazycatdevelopertools-app-1';
14
+ const DEBUG_BRIDGE_BINARY = '/lzcapp/pkg/content/debug.bridge';
15
+ const DEBUG_BRIDGE_APP_ID = 'cloud.lazycat.developer.tools';
12
16
 
13
17
  export function sshBinary() {
14
18
  if (isWindows) {
@@ -22,29 +26,289 @@ export function sshCmdArgs(...args) {
22
26
  return [...defaultOptions, ...args];
23
27
  }
24
28
 
29
+ export function sshCmdArgsRaw(...args) {
30
+ const defaultOptions = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-q', '-p', '22222'];
31
+ if (isTraceMode()) {
32
+ defaultOptions.push('-v');
33
+ }
34
+ return [...defaultOptions, ...args];
35
+ }
36
+
37
+ function stripAnsi(text = '') {
38
+ return String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
39
+ }
40
+
41
+ function extractInstallErrorDetail(rawOutput = '') {
42
+ const text = stripAnsi(rawOutput);
43
+
44
+ const detailMatch = text.match(/detail:"((?:\\"|[^"])*)"/);
45
+ if (detailMatch && detailMatch[1]) {
46
+ return detailMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
47
+ }
48
+
49
+ const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([^\n\r]+)/i);
50
+ if (descMatch && descMatch[1]) {
51
+ return descMatch[1].trim();
52
+ }
53
+
54
+ const lines = text
55
+ .split(/\r?\n/)
56
+ .map((line) => line.trim())
57
+ .filter((line) => {
58
+ if (!line) {
59
+ return false;
60
+ }
61
+ if (line.startsWith('Usage:') || line.startsWith('Flags:') || line.startsWith('cmd:')) {
62
+ return false;
63
+ }
64
+ return true;
65
+ });
66
+ if (lines.length > 0) {
67
+ return lines[0];
68
+ }
69
+
70
+ return t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败');
71
+ }
72
+
73
+ function streamInstallOutput(child, { printStdout = true, printStderr = false } = {}) {
74
+ let stdout = '';
75
+ let stderr = '';
76
+
77
+ child.stdout?.on('data', (chunk) => {
78
+ const text = String(chunk ?? '');
79
+ stdout += text;
80
+ if (printStdout) {
81
+ process.stdout.write(text);
82
+ }
83
+ });
84
+ child.stderr?.on('data', (chunk) => {
85
+ const text = String(chunk ?? '');
86
+ stderr += text;
87
+ if (printStderr) {
88
+ process.stderr.write(text);
89
+ }
90
+ });
91
+
92
+ return {
93
+ getStdout: () => stdout,
94
+ getStderr: () => stderr,
95
+ };
96
+ }
97
+
98
+ function normalizeErrorMessage(error) {
99
+ if (!error) {
100
+ return '';
101
+ }
102
+ if (typeof error === 'string') {
103
+ return error.trim();
104
+ }
105
+ if (typeof error.message === 'string') {
106
+ return error.message.trim();
107
+ }
108
+ return String(error).trim();
109
+ }
110
+
25
111
  export class DebugBridge {
26
- constructor() {
27
- this.uid = shellApi.uid;
28
- this.boxname = shellApi.boxname;
112
+ constructor(baseDir = process.cwd(), buildRemote = undefined) {
113
+ this.buildRemote = buildRemote === undefined ? resolveBuildRemoteFromFile(baseDir) : buildRemote;
114
+ if (this.buildRemote) {
115
+ this.uid = '';
116
+ this.boxname = this.buildRemote.boxname;
117
+ } else {
118
+ this.uid = shellApi.uid;
119
+ this.boxname = shellApi.boxname;
120
+ }
29
121
  this.domain = `dev.${this.boxname}.heiyu.space`;
30
122
  this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
31
123
  }
32
124
 
125
+ isBuildRemoteMode() {
126
+ return !!this.buildRemote;
127
+ }
128
+
129
+ remoteSshArgsRaw({ tty = false } = {}) {
130
+ if (!this.buildRemote) {
131
+ throw new Error('build remote is not configured');
132
+ }
133
+ const args = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-q', '-p', String(this.buildRemote.sshPort)];
134
+ if (isTraceMode()) {
135
+ args.push('-v');
136
+ }
137
+ if (tty) {
138
+ args.push('-t');
139
+ }
140
+ args.push(this.buildRemote.sshTarget);
141
+ return args;
142
+ }
143
+
144
+ remoteBridgeArgs(commandArgs = [], { tty = false } = {}) {
145
+ const args = [...this.remoteSshArgsRaw({ tty })];
146
+ args.push('lzc-docker', 'exec', tty ? '-it' : '-i', DEBUG_BRIDGE_CONTAINER, DEBUG_BRIDGE_BINARY, ...commandArgs);
147
+ return args;
148
+ }
149
+
150
+ remoteBridgeExec(commandArgs = [], { tty = false } = {}) {
151
+ return spawn.sync(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty }), {
152
+ shell: false,
153
+ encoding: 'utf-8',
154
+ stdio: ['pipe', 'pipe', 'pipe'],
155
+ });
156
+ }
157
+
158
+ remoteHostExec(argv = []) {
159
+ return spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), ...argv], {
160
+ shell: false,
161
+ encoding: 'utf-8',
162
+ stdio: ['pipe', 'pipe', 'pipe'],
163
+ });
164
+ }
165
+
166
+ async remoteCommon(commandArgs = []) {
167
+ const ssh = this.remoteBridgeExec(commandArgs);
168
+ logger.debug(`run remote command: ${commandArgs.join(' ')}`);
169
+ if (ssh.status === 0) {
170
+ return ssh.stdout;
171
+ }
172
+ throw new Error(`remote command failed: ${commandArgs.join(' ')}\n${ssh.stdout ?? ''}\n${ssh.stderr ?? ''}`);
173
+ }
174
+
175
+ collectUIDCandidatesFromContainerEnv() {
176
+ const candidates = new Set();
177
+ const ps = this.remoteHostExec(['lzc-docker', 'ps', '--format', '{{.Names}}']);
178
+ if (ps.status !== 0) {
179
+ return [];
180
+ }
181
+ const containers = String(ps.stdout ?? '')
182
+ .split(/\r?\n/)
183
+ .map((item) => item.trim())
184
+ .filter((item) => item);
185
+ for (const container of containers.slice(0, 48)) {
186
+ const envResult = this.remoteHostExec(['lzc-docker', 'exec', '-i', container, 'env']);
187
+ if (envResult.status !== 0) {
188
+ continue;
189
+ }
190
+ String(envResult.stdout ?? '')
191
+ .split(/\r?\n/)
192
+ .map((line) => line.trim())
193
+ .forEach((line) => {
194
+ if (!line.startsWith('LAZYCAT_USER_ID=') && !line.startsWith('LAZYCAT_APP_DEPLOY_UID=')) {
195
+ return;
196
+ }
197
+ const value = String(line.split('=').slice(1).join('=') ?? '').trim();
198
+ if (value) {
199
+ candidates.add(value);
200
+ }
201
+ });
202
+ }
203
+ return [...candidates];
204
+ }
205
+
206
+ async probeLegacyUID() {
207
+ const candidates = [];
208
+ const appendCandidate = (value) => {
209
+ const uid = String(value ?? '').trim();
210
+ if (!uid) {
211
+ return;
212
+ }
213
+ if (!candidates.includes(uid)) {
214
+ candidates.push(uid);
215
+ }
216
+ };
217
+
218
+ this.collectUIDCandidatesFromContainerEnv().forEach((item) => appendCandidate(item));
219
+ appendCandidate(process.env.BOX_UID);
220
+ ['c', 'admin', 'root'].forEach((item) => appendCandidate(item));
221
+ 'abcdefghijklmnopqrstuvwxyz0123456789'.split('').forEach((item) => appendCandidate(item));
222
+
223
+ for (const uid of candidates) {
224
+ const probe = this.remoteBridgeExec(['info', '--uid', uid, DEBUG_BRIDGE_APP_ID]);
225
+ if (probe.status !== 0) {
226
+ continue;
227
+ }
228
+ try {
229
+ const parsed = JSON.parse(String(probe.stdout ?? '').trim());
230
+ if (parsed?.appid === DEBUG_BRIDGE_APP_ID) {
231
+ logger.debug(`resolved legacy uid by probe: ${uid}`);
232
+ return uid;
233
+ }
234
+ } catch {
235
+ logger.debug(`legacy uid probe got non-json output, use uid candidate: ${uid}`);
236
+ return uid;
237
+ }
238
+ }
239
+
240
+ return '';
241
+ }
242
+
243
+ async resolveCurrentUID() {
244
+ if (!this.isBuildRemoteMode()) {
245
+ return this.uid;
246
+ }
247
+ const cached = String(this.uid ?? '').trim();
248
+ if (cached) {
249
+ return cached;
250
+ }
251
+
252
+ const uidReply = this.remoteBridgeExec(['uid']);
253
+ if (uidReply.status === 0) {
254
+ const resolved = String(uidReply.stdout ?? '').trim();
255
+ if (!resolved) {
256
+ throw new Error('resolve uid failed: empty output');
257
+ }
258
+ this.uid = resolved;
259
+ return resolved;
260
+ }
261
+
262
+ const legacyUID = await this.probeLegacyUID();
263
+ if (legacyUID) {
264
+ this.uid = legacyUID;
265
+ return legacyUID;
266
+ }
267
+
268
+ throw new Error(`resolve uid failed: ${String(uidReply.stderr ?? uidReply.stdout ?? '').trim() || 'unknown error'}`);
269
+ }
270
+
271
+ async remoteCommandWithUID(commandArgs = []) {
272
+ const uid = await this.resolveCurrentUID();
273
+ const args = [...commandArgs];
274
+ args.splice(1, 0, '--uid', uid);
275
+ return args;
276
+ }
277
+
33
278
  async init() {
34
279
  await this.checkDevTools();
35
280
  if (!(await this.canPublicKey())) {
281
+ if (this.isBuildRemoteMode()) {
282
+ throw new Error(`ssh login failed for remote ${this.buildRemote.sshTarget}:${this.buildRemote.sshPort}`);
283
+ }
36
284
  // 如果不能 ssh public key 登录则提示授权申请,否则后面可能会出现 rsync 询问密码的问题
37
285
  await this.sshApplyGrant();
38
286
  }
39
287
  }
40
288
 
41
289
  async checkDevTools() {
290
+ if (this.isBuildRemoteMode()) {
291
+ try {
292
+ await this.remoteCommon(['version']);
293
+ return;
294
+ } catch (error) {
295
+ const detail = normalizeErrorMessage(error);
296
+ if (detail.includes(`No such container: ${DEBUG_BRIDGE_CONTAINER}`)) {
297
+ throw new Error(`Lazycat Developer Tools is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
298
+ }
299
+ if (detail.includes('is not running')) {
300
+ throw new Error(`Lazycat Developer Tools container is not running on target box "${this.boxname}". Start app "cloud.lazycat.developer.tools" and retry.`);
301
+ }
302
+ throw new Error(`Failed to check Lazycat Developer Tools in remote mode.\n${detail || 'unknown error'}`);
303
+ }
304
+ }
305
+
42
306
  let domain = this.domain;
43
307
  if (this.checkUseResolve) {
44
308
  try {
45
309
  const _ipv6 = await resolveDomain(this.domain, true);
46
310
  domain = `[${_ipv6}]`;
47
- } catch { }
311
+ } catch {}
48
312
  }
49
313
  const url = `https://${domain}/bannerfile`;
50
314
  return new Promise((resolve, reject) => {
@@ -87,33 +351,76 @@ export class DebugBridge {
87
351
  ssh.status == 0
88
352
  ? resolve(ssh.stdout)
89
353
  : 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
- interpolation: { escapeValue: false }
96
- }),
97
- );
354
+ t('lzc_cli.lib.debug_bridge.common_exec_fail', `执行命令 {{ cmd }} {{ args }} 出错\n{{ stdout }}\n{{ stderr }}`, {
355
+ cmd,
356
+ args: args.join(' '),
357
+ stdout: ssh.stdout ?? '',
358
+ stdout: ssh.stderr ?? '',
359
+ interpolation: { escapeValue: false },
360
+ }),
361
+ );
98
362
  });
99
363
  }
100
364
 
101
365
  async install(lpkPath, pkgId) {
102
366
  const stream = fs.createReadStream(lpkPath);
367
+ if (this.isBuildRemoteMode()) {
368
+ const commandArgs = await this.remoteCommandWithUID(['install']);
369
+ if (pkgId) {
370
+ commandArgs.push('--pkgId', pkgId);
371
+ }
372
+ const ssh = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs), {
373
+ shell: false,
374
+ stdio: ['pipe', 'pipe', 'pipe'],
375
+ });
376
+ const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
377
+ stream.pipe(ssh.stdin);
378
+ return new Promise((resolve, reject) => {
379
+ ssh.on('error', (error) => {
380
+ reject(error);
381
+ });
382
+ ssh.on('close', (code) => {
383
+ if (code == 0) {
384
+ resolve();
385
+ return;
386
+ }
387
+ const detail = extractInstallErrorDetail(`${output.getStdout()}\n${output.getStderr()}`);
388
+ reject(detail || t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
389
+ });
390
+ });
391
+ }
392
+
103
393
  const resolvedIp = await resolveDomain(this.domain);
104
394
  const ssh = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `install --uid ${this.uid}`, pkgId ? `--pkgId ${pkgId}` : ''], {
105
395
  shell: true,
106
- stdio: ['pipe', 'inherit', 'inherit'],
396
+ stdio: ['pipe', 'pipe', 'pipe'],
107
397
  });
398
+ const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
108
399
  stream.pipe(ssh.stdin);
109
400
  return new Promise((resolve, reject) => {
401
+ ssh.on('error', (error) => {
402
+ reject(error);
403
+ });
110
404
  ssh.on('close', (code) => {
111
- code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
405
+ if (code == 0) {
406
+ resolve();
407
+ return;
408
+ }
409
+ const detail = extractInstallErrorDetail(`${output.getStdout()}\n${output.getStderr()}`);
410
+ reject(detail || t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败'));
112
411
  });
113
412
  });
114
413
  }
115
414
 
116
415
  async canPublicKey() {
416
+ if (this.isBuildRemoteMode()) {
417
+ const ssh = spawn.sync(sshBinary(), [...this.remoteSshArgsRaw(), 'true'], {
418
+ shell: false,
419
+ encoding: 'utf-8',
420
+ stdio: ['pipe', 'pipe', 'pipe'],
421
+ });
422
+ return ssh.status === 0;
423
+ }
117
424
  try {
118
425
  await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)]);
119
426
  return true;
@@ -128,6 +435,9 @@ export class DebugBridge {
128
435
  }
129
436
 
130
437
  async sshApplyGrant() {
438
+ if (this.isBuildRemoteMode()) {
439
+ throw new Error('build remote mode requires ssh key authorization in host ssh service');
440
+ }
131
441
  const keys = await findSshPublicKey();
132
442
  logger.info(t('lzc_cli.lib.debug_bridge.ssh_apply_grant_not_exist_tips', '检测到您当前的环境还没有添加 ssh 公钥到 ‘懒猫开发者工具’ 中,请选择您需要添加的公钥类型'));
133
443
  const sshInfo = await selectSshPublicKey(keys);
@@ -146,7 +456,7 @@ export class DebugBridge {
146
456
  boxname: this.boxname,
147
457
  domain: this.domain,
148
458
  pk,
149
- interpolation: { escapeValue: false }
459
+ interpolation: { escapeValue: false },
150
460
  },
151
461
  ),
152
462
  );
@@ -154,21 +464,52 @@ export class DebugBridge {
154
464
  }
155
465
 
156
466
  async status(appId) {
467
+ if (this.isBuildRemoteMode()) {
468
+ return this.remoteCommon(await this.remoteCommandWithUID(['status', appId]));
469
+ }
157
470
  return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `status --uid ${this.uid}`, appId]);
158
471
  }
159
472
 
473
+ async info(appId) {
474
+ let stdout;
475
+ if (this.isBuildRemoteMode()) {
476
+ stdout = await this.remoteCommon(await this.remoteCommandWithUID(['info', appId]));
477
+ } else {
478
+ stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `info --uid ${this.uid}`, appId]);
479
+ }
480
+ try {
481
+ return JSON.parse(stdout);
482
+ } catch (error) {
483
+ throw new Error(`parse app info failed: ${error.message}`);
484
+ }
485
+ }
486
+
160
487
  async isDevshell(appId) {
161
488
  await this.backendVersion020();
489
+ if (this.isBuildRemoteMode()) {
490
+ const stdout = await this.remoteCommon(await this.remoteCommandWithUID(['isDevshellV2', appId]));
491
+ return stdout.trim() == 'true';
492
+ }
162
493
  const stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `isDevshellV2 --uid ${this.uid}`, appId]);
163
494
  return stdout == 'true';
164
495
  }
165
496
 
166
497
  async resume(appId) {
498
+ if (this.isBuildRemoteMode()) {
499
+ return this.remoteCommon(await this.remoteCommandWithUID(['resume', appId]));
500
+ }
167
501
  return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `resume --uid ${this.uid}`, appId]);
168
502
  }
169
503
 
504
+ async pause(appId) {
505
+ if (this.isBuildRemoteMode()) {
506
+ return this.remoteCommon(await this.remoteCommandWithUID(['pause', appId]));
507
+ }
508
+ return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `pause --uid ${this.uid}`, appId]);
509
+ }
510
+
170
511
  async version() {
171
- const output = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
512
+ const output = this.isBuildRemoteMode() ? await this.remoteCommon(['version']) : await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
172
513
  logger.debug(`backend version:\n${output}`);
173
514
  try {
174
515
  const data = JSON.parse(output);
@@ -179,6 +520,14 @@ export class DebugBridge {
179
520
  }
180
521
 
181
522
  async uninstall(appId, deleteAppData = false) {
523
+ if (this.isBuildRemoteMode()) {
524
+ const commandArgs = await this.remoteCommandWithUID(['uninstall']);
525
+ if (deleteAppData) {
526
+ commandArgs.push('--delete-data');
527
+ }
528
+ commandArgs.push(appId);
529
+ return this.remoteCommon(commandArgs);
530
+ }
182
531
  return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `uninstall --uid ${this.uid}`, deleteAppData ? '--delete-data' : '', appId]);
183
532
  }
184
533
 
@@ -195,16 +544,28 @@ export class DebugBridge {
195
544
  await sleep(100);
196
545
  }
197
546
 
198
- const resolvedIp = await resolveDomain(this.domain);
199
-
200
- const stream = spawn(
201
- sshBinary(),
202
- [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'devshell', `--uid ${this.uid}`, isUserApp ? '--userapp' : '', appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh'],
203
- {
204
- shell: true,
547
+ let stream;
548
+ if (this.isBuildRemoteMode()) {
549
+ const commandArgs = await this.remoteCommandWithUID(['devshell']);
550
+ if (isUserApp) {
551
+ commandArgs.push('--userapp');
552
+ }
553
+ commandArgs.push(appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh');
554
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(commandArgs, { tty: true }), {
555
+ shell: false,
205
556
  stdio: 'inherit',
206
- },
207
- );
557
+ });
558
+ } else {
559
+ const resolvedIp = await resolveDomain(this.domain);
560
+ stream = spawn(
561
+ sshBinary(),
562
+ [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'devshell', `--uid ${this.uid}`, isUserApp ? '--userapp' : '', appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh'],
563
+ {
564
+ shell: true,
565
+ stdio: 'inherit',
566
+ },
567
+ );
568
+ }
208
569
  return new Promise((resolve, reject) => {
209
570
  stream.on('close', (code) => {
210
571
  code == 0 ? resolve() : reject();
@@ -218,51 +579,481 @@ export class DebugBridge {
218
579
  }
219
580
 
220
581
  async buildImage(label, contextTar) {
221
- const backendVersion = await this.version();
222
-
223
582
  const tag = `debug.bridge/${label}`;
224
- const resolvedIp = await resolveDomain(this.domain);
225
583
  const stream = fs.createReadStream(contextTar);
226
-
227
- const buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `build --tag ${tag}`], {
228
- shell: true,
229
- stdio: ['pipe', 'inherit', 'inherit'],
230
- });
584
+ let buildStream;
585
+ const buildArgs = ['build', '--tag', tag];
586
+ if (this.isBuildRemoteMode()) {
587
+ buildStream = spawn(sshBinary(), this.remoteBridgeArgs(buildArgs), {
588
+ shell: false,
589
+ stdio: ['pipe', 'inherit', 'inherit'],
590
+ });
591
+ } else {
592
+ const resolvedIp = await resolveDomain(this.domain);
593
+ buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), ['build', '--tag', tag].join(' ')], {
594
+ shell: true,
595
+ stdio: ['pipe', 'inherit', 'inherit'],
596
+ });
597
+ }
231
598
  stream.pipe(buildStream.stdin);
232
599
  return new Promise((resolve, reject) => {
233
600
  buildStream.on('close', (code) => {
234
- code == 0
235
- ? resolve(compareVersions('0.1.12', backendVersion) >= 0 ? `127.0.0.1:5000/${tag}` : `dev.${this.boxname}.heiyu.space/${tag}`)
236
- : reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
601
+ code == 0 ? resolve(tag) : reject(t('lzc_cli.lib.debug_bridge.build_image_fail', `在微服中构建 image 失败`));
237
602
  });
238
603
  }).finally(() => {
239
604
  fs.rmSync(contextTar);
240
605
  });
241
606
  }
242
607
 
608
+ async buildImageForPack(label, contextTar) {
609
+ const tag = `debug.bridge/${label}`;
610
+ const stream = fs.createReadStream(contextTar);
611
+ let buildStream;
612
+ const isBuildPackResultLine = (line) => {
613
+ const trimmed = String(line ?? '').trim();
614
+ if (!trimmed || !trimmed.startsWith('{') || !trimmed.endsWith('}')) {
615
+ return false;
616
+ }
617
+ try {
618
+ const parsed = JSON.parse(trimmed);
619
+ return (
620
+ parsed &&
621
+ typeof parsed === 'object' &&
622
+ typeof parsed.tag === 'string' &&
623
+ typeof parsed.archiveKey === 'string' &&
624
+ typeof parsed.imageID === 'string' &&
625
+ Array.isArray(parsed.diffIDs)
626
+ );
627
+ } catch {
628
+ return false;
629
+ }
630
+ };
631
+ if (this.isBuildRemoteMode()) {
632
+ buildStream = spawn(sshBinary(), this.remoteBridgeArgs(['build-pack', '--tag', tag]), {
633
+ shell: false,
634
+ stdio: ['pipe', 'pipe', 'inherit'],
635
+ });
636
+ } else {
637
+ const resolvedIp = await resolveDomain(this.domain);
638
+ buildStream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'build-pack', '--tag', tag], {
639
+ shell: false,
640
+ stdio: ['pipe', 'pipe', 'inherit'],
641
+ });
642
+ }
643
+ stream.pipe(buildStream.stdin);
644
+
645
+ return new Promise((resolve, reject) => {
646
+ let output = '';
647
+ let stdoutLineBuffer = '';
648
+ buildStream.stdout.on('data', (chunk) => {
649
+ const text = chunk.toString();
650
+ output += text;
651
+ stdoutLineBuffer += text;
652
+ while (true) {
653
+ const lineEndIndex = stdoutLineBuffer.indexOf('\n');
654
+ if (lineEndIndex < 0) {
655
+ break;
656
+ }
657
+ const lineWithNewline = stdoutLineBuffer.slice(0, lineEndIndex + 1);
658
+ stdoutLineBuffer = stdoutLineBuffer.slice(lineEndIndex + 1);
659
+ const line = lineWithNewline.replace(/\r?\n$/, '');
660
+ if (!isBuildPackResultLine(line)) {
661
+ process.stdout.write(lineWithNewline);
662
+ }
663
+ }
664
+ });
665
+ buildStream.on('error', (error) => {
666
+ reject(error);
667
+ });
668
+ buildStream.on('close', (code) => {
669
+ if (stdoutLineBuffer) {
670
+ if (!isBuildPackResultLine(stdoutLineBuffer)) {
671
+ process.stdout.write(stdoutLineBuffer);
672
+ }
673
+ stdoutLineBuffer = '';
674
+ }
675
+ if (code !== 0) {
676
+ reject(new Error('build-pack failed'));
677
+ return;
678
+ }
679
+ try {
680
+ const line = output
681
+ .split(/\r?\n/)
682
+ .map((item) => item.trim())
683
+ .filter((item) => item !== '')
684
+ .at(-1);
685
+ if (!line) {
686
+ throw new Error('build-pack output is empty');
687
+ }
688
+ const payload = JSON.parse(line);
689
+ resolve(payload);
690
+ } catch (error) {
691
+ reject(new Error(`parse build-pack output failed: ${error.message}`));
692
+ }
693
+ });
694
+ }).finally(() => {
695
+ fs.rmSync(contextTar, { force: true });
696
+ });
697
+ }
698
+
243
699
  async lzcDocker(argv) {
244
700
  await this.backendVersion020();
701
+ const stream = this.isBuildRemoteMode()
702
+ ? spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv], { tty: true }), {
703
+ shell: false,
704
+ stdio: 'inherit',
705
+ })
706
+ : spawn(sshBinary(), [...sshCmdArgs(`box@${await resolveDomain(this.domain)}`), '-t', 'lzc-docker', ...argv], {
707
+ shell: true,
708
+ stdio: 'inherit',
709
+ });
710
+ return new Promise((resolve, reject) => {
711
+ stream.on('close', (code) => {
712
+ code == 0 ? resolve() : reject();
713
+ });
714
+ });
715
+ }
716
+
717
+ async lzcDockerPipe(argv, stdinStream = null) {
718
+ await this.backendVersion020();
719
+ let stream;
720
+ if (this.isBuildRemoteMode()) {
721
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
722
+ shell: false,
723
+ stdio: ['pipe', 'inherit', 'inherit'],
724
+ });
725
+ } else {
726
+ const resolvedIp = await resolveDomain(this.domain);
727
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
728
+ shell: false,
729
+ stdio: ['pipe', 'inherit', 'inherit'],
730
+ });
731
+ }
245
732
 
246
- const resolvedIp = await resolveDomain(this.domain);
247
- const stream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'lzc-docker', ...argv], {
248
- shell: true,
249
- stdio: 'inherit',
733
+ return await new Promise((resolve, reject) => {
734
+ let done = false;
735
+ const fail = (error) => {
736
+ if (done) {
737
+ return;
738
+ }
739
+ done = true;
740
+ try {
741
+ stream.kill('SIGKILL');
742
+ } catch {}
743
+ reject(error);
744
+ };
745
+ stream.on('error', (error) => fail(error));
746
+ stream.on('close', (code) => {
747
+ if (done) {
748
+ return;
749
+ }
750
+ done = true;
751
+ code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
752
+ });
753
+ if (stdinStream) {
754
+ stdinStream.on('error', (error) => fail(error));
755
+ stdinStream.pipe(stream.stdin);
756
+ return;
757
+ }
758
+ stream.stdin.end();
250
759
  });
760
+ }
761
+
762
+ async lzcDockerPull(image) {
763
+ await this.backendVersion020();
764
+ let stream;
765
+ if (this.isBuildRemoteMode()) {
766
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'pull', image]), {
767
+ shell: false,
768
+ stdio: 'inherit',
769
+ });
770
+ } else {
771
+ const resolvedIp = await resolveDomain(this.domain);
772
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'pull', image], {
773
+ shell: false,
774
+ stdio: 'inherit',
775
+ });
776
+ }
251
777
  return new Promise((resolve, reject) => {
252
778
  stream.on('close', (code) => {
253
- code == 0 ? resolve() : reject();
779
+ code == 0 ? resolve() : reject(t('lzc_cli.lib.debug_bridge.lzc_docker_pull_fail', `lzc-docker pull 失败: ${image}`));
254
780
  });
255
781
  });
256
782
  }
257
783
 
258
- async lzcDockerCompose(argv) {
784
+ async lzcDockerSave(images, outputPath) {
785
+ await this.backendVersion020();
786
+ if (!images || images.length == 0) {
787
+ throw t('lzc_cli.lib.debug_bridge.lzc_docker_save_images_empty_fail', 'images 不能为空');
788
+ }
789
+ const output = fs.createWriteStream(outputPath);
790
+ let stream;
791
+ if (this.isBuildRemoteMode()) {
792
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', 'image', 'save', ...images]), {
793
+ shell: false,
794
+ stdio: ['ignore', 'pipe', 'inherit'],
795
+ });
796
+ } else {
797
+ const resolvedIp = await resolveDomain(this.domain);
798
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', 'image', 'save', ...images], {
799
+ shell: false,
800
+ stdio: ['ignore', 'pipe', 'inherit'],
801
+ });
802
+ }
803
+ stream.stdout.pipe(output);
804
+
805
+ return new Promise((resolve, reject) => {
806
+ let done = false;
807
+ const fail = (err) => {
808
+ if (done) {
809
+ return;
810
+ }
811
+ done = true;
812
+ try {
813
+ stream.kill('SIGKILL');
814
+ } catch {}
815
+ try {
816
+ output.destroy();
817
+ } catch {}
818
+ try {
819
+ fs.rmSync(outputPath, { force: true });
820
+ } catch {}
821
+ reject(err);
822
+ };
823
+
824
+ stream.on('error', (e) => {
825
+ fail(e);
826
+ });
827
+ output.on('error', (e) => {
828
+ fail(e);
829
+ });
830
+ stream.on('close', (code) => {
831
+ if (done) {
832
+ return;
833
+ }
834
+ output.end(() => {
835
+ if (code == 0) {
836
+ done = true;
837
+ resolve();
838
+ return;
839
+ }
840
+ fail(t('lzc_cli.lib.debug_bridge.lzc_docker_save_fail', 'lzc-docker image save 失败'));
841
+ });
842
+ });
843
+ });
844
+ }
845
+
846
+ async packImages(imagesSpec, outputPath) {
847
+ await this.backendVersion020();
848
+ if (!Array.isArray(imagesSpec) || imagesSpec.length === 0) {
849
+ throw new Error('imagesSpec cannot be empty');
850
+ }
851
+ const spec = Buffer.from(
852
+ JSON.stringify({
853
+ images: imagesSpec,
854
+ }),
855
+ ).toString('base64');
856
+
857
+ const output = fs.createWriteStream(outputPath);
858
+ let stream;
859
+ if (this.isBuildRemoteMode()) {
860
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['pack-images', '--spec', spec]), {
861
+ shell: false,
862
+ stdio: ['ignore', 'pipe', 'inherit'],
863
+ });
864
+ } else {
865
+ const resolvedIp = await resolveDomain(this.domain);
866
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'pack-images', '--spec', spec], {
867
+ shell: false,
868
+ stdio: ['ignore', 'pipe', 'inherit'],
869
+ });
870
+ }
871
+ stream.stdout.pipe(output);
872
+
873
+ return new Promise((resolve, reject) => {
874
+ let done = false;
875
+ const fail = (err) => {
876
+ if (done) {
877
+ return;
878
+ }
879
+ done = true;
880
+ try {
881
+ stream.kill('SIGKILL');
882
+ } catch {}
883
+ try {
884
+ output.destroy();
885
+ } catch {}
886
+ try {
887
+ fs.rmSync(outputPath, { force: true });
888
+ } catch {}
889
+ reject(err);
890
+ };
891
+
892
+ stream.on('error', (e) => {
893
+ fail(e);
894
+ });
895
+ output.on('error', (e) => {
896
+ fail(e);
897
+ });
898
+ stream.on('close', (code) => {
899
+ if (done) {
900
+ return;
901
+ }
902
+ output.end(() => {
903
+ if (code == 0) {
904
+ done = true;
905
+ resolve();
906
+ return;
907
+ }
908
+ fail(new Error('pack-images failed'));
909
+ });
910
+ });
911
+ });
912
+ }
913
+
914
+ async lzcDockerCapture(argv) {
915
+ await this.backendVersion020();
916
+ let stream;
917
+ if (this.isBuildRemoteMode()) {
918
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
919
+ shell: false,
920
+ stdio: ['ignore', 'pipe', 'inherit'],
921
+ });
922
+ } else {
923
+ const resolvedIp = await resolveDomain(this.domain);
924
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
925
+ shell: false,
926
+ stdio: ['ignore', 'pipe', 'inherit'],
927
+ });
928
+ }
929
+
930
+ return await new Promise((resolve, reject) => {
931
+ let output = '';
932
+ stream.stdout.on('data', (chunk) => {
933
+ output += chunk.toString();
934
+ });
935
+ stream.on('error', (err) => reject(err));
936
+ stream.on('close', (code) => {
937
+ if (code == 0) {
938
+ resolve(output.trim());
939
+ return;
940
+ }
941
+ reject(t('lzc_cli.lib.debug_bridge.lzc_docker_capture_fail', 'lzc-docker command failed'));
942
+ });
943
+ });
944
+ }
945
+
946
+ async lzcDockerComposeCapture(argv) {
259
947
  await this.backendVersion020();
948
+ let stream;
949
+ if (this.isBuildRemoteMode()) {
950
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv]), {
951
+ shell: false,
952
+ stdio: ['ignore', 'pipe', 'pipe'],
953
+ });
954
+ } else {
955
+ const resolvedIp = await resolveDomain(this.domain);
956
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker-compose', ...argv], {
957
+ shell: false,
958
+ stdio: ['ignore', 'pipe', 'pipe'],
959
+ });
960
+ }
961
+
962
+ return await new Promise((resolve, reject) => {
963
+ let stdout = '';
964
+ let stderr = '';
965
+ stream.stdout.on('data', (chunk) => {
966
+ stdout += chunk.toString();
967
+ });
968
+ stream.stderr.on('data', (chunk) => {
969
+ stderr += chunk.toString();
970
+ });
971
+ stream.on('error', (err) => reject(err));
972
+ stream.on('close', (code) => {
973
+ if (code == 0) {
974
+ resolve(stdout.trim());
975
+ return;
976
+ }
977
+ reject(new Error(`lzc-docker-compose command failed\n${stdout}\n${stderr}`));
978
+ });
979
+ });
980
+ }
981
+
982
+ async hostReadFile(pathname) {
983
+ const target = String(pathname ?? '').trim();
984
+ if (!target) {
985
+ throw new Error('hostReadFile path is empty');
986
+ }
987
+ if (this.isBuildRemoteMode()) {
988
+ const result = this.remoteHostExec(['cat', target]);
989
+ if (result.status === 0) {
990
+ return String(result.stdout ?? '');
991
+ }
992
+ throw new Error(`read remote file failed: ${target}`);
993
+ }
260
994
 
261
995
  const resolvedIp = await resolveDomain(this.domain);
262
- const stream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), '-t', 'lzc-docker-compose', ...argv], {
263
- shell: true,
264
- stdio: 'inherit',
996
+ const stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'cat', target], {
997
+ shell: false,
998
+ stdio: ['ignore', 'pipe', 'pipe'],
265
999
  });
1000
+ return await new Promise((resolve, reject) => {
1001
+ let stdout = '';
1002
+ let stderr = '';
1003
+ stream.stdout.on('data', (chunk) => {
1004
+ stdout += chunk.toString();
1005
+ });
1006
+ stream.stderr.on('data', (chunk) => {
1007
+ stderr += chunk.toString();
1008
+ });
1009
+ stream.on('error', (err) => reject(err));
1010
+ stream.on('close', (code) => {
1011
+ if (code === 0) {
1012
+ resolve(stdout);
1013
+ return;
1014
+ }
1015
+ reject(new Error(`read host file failed\n${stderr}`));
1016
+ });
1017
+ });
1018
+ }
1019
+
1020
+ async lzcDockerImageRepoDigests(image) {
1021
+ const inspect = await this.lzcDockerImageInspect(image);
1022
+ const repoDigests = inspect?.RepoDigests;
1023
+ if (!Array.isArray(repoDigests)) {
1024
+ return [];
1025
+ }
1026
+ return repoDigests.filter((item) => typeof item === 'string' && item.trim() !== '');
1027
+ }
1028
+
1029
+ async lzcDockerImageInspect(image) {
1030
+ const output = await this.lzcDockerCapture(['image', 'inspect', image]);
1031
+ if (!output) {
1032
+ throw new Error(`lzc-docker image inspect output is empty: ${image}`);
1033
+ }
1034
+ let parsed;
1035
+ try {
1036
+ parsed = JSON.parse(output);
1037
+ } catch (error) {
1038
+ throw new Error(`failed to parse image inspect output for ${image}: ${error.message}`);
1039
+ }
1040
+ if (!Array.isArray(parsed) || parsed.length === 0 || !parsed[0] || typeof parsed[0] !== 'object') {
1041
+ throw new Error(`invalid image inspect output for ${image}`);
1042
+ }
1043
+ return parsed[0];
1044
+ }
1045
+
1046
+ async lzcDockerCompose(argv) {
1047
+ await this.backendVersion020();
1048
+ const stream = this.isBuildRemoteMode()
1049
+ ? spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv], { tty: true }), {
1050
+ shell: false,
1051
+ stdio: 'inherit',
1052
+ })
1053
+ : spawn(sshBinary(), [...sshCmdArgs(`box@${await resolveDomain(this.domain)}`), '-t', 'lzc-docker-compose', ...argv], {
1054
+ shell: true,
1055
+ stdio: 'inherit',
1056
+ });
266
1057
  return new Promise((resolve, reject) => {
267
1058
  stream.on('close', (code) => {
268
1059
  code == 0 ? resolve() : reject();