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

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 (92) hide show
  1. package/README.md +46 -7
  2. package/changelog.md +56 -19
  3. package/lib/app/apkshell.js +7 -44
  4. package/lib/app/index.js +5 -1
  5. package/lib/app/lpk_build.js +266 -56
  6. package/lib/app/lpk_build_images.js +424 -229
  7. package/lib/app/lpk_build_images_local.js +425 -0
  8. package/lib/app/lpk_build_images_pack_local.js +409 -0
  9. package/lib/app/lpk_create.js +158 -83
  10. package/lib/app/lpk_create_generator.js +35 -42
  11. package/lib/app/lpk_devshell.js +6 -2
  12. package/lib/app/lpk_installer.js +4 -3
  13. package/lib/app/manifest_build.js +259 -0
  14. package/lib/app/project_cp.js +5 -10
  15. package/lib/app/project_deploy.js +80 -11
  16. package/lib/app/project_exec.js +48 -11
  17. package/lib/app/project_info.js +59 -59
  18. package/lib/app/project_log.js +5 -10
  19. package/lib/app/project_runtime.js +113 -18
  20. package/lib/app/project_start.js +6 -11
  21. package/lib/app/project_sync.js +499 -0
  22. package/lib/appstore/apkshell.js +50 -0
  23. package/lib/appstore/publish.js +54 -15
  24. package/lib/build_remote.js +0 -1
  25. package/lib/config/index.js +1 -1
  26. package/lib/debug_bridge.js +217 -47
  27. package/lib/i18n/locales/en/translation.json +262 -262
  28. package/lib/i18n/locales/zh/translation.json +262 -262
  29. package/lib/lpk/core.js +2 -1
  30. package/lib/migrate/index.js +52 -0
  31. package/lib/package_info.js +135 -0
  32. package/lib/shellapi.js +35 -1
  33. package/lib/sig/core.js +2 -2
  34. package/lib/utils.js +92 -15
  35. package/package.json +89 -89
  36. package/scripts/cli.js +2 -0
  37. package/scripts/smoke/frontend-dev-entry.mjs +104 -0
  38. package/scripts/smoke/template-project.mjs +311 -0
  39. package/template/_lpk/README.md +6 -3
  40. package/template/_lpk/gui-vnc.manifest.yml.in +0 -9
  41. package/template/_lpk/hello-vue.manifest.yml.in +38 -0
  42. package/template/_lpk/manifest.yml.in +0 -9
  43. package/template/_lpk/package.yml.in +7 -0
  44. package/template/_lpk/todolist-golang.manifest.yml.in +23 -9
  45. package/template/_lpk/todolist-java.manifest.yml.in +23 -9
  46. package/template/_lpk/todolist-python.manifest.yml.in +31 -9
  47. package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
  48. package/template/blank/lzc-build.dev.yml +4 -0
  49. package/template/blank/lzc-build.yml +0 -2
  50. package/template/blank/lzc-manifest.yml +3 -12
  51. package/template/blank/package.yml +7 -0
  52. package/template/golang/Dockerfile +1 -1
  53. package/template/golang/Dockerfile.dev +20 -0
  54. package/template/golang/README.md +22 -11
  55. package/template/golang/_lzcdevignore +21 -0
  56. package/template/golang/lzc-build.dev.yml +12 -0
  57. package/template/golang/lzc-build.yml +0 -5
  58. package/template/golang/main.go +1 -1
  59. package/template/golang/manifest.dev.page.js +24 -0
  60. package/template/golang/run.sh +7 -0
  61. package/template/gui-vnc/README.md +5 -1
  62. package/template/gui-vnc/lzc-build.dev.yml +4 -0
  63. package/template/gui-vnc/lzc-build.yml +0 -5
  64. package/template/python/Dockerfile +2 -2
  65. package/template/python/Dockerfile.dev +18 -0
  66. package/template/python/README.md +28 -11
  67. package/template/python/_lzcdevignore +21 -0
  68. package/template/python/app.py +1 -1
  69. package/template/python/lzc-build.dev.yml +12 -0
  70. package/template/python/lzc-build.yml +0 -5
  71. package/template/python/manifest.dev.page.js +25 -0
  72. package/template/python/run.sh +12 -1
  73. package/template/springboot/Dockerfile +1 -1
  74. package/template/springboot/Dockerfile.dev +20 -0
  75. package/template/springboot/README.md +22 -11
  76. package/template/springboot/_lzcdevignore +21 -0
  77. package/template/springboot/lzc-build.dev.yml +12 -0
  78. package/template/springboot/lzc-build.yml +0 -5
  79. package/template/springboot/manifest.dev.page.js +24 -0
  80. package/template/springboot/run.sh +7 -0
  81. package/template/vue/README.md +14 -27
  82. package/template/vue/_gitignore +0 -1
  83. package/template/vue/lzc-build.dev.yml +7 -0
  84. package/template/vue/lzc-build.yml +0 -2
  85. package/template/vue/manifest.dev.page.js +50 -0
  86. package/template/vue/src/App.vue +1 -1
  87. package/template/vue-minidb/README.md +11 -19
  88. package/template/vue-minidb/_gitignore +0 -1
  89. package/template/vue-minidb/lzc-build.dev.yml +7 -0
  90. package/template/vue-minidb/lzc-build.yml +0 -2
  91. package/template/vue-minidb/manifest.dev.page.js +50 -0
  92. package/template/blank/_gitignore +0 -1
@@ -0,0 +1,50 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Blob } from 'node:buffer';
4
+ import logger from 'loglevel';
5
+ import fetch, { FormData } from 'node-fetch';
6
+ import { t } from '../i18n/index.js';
7
+ import { appStoreServerUrl } from './env.js';
8
+
9
+ // axios@1.7.7 依赖的 form-data 使用了 util.isArray 导致 node 会输出弃用警告 (所以将逻辑迁移避免依赖)
10
+ export async function triggerApk(id, name, iconPath) {
11
+ if (!id) {
12
+ logger.error(t('lzc_cli.lib.appstore.apkshell.trigger_apk_empty_appid', 'Appid 为必填项!'));
13
+ return;
14
+ }
15
+
16
+ const appName = name || t('lzc_cli.lib.appstore.apkshell.trigger_apk_default_app_name', '懒猫应用');
17
+ const form = new FormData();
18
+ form.append('app_id', id);
19
+ form.append('app_name', appName);
20
+
21
+ if (iconPath && fs.existsSync(iconPath)) {
22
+ const iconBuffer = fs.readFileSync(iconPath);
23
+ form.append('app_icon', new Blob([iconBuffer]), path.basename(iconPath));
24
+ }
25
+
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), 5000);
28
+
29
+ try {
30
+ const resp = await fetch(`${appStoreServerUrl}/api/trigger_latest_for_app`, {
31
+ method: 'POST',
32
+ body: form,
33
+ signal: controller.signal,
34
+ });
35
+
36
+ logger.debug('triggerApk resp:', resp);
37
+ if (resp.status == 304) {
38
+ logger.debug(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_tips', `APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`));
39
+ } else if (resp.status <= 201) {
40
+ logger.info(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_ok_tips', `APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`));
41
+ } else if (resp.status >= 400) {
42
+ logger.debug(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_failed', '请求按钮应用出错:'), await resp.text());
43
+ throw t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_failed_tips', `请求生成应用出错! 使用 --apk=n 停止生成APK`);
44
+ }
45
+ } catch (error) {
46
+ logger.debug(error);
47
+ } finally {
48
+ clearTimeout(timer);
49
+ }
50
+ }
@@ -107,15 +107,33 @@ async function askPublishAppInfo(baseUrl, manifest, locale) {
107
107
  }
108
108
 
109
109
  async function askWhetherCreateLPK(baseUrl, manifest, locale) {
110
- const answers = await inquirer.prompt([
111
- {
112
- name: 'continue',
113
- type: 'input',
114
- message: t('lzc_cli.lib.publish.ask_whether_create_lpk_continue_prompt', '检测到您当前的应用,还没有在懒猫微服中创建,是否使用当前的安装包中的信息进行创建? [y/n]'),
115
- default: 'y',
116
- },
117
- ]);
118
- if (answers.continue.toLowerCase() === 'y') {
110
+ try {
111
+ // 是否允许创建应用预检请求
112
+ const _options = await request(`${baseUrl}/app/create`, {
113
+ method: 'OPTIONS',
114
+ });
115
+ logger.debug('create app preflight:', { url: _options.url, ok: _options.ok, status: _options.status });
116
+ if (!_options.ok || _options.status < 199 || _options.status > 299) {
117
+ throw undefined;
118
+ }
119
+
120
+ // 要求输入应用信息
121
+ const answers = await inquirer.prompt([
122
+ {
123
+ name: 'continue',
124
+ type: 'input',
125
+ message: t(
126
+ 'lzc_cli.lib.publish.ask_whether_create_lpk_continue_prompt',
127
+ '检测到您当前的应用,还没有在懒猫微服中创建,是否使用当前的安装包中的信息进行创建? [y/n]',
128
+ ),
129
+ default: 'y',
130
+ },
131
+ ]);
132
+ if (answers.continue.toLowerCase() != 'y') {
133
+ throw undefined;
134
+ }
135
+
136
+ // 发送创建应用请求
119
137
  const appInfo = await askPublishAppInfo(baseUrl, manifest, locale);
120
138
  const crateAppRes = await request(`${baseUrl}/app/create`, {
121
139
  method: 'POST',
@@ -127,9 +145,28 @@ async function askWhetherCreateLPK(baseUrl, manifest, locale) {
127
145
  source_author: appInfo.author,
128
146
  }),
129
147
  });
130
- logger.debug('create app res: ', await crateAppRes.text());
131
- logger.info(t('lzc_cli.lib.publish.ask_whether_create_lpk_success_tips', `创建 {{ package }} 应用成功!`, { package: manifest['package'], interpolation: { escapeValue: false } }));
132
- } else {
148
+ logger.debug('create app res:', await crateAppRes.text());
149
+ if (!crateAppRes.ok) {
150
+ const data = await crateAppRes.json();
151
+ throw new Error(data?.message || `status:${crateAppRes.status} statusText:${crateAppRes.statusText}`);
152
+ }
153
+ logger.info(
154
+ t('lzc_cli.lib.publish.ask_whether_create_lpk_success_tips', `创建 {{ package }} 应用成功!`, {
155
+ package: manifest['package'],
156
+ interpolation: { escapeValue: false },
157
+ }),
158
+ );
159
+ } catch (error) {
160
+ if (error) {
161
+ logger.debug('create app failed: ', error.message ?? 'unknown');
162
+ logger.error(
163
+ t('lzc_cli.lib.publish.ask_whether_create_lpk_failed_tips', `创建 {{ package }} 应用失败!`, {
164
+ package: manifest['package'],
165
+ interpolation: { escapeValue: false },
166
+ }),
167
+ error.message || '',
168
+ );
169
+ }
133
170
  logger.info(
134
171
  t(
135
172
  'lzc_cli.lib.publish.ask_whether_create_lpk_fail_tips',
@@ -212,7 +249,7 @@ export class Publish {
212
249
 
213
250
  const { manifest, appIdExisted } = await this.checkAppIdExist(pkgPath);
214
251
  if (!appIdExisted) {
215
- await askWhetherCreateLPK(this.baseUrl, manifest, locale);
252
+ await askWhetherCreateLPK(this.baseUrl, manifest, currentLocale);
216
253
  }
217
254
 
218
255
  await autoLogin();
@@ -239,7 +276,7 @@ export class Publish {
239
276
  logger.error(
240
277
  t('lzc_cli.lib.publish.publish_lpk_fail_tips', `LPK 文件上传失败,err: {{ message }}`, {
241
278
  message: lpkInfo?.message ?? lpkInfo,
242
- interpolation: { escapeValue: false }
279
+ interpolation: { escapeValue: false },
243
280
  }),
244
281
  );
245
282
  throw new Error(lpkInfo?.message ?? lpkInfo);
@@ -253,7 +290,7 @@ export class Publish {
253
290
 
254
291
  // changelogs 本地化
255
292
  const langKey = getLanguageForLocale(currentLocale);
256
- changelogs[langKey] = changelog
293
+ changelogs[langKey] = changelog;
257
294
  }
258
295
  logger.debug('publish changelogs is', changelogs);
259
296
 
@@ -270,6 +307,8 @@ export class Publish {
270
307
  pkg_path: lpkInfo.url,
271
308
  unsupported_platforms: lpkInfo.unsupportedPlatforms,
272
309
  min_os_version: lpkInfo.minOsVersion,
310
+ lpk_size: lpkInfo.lpkSize,
311
+ image_size: lpkInfo.imageSize,
273
312
  changelogs,
274
313
  },
275
314
  };
@@ -1,7 +1,6 @@
1
1
  import { loadBuildRemoteFromConfig } from './box/ssh_remote.js';
2
2
 
3
3
  export const DEFAULT_BUILD_CONFIG_FILE = 'lzc-build.yml';
4
- export const DEFAULT_BUILD_BASE_FILE = 'lzc-build.base.yml';
5
4
 
6
5
  export function resolveBuildRemoteFromOptions(options, source = DEFAULT_BUILD_CONFIG_FILE) {
7
6
  void options;
@@ -51,7 +51,7 @@ export function configCommand(program) {
51
51
  ];
52
52
  program.command({
53
53
  command: 'config',
54
- desc: false,
54
+ desc: t('lzc_cli.lib.config.index.config_cmd_desc', '配置管理'),
55
55
  builder: (args) => {
56
56
  args.command(subCommands);
57
57
  args.example('$0 config get', t('lzc_cli.lib.config.index.config_cmd_list_tips', '列出所有配置'))
@@ -34,6 +34,18 @@ export function sshCmdArgsRaw(...args) {
34
34
  return [...defaultOptions, ...args];
35
35
  }
36
36
 
37
+ export function buildLegacySSHArgs(target, commandArgs = [], { tty = false } = {}) {
38
+ const args = sshCmdArgsRaw();
39
+ if (tty) {
40
+ args.push('-t');
41
+ }
42
+ args.push(target);
43
+ if (Array.isArray(commandArgs) && commandArgs.length > 0) {
44
+ args.push(buildSSHRemoteCommand(commandArgs.filter((arg) => String(arg ?? '') !== '')));
45
+ }
46
+ return args;
47
+ }
48
+
37
49
  function stripAnsi(text = '') {
38
50
  return String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
39
51
  }
@@ -108,6 +120,21 @@ function normalizeErrorMessage(error) {
108
120
  return String(error).trim();
109
121
  }
110
122
 
123
+ export function shellEscapeArg(value) {
124
+ const text = String(value ?? '');
125
+ if (text === '') {
126
+ return "''";
127
+ }
128
+ if (/^[A-Za-z0-9_/.:=,@+-]+$/.test(text)) {
129
+ return text;
130
+ }
131
+ return "'" + text.replace(/'/g, "'\\''") + "'";
132
+ }
133
+
134
+ export function buildSSHRemoteCommand(args = []) {
135
+ return args.map((arg) => shellEscapeArg(arg)).join(' ');
136
+ }
137
+
111
138
  export class DebugBridge {
112
139
  constructor(baseDir = process.cwd(), buildRemote = undefined) {
113
140
  this.buildRemote = buildRemote === undefined ? resolveBuildRemoteFromFile(baseDir) : buildRemote;
@@ -275,6 +302,13 @@ export class DebugBridge {
275
302
  return args;
276
303
  }
277
304
 
305
+ async resolveDevId() {
306
+ if (this.isBuildRemoteMode()) {
307
+ return '';
308
+ }
309
+ return shellApi.resolveClientId();
310
+ }
311
+
278
312
  async init() {
279
313
  await this.checkDevTools();
280
314
  if (!(await this.canPublicKey())) {
@@ -340,22 +374,22 @@ export class DebugBridge {
340
374
 
341
375
  async common(cmd, args) {
342
376
  const resolvedIp = await resolveDomain(this.domain);
343
- args = args.map((arg) => arg.replace(this.domain, resolvedIp));
344
- const ssh = spawn.sync(cmd, args, {
345
- shell: true,
377
+ const normalizedArgs = args.map((arg) => String(arg).replace(this.domain, resolvedIp));
378
+ const ssh = spawn.sync(cmd, normalizedArgs, {
379
+ shell: false,
346
380
  encoding: 'utf-8',
347
381
  stdio: ['pipe', 'pipe', 'pipe'],
348
382
  });
349
- logger.debug(t('lzc_cli.lib.debug_bridge.common_start_log', `执行命令 {{ cmd }} {{ args }}`, { cmd, args: args.join(' '), interpolation: { escapeValue: false } }));
383
+ logger.debug(t('lzc_cli.lib.debug_bridge.common_start_log', `执行命令 {{ cmd }} {{ args }}`, { cmd, args: normalizedArgs.join(' '), interpolation: { escapeValue: false } }));
350
384
  return new Promise((resolve, reject) => {
351
385
  ssh.status == 0
352
386
  ? resolve(ssh.stdout)
353
387
  : reject(
354
388
  t('lzc_cli.lib.debug_bridge.common_exec_fail', `执行命令 {{ cmd }} {{ args }} 出错\n{{ stdout }}\n{{ stderr }}`, {
355
389
  cmd,
356
- args: args.join(' '),
390
+ args: normalizedArgs.join(' '),
357
391
  stdout: ssh.stdout ?? '',
358
- stdout: ssh.stderr ?? '',
392
+ stderr: ssh.stderr ?? '',
359
393
  interpolation: { escapeValue: false },
360
394
  }),
361
395
  );
@@ -391,8 +425,12 @@ export class DebugBridge {
391
425
  }
392
426
 
393
427
  const resolvedIp = await resolveDomain(this.domain);
394
- const ssh = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), `install --uid ${this.uid}`, pkgId ? `--pkgId ${pkgId}` : ''], {
395
- shell: true,
428
+ const commandArgs = ['install', '--uid', this.uid];
429
+ if (pkgId) {
430
+ commandArgs.push('--pkgId', pkgId);
431
+ }
432
+ const ssh = spawn(sshBinary(), buildLegacySSHArgs(`box@${resolvedIp}`, commandArgs), {
433
+ shell: false,
396
434
  stdio: ['pipe', 'pipe', 'pipe'],
397
435
  });
398
436
  const output = streamInstallOutput(ssh, { printStdout: false, printStderr: false });
@@ -422,7 +460,7 @@ export class DebugBridge {
422
460
  return ssh.status === 0;
423
461
  }
424
462
  try {
425
- await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`)]);
463
+ await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`));
426
464
  return true;
427
465
  } catch (err) {
428
466
  logger.debug('canPublicKey error: ', err);
@@ -467,7 +505,7 @@ export class DebugBridge {
467
505
  if (this.isBuildRemoteMode()) {
468
506
  return this.remoteCommon(await this.remoteCommandWithUID(['status', appId]));
469
507
  }
470
- return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `status --uid ${this.uid}`, appId]);
508
+ return this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['status', '--uid', this.uid, appId]));
471
509
  }
472
510
 
473
511
  async info(appId) {
@@ -475,7 +513,7 @@ export class DebugBridge {
475
513
  if (this.isBuildRemoteMode()) {
476
514
  stdout = await this.remoteCommon(await this.remoteCommandWithUID(['info', appId]));
477
515
  } else {
478
- stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `info --uid ${this.uid}`, appId]);
516
+ stdout = await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['info', '--uid', this.uid, appId]));
479
517
  }
480
518
  try {
481
519
  return JSON.parse(stdout);
@@ -484,13 +522,36 @@ export class DebugBridge {
484
522
  }
485
523
  }
486
524
 
525
+ async syncDevID(appId, devId = '', userApp = false) {
526
+ const normalizedDevId = String(devId ?? '').trim();
527
+ if (this.isBuildRemoteMode()) {
528
+ const commandArgs = await this.remoteCommandWithUID(['sync-dev-id']);
529
+ if (normalizedDevId) {
530
+ commandArgs.push('--dev-id', normalizedDevId);
531
+ }
532
+ if (userApp) {
533
+ commandArgs.push('--userapp');
534
+ }
535
+ commandArgs.push(appId);
536
+ return this.remoteCommon(commandArgs);
537
+ }
538
+ const rawArgs = ['sync-dev-id', '--uid', this.uid];
539
+ if (normalizedDevId) {
540
+ rawArgs.push('--dev-id', normalizedDevId);
541
+ }
542
+ if (userApp) {
543
+ rawArgs.push('--userapp');
544
+ }
545
+ rawArgs.push(appId);
546
+ return this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, rawArgs));
547
+ }
487
548
  async isDevshell(appId) {
488
549
  await this.backendVersion020();
489
550
  if (this.isBuildRemoteMode()) {
490
551
  const stdout = await this.remoteCommon(await this.remoteCommandWithUID(['isDevshellV2', appId]));
491
552
  return stdout.trim() == 'true';
492
553
  }
493
- const stdout = await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `isDevshellV2 --uid ${this.uid}`, appId]);
554
+ const stdout = await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['isDevshellV2', '--uid', this.uid, appId]));
494
555
  return stdout == 'true';
495
556
  }
496
557
 
@@ -498,18 +559,18 @@ export class DebugBridge {
498
559
  if (this.isBuildRemoteMode()) {
499
560
  return this.remoteCommon(await this.remoteCommandWithUID(['resume', appId]));
500
561
  }
501
- return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `resume --uid ${this.uid}`, appId]);
562
+ return this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['resume', '--uid', this.uid, appId]));
502
563
  }
503
564
 
504
565
  async pause(appId) {
505
566
  if (this.isBuildRemoteMode()) {
506
567
  return this.remoteCommon(await this.remoteCommandWithUID(['pause', appId]));
507
568
  }
508
- return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `pause --uid ${this.uid}`, appId]);
569
+ return this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['pause', '--uid', this.uid, appId]));
509
570
  }
510
571
 
511
572
  async version() {
512
- const output = this.isBuildRemoteMode() ? await this.remoteCommon(['version']) : await this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `version`]);
573
+ const output = this.isBuildRemoteMode() ? await this.remoteCommon(['version']) : await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['version']));
513
574
  logger.debug(`backend version:\n${output}`);
514
575
  try {
515
576
  const data = JSON.parse(output);
@@ -519,6 +580,23 @@ export class DebugBridge {
519
580
  }
520
581
  }
521
582
 
583
+ async platform() {
584
+ await this.backendVersion020();
585
+ const output = this.isBuildRemoteMode() ? await this.remoteCommon(['platform']) : await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['platform']));
586
+ logger.debug(`backend platform:\n${output}`);
587
+ let data;
588
+ try {
589
+ data = JSON.parse(output);
590
+ } catch (error) {
591
+ throw new Error(`parse platform output failed: ${error.message}`);
592
+ }
593
+ const platform = String(data?.platform ?? '').trim().toLowerCase();
594
+ if (!/^[a-z0-9]+\/[a-z0-9]+$/.test(platform)) {
595
+ throw new Error(`invalid platform output: ${output}`);
596
+ }
597
+ return platform;
598
+ }
599
+
522
600
  async uninstall(appId, deleteAppData = false) {
523
601
  if (this.isBuildRemoteMode()) {
524
602
  const commandArgs = await this.remoteCommandWithUID(['uninstall']);
@@ -528,7 +606,7 @@ export class DebugBridge {
528
606
  commandArgs.push(appId);
529
607
  return this.remoteCommon(commandArgs);
530
608
  }
531
- return this.common(sshBinary(), [...sshCmdArgs(`box@${this.domain}`), `uninstall --uid ${this.uid}`, deleteAppData ? '--delete-data' : '', appId]);
609
+ return this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`, ['uninstall', '--uid', this.uid, ...(deleteAppData ? ['--delete-data'] : []), appId]));
532
610
  }
533
611
 
534
612
  async devshell(appId, isUserApp, onconnect = null) {
@@ -557,14 +635,15 @@ export class DebugBridge {
557
635
  });
558
636
  } else {
559
637
  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
- );
638
+ const commandArgs = ['devshell', '--uid', this.uid];
639
+ if (isUserApp) {
640
+ commandArgs.push('--userapp');
641
+ }
642
+ commandArgs.push(appId, '/bin/sh', '/lzcapp/pkg/content/devshell/exec.sh');
643
+ stream = spawn(sshBinary(), buildLegacySSHArgs(`box@${resolvedIp}`, commandArgs, { tty: true }), {
644
+ shell: false,
645
+ stdio: 'inherit',
646
+ });
568
647
  }
569
648
  return new Promise((resolve, reject) => {
570
649
  stream.on('close', (code) => {
@@ -590,8 +669,8 @@ export class DebugBridge {
590
669
  });
591
670
  } else {
592
671
  const resolvedIp = await resolveDomain(this.domain);
593
- buildStream = spawn(sshBinary(), [...sshCmdArgs(`box@${resolvedIp}`), ['build', '--tag', tag].join(' ')], {
594
- shell: true,
672
+ buildStream = spawn(sshBinary(), buildLegacySSHArgs(`box@${resolvedIp}`, buildArgs), {
673
+ shell: false,
595
674
  stdio: ['pipe', 'inherit', 'inherit'],
596
675
  });
597
676
  }
@@ -698,15 +777,20 @@ export class DebugBridge {
698
777
 
699
778
  async lzcDocker(argv) {
700
779
  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
- });
780
+ let stream;
781
+ if (this.isBuildRemoteMode()) {
782
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv], { tty: true }), {
783
+ shell: false,
784
+ stdio: 'inherit',
785
+ });
786
+ } else {
787
+ const resolvedIp = await resolveDomain(this.domain);
788
+ const remoteCommand = buildSSHRemoteCommand(['lzc-docker', ...argv]);
789
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), '-t', remoteCommand], {
790
+ shell: false,
791
+ stdio: 'inherit',
792
+ });
793
+ }
710
794
  return new Promise((resolve, reject) => {
711
795
  stream.on('close', (code) => {
712
796
  code == 0 ? resolve() : reject();
@@ -911,24 +995,105 @@ export class DebugBridge {
911
995
  });
912
996
  }
913
997
 
914
- async lzcDockerCapture(argv) {
998
+ async packImagesArchive(imagesSpec, archivePath, outputPath) {
915
999
  await this.backendVersion020();
1000
+ if (!Array.isArray(imagesSpec) || imagesSpec.length === 0) {
1001
+ throw new Error('imagesSpec cannot be empty');
1002
+ }
1003
+ const spec = Buffer.from(
1004
+ JSON.stringify({
1005
+ images: imagesSpec,
1006
+ }),
1007
+ ).toString('base64');
1008
+
1009
+ const input = fs.createReadStream(archivePath);
1010
+ const output = fs.createWriteStream(outputPath);
1011
+ let stream;
1012
+ if (this.isBuildRemoteMode()) {
1013
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['pack-images-archive', '--spec', spec]), {
1014
+ shell: false,
1015
+ stdio: ['pipe', 'pipe', 'inherit'],
1016
+ });
1017
+ } else {
1018
+ const resolvedIp = await resolveDomain(this.domain);
1019
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'pack-images-archive', '--spec', spec], {
1020
+ shell: false,
1021
+ stdio: ['pipe', 'pipe', 'inherit'],
1022
+ });
1023
+ }
1024
+ input.pipe(stream.stdin);
1025
+ stream.stdout.pipe(output);
1026
+
1027
+ return new Promise((resolve, reject) => {
1028
+ let done = false;
1029
+ const fail = (err) => {
1030
+ if (done) {
1031
+ return;
1032
+ }
1033
+ done = true;
1034
+ try {
1035
+ stream.kill('SIGKILL');
1036
+ } catch {}
1037
+ try {
1038
+ input.destroy();
1039
+ } catch {}
1040
+ try {
1041
+ output.destroy();
1042
+ } catch {}
1043
+ try {
1044
+ fs.rmSync(outputPath, { force: true });
1045
+ } catch {}
1046
+ reject(err);
1047
+ };
1048
+
1049
+ input.on('error', (e) => {
1050
+ fail(e);
1051
+ });
1052
+ stream.on('error', (e) => {
1053
+ fail(e);
1054
+ });
1055
+ output.on('error', (e) => {
1056
+ fail(e);
1057
+ });
1058
+ stream.on('close', (code) => {
1059
+ if (done) {
1060
+ return;
1061
+ }
1062
+ output.end(() => {
1063
+ if (code == 0) {
1064
+ done = true;
1065
+ resolve();
1066
+ return;
1067
+ }
1068
+ fail(new Error('pack-images-archive failed'));
1069
+ });
1070
+ });
1071
+ });
1072
+ }
1073
+
1074
+ async lzcDockerCapture(argv, options = {}) {
1075
+ await this.backendVersion020();
1076
+ const stdinText = options && Object.prototype.hasOwnProperty.call(options, 'stdinText') ? String(options.stdinText ?? '') : '';
916
1077
  let stream;
917
1078
  if (this.isBuildRemoteMode()) {
918
1079
  stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker', ...argv]), {
919
1080
  shell: false,
920
- stdio: ['ignore', 'pipe', 'inherit'],
1081
+ stdio: ['pipe', 'pipe', 'inherit'],
921
1082
  });
922
1083
  } else {
923
1084
  const resolvedIp = await resolveDomain(this.domain);
924
1085
  stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), 'lzc-docker', ...argv], {
925
1086
  shell: false,
926
- stdio: ['ignore', 'pipe', 'inherit'],
1087
+ stdio: ['pipe', 'pipe', 'inherit'],
927
1088
  });
928
1089
  }
929
1090
 
930
1091
  return await new Promise((resolve, reject) => {
931
1092
  let output = '';
1093
+ if (stdinText) {
1094
+ stream.stdin.write(stdinText);
1095
+ }
1096
+ stream.stdin.end();
932
1097
  stream.stdout.on('data', (chunk) => {
933
1098
  output += chunk.toString();
934
1099
  });
@@ -1045,15 +1210,20 @@ export class DebugBridge {
1045
1210
 
1046
1211
  async lzcDockerCompose(argv) {
1047
1212
  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
- });
1213
+ let stream;
1214
+ if (this.isBuildRemoteMode()) {
1215
+ stream = spawn(sshBinary(), this.remoteBridgeArgs(['lzc-docker-compose', ...argv], { tty: true }), {
1216
+ shell: false,
1217
+ stdio: 'inherit',
1218
+ });
1219
+ } else {
1220
+ const resolvedIp = await resolveDomain(this.domain);
1221
+ const remoteCommand = buildSSHRemoteCommand(['lzc-docker-compose', ...argv]);
1222
+ stream = spawn(sshBinary(), [...sshCmdArgsRaw(`box@${resolvedIp}`), '-t', remoteCommand], {
1223
+ shell: false,
1224
+ stdio: 'inherit',
1225
+ });
1226
+ }
1057
1227
  return new Promise((resolve, reject) => {
1058
1228
  stream.on('close', (code) => {
1059
1229
  code == 0 ? resolve() : reject();