@lazycatcloud/lzc-cli 1.3.13 → 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 (104) hide show
  1. package/README.md +30 -5
  2. package/changelog.md +16 -0
  3. package/lib/app/index.js +174 -58
  4. package/lib/app/lpk_build.js +197 -18
  5. package/lib/app/lpk_build_images.js +728 -0
  6. package/lib/app/lpk_create.js +96 -23
  7. package/lib/app/lpk_create_generator.js +150 -12
  8. package/lib/app/lpk_devshell.js +35 -21
  9. package/lib/app/lpk_embed_images.js +257 -0
  10. package/lib/app/lpk_installer.js +15 -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/appstore/index.js +56 -16
  19. package/lib/appstore/publish.js +16 -13
  20. package/lib/box/index.js +103 -6
  21. package/lib/box/ssh_remote.js +259 -0
  22. package/lib/build_remote.js +22 -0
  23. package/lib/config/index.js +4 -3
  24. package/lib/debug_bridge.js +837 -44
  25. package/lib/docker/index.js +30 -10
  26. package/lib/i18n/index.js +1 -0
  27. package/lib/i18n/locales/en/translation.json +263 -250
  28. package/lib/i18n/locales/zh/translation.json +57 -44
  29. package/lib/lpk/core.js +487 -0
  30. package/lib/lpk/index.js +210 -0
  31. package/lib/shellapi.js +5 -5
  32. package/lib/sig/core.js +254 -0
  33. package/lib/sig/index.js +88 -0
  34. package/lib/utils.js +17 -12
  35. package/package.json +4 -3
  36. package/scripts/cli.js +4 -0
  37. package/template/_lpk/README.md +11 -3
  38. package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
  39. package/template/_lpk/manifest.yml.in +4 -2
  40. package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
  41. package/template/_lpk/todolist-java.manifest.yml.in +15 -0
  42. package/template/_lpk/todolist-python.manifest.yml.in +15 -0
  43. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  44. package/template/blank/_gitignore +1 -0
  45. package/template/blank/lzc-build.yml +25 -40
  46. package/template/blank/lzc-manifest.yml +14 -7
  47. package/template/golang/Dockerfile +19 -0
  48. package/template/golang/README.md +33 -0
  49. package/template/golang/_gitignore +3 -0
  50. package/template/golang/go.mod +3 -0
  51. package/template/golang/lzc-build.yml +21 -0
  52. package/template/golang/lzc-icon.png +0 -0
  53. package/template/golang/main.go +252 -0
  54. package/template/golang/run.sh +3 -0
  55. package/template/golang/web/index.html +238 -0
  56. package/template/gui-vnc/README.md +19 -0
  57. package/template/gui-vnc/_gitignore +2 -0
  58. package/template/gui-vnc/images/Dockerfile +30 -0
  59. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  60. package/template/gui-vnc/images/startup-script.desktop +9 -0
  61. package/template/gui-vnc/images/startup-script.sh +6 -0
  62. package/template/gui-vnc/lzc-build.yml +23 -0
  63. package/template/gui-vnc/lzc-icon.png +0 -0
  64. package/template/python/Dockerfile +15 -0
  65. package/template/python/README.md +33 -0
  66. package/template/python/_gitignore +3 -0
  67. package/template/python/app.py +110 -0
  68. package/template/python/lzc-build.yml +21 -0
  69. package/template/python/lzc-icon.png +0 -0
  70. package/template/python/requirements.txt +1 -0
  71. package/template/python/run.sh +3 -0
  72. package/template/python/web/index.html +238 -0
  73. package/template/springboot/Dockerfile +20 -0
  74. package/template/springboot/README.md +33 -0
  75. package/template/springboot/_gitignore +3 -0
  76. package/template/springboot/lzc-build.yml +21 -0
  77. package/template/springboot/lzc-icon.png +0 -0
  78. package/template/springboot/pom.xml +38 -0
  79. package/template/springboot/run.sh +3 -0
  80. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  81. package/template/springboot/src/main/resources/application.properties +1 -0
  82. package/template/springboot/src/main/resources/static/index.html +238 -0
  83. package/template/vue/README.md +17 -7
  84. package/template/vue/_gitignore +1 -0
  85. package/template/vue/lzc-build.yml +31 -42
  86. package/template/vue/src/App.vue +36 -25
  87. package/template/vue/src/style.css +106 -49
  88. package/template/vue-minidb/README.md +34 -0
  89. package/template/vue-minidb/_gitignore +26 -0
  90. package/template/vue-minidb/index.html +13 -0
  91. package/template/vue-minidb/lzc-build.yml +48 -0
  92. package/template/vue-minidb/lzc-icon.png +0 -0
  93. package/template/vue-minidb/package.json +21 -0
  94. package/template/vue-minidb/public/vite.svg +1 -0
  95. package/template/vue-minidb/src/App.vue +206 -0
  96. package/template/vue-minidb/src/assets/vue.svg +1 -0
  97. package/template/vue-minidb/src/main.ts +5 -0
  98. package/template/vue-minidb/src/style.css +136 -0
  99. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  100. package/template/vue-minidb/tsconfig.app.json +24 -0
  101. package/template/vue-minidb/tsconfig.json +7 -0
  102. package/template/vue-minidb/tsconfig.node.json +22 -0
  103. package/template/vue-minidb/vite.config.ts +10 -0
  104. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
@@ -0,0 +1,487 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import archiver from 'archiver';
6
+ import AdmZip from 'adm-zip';
7
+ import * as tar from 'tar';
8
+ import yaml from 'js-yaml';
9
+ import logger from 'loglevel';
10
+ import shellApi from '../shellapi.js';
11
+ import { DebugBridge } from '../debug_bridge.js';
12
+ import { resolveBuildRemoteFromFile } from '../build_remote.js';
13
+
14
+ function toPosixPath(filePath) {
15
+ return filePath.split(path.sep).join('/');
16
+ }
17
+
18
+ export function formatBytes(bytes) {
19
+ const value = Number(bytes ?? 0);
20
+ if (!Number.isFinite(value) || value <= 0) {
21
+ return '0 B';
22
+ }
23
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
24
+ let size = value;
25
+ let unitIndex = 0;
26
+ while (size >= 1024 && unitIndex < units.length - 1) {
27
+ size /= 1024;
28
+ unitIndex += 1;
29
+ }
30
+ const digits = unitIndex === 0 ? 0 : 2;
31
+ return `${size.toFixed(digits)} ${units[unitIndex]}`;
32
+ }
33
+
34
+ function normalizeDigest(raw) {
35
+ const value = String(raw ?? '').trim().toLowerCase();
36
+ if (!value.startsWith('sha256:')) {
37
+ return '';
38
+ }
39
+ const hexPart = value.slice('sha256:'.length);
40
+ if (!/^[0-9a-f]{64}$/.test(hexPart)) {
41
+ return '';
42
+ }
43
+ return `sha256:${hexPart}`;
44
+ }
45
+
46
+ function digestToBlobPath(blobsDir, digest) {
47
+ const normalized = normalizeDigest(digest);
48
+ if (!normalized) {
49
+ throw new Error(`Invalid digest: ${digest}`);
50
+ }
51
+ return path.join(blobsDir, normalized.slice('sha256:'.length));
52
+ }
53
+
54
+ function detectPackageFormat(pkgPath) {
55
+ const ext = path.basename(pkgPath).toLowerCase();
56
+ if (ext.endsWith('.lpk.tar') || ext.endsWith('.tar')) {
57
+ return 'tar';
58
+ }
59
+ const fd = fs.openSync(pkgPath, 'r');
60
+ try {
61
+ const header = Buffer.alloc(4);
62
+ fs.readSync(fd, header, 0, 4, 0);
63
+ if (header[0] === 0x50 && header[1] === 0x4b) {
64
+ return 'zip';
65
+ }
66
+ } finally {
67
+ fs.closeSync(fd);
68
+ }
69
+ return 'tar';
70
+ }
71
+
72
+ async function extractPackage(pkgPath, format, destDir) {
73
+ if (format === 'zip') {
74
+ const zip = new AdmZip(pkgPath);
75
+ zip.extractAllTo(destDir, true);
76
+ return;
77
+ }
78
+ await tar.x({
79
+ file: pkgPath,
80
+ cwd: destDir,
81
+ });
82
+ }
83
+
84
+ async function packAsZip(srcDir, outPath) {
85
+ return new Promise((resolve, reject) => {
86
+ const output = fs.createWriteStream(outPath);
87
+ const archive = archiver('zip');
88
+ archive.on('error', reject);
89
+ output.on('error', reject);
90
+ output.on('close', resolve);
91
+ archive.pipe(output);
92
+ archive.directory(srcDir, false);
93
+ archive.finalize();
94
+ });
95
+ }
96
+
97
+ async function packAsTar(srcDir, outPath) {
98
+ const entries = fs.readdirSync(srcDir).sort();
99
+ await tar.c(
100
+ {
101
+ cwd: srcDir,
102
+ file: outPath,
103
+ portable: true,
104
+ },
105
+ entries,
106
+ );
107
+ }
108
+
109
+ async function packPackage(srcDir, format, outPath) {
110
+ if (format === 'zip') {
111
+ return packAsZip(srcDir, outPath);
112
+ }
113
+ return packAsTar(srcDir, outPath);
114
+ }
115
+
116
+ function walkFilesRecursive(dir, baseDir = dir) {
117
+ const result = [];
118
+ if (!fs.existsSync(dir)) {
119
+ return result;
120
+ }
121
+ for (const entry of fs.readdirSync(dir)) {
122
+ const absPath = path.join(dir, entry);
123
+ const stat = fs.statSync(absPath);
124
+ if (stat.isDirectory()) {
125
+ result.push(...walkFilesRecursive(absPath, baseDir));
126
+ continue;
127
+ }
128
+ if (stat.isFile()) {
129
+ result.push(toPosixPath(path.relative(baseDir, absPath)));
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+
135
+ async function sha256File(filePath) {
136
+ return new Promise((resolve, reject) => {
137
+ const hash = crypto.createHash('sha256');
138
+ let size = 0;
139
+ const stream = fs.createReadStream(filePath);
140
+ stream.on('data', (chunk) => {
141
+ size += chunk.length;
142
+ hash.update(chunk);
143
+ });
144
+ stream.on('error', reject);
145
+ stream.on('end', () => {
146
+ resolve({
147
+ digest: hash.digest('hex'),
148
+ size,
149
+ });
150
+ });
151
+ });
152
+ }
153
+
154
+ function readYamlFile(filePath) {
155
+ if (!fs.existsSync(filePath)) {
156
+ return {};
157
+ }
158
+ return yaml.load(fs.readFileSync(filePath, 'utf-8')) ?? {};
159
+ }
160
+
161
+ function isSignedPackage(workDir) {
162
+ const releaseLock = path.join(workDir, 'META', 'release.lock');
163
+ const sigDir = path.join(workDir, 'META', 'signatures');
164
+ if (fs.existsSync(releaseLock)) {
165
+ return true;
166
+ }
167
+ if (!fs.existsSync(sigDir) || !fs.statSync(sigDir).isDirectory()) {
168
+ return false;
169
+ }
170
+ return fs.readdirSync(sigDir).some((name) => name.endsWith('.sig'));
171
+ }
172
+
173
+ function buildAliasInfo(lock, blobsDir) {
174
+ const lockImages = lock?.images ?? {};
175
+ const aliases = Object.keys(lockImages).sort();
176
+
177
+ const allEmbeddedDigests = new Set();
178
+ const aliasDetails = [];
179
+ for (const alias of aliases) {
180
+ const imageInfo = lockImages[alias] ?? {};
181
+ const layers = Array.isArray(imageInfo.layers) ? imageInfo.layers : [];
182
+
183
+ let embedLayerCount = 0;
184
+ let upstreamLayerCount = 0;
185
+ let embedSize = 0;
186
+ let missingEmbedLayerCount = 0;
187
+ const uniqueEmbedDigests = new Set();
188
+
189
+ for (const layer of layers) {
190
+ const digest = normalizeDigest(layer?.digest ?? '');
191
+ if (!digest) {
192
+ continue;
193
+ }
194
+ const source = String(layer?.source ?? '').trim().toLowerCase();
195
+ if (source === 'embed') {
196
+ embedLayerCount += 1;
197
+ uniqueEmbedDigests.add(digest);
198
+ allEmbeddedDigests.add(digest);
199
+ const blobPath = digestToBlobPath(blobsDir, digest);
200
+ if (fs.existsSync(blobPath)) {
201
+ embedSize += fs.statSync(blobPath).size;
202
+ } else {
203
+ missingEmbedLayerCount += 1;
204
+ }
205
+ } else {
206
+ upstreamLayerCount += 1;
207
+ }
208
+ }
209
+
210
+ aliasDetails.push({
211
+ alias,
212
+ imageID: String(imageInfo.image_id ?? ''),
213
+ upstream: String(imageInfo.upstream ?? ''),
214
+ embedLayerCount,
215
+ upstreamLayerCount,
216
+ embedSize,
217
+ uniqueEmbedLayerCount: uniqueEmbedDigests.size,
218
+ missingEmbedLayerCount,
219
+ });
220
+ }
221
+
222
+ let totalEmbeddedSize = 0;
223
+ let totalMissingEmbeddedLayerCount = 0;
224
+ for (const digest of allEmbeddedDigests) {
225
+ const blobPath = digestToBlobPath(blobsDir, digest);
226
+ if (fs.existsSync(blobPath)) {
227
+ totalEmbeddedSize += fs.statSync(blobPath).size;
228
+ } else {
229
+ totalMissingEmbeddedLayerCount += 1;
230
+ }
231
+ }
232
+
233
+ return {
234
+ aliases,
235
+ aliasDetails,
236
+ totalEmbeddedLayerCount: allEmbeddedDigests.size,
237
+ totalEmbeddedSize,
238
+ totalMissingEmbeddedLayerCount,
239
+ };
240
+ }
241
+
242
+ export async function inspectLpkPackage(pkgPath) {
243
+ const resolvedPkgPath = path.resolve(pkgPath);
244
+ if (!fs.existsSync(resolvedPkgPath)) {
245
+ throw new Error(`Package not found: ${resolvedPkgPath}`);
246
+ }
247
+
248
+ const format = detectPackageFormat(resolvedPkgPath);
249
+ const pkgStat = fs.statSync(resolvedPkgPath);
250
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lzc-cli-lpk-info-'));
251
+ const workDir = path.join(tempDir, 'work');
252
+ fs.mkdirSync(workDir, { recursive: true });
253
+ try {
254
+ await extractPackage(resolvedPkgPath, format, workDir);
255
+
256
+ const manifest = readYamlFile(path.join(workDir, 'manifest.yml'));
257
+ const imagesDir = path.join(workDir, 'images');
258
+ const imagesLockPath = path.join(workDir, 'images.lock');
259
+ const hasImagesDir = fs.existsSync(imagesDir) && fs.statSync(imagesDir).isDirectory();
260
+ const hasImagesLock = fs.existsSync(imagesLockPath);
261
+ const lpkVersion = hasImagesDir ? 'v2' : 'v1';
262
+
263
+ let imageInfo = {
264
+ aliases: [],
265
+ aliasDetails: [],
266
+ totalEmbeddedLayerCount: 0,
267
+ totalEmbeddedSize: 0,
268
+ totalMissingEmbeddedLayerCount: 0,
269
+ };
270
+ if (hasImagesLock) {
271
+ const lock = readYamlFile(imagesLockPath);
272
+ const blobsDir = path.join(imagesDir, 'blobs', 'sha256');
273
+ imageInfo = buildAliasInfo(lock, blobsDir);
274
+ }
275
+
276
+ return {
277
+ path: resolvedPkgPath,
278
+ size: pkgStat.size,
279
+ format,
280
+ lpkVersion,
281
+ signed: isSignedPackage(workDir),
282
+ packageID: String(manifest?.package ?? ''),
283
+ appVersion: String(manifest?.version ?? ''),
284
+ hasImagesDir,
285
+ hasImagesLock,
286
+ imageInfo,
287
+ };
288
+ } finally {
289
+ fs.rmSync(tempDir, { recursive: true, force: true });
290
+ }
291
+ }
292
+
293
+ async function ensureBridge(baseDir) {
294
+ const buildRemote = resolveBuildRemoteFromFile(baseDir);
295
+ if (!buildRemote) {
296
+ await shellApi.init();
297
+ }
298
+ const bridge = new DebugBridge(baseDir, buildRemote);
299
+ await bridge.init();
300
+ return bridge;
301
+ }
302
+
303
+ async function fillMissingDigestsFromUpstream(bridge, upstreamImage, digestSet, blobsDir, tempRoot) {
304
+ if (!upstreamImage) {
305
+ throw new Error('Upstream image is required for filling missing blobs');
306
+ }
307
+ const pending = new Set([...digestSet].filter((digest) => !fs.existsSync(digestToBlobPath(blobsDir, digest))));
308
+ if (pending.size === 0) {
309
+ return { addedCount: 0, addedBytes: 0 };
310
+ }
311
+
312
+ await bridge.lzcDockerPull(upstreamImage);
313
+ const archivePath = path.join(tempRoot, `upstream-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`);
314
+ await bridge.lzcDockerSave([upstreamImage], archivePath);
315
+
316
+ let addedCount = 0;
317
+ let addedBytes = 0;
318
+ const extractDir = fs.mkdtempSync(path.join(tempRoot, 'upstream-extract-'));
319
+ try {
320
+ await tar.x({
321
+ file: archivePath,
322
+ cwd: extractDir,
323
+ });
324
+
325
+ for (const digest of [...pending]) {
326
+ const hex = digest.slice('sha256:'.length);
327
+ const blobSourcePath = path.join(extractDir, 'blobs', 'sha256', hex);
328
+ if (!fs.existsSync(blobSourcePath)) {
329
+ continue;
330
+ }
331
+ const blobTargetPath = path.join(blobsDir, hex);
332
+ fs.mkdirSync(path.dirname(blobTargetPath), { recursive: true });
333
+ if (!fs.existsSync(blobTargetPath)) {
334
+ fs.copyFileSync(blobSourcePath, blobTargetPath);
335
+ addedCount += 1;
336
+ addedBytes += fs.statSync(blobTargetPath).size;
337
+ }
338
+ pending.delete(digest);
339
+ }
340
+
341
+ if (pending.size > 0) {
342
+ const files = walkFilesRecursive(extractDir).filter((relPath) => relPath.endsWith('.tar')).sort();
343
+ for (const relPath of files) {
344
+ if (pending.size === 0) {
345
+ break;
346
+ }
347
+ const absPath = path.join(extractDir, relPath);
348
+ const { digest } = await sha256File(absPath);
349
+ const fullDigest = `sha256:${digest}`;
350
+ if (!pending.has(fullDigest)) {
351
+ continue;
352
+ }
353
+ const blobTargetPath = path.join(blobsDir, digest);
354
+ fs.mkdirSync(path.dirname(blobTargetPath), { recursive: true });
355
+ if (!fs.existsSync(blobTargetPath)) {
356
+ fs.copyFileSync(absPath, blobTargetPath);
357
+ addedCount += 1;
358
+ addedBytes += fs.statSync(blobTargetPath).size;
359
+ }
360
+ pending.delete(fullDigest);
361
+ }
362
+ }
363
+
364
+ if (pending.size > 0) {
365
+ throw new Error(`Missing upstream layer blobs after sync: ${[...pending].sort().join(', ')}`);
366
+ }
367
+ } finally {
368
+ fs.rmSync(archivePath, { force: true });
369
+ fs.rmSync(extractDir, { recursive: true, force: true });
370
+ }
371
+
372
+ return { addedCount, addedBytes };
373
+ }
374
+
375
+ export async function embedLpkPackage(pkgPath, options = {}) {
376
+ const resolvedPkgPath = path.resolve(pkgPath);
377
+ if (!fs.existsSync(resolvedPkgPath)) {
378
+ throw new Error(`Package not found: ${resolvedPkgPath}`);
379
+ }
380
+
381
+ const format = detectPackageFormat(resolvedPkgPath);
382
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lzc-cli-lpk-embed-'));
383
+ const workDir = path.join(tempDir, 'work');
384
+ const outTemp = path.join(tempDir, format === 'zip' ? 'embedded.lpk' : 'embedded.lpk.tar');
385
+ fs.mkdirSync(workDir, { recursive: true });
386
+
387
+ try {
388
+ await extractPackage(resolvedPkgPath, format, workDir);
389
+ const imagesDir = path.join(workDir, 'images');
390
+ const imagesLockPath = path.join(workDir, 'images.lock');
391
+ if (!fs.existsSync(imagesDir) || !fs.statSync(imagesDir).isDirectory() || !fs.existsSync(imagesLockPath)) {
392
+ throw new Error('This package does not contain images.lock/images directory, only lpk v2 is supported');
393
+ }
394
+
395
+ const lock = readYamlFile(imagesLockPath);
396
+ const lockImages = lock?.images;
397
+ if (!lockImages || typeof lockImages !== 'object') {
398
+ throw new Error('Invalid images.lock: images field is missing');
399
+ }
400
+ const allAliases = Object.keys(lockImages).sort();
401
+ if (allAliases.length === 0) {
402
+ throw new Error('Invalid images.lock: images is empty');
403
+ }
404
+
405
+ const inputAliases = Array.isArray(options.images)
406
+ ? options.images
407
+ .map((item) => String(item ?? '').trim())
408
+ .filter((item) => item !== '')
409
+ : [];
410
+ const targetAliases = inputAliases.length > 0 ? [...new Set(inputAliases)] : allAliases;
411
+ for (const alias of targetAliases) {
412
+ if (!Object.prototype.hasOwnProperty.call(lockImages, alias)) {
413
+ throw new Error(`Image alias not found in images.lock: ${alias}`);
414
+ }
415
+ }
416
+
417
+ const blobsDir = path.join(imagesDir, 'blobs', 'sha256');
418
+ fs.mkdirSync(blobsDir, { recursive: true });
419
+
420
+ const missingByUpstream = new Map();
421
+ let changedLayerCount = 0;
422
+ for (const alias of targetAliases) {
423
+ const image = lockImages[alias];
424
+ const upstream = String(image?.upstream ?? '').trim();
425
+ const layers = Array.isArray(image?.layers) ? image.layers : [];
426
+
427
+ let hasUpstreamLayer = false;
428
+ for (const layer of layers) {
429
+ const digest = normalizeDigest(layer?.digest ?? '');
430
+ if (!digest) {
431
+ continue;
432
+ }
433
+ const source = String(layer?.source ?? '').trim().toLowerCase();
434
+ if (source === 'embed') {
435
+ continue;
436
+ }
437
+ hasUpstreamLayer = true;
438
+ changedLayerCount += 1;
439
+ layer.source = 'embed';
440
+
441
+ const blobPath = digestToBlobPath(blobsDir, digest);
442
+ if (!fs.existsSync(blobPath)) {
443
+ if (!upstream) {
444
+ throw new Error(`Alias "${alias}" layer ${digest} requires upstream blob, but upstream is empty`);
445
+ }
446
+ if (!missingByUpstream.has(upstream)) {
447
+ missingByUpstream.set(upstream, new Set());
448
+ }
449
+ missingByUpstream.get(upstream).add(digest);
450
+ }
451
+ }
452
+
453
+ if (hasUpstreamLayer) {
454
+ image.upstream = '';
455
+ }
456
+ }
457
+
458
+ let addedBlobCount = 0;
459
+ let addedBlobBytes = 0;
460
+ if (missingByUpstream.size > 0) {
461
+ const bridge = await ensureBridge(process.cwd());
462
+ for (const [upstream, digestSet] of missingByUpstream.entries()) {
463
+ logger.info(`Sync upstream blobs from: ${upstream}`);
464
+ const result = await fillMissingDigestsFromUpstream(bridge, upstream, digestSet, blobsDir, tempDir);
465
+ addedBlobCount += result.addedCount;
466
+ addedBlobBytes += result.addedBytes;
467
+ }
468
+ }
469
+
470
+ fs.writeFileSync(imagesLockPath, yaml.dump(lock, { lineWidth: -1 }));
471
+ await packPackage(workDir, format, outTemp);
472
+
473
+ const finalPath = options.output ? path.resolve(options.output) : resolvedPkgPath;
474
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true });
475
+ fs.copyFileSync(outTemp, finalPath);
476
+
477
+ return {
478
+ outputPath: finalPath,
479
+ targetAliases,
480
+ changedLayerCount,
481
+ addedBlobCount,
482
+ addedBlobBytes,
483
+ };
484
+ } finally {
485
+ fs.rmSync(tempDir, { recursive: true, force: true });
486
+ }
487
+ }
@@ -0,0 +1,210 @@
1
+ import logger from 'loglevel';
2
+ import { t } from '../i18n/index.js';
3
+ import { signPackageFile } from '../sig/core.js';
4
+ import { embedLpkPackage, formatBytes, inspectLpkPackage } from './core.js';
5
+ import { resolveBuildRemoteFromFile } from '../build_remote.js';
6
+ import shellApi from '../shellapi.js';
7
+ import { LpkInstaller, installConfig } from '../app/lpk_installer.js';
8
+ import { DebugBridge } from '../debug_bridge.js';
9
+
10
+ function normalizeErrorMessage(error) {
11
+ if (!error) {
12
+ return 'unknown error';
13
+ }
14
+ if (typeof error === 'string') {
15
+ return error;
16
+ }
17
+ if (typeof error.message === 'string' && error.message.trim() !== '') {
18
+ return error.message;
19
+ }
20
+ return String(error);
21
+ }
22
+
23
+ function withErrorHandled(handler) {
24
+ return async (argv) => {
25
+ try {
26
+ await handler(argv);
27
+ } catch (error) {
28
+ logger.error(normalizeErrorMessage(error));
29
+ process.exit(1);
30
+ }
31
+ };
32
+ }
33
+
34
+ function signBuilder(args) {
35
+ args.option('private-key', {
36
+ alias: 'k',
37
+ describe: t('lzc_cli.lib.sig.index.sign_private_key_desc', 'Private key path'),
38
+ type: 'string',
39
+ });
40
+ args.option('public-key', {
41
+ alias: 'p',
42
+ describe: t('lzc_cli.lib.sig.index.sign_public_key_desc', 'Public key path'),
43
+ type: 'string',
44
+ });
45
+ args.option('key-id', {
46
+ describe: t('lzc_cli.lib.sig.index.sign_key_id_desc', 'Signature key id'),
47
+ type: 'string',
48
+ default: 'dev',
49
+ });
50
+ args.option('o', {
51
+ alias: 'output',
52
+ describe: t('lzc_cli.lib.sig.index.sign_output_desc', 'Output package path'),
53
+ type: 'string',
54
+ });
55
+ args.option('resign', {
56
+ describe: t('lzc_cli.lib.sig.index.sign_resign_desc', 'Force replace existing META signature'),
57
+ type: 'boolean',
58
+ default: false,
59
+ });
60
+ }
61
+
62
+ async function signHandler({ pkgPath, privateKey, publicKey, keyId, output, resign }) {
63
+ const out = await signPackageFile(pkgPath, {
64
+ privateKey,
65
+ publicKey,
66
+ keyId,
67
+ output,
68
+ resign: !!resign,
69
+ });
70
+ logger.info(`${resign ? 're-signed' : 'signed'} package: ${out}`);
71
+ }
72
+
73
+ async function infoHandler({ pkgPath }) {
74
+ const info = await inspectLpkPackage(pkgPath);
75
+
76
+ logger.info(`path: ${info.path}`);
77
+ logger.info(`size: ${formatBytes(info.size)} (${info.size} bytes)`);
78
+ logger.info(`format: ${info.format}`);
79
+ logger.info(`lpk_version: ${info.lpkVersion}`);
80
+ logger.info(`signed: ${info.signed ? 'yes' : 'no'}`);
81
+ logger.info(`package: ${info.packageID || '(unknown)'}`);
82
+ logger.info(`version: ${info.appVersion || '(unknown)'}`);
83
+
84
+ if (!info.hasImagesDir || !info.hasImagesLock) {
85
+ logger.info('images: none');
86
+ return;
87
+ }
88
+
89
+ logger.info(`image_count: ${info.imageInfo.aliases.length}`);
90
+ logger.info(
91
+ `embedded_layer_size: ${formatBytes(info.imageInfo.totalEmbeddedSize)} (${info.imageInfo.totalEmbeddedSize} bytes, ${info.imageInfo.totalEmbeddedLayerCount} unique layers)`,
92
+ );
93
+ if (info.imageInfo.totalMissingEmbeddedLayerCount > 0) {
94
+ logger.info(`missing_embedded_layers: ${info.imageInfo.totalMissingEmbeddedLayerCount}`);
95
+ }
96
+ logger.info('images:');
97
+ for (const detail of info.imageInfo.aliasDetails) {
98
+ logger.info(`- alias: ${detail.alias}`);
99
+ logger.info(` image_id: ${detail.imageID || '(unknown)'}`);
100
+ logger.info(` upstream: ${detail.upstream || '(none)'}`);
101
+ logger.info(` embedded_layers: ${detail.embedLayerCount}`);
102
+ logger.info(` upstream_layers: ${detail.upstreamLayerCount}`);
103
+ logger.info(` embedded_size: ${formatBytes(detail.embedSize)} (${detail.embedSize} bytes)`);
104
+ if (detail.missingEmbedLayerCount > 0) {
105
+ logger.info(` missing_embedded_layers: ${detail.missingEmbedLayerCount}`);
106
+ }
107
+ }
108
+ }
109
+
110
+ async function embedHandler({ pkgPath, output, image }) {
111
+ const images = Array.isArray(image) ? image : image ? [image] : [];
112
+ const result = await embedLpkPackage(pkgPath, {
113
+ output,
114
+ images,
115
+ });
116
+
117
+ logger.info(`output: ${result.outputPath}`);
118
+ logger.info(`target_images: ${result.targetAliases.join(', ')}`);
119
+ logger.info(`converted_layers: ${result.changedLayerCount}`);
120
+ logger.info(`added_blobs: ${result.addedBlobCount}`);
121
+ logger.info(`added_blob_size: ${formatBytes(result.addedBlobBytes)} (${result.addedBlobBytes} bytes)`);
122
+ }
123
+
124
+ async function installHandler({ pkgPath, apk }) {
125
+ if (!resolveBuildRemoteFromFile(process.cwd())) {
126
+ await shellApi.init();
127
+ }
128
+ installConfig.apk = apk == 'y';
129
+
130
+ const installPath = pkgPath ?? process.cwd();
131
+ const installer = new LpkInstaller();
132
+ await installer.init();
133
+ await installer.install(installPath);
134
+ }
135
+
136
+ async function uninstallHandler({ pkgId, deleteData }) {
137
+ const buildRemote = resolveBuildRemoteFromFile(process.cwd());
138
+ if (!buildRemote) {
139
+ await shellApi.init();
140
+ }
141
+
142
+ const bridge = new DebugBridge(process.cwd(), buildRemote);
143
+ await bridge.init();
144
+ await bridge.uninstall(pkgId, deleteData);
145
+ logger.debug(`default lcmd device: ${bridge.boxname} , uninstall the app ${pkgId} finish`);
146
+ }
147
+
148
+ export function lpkCommand(program) {
149
+ const subCommands = [
150
+ {
151
+ command: 'info <pkgPath>',
152
+ desc: 'Show package metadata and embedded image summary',
153
+ handler: withErrorHandled(infoHandler),
154
+ },
155
+ {
156
+ command: 'embed <pkgPath>',
157
+ desc: 'Convert selected images to fully embedded mode',
158
+ builder: (args) => {
159
+ args.option('image', {
160
+ describe: 'Target image alias from images.lock, repeatable',
161
+ type: 'array',
162
+ });
163
+ args.option('o', {
164
+ alias: 'output',
165
+ describe: 'Output package path, default overwrite input package',
166
+ type: 'string',
167
+ });
168
+ },
169
+ handler: withErrorHandled(embedHandler),
170
+ },
171
+ {
172
+ command: 'sign <pkgPath>',
173
+ desc: t('lzc_cli.lib.sig.index.sign_desc', 'Sign lpk package'),
174
+ builder: signBuilder,
175
+ handler: withErrorHandled(signHandler),
176
+ },
177
+ {
178
+ command: 'install [pkgPath]',
179
+ desc: 'Install LPK package to target device',
180
+ builder: (args) => {
181
+ args.option('apk', {
182
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_index_rags_apk_desc', '是否生成APK(y/n)'),
183
+ type: 'string',
184
+ default: 'y',
185
+ });
186
+ },
187
+ handler: withErrorHandled(installHandler),
188
+ },
189
+ {
190
+ command: 'uninstall <pkgId>',
191
+ desc: 'Uninstall package from target device',
192
+ builder: (args) => {
193
+ args.option('delete-data', {
194
+ describe: t('lzc_cli.lib.app.index.lpk_cmd_uninstall_rags_delete_data_desc', '删除应用数据 ⚠️ 警告: 应用数据删除后无法恢复'),
195
+ type: 'boolean',
196
+ default: false,
197
+ });
198
+ },
199
+ handler: withErrorHandled(uninstallHandler),
200
+ },
201
+ ];
202
+
203
+ program.command({
204
+ command: 'lpk',
205
+ desc: 'LPK package tools',
206
+ builder: (args) => {
207
+ args.command(subCommands);
208
+ },
209
+ });
210
+ }