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

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,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [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
+
5
+ ### Features
6
+
7
+ - dump more failed message when installing lpk
8
+
9
+ ## [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)
10
+
11
+ ### Features
12
+
13
+ - add manifest lint command and build-time warnings for deprecated fields, unknown fields, and app store requirements
14
+ - reject appstore publish and pre-publish when the packaged LPK does not satisfy store requirements
15
+
3
16
  ## [2.0.0-pre.5](https://gitee.com/linakesi/lzc-cli/compare/v2.0.0-pre.4...v2.0.0-pre.5) (2026-03-19)
4
17
 
5
18
  ### Features
package/lib/app/index.js CHANGED
@@ -181,6 +181,29 @@ export function lpkProjectCommand(program) {
181
181
  await lpk.exec();
182
182
  },
183
183
  },
184
+ {
185
+ command: 'lint [context]',
186
+ desc: 'Lint manifest compatibility and App Store hints',
187
+ builder: (args) => {
188
+ args.option('f', {
189
+ alias: 'file',
190
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_build_args_file_desc', '指定构建的lzc-build.yml文件'),
191
+ default: 'lzc-build.yml',
192
+ type: 'string',
193
+ });
194
+ },
195
+ handler: async ({ context, file }) => {
196
+ const cwd = context ? path.resolve(context) : process.cwd();
197
+ const lpk = await new LpkBuild(cwd, file).init();
198
+ logger.info(`Build config: ${lpk.optionsFilePath}`);
199
+ const warnings = await lpk.lint();
200
+ if (warnings.length === 0) {
201
+ logger.info('No manifest lint warnings found.');
202
+ return;
203
+ }
204
+ logger.warn(`Found ${warnings.length} manifest lint warning(s).`);
205
+ },
206
+ },
184
207
  {
185
208
  command: 'devshell [context]',
186
209
  desc: false,
@@ -2,10 +2,11 @@ import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import zlib from 'node:zlib';
4
4
  import logger from 'loglevel';
5
- import { isDirExist, isDirSync, isFileExist, dumpToYaml, envTemplateFile, isValidPackageName, tarContentDir, isPngWithFile, isMacOs, isWindows, isLinux } from '../utils.js';
5
+ import { isDirExist, isDirSync, isFileExist, dumpToYaml, envTemplateFile, isValidPackageName, tarContentDir, isPngWithFile, isMacOs, isWindows, isLinux, REPRODUCIBLE_ARCHIVE_DATE } from '../utils.js';
6
6
  import spawn from 'cross-spawn';
7
7
  import { loadEffectiveManifest, splitManifestAndPackageInfo, findManifestStaticFields, PACKAGE_FILE_NAME } from '../package_info.js';
8
8
  import { buildVarsFromEnvEntries, normalizeBuildEnvEntries, preprocessManifestFile } from './manifest_build.js';
9
+ import { collectManifestLintWarnings, logManifestLintWarnings } from './manifest_lint.js';
9
10
  import archiver from 'archiver';
10
11
  import yaml from 'js-yaml';
11
12
  import { buildConfiguredImagesToTempDir } from './lpk_build_images.js';
@@ -32,7 +33,10 @@ async function archiveFolderTo(appDir, out, format = 'zip') {
32
33
  });
33
34
  archive.pipe(output);
34
35
 
35
- archive.directory(appDir, false);
36
+ archive.directory(appDir, false, (entryData) => ({
37
+ ...entryData,
38
+ date: REPRODUCIBLE_ARCHIVE_DATE,
39
+ }));
36
40
  archive.finalize();
37
41
  });
38
42
  }
@@ -432,6 +436,7 @@ export class LpkBuild {
432
436
  this.hasPackageMetadataOverrides = false;
433
437
  this.buildVars = null;
434
438
  this.preprocessedManifestText = '';
439
+ this.lintWarnings = [];
435
440
 
436
441
  this.beforeBuildPackageFn = [];
437
442
  this.beforeDumpYamlFn = [];
@@ -529,6 +534,27 @@ export class LpkBuild {
529
534
  return this.manifest;
530
535
  }
531
536
 
537
+ async collectLintWarnings() {
538
+ await this.getManifest();
539
+ this.lintWarnings = await collectManifestLintWarnings({
540
+ cwd: this.pwd,
541
+ options: this.options,
542
+ manifestPath: this.manifestFilePath,
543
+ sourceManifest: this.sourceManifest,
544
+ manifest: this.manifest,
545
+ packageInfo: this.packageInfo,
546
+ });
547
+ return this.lintWarnings;
548
+ }
549
+
550
+ async lint({ silent = false } = {}) {
551
+ const warnings = await this.collectLintWarnings();
552
+ if (!silent) {
553
+ logManifestLintWarnings(warnings, logger);
554
+ }
555
+ return warnings;
556
+ }
557
+
532
558
  async exec() {
533
559
  await this.getManifest();
534
560
 
@@ -538,6 +564,8 @@ export class LpkBuild {
538
564
  }, this.options);
539
565
  }
540
566
 
567
+ await this.lint();
568
+
541
569
  if (this.options['buildscript']) {
542
570
  const cmd = isWindows ? 'cmd' : 'sh';
543
571
  const cmdArgs = isWindows ? '/c' : '-c';
@@ -633,6 +633,7 @@ export async function buildConfiguredImagesToTempDir(rawConfig, manifest, cwd, t
633
633
  }
634
634
  bridge = new DebugBridge(cwd, buildRemote);
635
635
  await bridge.init();
636
+ await bridge.ensureLpkV2Supported();
636
637
  }
637
638
  if (hasLocalBuilder) {
638
639
  targetPlatform = bridge ? await bridge.platform() : DEFAULT_LOCAL_TARGET_PLATFORM;
@@ -1,7 +1,7 @@
1
1
  import logger from 'loglevel';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { Downloader, extractLpkSync } from '../utils.js';
4
+ import { Downloader, detectLpkArchiveFormat, extractLpkSync } from '../utils.js';
5
5
  import { loadEffectiveManifestFromFiles } from '../package_info.js';
6
6
  import { DebugBridge } from '../debug_bridge.js';
7
7
  import shellapi from '../shellapi.js';
@@ -10,6 +10,11 @@ import { triggerApk } from './apkshell.js';
10
10
  import { resolveBuildRemoteFromFile } from '../build_remote.js';
11
11
 
12
12
  export const installConfig = { apk: true };
13
+
14
+ function isLpkV2Package(pkgPath) {
15
+ return detectLpkArchiveFormat(pkgPath) === 'tar';
16
+ }
17
+
13
18
  // 从一个目录中找出修改时间最新的包
14
19
  function findOnceLpkByDir(dir = process.cwd()) {
15
20
  const pkg = fs
@@ -123,6 +128,9 @@ export class LpkInstaller {
123
128
  }
124
129
  const bridge = new DebugBridge(process.cwd(), buildRemote);
125
130
  await bridge.init();
131
+ if (isLpkV2Package(pkgPath)) {
132
+ await bridge.ensureLpkV2Supported();
133
+ }
126
134
  logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_start_tips', '开始安装应用'));
127
135
  await bridge.install(pkgPath, manifest ? manifest['package'] : '');
128
136
  logger.info('\n');
@@ -144,3 +152,7 @@ export class LpkInstaller {
144
152
  }
145
153
  }
146
154
  }
155
+
156
+ export const __test__ = {
157
+ isLpkV2Package,
158
+ };
@@ -0,0 +1,464 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import semver from 'semver';
4
+ import { findManifestStaticFields } from '../package_info.js';
5
+
6
+ export const STORE_ICON_MAX_BYTES = 200 * 1024;
7
+ export const STORE_IMAGE_REGISTRY_PREFIX = 'registry.lazycat.cloud';
8
+
9
+ function hasOwn(obj, key) {
10
+ return Object.prototype.hasOwnProperty.call(obj ?? {}, key);
11
+ }
12
+
13
+ function hasPath(obj, pathList) {
14
+ let current = obj;
15
+ for (const key of pathList) {
16
+ if (!current || typeof current !== 'object' || !hasOwn(current, key)) {
17
+ return false;
18
+ }
19
+ current = current[key];
20
+ }
21
+ return true;
22
+ }
23
+
24
+ function createWarning(code, message) {
25
+ return { code, message };
26
+ }
27
+
28
+ function quoteFieldList(fields) {
29
+ return fields.map((field) => `\`${field}\``).join(', ');
30
+ }
31
+
32
+ function normalizeString(value) {
33
+ return String(value ?? '').trim();
34
+ }
35
+
36
+ const ANY = Symbol('any');
37
+
38
+ function arrayOf(value) {
39
+ return { kind: 'array', value };
40
+ }
41
+
42
+ function mapOf(value) {
43
+ return { kind: 'map', value };
44
+ }
45
+
46
+ const APP_HEALTHCHECK_SCHEMA = {
47
+ test_url: ANY,
48
+ disable: ANY,
49
+ start_period: ANY,
50
+ timeout: ANY,
51
+ };
52
+
53
+ const SERVICE_HEALTHCHECK_SCHEMA = {
54
+ test: ANY,
55
+ timeout: ANY,
56
+ interval: ANY,
57
+ retries: ANY,
58
+ start_period: ANY,
59
+ start_interval: ANY,
60
+ disable: ANY,
61
+ test_url: ANY,
62
+ };
63
+
64
+ const MANIFEST_SCHEMA = {
65
+ package: ANY,
66
+ version: ANY,
67
+ name: ANY,
68
+ description: ANY,
69
+ usage: ANY,
70
+ license: ANY,
71
+ homepage: ANY,
72
+ author: ANY,
73
+ min_os_version: ANY,
74
+ unsupported_platforms: ANY,
75
+ locales: ANY,
76
+ ext_config: {
77
+ permissions: ANY,
78
+ enable_document_access: ANY,
79
+ enable_media_access: ANY,
80
+ enable_clientfs_access: ANY,
81
+ disable_grpc_web_on_root: ANY,
82
+ default_prefix_domain: ANY,
83
+ enable_bind_mime_globs: ANY,
84
+ disable_url_raw_path: ANY,
85
+ remove_this_request_headers: ANY,
86
+ fix_websocket_header: ANY,
87
+ },
88
+ application: {
89
+ image: ANY,
90
+ background_task: ANY,
91
+ subdomain: ANY,
92
+ secondary_domains: ANY,
93
+ multi_instance: ANY,
94
+ usb_accel: ANY,
95
+ gpu_accel: ANY,
96
+ kvm_accel: ANY,
97
+ file_handler: {
98
+ mime: ANY,
99
+ actions: ANY,
100
+ },
101
+ entries: arrayOf({
102
+ id: ANY,
103
+ title: ANY,
104
+ path: ANY,
105
+ prefix_domain: ANY,
106
+ }),
107
+ routes: ANY,
108
+ upstreams: arrayOf({
109
+ location: ANY,
110
+ disable_trim_location: ANY,
111
+ domain_prefix: ANY,
112
+ backend: ANY,
113
+ use_backend_host: ANY,
114
+ backend_launch_command: ANY,
115
+ trim_url_suffix: ANY,
116
+ disable_backend_ssl_verify: ANY,
117
+ disable_auto_health_checking: ANY,
118
+ disable_url_raw_path: ANY,
119
+ remove_this_request_headers: ANY,
120
+ fix_websocket_header: ANY,
121
+ dump_http_headers_when_5xx: ANY,
122
+ dump_http_headers_when_paths: ANY,
123
+ }),
124
+ injects: arrayOf({
125
+ id: ANY,
126
+ on: ANY,
127
+ auth_required: ANY,
128
+ prefix_domain: ANY,
129
+ when: ANY,
130
+ unless: ANY,
131
+ do: arrayOf({
132
+ src: ANY,
133
+ params: ANY,
134
+ }),
135
+ }),
136
+ public_path: ANY,
137
+ workdir: ANY,
138
+ ingress: arrayOf({
139
+ protocol: ANY,
140
+ port: ANY,
141
+ service: ANY,
142
+ description: ANY,
143
+ publish_port: ANY,
144
+ send_port_info: ANY,
145
+ yes_i_want_80_443: ANY,
146
+ }),
147
+ environment: ANY,
148
+ health_check: APP_HEALTHCHECK_SCHEMA,
149
+ oidc_redirect_path: ANY,
150
+ handlers: {
151
+ acl_handler: ANY,
152
+ error_page_templates: ANY,
153
+ },
154
+ user_app: ANY,
155
+ depends_on: ANY,
156
+ },
157
+ services: mapOf({
158
+ init: ANY,
159
+ image: ANY,
160
+ environment: ANY,
161
+ entrypoint: ANY,
162
+ command: ANY,
163
+ tmpfs: ANY,
164
+ depends_on: ANY,
165
+ healthcheck: SERVICE_HEALTHCHECK_SCHEMA,
166
+ health_check: SERVICE_HEALTHCHECK_SCHEMA,
167
+ user: ANY,
168
+ cpu_shares: ANY,
169
+ cpus: ANY,
170
+ mem_limit: ANY,
171
+ shm_size: ANY,
172
+ network_mode: ANY,
173
+ netadmin: ANY,
174
+ setup_script: ANY,
175
+ binds: ANY,
176
+ runtime: ANY,
177
+ }),
178
+ };
179
+
180
+ function collectUnknownFieldPaths(value, schema, currentPath = '') {
181
+ if (schema === ANY || !value || typeof value !== 'object') {
182
+ return [];
183
+ }
184
+
185
+ if (Array.isArray(value)) {
186
+ if (!schema || schema.kind !== 'array') {
187
+ return [];
188
+ }
189
+ return value.flatMap((item, index) => collectUnknownFieldPaths(item, schema.value, `${currentPath}[${index}]`));
190
+ }
191
+
192
+ if (schema?.kind === 'map') {
193
+ return Object.entries(value).flatMap(([key, item]) => collectUnknownFieldPaths(item, schema.value, currentPath ? `${currentPath}.${key}` : key));
194
+ }
195
+
196
+ const unknownPaths = [];
197
+ for (const [key, item] of Object.entries(value)) {
198
+ const nextPath = currentPath ? `${currentPath}.${key}` : key;
199
+ if (!hasOwn(schema, key)) {
200
+ unknownPaths.push(nextPath);
201
+ continue;
202
+ }
203
+ unknownPaths.push(...collectUnknownFieldPaths(item, schema[key], nextPath));
204
+ }
205
+ return unknownPaths;
206
+ }
207
+
208
+ function collectUnknownFieldWarnings(sourceManifest) {
209
+ const unknownFields = collectUnknownFieldPaths(sourceManifest ?? {}, MANIFEST_SCHEMA);
210
+ if (unknownFields.length === 0) {
211
+ return [];
212
+ }
213
+ return [createWarning('unknown-manifest-fields', `Unknown manifest fields detected: ${quoteFieldList(unknownFields)}. Please confirm whether they take effect.`)];
214
+ }
215
+
216
+ function collectStaticFieldWarnings(sourceManifest) {
217
+ const staticFields = findManifestStaticFields(sourceManifest ?? {});
218
+ if (staticFields.length === 0) {
219
+ return [];
220
+ }
221
+ return [
222
+ createWarning(
223
+ 'legacy-static-package-fields',
224
+ `Top-level static package fields in lzc-manifest.yml are a legacy LPK v1 layout. Move ${quoteFieldList(staticFields)} to package.yml for LPK v2.`,
225
+ ),
226
+ ];
227
+ }
228
+
229
+ function collectApplicationWarnings(sourceManifest) {
230
+ const warnings = [];
231
+ if (hasPath(sourceManifest, ['application', 'handlers'])) {
232
+ warnings.push(
233
+ createWarning(
234
+ 'application-handlers-deprecated',
235
+ '`application.handlers` is deprecated and kept for compatibility only. Avoid introducing new projects that depend on it.',
236
+ ),
237
+ );
238
+ }
239
+ if (hasPath(sourceManifest, ['application', 'user_app'])) {
240
+ warnings.push(
241
+ createWarning(
242
+ 'application-user-app-deprecated',
243
+ '`application.user_app` is deprecated. Use `application.multi_instance` when you need multi-instance deployment semantics.',
244
+ ),
245
+ );
246
+ }
247
+ if (hasPath(sourceManifest, ['application', 'depends_on'])) {
248
+ warnings.push(
249
+ createWarning(
250
+ 'application-depends-on-deprecated',
251
+ '`application.depends_on` is deprecated. Use `services.<name>.depends_on`, or rely on automatic health checking from `application.routes` and `application.upstreams`.',
252
+ ),
253
+ );
254
+ }
255
+ return warnings;
256
+ }
257
+
258
+ function collectServiceWarnings(sourceManifest) {
259
+ const deprecatedServices = Object.entries(sourceManifest?.services ?? {})
260
+ .filter(([, serviceConfig]) => hasOwn(serviceConfig, 'health_check'))
261
+ .map(([serviceName]) => `services.${serviceName}.health_check`);
262
+ if (deprecatedServices.length === 0) {
263
+ return [];
264
+ }
265
+ return [
266
+ createWarning(
267
+ 'service-health-check-deprecated',
268
+ `Legacy service health check fields detected: ${quoteFieldList(deprecatedServices)}. Rename them to \`healthcheck\`.`,
269
+ ),
270
+ ];
271
+ }
272
+
273
+ function collectExtConfigWarnings(sourceManifest) {
274
+ const legacyFields = ['disable_url_raw_path', 'remove_this_request_headers', 'fix_websocket_header'].filter((field) =>
275
+ hasPath(sourceManifest, ['ext_config', field]),
276
+ );
277
+ if (legacyFields.length === 0) {
278
+ return [];
279
+ }
280
+ return [
281
+ createWarning(
282
+ 'ext-config-http-routing-deprecated',
283
+ `Legacy HTTP routing fields in \`ext_config\` are deprecated: ${quoteFieldList(legacyFields)}. Move them to \`application.upstreams[*]\` with the same field names.`,
284
+ ),
285
+ ];
286
+ }
287
+
288
+ function hasLocales(manifest, sourceManifest, packageInfo) {
289
+ return hasOwn(packageInfo, 'locales') || hasOwn(manifest, 'locales') || hasOwn(sourceManifest, 'locales');
290
+ }
291
+
292
+ function resolvePackageVersion(manifest, sourceManifest, packageInfo) {
293
+ const candidates = [packageInfo?.version, manifest?.version, sourceManifest?.version];
294
+ for (const candidate of candidates) {
295
+ const value = normalizeString(candidate);
296
+ if (value) {
297
+ return value;
298
+ }
299
+ }
300
+ return '';
301
+ }
302
+
303
+ function parseEmbedAlias(imageRef) {
304
+ const value = normalizeString(imageRef);
305
+ if (!value.startsWith('embed:')) {
306
+ return '';
307
+ }
308
+ const rest = value.slice('embed:'.length).trim();
309
+ const at = rest.indexOf('@');
310
+ return normalizeString(at >= 0 ? rest.slice(0, at) : rest);
311
+ }
312
+
313
+ function collectImageRefs(sourceManifest) {
314
+ const refs = [];
315
+ const applicationImage = normalizeString(sourceManifest?.application?.image);
316
+ if (applicationImage) {
317
+ refs.push({
318
+ path: 'application.image',
319
+ value: applicationImage,
320
+ });
321
+ }
322
+ for (const [serviceName, serviceConfig] of Object.entries(sourceManifest?.services ?? {})) {
323
+ const imageValue = normalizeString(serviceConfig?.image);
324
+ if (!imageValue) {
325
+ continue;
326
+ }
327
+ refs.push({
328
+ path: `services.${serviceName}.image`,
329
+ value: imageValue,
330
+ });
331
+ }
332
+ return refs;
333
+ }
334
+
335
+ function collectStoreVersionWarnings(manifest, sourceManifest, packageInfo) {
336
+ const version = resolvePackageVersion(manifest, sourceManifest, packageInfo);
337
+ if (!version) {
338
+ return [];
339
+ }
340
+ if (semver.valid(version)) {
341
+ return [];
342
+ }
343
+ return [createWarning('store-version-invalid-semver', `App Store submission requires a valid semver version. Current version is \`${version}\`.`)];
344
+ }
345
+
346
+ function collectStoreImageWarnings(sourceManifest, options, embeddedImageUpstreams) {
347
+ const warnings = [];
348
+ const imageBuildOptions = options?.images ?? {};
349
+ for (const imageRef of collectImageRefs(sourceManifest)) {
350
+ const embedAlias = parseEmbedAlias(imageRef.value);
351
+ if (embedAlias) {
352
+ const actualUpstream = normalizeString(embeddedImageUpstreams?.[embedAlias]);
353
+ if (actualUpstream) {
354
+ if (!actualUpstream.startsWith(STORE_IMAGE_REGISTRY_PREFIX)) {
355
+ warnings.push(
356
+ createWarning(
357
+ 'store-image-embed-upstream-invalid',
358
+ `App Store submission requires \`${imageRef.path}\` to use upstream images from ${STORE_IMAGE_REGISTRY_PREFIX}. Current upstream for \`${embedAlias}\` is \`${actualUpstream}\`.`,
359
+ ),
360
+ );
361
+ }
362
+ continue;
363
+ }
364
+ const buildConfig = imageBuildOptions?.[embedAlias];
365
+ if (!buildConfig || typeof buildConfig !== 'object') {
366
+ warnings.push(
367
+ createWarning(
368
+ 'store-image-embed-alias-missing',
369
+ `App Store submission requires \`${imageRef.path}\` to resolve to ${STORE_IMAGE_REGISTRY_PREFIX}. Embed alias \`${embedAlias}\` has no matching \`images.${embedAlias}\` config; please confirm whether it takes effect.`,
370
+ ),
371
+ );
372
+ continue;
373
+ }
374
+ const upstreamMatch = normalizeString(buildConfig['upstream-match'] ?? STORE_IMAGE_REGISTRY_PREFIX);
375
+ if (!upstreamMatch.startsWith(STORE_IMAGE_REGISTRY_PREFIX)) {
376
+ warnings.push(
377
+ createWarning(
378
+ 'store-image-embed-upstream-invalid',
379
+ `App Store submission requires \`${imageRef.path}\` to use upstream images from ${STORE_IMAGE_REGISTRY_PREFIX}. Current \`images.${embedAlias}.upstream-match\` is \`${upstreamMatch}\`.`,
380
+ ),
381
+ );
382
+ }
383
+ continue;
384
+ }
385
+ if (!imageRef.value.startsWith(STORE_IMAGE_REGISTRY_PREFIX)) {
386
+ warnings.push(
387
+ createWarning(
388
+ 'store-image-registry-invalid',
389
+ `App Store submission requires \`${imageRef.path}\` to start with ${STORE_IMAGE_REGISTRY_PREFIX}. Current value is \`${imageRef.value}\`.`,
390
+ ),
391
+ );
392
+ }
393
+ }
394
+ return warnings;
395
+ }
396
+
397
+ function collectStoreWarnings({ cwd, options, manifest, sourceManifest, packageInfo, embeddedImageUpstreams }) {
398
+ const warnings = [];
399
+ warnings.push(...collectStoreVersionWarnings(manifest, sourceManifest, packageInfo));
400
+ if (!hasLocales(manifest, sourceManifest, packageInfo)) {
401
+ warnings.push(
402
+ createWarning(
403
+ 'store-locales-required',
404
+ 'App Store submission requires `locales`. For LPK v2, define it in package.yml.',
405
+ ),
406
+ );
407
+ }
408
+ warnings.push(...collectStoreImageWarnings(sourceManifest, options, embeddedImageUpstreams));
409
+
410
+ const rawIconPath = String(options?.icon ?? '').trim();
411
+ if (!rawIconPath) {
412
+ return warnings;
413
+ }
414
+
415
+ const iconPath = path.isAbsolute(rawIconPath) ? rawIconPath : path.resolve(cwd || process.cwd(), rawIconPath);
416
+ if (!fs.existsSync(iconPath)) {
417
+ return warnings;
418
+ }
419
+
420
+ const stats = fs.statSync(iconPath);
421
+ if (!stats.isFile()) {
422
+ return warnings;
423
+ }
424
+
425
+ if (stats.size > STORE_ICON_MAX_BYTES) {
426
+ warnings.push(
427
+ createWarning(
428
+ 'store-icon-too-large',
429
+ `App Store submission requires icon.png smaller than 200 KiB. Current icon source is ${stats.size} bytes: ${iconPath}`,
430
+ ),
431
+ );
432
+ }
433
+
434
+ return warnings;
435
+ }
436
+
437
+ export async function collectManifestLintWarnings({ cwd, options, manifestPath, sourceManifest, manifest, packageInfo, embeddedImageUpstreams } = {}) {
438
+ return [
439
+ ...collectUnknownFieldWarnings(sourceManifest),
440
+ ...collectStaticFieldWarnings(sourceManifest),
441
+ ...collectApplicationWarnings(sourceManifest),
442
+ ...collectServiceWarnings(sourceManifest),
443
+ ...collectExtConfigWarnings(sourceManifest),
444
+ ...collectStoreWarnings({
445
+ cwd,
446
+ options,
447
+ manifestPath,
448
+ sourceManifest,
449
+ manifest,
450
+ packageInfo,
451
+ embeddedImageUpstreams,
452
+ }),
453
+ ];
454
+ }
455
+
456
+ export function isStoreLintWarning(warning) {
457
+ return String(warning?.code ?? '').startsWith('store-');
458
+ }
459
+
460
+ export function logManifestLintWarnings(warnings, loggerLike) {
461
+ for (const warning of warnings ?? []) {
462
+ loggerLike.warn(`[lint] ${warning.message}`);
463
+ }
464
+ }
@@ -191,6 +191,7 @@ export async function resolveProjectRuntime(startDir = process.cwd(), selection
191
191
 
192
192
  const bridge = new DebugBridge(projectCwd, buildRemote);
193
193
  await bridge.init();
194
+ await bridge.ensureLpkV2Supported();
194
195
 
195
196
  const userApp = isUserApp(manifest);
196
197
  const composeProjectName = pkgId.replaceAll('.', '');
@@ -87,7 +87,7 @@ export class PrePublish {
87
87
  * @param {string} changelog
88
88
  */
89
89
  async publish(pkgPath, changelog, gid) {
90
- if (!Publish.preCheck(pkgPath)) return;
90
+ if (!(await Publish.preCheck(pkgPath))) return;
91
91
 
92
92
  await autoLogin();
93
93
 
@@ -4,9 +4,12 @@ import FormData from 'form-data';
4
4
  import fs from 'node:fs';
5
5
  import inquirer from 'inquirer';
6
6
  import path from 'node:path';
7
- import { isFileExist, isPngWithFile, unzipSync, loadFromYaml, isValidPackageName, getLanguageForLocale } from '../utils.js';
7
+ import yaml from 'js-yaml';
8
+ import { isFileExist, isPngWithFile, extractLpkSync, isValidPackageName, getLanguageForLocale } from '../utils.js';
8
9
  import { t } from '../i18n/index.js';
9
10
  import { appStoreServerUrl } from './env.js';
11
+ import { loadEffectiveManifestFromFiles } from '../package_info.js';
12
+ import { collectManifestLintWarnings, isStoreLintWarning } from '../app/manifest_lint.js';
10
13
 
11
14
  async function askChangeLog(locale) {
12
15
  const noEmpty = (value) => value != '';
@@ -199,10 +202,54 @@ export class Publish {
199
202
  return raw[0] == '{' && raw[ml - 1] == '}';
200
203
  }
201
204
 
202
- static preCheck(pkgPath) {
205
+ static loadPackageForPublish(pkgPath, tempDir) {
206
+ extractLpkSync(pkgPath, tempDir);
207
+ const manifestPath = path.join(tempDir, 'manifest.yml');
208
+ const loaded = loadEffectiveManifestFromFiles(manifestPath);
209
+ const imagesLockPath = path.join(tempDir, 'images.lock');
210
+ let embeddedImageUpstreams = {};
211
+ if (isFileExist(imagesLockPath)) {
212
+ const imagesLock = yaml.load(fs.readFileSync(imagesLockPath, 'utf8')) ?? {};
213
+ const images = imagesLock?.images ?? {};
214
+ embeddedImageUpstreams = Object.fromEntries(
215
+ Object.entries(images).map(([alias, imageInfo]) => [alias, String(imageInfo?.upstream ?? '').trim()]),
216
+ );
217
+ }
218
+ return {
219
+ manifest: loaded.manifest,
220
+ sourceManifest: loaded.sourceManifest,
221
+ packageInfo: loaded.packageInfo,
222
+ manifestPath,
223
+ embeddedImageUpstreams,
224
+ };
225
+ }
226
+
227
+ static async collectStorePublishWarnings(pkgPath) {
203
228
  const tempDir = fs.mkdtempSync('.lzc-cli-publish');
204
229
  try {
205
- unzipSync(pkgPath, tempDir, ['devshell', 'icon.png']);
230
+ extractLpkSync(pkgPath, tempDir, ['devshell', 'icon.png', 'manifest.yml', 'package.yml', 'images.lock']);
231
+ const { manifest, sourceManifest, packageInfo, manifestPath, embeddedImageUpstreams } = Publish.loadPackageForPublish(pkgPath, tempDir);
232
+ const warnings = await collectManifestLintWarnings({
233
+ cwd: tempDir,
234
+ manifestPath,
235
+ sourceManifest,
236
+ manifest,
237
+ packageInfo,
238
+ embeddedImageUpstreams,
239
+ options: {
240
+ icon: path.join(tempDir, 'icon.png'),
241
+ },
242
+ });
243
+ return warnings.filter(isStoreLintWarning);
244
+ } finally {
245
+ fs.rmSync(tempDir, { recursive: true, force: true });
246
+ }
247
+ }
248
+
249
+ static async preCheck(pkgPath) {
250
+ const tempDir = fs.mkdtempSync('.lzc-cli-publish');
251
+ try {
252
+ extractLpkSync(pkgPath, tempDir, ['devshell', 'icon.png']);
206
253
  if (isFileExist(path.join(tempDir, 'devshell'))) {
207
254
  logger.error(t('lzc_cli.lib.publish.pre_check_fail_tips', '不能发布一个devshell的版本,请重新使用 `lzc-cli project build` 构建'));
208
255
  return false;
@@ -212,17 +259,25 @@ export class Publish {
212
259
  logger.error(t('lzc_cli.lib.publish.pre_check_icon_not_exist_tips', 'icon 必须存在且要求是 png 格式'));
213
260
  return false;
214
261
  }
262
+ const storeWarnings = await Publish.collectStorePublishWarnings(pkgPath);
263
+ if (storeWarnings.length > 0) {
264
+ for (const warning of storeWarnings) {
265
+ logger.error(`[appstore] ${warning.message}`);
266
+ }
267
+ logger.error('当前 LPK 不满足商店发布要求,已拒绝发布');
268
+ return false;
269
+ }
215
270
  return true;
216
271
  } finally {
217
- fs.rmSync(tempDir, { recursive: true });
272
+ fs.rmSync(tempDir, { recursive: true, force: true });
218
273
  }
219
274
  }
220
275
 
221
276
  async checkAppIdExist(pkgPath) {
222
277
  const tempDir = fs.mkdtempSync('.lzc-cli-publish');
223
278
  try {
224
- unzipSync(pkgPath, tempDir, ['manifest.yml']);
225
- const manifest = loadFromYaml(path.join(tempDir, 'manifest.yml'));
279
+ extractLpkSync(pkgPath, tempDir, ['manifest.yml', 'package.yml']);
280
+ const { manifest } = Publish.loadPackageForPublish(pkgPath, tempDir);
226
281
  const checkUrl = this.baseUrl + `/app/check/exist?package=${manifest['package']}`;
227
282
  const res = await request(checkUrl, { method: 'GET' });
228
283
  if (res.status >= 400) {
@@ -235,7 +290,7 @@ export class Publish {
235
290
  return { manifest, appIdExisted: exist };
236
291
  }
237
292
  } finally {
238
- fs.rmSync(tempDir, { recursive: true });
293
+ fs.rmSync(tempDir, { recursive: true, force: true });
239
294
  }
240
295
  }
241
296
 
@@ -245,7 +300,7 @@ export class Publish {
245
300
  * @param {string} currentLocale
246
301
  */
247
302
  async publish(pkgPath, changelogs, currentLocale = 'zh') {
248
- if (!Publish.preCheck(pkgPath)) return;
303
+ if (!(await Publish.preCheck(pkgPath))) return;
249
304
 
250
305
  const { manifest, appIdExisted } = await this.checkAppIdExist(pkgPath);
251
306
  if (!appIdExisted) {
@@ -13,6 +13,12 @@ const bannerfileContent = `˄=ᆽ=ᐟ \\`;
13
13
  const DEBUG_BRIDGE_CONTAINER = 'cloudlazycatdevelopertools-app-1';
14
14
  const DEBUG_BRIDGE_BINARY = '/lzcapp/pkg/content/debug.bridge';
15
15
  const DEBUG_BRIDGE_APP_ID = 'cloud.lazycat.developer.tools';
16
+ export const MIN_LPK_V2_BACKEND_VERSION = '1.0.0';
17
+
18
+ export function isBackendVersionAtLeast(minimumVersion, currentVersion = '0.0.0') {
19
+ const normalizedCurrentVersion = String(currentVersion ?? '').trim() || '0.0.0';
20
+ return compareVersions(minimumVersion, normalizedCurrentVersion) >= 0;
21
+ }
16
22
 
17
23
  export function sshBinary() {
18
24
  if (isWindows) {
@@ -58,13 +64,18 @@ function extractInstallErrorDetail(rawOutput = '') {
58
64
  return detailMatch[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').trim();
59
65
  }
60
66
 
61
- const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([^\n\r]+)/i);
67
+ const descMatch = text.match(/rpc error:\s*code\s*=\s*\S+\s*desc\s*=\s*([\s\S]*)/i);
62
68
  if (descMatch && descMatch[1]) {
63
- return descMatch[1].trim();
69
+ const detail = descMatch[1]
70
+ .split(/\nUsage:\s*\n|\nUsage:\s*/i)[0]
71
+ .trim();
72
+ if (detail) {
73
+ return detail;
74
+ }
64
75
  }
65
76
 
66
77
  const lines = text
67
- .split(/\r?\n/)
78
+ .split(/[\r\n]+/)
68
79
  .map((line) => line.trim())
69
80
  .filter((line) => {
70
81
  if (!line) {
@@ -76,7 +87,7 @@ function extractInstallErrorDetail(rawOutput = '') {
76
87
  return true;
77
88
  });
78
89
  if (lines.length > 0) {
79
- return lines[0];
90
+ return lines[lines.length - 1];
80
91
  }
81
92
 
82
93
  return t('lzc_cli.lib.debug_bridge.install_fail', 'install 失败');
@@ -1233,12 +1244,11 @@ export class DebugBridge {
1233
1244
 
1234
1245
  async backendVersion020() {
1235
1246
  const backendVersion = await this.version();
1236
- if (compareVersions('0.2.0', backendVersion) < 0) {
1247
+ if (!isBackendVersionAtLeast('0.2.0', backendVersion)) {
1237
1248
  logger.warn(
1238
1249
  t(
1239
1250
  'lzc_cli.lib.debug_bridge.backend_version_020_no_match_tips',
1240
- `
1241
- 检测到您当前的 lzc-cli 版本较新,而 '懒猫开发者工具' 比较旧,请先到微服的商店升级 '懒猫开发者工具',点击下面的连接跳转:
1251
+ `Detected that your current lzc-cli version is newer, but the installed Lazycat Developer Tools backend is relatively old. Please upgrade Lazycat Developer Tools from the app store first:
1242
1252
 
1243
1253
  -> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
1244
1254
  `,
@@ -1251,4 +1261,30 @@ export class DebugBridge {
1251
1261
  return;
1252
1262
  }
1253
1263
  }
1264
+
1265
+ async ensureLpkV2Supported() {
1266
+ const backendVersion = await this.version();
1267
+ if (isBackendVersionAtLeast(MIN_LPK_V2_BACKEND_VERSION, backendVersion)) {
1268
+ return;
1269
+ }
1270
+ logger.warn(
1271
+ t(
1272
+ 'lzc_cli.lib.debug_bridge.backend_version_lpk_v2_no_match_tips',
1273
+ `LPK v2 features require Lazycat Developer Tools backend >= {{ minimumVersion }}, but the current backend version is {{ backendVersion }}. Please upgrade Lazycat Developer Tools from the app store first:
1274
+
1275
+ -> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools
1276
+ `,
1277
+ {
1278
+ boxname: this.boxname,
1279
+ minimumVersion: MIN_LPK_V2_BACKEND_VERSION,
1280
+ backendVersion,
1281
+ },
1282
+ ),
1283
+ );
1284
+ process.exit(1);
1285
+ }
1254
1286
  }
1287
+
1288
+ export const __test__ = {
1289
+ extractInstallErrorDetail,
1290
+ };
@@ -162,6 +162,7 @@
162
162
  },
163
163
  "debug_bridge": {
164
164
  "backend_version_020_no_match_tips": "It is detected that your current lzc-cli version is newer, and the 'Lazy Cat Developer Tools' is relatively old. Please go to the Microservice store to upgrade the 'Lazy Cat Developer Tools' first, and click the link below to jump:\n\n-> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools",
165
+ "backend_version_lpk_v2_no_match_tips": "LPK v2 features require Lazycat Developer Tools backend >= {{minimumVersion}}, but the current backend version is {{backendVersion}}. Please upgrade Lazycat Developer Tools from the app store first:\n\n-> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools",
165
166
  "build_image_fail": "Failed to build image in LCMD",
166
167
  "can_public_key_resolve_fail": "Domain name resolution failed, please check whether the proxy software intercepts *.heiyu.space",
167
168
  "check_dev_tools_fail_tips": "Failed to detect the Lazycat developer tools. Please check whether your current network or the Lazycat LCMD client is started normally.",
@@ -162,6 +162,7 @@
162
162
  },
163
163
  "debug_bridge": {
164
164
  "backend_version_020_no_match_tips": "\n检测到您当前的 lzc-cli 版本较新,而 '懒猫开发者工具' 比较旧,请先到微服的商店升级 '懒猫开发者工具',点击下面的连接跳转:\n\n-> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools\n",
165
+ "backend_version_lpk_v2_no_match_tips": "\nLPK v2 功能要求 '懒猫开发者工具' backend 版本至少为 {{ minimumVersion }},当前 backend 版本为 {{ backendVersion }},请先到微服的商店升级 '懒猫开发者工具',点击下面的连接跳转:\n\n-> https://appstore.{{boxname}}.heiyu.space/#/shop/detail/cloud.lazycat.developer.tools\n",
165
166
  "build_image_fail": "在微服中构建 image 失败",
166
167
  "can_public_key_resolve_fail": "域名解析失败,请检查代理软件是否对 *.heiyu.space 拦截",
167
168
  "check_dev_tools_fail_tips": "检测懒猫开发者工具失败,请检测您当前的网络或者懒猫微服客户端是否正常启动。",
package/lib/utils.js CHANGED
@@ -78,6 +78,7 @@ export async function getLatestVersion(controller, pkgName = pkgInfo.name) {
78
78
  export const isMacOs = process.platform == 'darwin';
79
79
  export const isLinux = process.platform == 'linux';
80
80
  export const isWindows = process.platform == 'win32';
81
+ export const REPRODUCIBLE_ARCHIVE_DATE = new Date('1980-01-01T00:00:00.000Z');
81
82
 
82
83
  export const envsubstr = async (templateContents, args) => {
83
84
  const parse = await importDefault('envsub/js/envsub-parser.js');
@@ -451,16 +452,13 @@ export async function tarContentDir(from, to, cwd = './', options = {}) {
451
452
  tar.c(
452
453
  {
453
454
  cwd: cwd,
454
- gzip: gzipEnabled,
455
+ gzip: gzipEnabled ? { mtime: 0 } : false,
455
456
  filter: (filePath) => {
456
457
  logger.debug(`tar gz ${filePath}`);
457
458
  return true;
458
459
  },
459
460
  sync: true,
460
- portable: {
461
- uid: 0,
462
- gid: 0,
463
- },
461
+ portable: true,
464
462
  },
465
463
  from,
466
464
  )
@@ -561,6 +559,10 @@ function detectLpkArchiveFormatSync(filePath) {
561
559
  }
562
560
  }
563
561
 
562
+ export function detectLpkArchiveFormat(filePath) {
563
+ return detectLpkArchiveFormatSync(filePath);
564
+ }
565
+
564
566
  function normalizeArchiveEntryPath(entryPath) {
565
567
  return String(entryPath ?? '').replace(/^\.\//, '').replace(/\\/g, '/');
566
568
  }
@@ -611,7 +613,7 @@ export function extractLpkSync(lpkPath, destPath, entries = []) {
611
613
  }
612
614
 
613
615
  fs.mkdirSync(destPath, { recursive: true });
614
- const format = detectLpkArchiveFormatSync(lpkPath);
616
+ const format = detectLpkArchiveFormat(lpkPath);
615
617
  if (format === 'zip') {
616
618
  extractZipEntriesSync(lpkPath, destPath, entries);
617
619
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazycatcloud/lzc-cli",
3
- "version": "2.0.0-pre.5",
3
+ "version": "2.0.0-pre.7",
4
4
  "description": "lazycat cloud developer kit",
5
5
  "scripts": {
6
6
  "release": "release-it patch",