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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +30 -5
  2. package/changelog.md +4 -0
  3. package/lib/app/index.js +174 -58
  4. package/lib/app/lpk_build.js +192 -17
  5. package/lib/app/lpk_build_images.js +728 -0
  6. package/lib/app/lpk_create.js +93 -21
  7. package/lib/app/lpk_create_generator.js +144 -9
  8. package/lib/app/lpk_devshell.js +33 -19
  9. package/lib/app/lpk_embed_images.js +257 -0
  10. package/lib/app/lpk_installer.js +14 -7
  11. package/lib/app/project_cp.js +64 -0
  12. package/lib/app/project_deploy.js +33 -0
  13. package/lib/app/project_exec.js +45 -0
  14. package/lib/app/project_info.js +106 -0
  15. package/lib/app/project_log.js +67 -0
  16. package/lib/app/project_runtime.js +261 -0
  17. package/lib/app/project_start.js +100 -0
  18. package/lib/box/index.js +101 -4
  19. package/lib/box/ssh_remote.js +259 -0
  20. package/lib/build_remote.js +22 -0
  21. package/lib/config/index.js +1 -1
  22. package/lib/debug_bridge.js +837 -46
  23. package/lib/docker/index.js +30 -10
  24. package/lib/i18n/index.js +1 -0
  25. package/lib/i18n/locales/en/translation.json +17 -5
  26. package/lib/i18n/locales/zh/translation.json +16 -4
  27. package/lib/lpk/core.js +487 -0
  28. package/lib/lpk/index.js +210 -0
  29. package/lib/sig/core.js +254 -0
  30. package/lib/sig/index.js +88 -0
  31. package/lib/utils.js +3 -1
  32. package/package.json +2 -1
  33. package/scripts/cli.js +4 -0
  34. package/template/_lpk/README.md +11 -3
  35. package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
  36. package/template/_lpk/manifest.yml.in +4 -2
  37. package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
  38. package/template/_lpk/todolist-java.manifest.yml.in +15 -0
  39. package/template/_lpk/todolist-python.manifest.yml.in +15 -0
  40. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  41. package/template/blank/_gitignore +1 -0
  42. package/template/blank/lzc-build.yml +25 -40
  43. package/template/blank/lzc-manifest.yml +14 -7
  44. package/template/golang/Dockerfile +19 -0
  45. package/template/golang/README.md +33 -0
  46. package/template/golang/_gitignore +3 -0
  47. package/template/golang/go.mod +3 -0
  48. package/template/golang/lzc-build.yml +21 -0
  49. package/template/golang/lzc-icon.png +0 -0
  50. package/template/golang/main.go +252 -0
  51. package/template/golang/run.sh +3 -0
  52. package/template/golang/web/index.html +238 -0
  53. package/template/gui-vnc/README.md +19 -0
  54. package/template/gui-vnc/_gitignore +2 -0
  55. package/template/gui-vnc/images/Dockerfile +30 -0
  56. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  57. package/template/gui-vnc/images/startup-script.desktop +9 -0
  58. package/template/gui-vnc/images/startup-script.sh +6 -0
  59. package/template/gui-vnc/lzc-build.yml +23 -0
  60. package/template/gui-vnc/lzc-icon.png +0 -0
  61. package/template/python/Dockerfile +15 -0
  62. package/template/python/README.md +33 -0
  63. package/template/python/_gitignore +3 -0
  64. package/template/python/app.py +110 -0
  65. package/template/python/lzc-build.yml +21 -0
  66. package/template/python/lzc-icon.png +0 -0
  67. package/template/python/requirements.txt +1 -0
  68. package/template/python/run.sh +3 -0
  69. package/template/python/web/index.html +238 -0
  70. package/template/springboot/Dockerfile +20 -0
  71. package/template/springboot/README.md +33 -0
  72. package/template/springboot/_gitignore +3 -0
  73. package/template/springboot/lzc-build.yml +21 -0
  74. package/template/springboot/lzc-icon.png +0 -0
  75. package/template/springboot/pom.xml +38 -0
  76. package/template/springboot/run.sh +3 -0
  77. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  78. package/template/springboot/src/main/resources/application.properties +1 -0
  79. package/template/springboot/src/main/resources/static/index.html +238 -0
  80. package/template/vue/README.md +17 -7
  81. package/template/vue/_gitignore +1 -0
  82. package/template/vue/lzc-build.yml +31 -42
  83. package/template/vue/src/App.vue +36 -25
  84. package/template/vue/src/style.css +106 -49
  85. package/template/vue-minidb/README.md +34 -0
  86. package/template/vue-minidb/_gitignore +26 -0
  87. package/template/vue-minidb/index.html +13 -0
  88. package/template/vue-minidb/lzc-build.yml +48 -0
  89. package/template/vue-minidb/lzc-icon.png +0 -0
  90. package/template/vue-minidb/package.json +21 -0
  91. package/template/vue-minidb/public/vite.svg +1 -0
  92. package/template/vue-minidb/src/App.vue +206 -0
  93. package/template/vue-minidb/src/assets/vue.svg +1 -0
  94. package/template/vue-minidb/src/main.ts +5 -0
  95. package/template/vue-minidb/src/style.css +136 -0
  96. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  97. package/template/vue-minidb/tsconfig.app.json +24 -0
  98. package/template/vue-minidb/tsconfig.json +7 -0
  99. package/template/vue-minidb/tsconfig.node.json +22 -0
  100. package/template/vue-minidb/vite.config.ts +10 -0
  101. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
package/README.md CHANGED
@@ -17,19 +17,44 @@ lzc-cli completion >> ~/.zshrc
17
17
  下面开始使用 `lzc-cli` 去创建一个项目吧!
18
18
 
19
19
  ```bash
20
+ # 在新目录创建项目(交互选择模板)
20
21
  lzc-cli project create you_project
21
22
 
22
- # 构建懒猫云平台lpk包
23
- lzc-cli project build
23
+ # 在当前目录初始化空白项目
24
+ lzc-cli project create --in-place
25
+
26
+ # 构建 release lpk 包(优先 lzc-build.release.yml,不存在则回退 lzc-build.yml)
27
+ lzc-cli project release
24
28
 
25
29
  # 将lpk包安装到盒子中去
26
- lzc-cli app install
30
+ lzc-cli lpk install
31
+
32
+ # 部署后默认自动启动
33
+ lzc-cli project deploy
27
34
 
28
- # 使用 devshell 可以让你在盒子中开发调试
29
- lzc-cli project devshell
35
+ # 进入运行容器调试
36
+ lzc-cli project exec /bin/sh
30
37
 
31
38
  # 经过测试后,将包发布到懒猫云商店中去
32
39
  lzc-cli appstore publish
33
40
  ```
34
41
 
35
42
  [changelog](./changelog.md)
43
+
44
+ #### box add-by-ssh 远端直连模式
45
+
46
+ 当运行环境无法使用 `hclient` 时,可通过 `box add-by-ssh` 配置远端 ssh 目标,由 `lzc-cli` 直连 `lzcos ssh` 并在远端执行 debug bridge 命令。
47
+
48
+ 示例:
49
+
50
+ ```bash
51
+ lzc-cli box add-by-ssh root 192.168.31.13
52
+ ```
53
+
54
+ 说明:
55
+
56
+ 1. 参数格式为 `loginUser address`,地址支持 `host` 或 `host:port`
57
+ 2. 配置后会自动设为默认盒子,可通过 `box list/switch/default` 管理
58
+ 3. `project release/deploy/start/exec/cp/log/info`、`lpk install/uninstall`、`docker/docker-compose` 都会优先使用该远端
59
+ 4. `lzc-build.yml` 不再支持 `remote` 字段
60
+ 5. 可选基础配置文件为 `lzc-build.base.yml`(与构建配置同目录)
package/changelog.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0-pre.0
4
+
5
+ 1. Add lpkv2 features for internal testing
6
+
3
7
  ## [1.3.14](https://gitee.com/linakesi/lzc-cli/compare/v1.3.13...v1.3.14) (2025-12-17)
4
8
 
5
9
 
package/lib/app/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  import path from 'node:path';
2
- import lpkCreate from './lpk_create.js';
2
+ import lpkCreate, { listProjectTemplateValues, normalizeTemplateType } from './lpk_create.js';
3
3
  import { LpkBuild } from './lpk_build.js';
4
4
  import { AppDevShell } from './lpk_devshell.js';
5
+ import { projectExecCommand } from './project_exec.js';
6
+ import { projectCpCommand } from './project_cp.js';
7
+ import { projectLogCommand } from './project_log.js';
8
+ import { projectInfoCommand } from './project_info.js';
9
+ import { projectStartCommand } from './project_start.js';
10
+ import { projectDeployCommand } from './project_deploy.js';
5
11
  import { LpkInstaller, installConfig } from './lpk_installer.js';
6
12
  import logger from 'loglevel';
7
13
  import { sleep, checkRsync } from '../utils.js';
@@ -9,28 +15,110 @@ import { DebugBridge } from '../debug_bridge.js';
9
15
  import shellApi from '../shellapi.js';
10
16
  import { generate } from './lpk_create_generator.js';
11
17
  import { t } from '../i18n/index.js';
18
+ import { resolveBuildRemoteFromFile } from '../build_remote.js';
19
+ import { resolveProjectReleaseConfigPath } from './project_runtime.js';
20
+
21
+ async function installLpk(pkgPath, apk) {
22
+ if (!resolveBuildRemoteFromFile(process.cwd())) {
23
+ await shellApi.init();
24
+ }
25
+ installConfig.apk = apk == 'y';
26
+
27
+ const installPath = pkgPath ?? process.cwd();
28
+ const installer = new LpkInstaller();
29
+ await installer.init();
30
+ await installer.install(installPath);
31
+ }
32
+
33
+ async function uninstallLpk(pkgId, deleteData) {
34
+ const buildRemote = resolveBuildRemoteFromFile(process.cwd());
35
+ if (!buildRemote) {
36
+ await shellApi.init();
37
+ }
38
+
39
+ const bridge = new DebugBridge(process.cwd(), buildRemote);
40
+ await bridge.init();
41
+ await bridge.uninstall(pkgId, deleteData);
42
+ logger.debug(`default lcmd device: ${bridge.boxname} , uninstall the app ${pkgId} finish`);
43
+ }
44
+
45
+ function lpkInstallSubCommand({ desc }) {
46
+ return {
47
+ command: 'install [pkgPath]',
48
+ desc,
49
+ builder: (args) => {
50
+ args.option('apk', {
51
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_index_rags_apk_desc', '是否生成APK(y/n)'),
52
+ type: 'string',
53
+ default: 'y',
54
+ });
55
+ },
56
+ handler: async ({ pkgPath, apk }) => {
57
+ await installLpk(pkgPath, apk);
58
+ },
59
+ };
60
+ }
61
+
62
+ function lpkUninstallSubCommand({ desc }) {
63
+ return {
64
+ command: 'uninstall <pkgId>',
65
+ desc,
66
+ builder: (args) => {
67
+ args.option('delete-data', {
68
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_uninstall_rags_delete_data_desc', '删除应用数据 ⚠️ 警告: 应用数据删除后无法恢复'),
69
+ type: 'boolean',
70
+ default: false,
71
+ });
72
+ },
73
+ handler: async ({ pkgId, deleteData }) => {
74
+ await uninstallLpk(pkgId, deleteData);
75
+ },
76
+ };
77
+ }
12
78
 
13
79
  export function lpkProjectCommand(program) {
14
80
  let subCommands = [
15
81
  {
16
- command: 'init',
17
- desc: t('lzc_cli.lib.app.index.lpk_cmd_init_desc', '初始化懒猫云应用(提供最基础的模板)'),
18
- handler: async () => {
19
- generate('blank', './');
20
- logger.info(t('lzc_cli.lib.app.index.lpk_cmd_init_success', '应用初始化完成'));
21
- },
22
- },
23
- {
24
- command: 'create <name>',
82
+ command: 'create [name]',
25
83
  desc: t('lzc_cli.lib.app.index.lpk_cmd_create_desc', '创建懒猫云应用'),
26
- handler: async ({ name }) => {
27
- name = String(name);
28
- await lpkCreate(name);
84
+ builder: (args) => {
85
+ args.option('in-place', {
86
+ describe: 'Initialize current directory with blank template',
87
+ type: 'boolean',
88
+ default: false,
89
+ });
90
+ args.option('t', {
91
+ alias: 'template',
92
+ describe: `Template name (${listProjectTemplateValues().join(', ')})`,
93
+ type: 'string',
94
+ });
95
+ },
96
+ handler: async ({ name, inPlace, template }) => {
97
+ if (name && inPlace) {
98
+ throw new Error('--in-place cannot be used with <name>.');
99
+ }
100
+ const normalizedTemplate = normalizeTemplateType(template);
101
+ if (name) {
102
+ name = String(name);
103
+ await lpkCreate(name, process.cwd(), normalizedTemplate);
104
+ return;
105
+ }
106
+
107
+ if (inPlace) {
108
+ if (normalizedTemplate) {
109
+ throw new Error('--template cannot be used with --in-place.');
110
+ }
111
+ generate('blank', './');
112
+ logger.info(t('lzc_cli.lib.app.index.lpk_cmd_init_success', '应用初始化完成'));
113
+ return;
114
+ }
115
+
116
+ throw new Error('Project name is required unless --in-place is used.');
29
117
  },
30
118
  },
31
119
  {
32
120
  command: 'build [context]',
33
- desc: t('lzc_cli.lib.app.index.lpk_cmd_build_desc', '构建'),
121
+ desc: false,
34
122
  builder: (args) => {
35
123
  args.option('o', {
36
124
  alias: 'output',
@@ -58,9 +146,41 @@ export function lpkProjectCommand(program) {
58
146
  await lpk.exec();
59
147
  },
60
148
  },
149
+ {
150
+ command: 'release [context]',
151
+ desc: 'Build release package',
152
+ builder: (args) => {
153
+ args.option('o', {
154
+ alias: 'output',
155
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_build_args_output_desc', '输出文件'),
156
+ type: 'string',
157
+ });
158
+ args.option('f', {
159
+ alias: 'file',
160
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_build_args_file_desc', '指定构建的lzc-build.yml文件'),
161
+ type: 'string',
162
+ });
163
+ },
164
+ handler: async ({ context, output, file }) => {
165
+ const cwd = context ? path.resolve(context) : process.cwd();
166
+ const configPath = resolveProjectReleaseConfigPath(cwd, file);
167
+ const projectCwd = path.dirname(configPath);
168
+ const configName = path.basename(configPath);
169
+ const lpk = await new LpkBuild(projectCwd, configName).init();
170
+ lpk.onBeforeBuildPackage(async (options) => {
171
+ delete options['devshell'];
172
+
173
+ if (output) {
174
+ options['lpkPath'] = output;
175
+ }
176
+ return options;
177
+ });
178
+ await lpk.exec();
179
+ },
180
+ },
61
181
  {
62
182
  command: 'devshell [context]',
63
- desc: t('lzc_cli.lib.app.index.lpk_cmd_devshell_desc', '进入盒子的开发环境'),
183
+ desc: false,
64
184
  builder: (args) => {
65
185
  args.option('f', {
66
186
  alias: 'force',
@@ -84,12 +204,16 @@ export function lpkProjectCommand(program) {
84
204
  });
85
205
  },
86
206
  handler: async ({ context, force, config, contentdir, apk }) => {
87
- await shellApi.init();
207
+ logger.warn('project devshell is deprecated. Use "project deploy", "project start", "project exec", "project cp", and "project log" instead.');
208
+ const cwd = context ? path.resolve(context) : process.cwd();
209
+ const buildRemote = resolveBuildRemoteFromFile(cwd, config);
210
+ if (!buildRemote) {
211
+ await shellApi.init();
212
+ }
88
213
  // 检测 rsync 满足
89
214
  await checkRsync();
90
215
 
91
216
  installConfig.apk = apk == 'y';
92
- const cwd = context ? path.resolve(context) : process.cwd();
93
217
  const lpkBuild = await new LpkBuild(cwd, config).init();
94
218
  lpkBuild.onBeforeBuildPackage(async (options) => {
95
219
  // devshell 正常情况下,不需要执行 buildscript 和 contentdir 字段
@@ -109,6 +233,26 @@ export function lpkProjectCommand(program) {
109
233
  await app.rsyncShell();
110
234
  },
111
235
  },
236
+ {
237
+ command: 'app',
238
+ desc: false,
239
+ builder: (args) => {
240
+ args.command([
241
+ lpkInstallSubCommand({
242
+ desc: false,
243
+ }),
244
+ lpkUninstallSubCommand({
245
+ desc: false,
246
+ }),
247
+ ]);
248
+ },
249
+ },
250
+ projectExecCommand(),
251
+ projectCpCommand(),
252
+ projectLogCommand(),
253
+ projectInfoCommand(),
254
+ projectStartCommand(),
255
+ projectDeployCommand(),
112
256
  ];
113
257
  program.command({
114
258
  command: 'project',
@@ -121,52 +265,22 @@ export function lpkProjectCommand(program) {
121
265
 
122
266
  export function lpkAppCommand(program) {
123
267
  let subCommands = [
124
- {
125
- command: 'install [pkgPath]',
268
+ lpkInstallSubCommand({
126
269
  desc: t('lzc_cli.lib.app.index.lpk_cmd_install_desc', '部署应用至设备, pkgPath 可以为路径,或者https://,http://请求地址, 如果不填写,将默认为当前目录下的lpk'),
127
- builder: (args) => {
128
- args.option('apk', {
129
- describe: t('lzc_cli.lib.app.index.lpk_cmd_index_rags_apk_desc', '是否生成APK(y/n)'),
130
- type: 'string',
131
- default: 'y',
132
- });
133
- },
134
- handler: async ({ pkgPath, apk }) => {
135
- await shellApi.init();
136
- installConfig.apk = apk == 'y';
137
-
138
- pkgPath = pkgPath ?? process.cwd();
139
- const installer = new LpkInstaller();
140
- await installer.init();
141
- await installer.install(pkgPath);
142
- },
143
- },
144
- {
145
- command: 'uninstall <pkgId>',
270
+ }),
271
+ lpkUninstallSubCommand({
146
272
  desc: t('lzc_cli.lib.app.index.lpk_cmd_uninstall_desc', '从设备中卸载某一个应用'),
147
- builder: (args) => {
148
- args.option('delete-data', {
149
- describe: t('lzc_cli.lib.app.index.lpk_cmd_uninstall_rags_delete_data_desc', '删除应用数据 ⚠️ 警告: 应用数据删除后无法恢复'),
150
- type: 'boolean',
151
- default: false,
152
- });
153
- },
154
- handler: async ({ pkgId, deleteData }) => {
155
- await shellApi.init();
156
-
157
- const bridge = new DebugBridge();
158
- await bridge.init();
159
- await bridge.uninstall(pkgId, deleteData);
160
- logger.debug(`default lcmd device: ${bridge.boxname} , uninstall the app ${pkgId} finish`);
161
- },
162
- },
273
+ }),
163
274
  {
164
275
  command: 'status <pkgId>',
165
276
  desc: t('lzc_cli.lib.app.index.lpk_cmd_status_desc', '获取某一个应用的状态'),
166
277
  handler: async ({ pkgId }) => {
167
- await shellApi.init();
278
+ const buildRemote = resolveBuildRemoteFromFile(process.cwd());
279
+ if (!buildRemote) {
280
+ await shellApi.init();
281
+ }
168
282
 
169
- const bridge = new DebugBridge();
283
+ const bridge = new DebugBridge(process.cwd(), buildRemote);
170
284
  await bridge.init();
171
285
  const status = await bridge.status(pkgId);
172
286
  console.log(status);
@@ -184,7 +298,9 @@ export function lpkAppCommand(program) {
184
298
  });
185
299
  },
186
300
  handler: async () => {
187
- await shellApi.init();
301
+ if (!resolveBuildRemoteFromFile(process.cwd())) {
302
+ await shellApi.init();
303
+ }
188
304
 
189
305
  throw 'not yet realized';
190
306
  },
@@ -192,7 +308,7 @@ export function lpkAppCommand(program) {
192
308
  ];
193
309
  program.command({
194
310
  command: 'app',
195
- desc: t('lzc_cli.lib.app.index.lpk_cmd_app_desc', '应用管理'),
311
+ desc: false,
196
312
  builder: async (args) => {
197
313
  args.command(subCommands);
198
314
  },
@@ -1,8 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import zlib from 'node:zlib';
3
4
  import logger from 'loglevel';
4
5
  import {
5
- loadFromYaml,
6
6
  isDirExist,
7
7
  isDirSync,
8
8
  isFileExist,
@@ -19,7 +19,11 @@ import spawn from 'cross-spawn';
19
19
  import { LpkManifest } from './lpk_create.js';
20
20
  import archiver from 'archiver';
21
21
  import yaml from 'js-yaml';
22
+ import { buildConfiguredImagesToTempDir } from './lpk_build_images.js';
23
+ import { pipeline } from 'node:stream/promises';
22
24
  import { t } from '../i18n/index.js';
25
+ import mergeWith from 'lodash.mergewith';
26
+ import { DEFAULT_BUILD_BASE_FILE } from '../build_remote.js';
23
27
 
24
28
  async function archiveFolderTo(appDir, out, format = 'zip') {
25
29
  return new Promise(async (resolve, reject) => {
@@ -46,6 +50,66 @@ async function archiveFolderTo(appDir, out, format = 'zip') {
46
50
  });
47
51
  }
48
52
 
53
+ async function gzipFileTo(inputPath, outputPath) {
54
+ const source = fs.createReadStream(inputPath);
55
+ const gzip = zlib.createGzip({ mtime: 0 });
56
+ const target = fs.createWriteStream(outputPath);
57
+ await pipeline(source, gzip, target);
58
+ }
59
+
60
+ function rewriteManifestEmbeddedImages(manifest, resolvedImageByAlias) {
61
+ const EMBED_PREFIX = 'embed:';
62
+ if (!resolvedImageByAlias || typeof resolvedImageByAlias !== 'object') {
63
+ return manifest;
64
+ }
65
+
66
+ const parseAlias = (rawValue) => {
67
+ const trimmed = String(rawValue ?? '').trim();
68
+ if (!trimmed.startsWith(EMBED_PREFIX)) {
69
+ return '';
70
+ }
71
+ const rest = trimmed.slice(EMBED_PREFIX.length).trim();
72
+ if (!rest) {
73
+ throw new Error(`Invalid image reference "${rawValue}", alias is required after "${EMBED_PREFIX}"`);
74
+ }
75
+ const at = rest.indexOf('@');
76
+ const alias = (at >= 0 ? rest.slice(0, at) : rest).trim();
77
+ if (!alias) {
78
+ throw new Error(`Invalid image reference "${rawValue}", alias is required after "${EMBED_PREFIX}"`);
79
+ }
80
+ return alias;
81
+ };
82
+
83
+ const walk = (value) => {
84
+ if (typeof value === 'string') {
85
+ const alias = parseAlias(value);
86
+ if (alias) {
87
+ const resolved = resolvedImageByAlias[alias];
88
+ if (!resolved) {
89
+ throw new Error(`Cannot resolve embedded image alias "${alias}" to final image reference`);
90
+ }
91
+ return `${EMBED_PREFIX}${alias}@${resolved}`;
92
+ }
93
+ return value;
94
+ }
95
+ if (Array.isArray(value)) {
96
+ for (let index = 0; index < value.length; index += 1) {
97
+ value[index] = walk(value[index]);
98
+ }
99
+ return value;
100
+ }
101
+ if (value && typeof value === 'object') {
102
+ for (const [key, item] of Object.entries(value)) {
103
+ value[key] = walk(item);
104
+ }
105
+ return value;
106
+ }
107
+ return value;
108
+ };
109
+
110
+ return walk(manifest);
111
+ }
112
+
49
113
  async function fetchIconTo(options, cwd, destDir) {
50
114
  if (!options['icon']) {
51
115
  logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_empty_fail', '图标icon 没有指定'));
@@ -68,7 +132,12 @@ async function fetchIconTo(options, cwd, destDir) {
68
132
  }
69
133
 
70
134
  if (!isPngWithFile(iconPath)) {
71
- logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_not_is_png_fail', `图标icon {{ iconPath }} 验证失败(不是一个png格式)`, { iconPath, interpolation: { escapeValue: false } }));
135
+ logger.warn(
136
+ t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_not_is_png_fail', `图标icon {{ iconPath }} 验证失败(不是一个png格式)`, {
137
+ iconPath,
138
+ interpolation: { escapeValue: false },
139
+ }),
140
+ );
72
141
  return;
73
142
  } else {
74
143
  logger.debug(t('lzc_cli.lib.app.lpk_build.fetch_icon_to_icon_is_png', `图标icon {{ iconPath }} 验证成功(png格式)`, { iconPath, interpolation: { escapeValue: false } }));
@@ -97,7 +166,12 @@ async function fetchLzcDeployParamTo(options, cwd, destDir) {
97
166
  }
98
167
 
99
168
  if (!isFileExist(deployParamsPath)) {
100
- logger.warn(t('lzc_cli.lib.app.lpk_build.fetch_lzc_deploy_param_to_not_exist', `deploy_params {{ deployParamsPath }} 不存在`, { deployParamsPath, interpolation: { escapeValue: false } }));
169
+ logger.warn(
170
+ t('lzc_cli.lib.app.lpk_build.fetch_lzc_deploy_param_to_not_exist', `deploy_params {{ deployParamsPath }} 不存在`, {
171
+ deployParamsPath,
172
+ interpolation: { escapeValue: false },
173
+ }),
174
+ );
101
175
  return;
102
176
  }
103
177
 
@@ -151,12 +225,52 @@ function convenientEnv() {
151
225
  );
152
226
  }
153
227
 
228
+ function formatBytes(bytes) {
229
+ const value = Number(bytes ?? 0);
230
+ if (!Number.isFinite(value) || value <= 0) {
231
+ return '0 B';
232
+ }
233
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
234
+ let size = value;
235
+ let unitIndex = 0;
236
+ while (size >= 1024 && unitIndex < units.length - 1) {
237
+ size /= 1024;
238
+ unitIndex += 1;
239
+ }
240
+ const digits = unitIndex === 0 ? 0 : 2;
241
+ return `${size.toFixed(digits)} ${units[unitIndex]}`;
242
+ }
243
+
244
+ function mergeBuildOptions(baseOptions, topOptions) {
245
+ return mergeWith({}, baseOptions ?? {}, topOptions ?? {}, (objValue, srcValue) => {
246
+ if (Array.isArray(srcValue)) {
247
+ return srcValue;
248
+ }
249
+ return undefined;
250
+ });
251
+ }
252
+
253
+ function loadYamlIfExists(filePath) {
254
+ if (!filePath || !isFileExist(filePath)) {
255
+ return {};
256
+ }
257
+ return yaml.load(fs.readFileSync(filePath, 'utf8')) ?? {};
258
+ }
259
+
260
+ async function loadTemplatedYamlIfExists(filePath, env) {
261
+ if (!filePath || !isFileExist(filePath)) {
262
+ return {};
263
+ }
264
+ return yaml.load(await envTemplateFile(filePath, env)) ?? {};
265
+ }
266
+
154
267
  export class LpkBuild {
155
268
  constructor(cwd, buildConfigFile) {
156
269
  this.pwd = cwd ?? process.cwd();
157
270
 
158
271
  this.optionsFilePath = path.join(this.pwd, buildConfigFile);
159
- this.options = loadFromYaml(this.optionsFilePath);
272
+ this.optionsBaseFilePath = path.join(path.dirname(this.optionsFilePath), DEFAULT_BUILD_BASE_FILE);
273
+ this.options = mergeBuildOptions(loadYamlIfExists(this.optionsBaseFilePath), loadYamlIfExists(this.optionsFilePath));
160
274
 
161
275
  this.manifestFilePath = this.options['manifest'] ? path.join(this.pwd, this.options['manifest']) : path.join(this.pwd, 'lzc-manifest.yml');
162
276
  this.manifest = null;
@@ -171,7 +285,10 @@ export class LpkBuild {
171
285
  async init() {
172
286
  const manifest = await this.getManifest();
173
287
  const primitive = convenientEnv();
174
- this.options = yaml.load(await envTemplateFile(this.optionsFilePath, Object.assign({}, primitive, manifest)));
288
+ const env = Object.assign({}, primitive, manifest);
289
+ const baseOptions = await loadTemplatedYamlIfExists(this.optionsBaseFilePath, env);
290
+ const topOptions = await loadTemplatedYamlIfExists(this.optionsFilePath, env);
291
+ this.options = mergeBuildOptions(baseOptions, topOptions);
175
292
  return this;
176
293
  }
177
294
 
@@ -245,9 +362,16 @@ export class LpkBuild {
245
362
 
246
363
  // 输出路径
247
364
  let packName = this.options['lpkPath'];
248
- const pkgout = path.resolve(this.pwd, this.options['pkgout']);
249
- if (!packName && !isDirExist(pkgout)) {
250
- throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_not_exist', `{{ pkgout }} 不存在`, { pkgout, interpolation: { escapeValue: false } });
365
+ let pkgout = '';
366
+ if (!packName) {
367
+ const rawPkgout = this.options['pkgout'];
368
+ if (rawPkgout !== undefined && rawPkgout !== null && typeof rawPkgout !== 'string') {
369
+ throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_invalid_type', 'pkgout must be a string when specified');
370
+ }
371
+ pkgout = path.resolve(this.pwd, typeof rawPkgout === 'string' && rawPkgout.trim() !== '' ? rawPkgout : './');
372
+ if (!isDirExist(pkgout)) {
373
+ throw t('lzc_cli.lib.app.lpk_build.exec_pkgout_not_exist', `{{ pkgout }} 不存在`, { pkgout, interpolation: { escapeValue: false } });
374
+ }
251
375
  }
252
376
 
253
377
  const tempDir = fs.mkdtempSync('.lzc-cli-build');
@@ -291,7 +415,10 @@ export class LpkBuild {
291
415
  } else if (isFileExist(browserExtension)) {
292
416
  fs.copyFileSync(browserExtension, path.join(tempDir, 'extension.zip'));
293
417
  } else {
294
- throw t('lzc_cli.lib.app.lpk_build.exec_browser_extension_not_exist', `{{ browserExtension }} 不存在`, { browserExtension, interpolation: { escapeValue: false } });
418
+ throw t('lzc_cli.lib.app.lpk_build.exec_browser_extension_not_exist', `{{ browserExtension }} 不存在`, {
419
+ browserExtension,
420
+ interpolation: { escapeValue: false },
421
+ });
295
422
  }
296
423
  }
297
424
 
@@ -300,7 +427,7 @@ export class LpkBuild {
300
427
  if (!isDirExist(aiPodService)) {
301
428
  throw t('lzc_cli.lib.app.lpk_build.exec_ai_pos_service_not_exist', `{{ aiPodService }} 不存在`, {
302
429
  aiPodService,
303
- interpolation: { escapeValue: false }
430
+ interpolation: { escapeValue: false },
304
431
  });
305
432
  }
306
433
  fs.cpSync(aiPodService, path.join(tempDir, 'ai-pod-service'), {
@@ -319,7 +446,43 @@ export class LpkBuild {
319
446
  }, manifest);
320
447
  }
321
448
 
322
- if (process.env.LZC_MANIFEST_TEMPLATE) {
449
+ if (Object.prototype.hasOwnProperty.call(this.options, 'embed_images')) {
450
+ throw new Error('embed_images is removed, please use lzc-build.yml images and manifest embed:alias');
451
+ }
452
+ if (Object.prototype.hasOwnProperty.call(this.options, 'embed_all_images')) {
453
+ throw new Error('embed_all_images is removed');
454
+ }
455
+ if (Object.prototype.hasOwnProperty.call(this.options, 'upstream_registry')) {
456
+ throw new Error('upstream_registry is renamed to upstream_match');
457
+ }
458
+ if (Object.prototype.hasOwnProperty.call(this.options, 'upstream_match') || Object.prototype.hasOwnProperty.call(this.options, 'upstream-match')) {
459
+ throw new Error('upstream_match is moved to lzc-build.yml images.<alias>.upstream-match');
460
+ }
461
+
462
+ const hasImagesConfig = !!this.options['images'];
463
+
464
+ let useTarPackage = false;
465
+ let embeddedImageSummary = null;
466
+ if (hasImagesConfig) {
467
+ const buildResult = await buildConfiguredImagesToTempDir(this.options['images'], manifest, this.pwd, tempDir, {
468
+ remote: this.options['remote'],
469
+ });
470
+ if (buildResult.imageCount > 0) {
471
+ useTarPackage = true;
472
+ embeddedImageSummary = buildResult;
473
+ manifest = rewriteManifestEmbeddedImages(manifest, buildResult.resolvedImageByAlias);
474
+ }
475
+ }
476
+ if (useTarPackage) {
477
+ const contentTar = path.join(tempDir, 'content.tar');
478
+ const contentTarGz = path.join(tempDir, 'content.tar.gz');
479
+ if (isFileExist(contentTar)) {
480
+ await gzipFileTo(contentTar, contentTarGz);
481
+ fs.rmSync(contentTar, { force: true });
482
+ }
483
+ }
484
+
485
+ if (process.env.LZC_MANIFEST_TEMPLATE && !useTarPackage) {
323
486
  logger.debug('copy origin manifest\n', this.manifestFilePath);
324
487
  fs.copyFileSync(this.manifestFilePath, path.join(tempDir, 'manifest.yml'));
325
488
  } else {
@@ -346,14 +509,26 @@ export class LpkBuild {
346
509
  }
347
510
 
348
511
  if (!packName) {
349
- packName = path.resolve(pkgout, `${manifest.package}-v${manifest.version}.lpk`);
512
+ const ext = '.lpk';
513
+ packName = path.resolve(pkgout, `${manifest.package}-v${manifest.version}${ext}`);
350
514
  }
351
515
 
352
- const lpkPath = await archiveFolderTo(tempDir, packName);
353
- logger.info(`${t('lzc_cli.lib.app.lpk_build.exec_output_lpk_path', '输出lpk包 {{ path }}', {
354
- path: lpkPath.path,
355
- interpolation: { escapeValue: false } // https://www.i18next.com/translation-function/interpolation#unescape
356
- })}`);
516
+ const lpkPath = await archiveFolderTo(tempDir, packName, useTarPackage ? 'tar' : 'zip');
517
+ logger.info(
518
+ `${t('lzc_cli.lib.app.lpk_build.exec_output_lpk_path', '输出lpk包 {{ path }}', {
519
+ path: lpkPath.path,
520
+ interpolation: { escapeValue: false }, // https://www.i18next.com/translation-function/interpolation#unescape
521
+ })}`,
522
+ );
523
+ if (embeddedImageSummary && embeddedImageSummary.imageCount > 0) {
524
+ logger.info('Embedded image upstream summary:');
525
+ for (const [alias, upstream] of Object.entries(embeddedImageSummary.upstreamByAlias ?? {})) {
526
+ logger.info(`- ${alias}: ${upstream || '(none, full embed)'}`);
527
+ }
528
+ logger.info(
529
+ `Embedded image layer size: ${formatBytes(embeddedImageSummary.embeddedLayerBytes)} (${embeddedImageSummary.embeddedLayerBytes} bytes, ${embeddedImageSummary.embeddedLayerCount} unique layers)`,
530
+ );
531
+ }
357
532
 
358
533
  return lpkPath.path;
359
534
  } finally {