@lazycatcloud/lzc-cli 2.0.0-pre.7 → 2.0.0-pre.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0-pre.8](https://gitee.com/linakesi/lzc-cli/compare/v2.0.0-pre.7...v2.0.0-pre.8) (2026-03-27)
4
+
5
+ ### Bug Fixes
6
+
7
+ - keep waiting for dev id sync until app reaches a terminal state
8
+ - use app name map for apk trigger
9
+ - include pkg hash in appstore publish payload
10
+ - use debug bridge version for ssh probe
11
+ - fix backend/frontend build
12
+
3
13
  ## [2.0.0-pre.7](https://gitee.com/linakesi/lzc-cli/compare/v2.0.0-pre.6...v2.0.0-pre.7) (2026-03-26)
4
14
 
5
15
  ### Features
@@ -109,7 +109,7 @@ export class LpkInstaller {
109
109
 
110
110
  logger.debug(t('lzc_cli.lib.app.lpk_installer.install_from_file_gen_apk_tips', '是否生成APK:'), installConfig.apk);
111
111
  if (installConfig.apk) {
112
- triggerApk(manifest['package'], manifest['name'], path.resolve(path.join(tempDir, 'icon.png'))).catch((err) => {
112
+ triggerApk(manifest['package'], manifest, path.resolve(path.join(tempDir, 'icon.png'))).catch((err) => {
113
113
  logger.debug(t('lzc_cli.lib.app.lpk_installer.install_from_file_gen_apk_error', '生成 APK 失败: '), err);
114
114
  logger.debug(t('lzc_cli.lib.app.lpk_installer.install_from_file_gen_apk_error_tips', '生成 APK 失败,使用 lzc-cli project devshell --apk n 忽略该错误'));
115
115
  });
@@ -33,15 +33,18 @@ export function buildProjectStartupFailureMessage(deploy = {}, errmsg = '') {
33
33
  return `Project app entered error state before dev.id sync. Instance status: ${instanceStatus}.${suffix}`;
34
34
  }
35
35
 
36
+ export function isProjectStartupTerminalState(deploy = {}) {
37
+ const appStatus = String(deploy?.appStatus ?? '').trim().toLowerCase();
38
+ const instanceStatus = String(deploy?.instanceStatus ?? '').trim().toLowerCase();
39
+ if (appStatus === 'failed' || appStatus === 'paused') {
40
+ return true;
41
+ }
42
+ return instanceStatus.includes('error') || instanceStatus.includes('paused');
43
+ }
44
+
36
45
  async function detectProjectStartupFailure(runtime) {
37
46
  const deploy = await getProjectDeployInfo(runtime);
38
- const appStatus = String(deploy?.appStatus ?? '').trim();
39
- const instanceStatus = String(deploy?.instanceStatus ?? '').trim();
40
- const instanceStatusLower = instanceStatus.toLowerCase();
41
- if (appStatus === 'NotInstalled') {
42
- throw new Error('Project package is not installed after deploy. Please retry and run "lzc-cli project info" for details.');
43
- }
44
- if (!instanceStatusLower.includes('error')) {
47
+ if (!isProjectStartupTerminalState(deploy)) {
45
48
  return null;
46
49
  }
47
50
  const errmsg = await getProjectErrmsgByDeployId(runtime, deploy.deployId);
@@ -51,8 +54,8 @@ async function detectProjectStartupFailure(runtime) {
51
54
  async function syncProjectDevID(runtime, { waitForContainer = false } = {}) {
52
55
  const devId = await runtime.bridge.resolveDevId();
53
56
  logger.info(`Sync dev.id: ${devId || '<empty>'}`);
54
- const maxAttempts = waitForContainer ? 30 : 1;
55
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
57
+ let attempt = 1;
58
+ for (;;) {
56
59
  try {
57
60
  await runtime.bridge.syncDevID(runtime.pkgId, devId, runtime.userApp);
58
61
  logger.info('dev id synced successfully.');
@@ -62,13 +65,11 @@ async function syncProjectDevID(runtime, { waitForContainer = false } = {}) {
62
65
  throw error;
63
66
  }
64
67
  await detectProjectStartupFailure(runtime);
65
- if (attempt === maxAttempts) {
66
- throw error;
67
- }
68
68
  if (attempt === 1) {
69
69
  logger.info('Waiting for app container before syncing dev.id...');
70
70
  }
71
71
  await new Promise((resolve) => setTimeout(resolve, 1000));
72
+ attempt += 1;
72
73
  }
73
74
  }
74
75
  }
@@ -7,16 +7,53 @@ import { t } from '../i18n/index.js';
7
7
  import { appStoreServerUrl } from './env.js';
8
8
 
9
9
  // axios@1.7.7 依赖的 form-data 使用了 util.isArray 导致 node 会输出弃用警告 (所以将逻辑迁移避免依赖)
10
- export async function triggerApk(id, name, iconPath) {
10
+ function buildAppNameMap(appInfo) {
11
+ if (typeof appInfo === 'string') {
12
+ const name = appInfo.trim();
13
+ return name ? { zh: name } : {};
14
+ }
15
+
16
+ if (!appInfo || typeof appInfo !== 'object' || Array.isArray(appInfo)) {
17
+ return {};
18
+ }
19
+
20
+ if (appInfo.name && typeof appInfo.name === 'object' && !Array.isArray(appInfo.name)) {
21
+ const directNameMap = Object.fromEntries(
22
+ Object.entries(appInfo.name).filter(([, value]) => typeof value === 'string' && value.trim() !== ''),
23
+ );
24
+ if (Object.keys(directNameMap).length > 0) {
25
+ return directNameMap;
26
+ }
27
+ }
28
+
29
+ if (appInfo.locales && typeof appInfo.locales === 'object' && !Array.isArray(appInfo.locales)) {
30
+ const localizedNameMap = Object.fromEntries(
31
+ Object.entries(appInfo.locales)
32
+ .map(([locale, value]) => [locale, typeof value?.name === 'string' ? value.name.trim() : ''])
33
+ .filter(([, value]) => value !== ''),
34
+ );
35
+ if (Object.keys(localizedNameMap).length > 0) {
36
+ return localizedNameMap;
37
+ }
38
+ }
39
+
40
+ const name = typeof appInfo.name === 'string' ? appInfo.name.trim() : '';
41
+ return name ? { zh: name } : {};
42
+ }
43
+
44
+ export async function triggerApk(id, appInfo, iconPath) {
11
45
  if (!id) {
12
46
  logger.error(t('lzc_cli.lib.appstore.apkshell.trigger_apk_empty_appid', 'Appid 为必填项!'));
13
47
  return;
14
48
  }
15
49
 
16
- const appName = name || t('lzc_cli.lib.appstore.apkshell.trigger_apk_default_app_name', '懒猫应用');
50
+ const requestUrl = `${appStoreServerUrl}/api/trigger_latest_for_app`;
17
51
  const form = new FormData();
18
52
  form.append('app_id', id);
19
- form.append('app_name', appName);
53
+ const appNameMap = buildAppNameMap(appInfo);
54
+ if (Object.keys(appNameMap).length > 0) {
55
+ form.append('app_name_map', JSON.stringify(appNameMap));
56
+ }
20
57
 
21
58
  if (iconPath && fs.existsSync(iconPath)) {
22
59
  const iconBuffer = fs.readFileSync(iconPath);
@@ -27,13 +64,28 @@ export async function triggerApk(id, name, iconPath) {
27
64
  const timer = setTimeout(() => controller.abort(), 5000);
28
65
 
29
66
  try {
30
- const resp = await fetch(`${appStoreServerUrl}/api/trigger_latest_for_app`, {
67
+ logger.debug('triggerApk request:', {
68
+ url: requestUrl,
69
+ appId: id,
70
+ hasAppNameMap: Object.keys(appNameMap).length > 0,
71
+ appNameMapKeys: Object.keys(appNameMap),
72
+ hasAppIcon: Boolean(iconPath && fs.existsSync(iconPath)),
73
+ appIconPath: iconPath || '',
74
+ });
75
+
76
+ const resp = await fetch(requestUrl, {
31
77
  method: 'POST',
32
78
  body: form,
33
79
  signal: controller.signal,
34
80
  });
35
81
 
36
- logger.debug('triggerApk resp:', resp);
82
+ logger.debug('triggerApk response:', {
83
+ url: requestUrl,
84
+ appId: id,
85
+ status: resp.status,
86
+ statusText: resp.statusText,
87
+ ok: resp.ok,
88
+ });
37
89
  if (resp.status == 304) {
38
90
  logger.debug(t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_tips', `APK构建任务已创建成功,如需使用安卓端,请耐心等待1分钟左右`));
39
91
  } else if (resp.status <= 201) {
@@ -43,8 +95,16 @@ export async function triggerApk(id, name, iconPath) {
43
95
  throw t('lzc_cli.lib.appstore.apkshell.trigger_apk_build_failed_tips', `请求生成应用出错! 使用 --apk=n 停止生成APK`);
44
96
  }
45
97
  } catch (error) {
46
- logger.debug(error);
98
+ logger.debug('triggerApk error:', {
99
+ url: requestUrl,
100
+ appId: id,
101
+ error,
102
+ });
47
103
  } finally {
48
104
  clearTimeout(timer);
49
105
  }
50
106
  }
107
+
108
+ export const __test__ = {
109
+ buildAppNameMap,
110
+ };
@@ -360,6 +360,7 @@ export class Publish {
360
360
  name: lpkInfo.version,
361
361
  icon_path: lpkInfo.iconPath,
362
362
  pkg_path: lpkInfo.url,
363
+ pkg_hash: lpkInfo.sha256,
363
364
  unsupported_platforms: lpkInfo.unsupportedPlatforms,
364
365
  min_os_version: lpkInfo.minOsVersion,
365
366
  lpk_size: lpkInfo.lpkSize,
@@ -1,15 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import spawn from 'cross-spawn';
3
3
  import logger from 'loglevel';
4
- import fetch from 'node-fetch';
5
4
 
6
5
  import shellApi from './shellapi.js';
7
- import { _SYSTEM_ENV_PREFIX } from './config/env.js';
8
6
  import { isTraceMode, resolveDomain, sleep, findSshPublicKey, selectSshPublicKey, isWindows, compareVersions } from './utils.js';
9
7
  import { t } from './i18n/index.js';
10
8
  import { resolveBuildRemoteFromFile } from './build_remote.js';
11
9
 
12
- const bannerfileContent = `˄=ᆽ=ᐟ \\`;
13
10
  const DEBUG_BRIDGE_CONTAINER = 'cloudlazycatdevelopertools-app-1';
14
11
  const DEBUG_BRIDGE_BINARY = '/lzcapp/pkg/content/debug.bridge';
15
12
  const DEBUG_BRIDGE_APP_ID = 'cloud.lazycat.developer.tools';
@@ -52,6 +49,15 @@ export function buildLegacySSHArgs(target, commandArgs = [], { tty = false } = {
52
49
  return args;
53
50
  }
54
51
 
52
+ export function buildLegacySSHProbeArgs(target) {
53
+ const args = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'ControlMaster=no', '-o', 'BatchMode=yes', '-o', 'PreferredAuthentications=publickey', '-o', 'PasswordAuthentication=no', '-o', 'KbdInteractiveAuthentication=no', '-p', '22222'];
54
+ if (isTraceMode()) {
55
+ args.push('-v');
56
+ }
57
+ args.push(target, 'version');
58
+ return args;
59
+ }
60
+
55
61
  function stripAnsi(text = '') {
56
62
  return String(text).replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
57
63
  }
@@ -131,6 +137,38 @@ function normalizeErrorMessage(error) {
131
137
  return String(error).trim();
132
138
  }
133
139
 
140
+ function extractLegacySSHProbeDetail(result = {}) {
141
+ const parts = [];
142
+ const errorMessage = normalizeErrorMessage(result?.error);
143
+ if (errorMessage) {
144
+ parts.push(errorMessage);
145
+ }
146
+ const stderr = String(result?.stderr ?? '').trim();
147
+ if (stderr) {
148
+ parts.push(stderr);
149
+ }
150
+ const stdout = String(result?.stdout ?? '').trim();
151
+ if (stdout) {
152
+ parts.push(stdout);
153
+ }
154
+ return stripAnsi(parts.join('\n')).trim();
155
+ }
156
+
157
+ export function classifyLegacySSHProbeResult(result = {}) {
158
+ if (result?.status === 0) {
159
+ return 'ok';
160
+ }
161
+ const detail = extractLegacySSHProbeDetail(result).toLowerCase();
162
+ if (
163
+ detail.includes('permission denied') ||
164
+ detail.includes('publickey') ||
165
+ detail.includes('too many authentication failures')
166
+ ) {
167
+ return 'unauthorized';
168
+ }
169
+ return 'unavailable';
170
+ }
171
+
134
172
  export function shellEscapeArg(value) {
135
173
  const text = String(value ?? '');
136
174
  if (text === '') {
@@ -157,7 +195,6 @@ export class DebugBridge {
157
195
  this.boxname = shellApi.boxname;
158
196
  }
159
197
  this.domain = `dev.${this.boxname}.heiyu.space`;
160
- this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
161
198
  }
162
199
 
163
200
  isBuildRemoteMode() {
@@ -320,6 +357,19 @@ export class DebugBridge {
320
357
  return shellApi.resolveClientId();
321
358
  }
322
359
 
360
+ async probeLegacySSHAccess() {
361
+ const resolvedIp = await resolveDomain(this.domain);
362
+ const ssh = spawn.sync(sshBinary(), buildLegacySSHProbeArgs(`box@${resolvedIp}`), {
363
+ shell: false,
364
+ encoding: 'utf-8',
365
+ stdio: ['pipe', 'pipe', 'pipe'],
366
+ });
367
+ return {
368
+ kind: classifyLegacySSHProbeResult(ssh),
369
+ detail: extractLegacySSHProbeDetail(ssh),
370
+ };
371
+ }
372
+
323
373
  async init() {
324
374
  await this.checkDevTools();
325
375
  if (!(await this.canPublicKey())) {
@@ -348,39 +398,26 @@ export class DebugBridge {
348
398
  }
349
399
  }
350
400
 
351
- let domain = this.domain;
352
- if (this.checkUseResolve) {
353
- try {
354
- const _ipv6 = await resolveDomain(this.domain, true);
355
- domain = `[${_ipv6}]`;
356
- } catch {}
357
- }
358
- const url = `https://${domain}/bannerfile`;
359
- return new Promise((resolve, reject) => {
360
- fetch(url, { redirect: 'error' })
361
- .then(async (res) => {
362
- const content = await res.text();
363
- if (res.status == 200 && content == bannerfileContent) {
364
- resolve();
365
- return;
366
- }
367
- logger.warn(
368
- t(
369
- 'lzc_cli.lib.debug_bridge.check_dev_tools_not_exist_tips',
370
- `检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
401
+ try {
402
+ const probe = await this.probeLegacySSHAccess();
403
+ if (probe.kind !== 'unavailable') {
404
+ return;
405
+ }
406
+ logger.warn(
407
+ t(
408
+ 'lzc_cli.lib.debug_bridge.check_dev_tools_not_exist_tips',
409
+ `检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
371
410
  点击直接跳转 https://appstore.{{ boxname }}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
372
411
  点击打开应用 https://dev.{{ boxname }}.heiyu.space 查看应用状态
373
412
  `,
374
- { boxname: this.boxname, interpolation: { escapeValue: false } },
375
- ),
376
- );
377
- reject();
378
- })
379
- .catch((err) => {
380
- logger.error(t('lzc_cli.lib.debug_bridge.check_dev_tools_fail_tips', `检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。`), err);
381
- reject();
382
- });
383
- });
413
+ { boxname: this.boxname, interpolation: { escapeValue: false } },
414
+ ),
415
+ );
416
+ throw new Error(probe.detail || t('lzc_cli.lib.debug_bridge.check_dev_tools_fail_tips', `检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。`));
417
+ } catch (err) {
418
+ logger.error(t('lzc_cli.lib.debug_bridge.check_dev_tools_fail_tips', `检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。`), err);
419
+ throw err;
420
+ }
384
421
  }
385
422
 
386
423
  async common(cmd, args) {
@@ -471,8 +508,8 @@ export class DebugBridge {
471
508
  return ssh.status === 0;
472
509
  }
473
510
  try {
474
- await this.common(sshBinary(), buildLegacySSHArgs(`box@${this.domain}`));
475
- return true;
511
+ const probe = await this.probeLegacySSHAccess();
512
+ return probe.kind === 'ok';
476
513
  } catch (err) {
477
514
  logger.debug('canPublicKey error: ', err);
478
515
  if (err?.code == 'ETIMEOUT') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazycatcloud/lzc-cli",
3
- "version": "2.0.0-pre.7",
3
+ "version": "2.0.0-pre.8",
4
4
  "description": "lazycat cloud developer kit",
5
5
  "scripts": {
6
6
  "release": "release-it patch",