@lazycatcloud/lzc-cli 2.0.0-pre.6 → 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 +16 -0
- package/lib/app/lpk_installer.js +1 -1
- package/lib/app/project_deploy.js +13 -12
- package/lib/appstore/apkshell.js +66 -6
- package/lib/appstore/publish.js +1 -0
- package/lib/debug_bridge.js +86 -40
- package/package.json +1 -1
package/changelog.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
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)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- dump more failed message when installing lpk
|
|
18
|
+
|
|
3
19
|
## [2.0.0-pre.6](https://gitee.com/linakesi/lzc-cli/compare/v2.0.0-pre.5...v2.0.0-pre.6) (2026-03-25)
|
|
4
20
|
|
|
5
21
|
### Features
|
package/lib/app/lpk_installer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
for (
|
|
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
|
}
|
package/lib/appstore/apkshell.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
50
|
+
const requestUrl = `${appStoreServerUrl}/api/trigger_latest_for_app`;
|
|
17
51
|
const form = new FormData();
|
|
18
52
|
form.append('app_id', id);
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
};
|
package/lib/appstore/publish.js
CHANGED
|
@@ -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,
|
package/lib/debug_bridge.js
CHANGED
|
@@ -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
|
}
|
|
@@ -64,13 +70,18 @@ function extractInstallErrorDetail(rawOutput = '') {
|
|
|
64
70
|
return detailMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([
|
|
73
|
+
const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([\s\S]*)/i);
|
|
68
74
|
if (descMatch && descMatch[1]) {
|
|
69
|
-
|
|
75
|
+
const detail = descMatch[1]
|
|
76
|
+
.split(/\nUsage:\s*\n|\nUsage:\s*/i)[0]
|
|
77
|
+
.trim();
|
|
78
|
+
if (detail) {
|
|
79
|
+
return detail;
|
|
80
|
+
}
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
const lines = text
|
|
73
|
-
.split(
|
|
84
|
+
.split(/[\r\n]+/)
|
|
74
85
|
.map((line) => line.trim())
|
|
75
86
|
.filter((line) => {
|
|
76
87
|
if (!line) {
|
|
@@ -82,7 +93,7 @@ function extractInstallErrorDetail(rawOutput = '') {
|
|
|
82
93
|
return true;
|
|
83
94
|
});
|
|
84
95
|
if (lines.length > 0) {
|
|
85
|
-
return lines[
|
|
96
|
+
return lines[lines.length - 1];
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
return t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败');
|
|
@@ -126,6 +137,38 @@ function normalizeErrorMessage(error) {
|
|
|
126
137
|
return String(error).trim();
|
|
127
138
|
}
|
|
128
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
|
+
|
|
129
172
|
export function shellEscapeArg(value) {
|
|
130
173
|
const text = String(value ?? '');
|
|
131
174
|
if (text === '') {
|
|
@@ -152,7 +195,6 @@ export class DebugBridge {
|
|
|
152
195
|
this.boxname = shellApi.boxname;
|
|
153
196
|
}
|
|
154
197
|
this.domain = `dev.${this.boxname}.heiyu.space`;
|
|
155
|
-
this.checkUseResolve = !!process.env[`${_SYSTEM_ENV_PREFIX}_CHECK_DNS_RESOLVE`];
|
|
156
198
|
}
|
|
157
199
|
|
|
158
200
|
isBuildRemoteMode() {
|
|
@@ -315,6 +357,19 @@ export class DebugBridge {
|
|
|
315
357
|
return shellApi.resolveClientId();
|
|
316
358
|
}
|
|
317
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
|
+
|
|
318
373
|
async init() {
|
|
319
374
|
await this.checkDevTools();
|
|
320
375
|
if (!(await this.canPublicKey())) {
|
|
@@ -343,39 +398,26 @@ export class DebugBridge {
|
|
|
343
398
|
}
|
|
344
399
|
}
|
|
345
400
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
fetch(url, { redirect: 'error' })
|
|
356
|
-
.then(async (res) => {
|
|
357
|
-
const content = await res.text();
|
|
358
|
-
if (res.status == 200 && content == bannerfileContent) {
|
|
359
|
-
resolve();
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
logger.warn(
|
|
363
|
-
t(
|
|
364
|
-
'lzc_cli.lib.debug_bridge.check_dev_tools_not_exist_tips',
|
|
365
|
-
`检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
|
|
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
|
+
`检测到你还没有安装 '懒猫开发者工具',请先到商店中搜索安装
|
|
366
410
|
点击直接跳转 https://appstore.{{ boxname }}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
|
|
367
411
|
点击打开应用 https://dev.{{ boxname }}.heiyu.space 查看应用状态
|
|
368
412
|
`,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
});
|
|
378
|
-
});
|
|
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
|
+
}
|
|
379
421
|
}
|
|
380
422
|
|
|
381
423
|
async common(cmd, args) {
|
|
@@ -466,8 +508,8 @@ export class DebugBridge {
|
|
|
466
508
|
return ssh.status === 0;
|
|
467
509
|
}
|
|
468
510
|
try {
|
|
469
|
-
await this.
|
|
470
|
-
return
|
|
511
|
+
const probe = await this.probeLegacySSHAccess();
|
|
512
|
+
return probe.kind === 'ok';
|
|
471
513
|
} catch (err) {
|
|
472
514
|
logger.debug('canPublicKey error: ', err);
|
|
473
515
|
if (err?.code == 'ETIMEOUT') {
|
|
@@ -1279,3 +1321,7 @@ export class DebugBridge {
|
|
|
1279
1321
|
process.exit(1);
|
|
1280
1322
|
}
|
|
1281
1323
|
}
|
|
1324
|
+
|
|
1325
|
+
export const __test__ = {
|
|
1326
|
+
extractInstallErrorDetail,
|
|
1327
|
+
};
|