@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
@@ -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
+ }
@@ -0,0 +1,254 @@
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 { t } from '../i18n/index.js';
10
+
11
+ function toPosixPath(filePath) {
12
+ return filePath.split(path.sep).join('/');
13
+ }
14
+
15
+ function detectPackageFormat(pkgPath) {
16
+ const ext = path.basename(pkgPath).toLowerCase();
17
+ if (ext.endsWith('.lpk.tar') || ext.endsWith('.tar')) {
18
+ return 'tar';
19
+ }
20
+ const fd = fs.openSync(pkgPath, 'r');
21
+ try {
22
+ const header = Buffer.alloc(4);
23
+ fs.readSync(fd, header, 0, 4, 0);
24
+ if (header[0] === 0x50 && header[1] === 0x4b) {
25
+ return 'zip';
26
+ }
27
+ } finally {
28
+ fs.closeSync(fd);
29
+ }
30
+ return 'tar';
31
+ }
32
+
33
+ async function extractPackage(pkgPath, format, destDir) {
34
+ if (format === 'zip') {
35
+ const zip = new AdmZip(pkgPath);
36
+ zip.extractAllTo(destDir, true);
37
+ return;
38
+ }
39
+ await tar.x({
40
+ file: pkgPath,
41
+ cwd: destDir,
42
+ });
43
+ }
44
+
45
+ async function packAsZip(srcDir, outPath) {
46
+ return new Promise((resolve, reject) => {
47
+ const output = fs.createWriteStream(outPath);
48
+ const archive = archiver('zip');
49
+ archive.on('error', reject);
50
+ output.on('error', reject);
51
+ output.on('close', resolve);
52
+ archive.pipe(output);
53
+ archive.directory(srcDir, false);
54
+ archive.finalize();
55
+ });
56
+ }
57
+
58
+ async function packAsTar(srcDir, outPath) {
59
+ const entries = fs.readdirSync(srcDir).sort();
60
+ await tar.c(
61
+ {
62
+ cwd: srcDir,
63
+ file: outPath,
64
+ portable: true,
65
+ },
66
+ entries,
67
+ );
68
+ }
69
+
70
+ async function packPackage(srcDir, format, outPath) {
71
+ if (format === 'zip') {
72
+ return packAsZip(srcDir, outPath);
73
+ }
74
+ return packAsTar(srcDir, outPath);
75
+ }
76
+
77
+ function walkFilesRecursive(dir, baseDir = dir) {
78
+ const result = [];
79
+ if (!fs.existsSync(dir)) {
80
+ return result;
81
+ }
82
+ for (const entry of fs.readdirSync(dir)) {
83
+ const absPath = path.join(dir, entry);
84
+ const stat = fs.statSync(absPath);
85
+ if (stat.isDirectory()) {
86
+ result.push(...walkFilesRecursive(absPath, baseDir));
87
+ continue;
88
+ }
89
+ if (stat.isFile()) {
90
+ result.push(toPosixPath(path.relative(baseDir, absPath)));
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ async function sha256File(filePath) {
97
+ return new Promise((resolve, reject) => {
98
+ const hash = crypto.createHash('sha256');
99
+ let size = 0;
100
+ const stream = fs.createReadStream(filePath);
101
+ stream.on('data', (chunk) => {
102
+ size += chunk.length;
103
+ hash.update(chunk);
104
+ });
105
+ stream.on('error', reject);
106
+ stream.on('end', () => {
107
+ resolve({
108
+ digest: hash.digest('hex'),
109
+ size,
110
+ });
111
+ });
112
+ });
113
+ }
114
+
115
+ function loadManifestInfo(workDir) {
116
+ const manifestPath = path.join(workDir, 'manifest.yml');
117
+ if (!fs.existsSync(manifestPath)) {
118
+ return { appid: '', version: '' };
119
+ }
120
+ try {
121
+ const manifest = yaml.load(fs.readFileSync(manifestPath, 'utf-8'));
122
+ return {
123
+ appid: manifest?.package ?? '',
124
+ version: manifest?.version ? String(manifest.version) : '',
125
+ };
126
+ } catch {
127
+ return { appid: '', version: '' };
128
+ }
129
+ }
130
+
131
+ function hasMetaSignData(workDir) {
132
+ const releaseLock = path.join(workDir, 'META', 'release.lock');
133
+ const sigDir = path.join(workDir, 'META', 'signatures');
134
+ if (fs.existsSync(releaseLock)) {
135
+ return true;
136
+ }
137
+ if (!fs.existsSync(sigDir) || !fs.statSync(sigDir).isDirectory()) {
138
+ return false;
139
+ }
140
+ return fs.readdirSync(sigDir).some((f) => f.endsWith('.sig'));
141
+ }
142
+
143
+ async function writeMetaSignatureFiles(workDir, privateKeyPath, publicKeyPath, keyId) {
144
+ const filePaths = walkFilesRecursive(workDir)
145
+ .filter((p) => !p.startsWith('META/'))
146
+ .sort();
147
+ const objects = [];
148
+ for (const relPath of filePaths) {
149
+ const absPath = path.join(workDir, relPath);
150
+ const { digest, size } = await sha256File(absPath);
151
+ objects.push({
152
+ path: relPath,
153
+ digest: `sha256:${digest}`,
154
+ size,
155
+ });
156
+ }
157
+
158
+ const manifestInfo = loadManifestInfo(workDir);
159
+ const releaseLock = {
160
+ schema: 'lazycat.lpk.release-lock/v1',
161
+ appid: manifestInfo.appid,
162
+ version: manifestInfo.version,
163
+ objects,
164
+ };
165
+ const releaseLockContent = Buffer.from(JSON.stringify(releaseLock, null, 2));
166
+
167
+ const privateKeyPem = fs.readFileSync(privateKeyPath, 'utf-8');
168
+ const publicKeyPem = fs.readFileSync(publicKeyPath, 'utf-8');
169
+ const privateKey = crypto.createPrivateKey(privateKeyPem);
170
+ const signature = crypto.sign(null, releaseLockContent, privateKey).toString('base64');
171
+
172
+ const metaDir = path.join(workDir, 'META');
173
+ const keyDir = path.join(metaDir, 'keys');
174
+ const sigDir = path.join(metaDir, 'signatures');
175
+ fs.mkdirSync(keyDir, { recursive: true });
176
+ fs.mkdirSync(sigDir, { recursive: true });
177
+ fs.writeFileSync(path.join(metaDir, 'release.lock'), releaseLockContent);
178
+ fs.writeFileSync(path.join(keyDir, `${keyId}.pub`), publicKeyPem);
179
+ fs.writeFileSync(
180
+ path.join(sigDir, `${keyId}.sig`),
181
+ JSON.stringify(
182
+ {
183
+ schema: 'lazycat.lpk.signature/v1',
184
+ algorithm: 'ed25519',
185
+ key_id: keyId,
186
+ signed_file: 'META/release.lock',
187
+ signature,
188
+ },
189
+ null,
190
+ 2,
191
+ ),
192
+ );
193
+ }
194
+
195
+ export async function signPackageFile(pkgPath, options = {}) {
196
+ const { privateKey, publicKey, keyId = 'dev', output, resign = false } = options;
197
+
198
+ if (!pkgPath || !fs.existsSync(pkgPath)) {
199
+ throw new Error(t('lzc_cli.lib.sig.index.pkg_not_found_fail', `Package not found: ${pkgPath}`));
200
+ }
201
+
202
+ const resolvedPrivateKey = privateKey ? path.resolve(privateKey) : path.resolve('.sig', `${keyId}.ed25519.private.pem`);
203
+ const resolvedPublicKey = publicKey ? path.resolve(publicKey) : path.resolve('.sig', `${keyId}.ed25519.public.pem`);
204
+ if (!fs.existsSync(resolvedPrivateKey)) {
205
+ throw new Error(t('lzc_cli.lib.sig.index.private_key_not_found_fail', `Private key not found: ${resolvedPrivateKey}`));
206
+ }
207
+ if (!fs.existsSync(resolvedPublicKey)) {
208
+ throw new Error(t('lzc_cli.lib.sig.index.public_key_not_found_fail', `Public key not found: ${resolvedPublicKey}`));
209
+ }
210
+
211
+ const format = detectPackageFormat(pkgPath);
212
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lzc-cli-sign-'));
213
+ const outTemp = path.join(tempDir, format === 'zip' ? 'signed.lpk' : 'signed.lpk.tar');
214
+ const workDir = path.join(tempDir, 'work');
215
+ fs.mkdirSync(workDir, { recursive: true });
216
+ try {
217
+ await extractPackage(pkgPath, format, workDir);
218
+ if (resign) {
219
+ fs.rmSync(path.join(workDir, 'META'), { recursive: true, force: true });
220
+ } else if (hasMetaSignData(workDir)) {
221
+ throw new Error(t('lzc_cli.lib.sig.index.package_already_signed_fail', 'Package already signed, use `lzc-cli lpk sign --resign`'));
222
+ }
223
+
224
+ await writeMetaSignatureFiles(workDir, resolvedPrivateKey, resolvedPublicKey, keyId);
225
+ await packPackage(workDir, format, outTemp);
226
+
227
+ const finalPath = output ? path.resolve(output) : path.resolve(pkgPath);
228
+ fs.mkdirSync(path.dirname(finalPath), { recursive: true });
229
+ fs.copyFileSync(outTemp, finalPath);
230
+ return finalPath;
231
+ } finally {
232
+ fs.rmSync(tempDir, { recursive: true, force: true });
233
+ }
234
+ }
235
+
236
+ export function generateKeyPair(outputDir, name, force = false) {
237
+ const targetDir = path.resolve(outputDir || process.cwd());
238
+ fs.mkdirSync(targetDir, { recursive: true });
239
+
240
+ const privateKeyPath = path.join(targetDir, `${name}.ed25519.private.pem`);
241
+ const publicKeyPath = path.join(targetDir, `${name}.ed25519.public.pem`);
242
+ if (!force && (fs.existsSync(privateKeyPath) || fs.existsSync(publicKeyPath))) {
243
+ throw new Error(t('lzc_cli.lib.sig.index.key_exists_fail', `Key files already exist: ${privateKeyPath}, ${publicKeyPath}`));
244
+ }
245
+
246
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
247
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
248
+ const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
249
+
250
+ fs.writeFileSync(privateKeyPath, privateKeyPem);
251
+ fs.chmodSync(privateKeyPath, 0o600);
252
+ fs.writeFileSync(publicKeyPath, publicKeyPem);
253
+ return { privateKeyPath, publicKeyPath };
254
+ }
@@ -0,0 +1,88 @@
1
+ import logger from 'loglevel';
2
+ import { t } from '../i18n/index.js';
3
+ import { generateKeyPair, signPackageFile } from './core.js';
4
+
5
+ function signBuilder(args) {
6
+ args.option('private-key', {
7
+ alias: 'k',
8
+ describe: t('lzc_cli.lib.sig.index.sign_private_key_desc', 'Private key path'),
9
+ type: 'string',
10
+ });
11
+ args.option('public-key', {
12
+ alias: 'p',
13
+ describe: t('lzc_cli.lib.sig.index.sign_public_key_desc', 'Public key path'),
14
+ type: 'string',
15
+ });
16
+ args.option('key-id', {
17
+ describe: t('lzc_cli.lib.sig.index.sign_key_id_desc', 'Signature key id'),
18
+ type: 'string',
19
+ default: 'dev',
20
+ });
21
+ args.option('o', {
22
+ alias: 'output',
23
+ describe: t('lzc_cli.lib.sig.index.sign_output_desc', 'Output package path'),
24
+ type: 'string',
25
+ });
26
+ args.option('resign', {
27
+ describe: t('lzc_cli.lib.sig.index.sign_resign_desc', 'Force replace existing META signature'),
28
+ type: 'boolean',
29
+ default: false,
30
+ });
31
+ }
32
+
33
+ async function signHandler({ pkgPath, privateKey, publicKey, keyId, output, resign }) {
34
+ const out = await signPackageFile(pkgPath, {
35
+ privateKey,
36
+ publicKey,
37
+ keyId,
38
+ output,
39
+ resign: !!resign,
40
+ });
41
+ logger.info(`${resign ? 're-signed' : 'signed'} package: ${out}`);
42
+ }
43
+
44
+ export function sigCommand(program) {
45
+ const subCommands = [
46
+ {
47
+ command: '$0 <pkgPath>',
48
+ desc: t('lzc_cli.lib.sig.index.sign_desc', 'Sign lpk package'),
49
+ builder: signBuilder,
50
+ handler: signHandler,
51
+ },
52
+ {
53
+ command: 'gen-key [outputDir]',
54
+ desc: t('lzc_cli.lib.sig.index.gen_key_desc', 'Generate developer key pair'),
55
+ builder: (args) => {
56
+ args.option('name', {
57
+ describe: t('lzc_cli.lib.sig.index.gen_key_name_desc', 'Key name'),
58
+ type: 'string',
59
+ default: 'dev',
60
+ });
61
+ args.option('force', {
62
+ describe: t('lzc_cli.lib.sig.index.gen_key_force_desc', 'Overwrite existing key files'),
63
+ type: 'boolean',
64
+ default: false,
65
+ });
66
+ },
67
+ handler: async ({ outputDir, name, force }) => {
68
+ const { privateKeyPath, publicKeyPath } = generateKeyPair(outputDir || '.sig', name, force);
69
+ logger.info(`private key: ${privateKeyPath}`);
70
+ logger.info(`public key: ${publicKeyPath}`);
71
+ },
72
+ },
73
+ {
74
+ command: 'sign <pkgPath>',
75
+ desc: t('lzc_cli.lib.sig.index.sign_desc', 'Sign lpk package'),
76
+ builder: signBuilder,
77
+ handler: signHandler,
78
+ },
79
+ ];
80
+
81
+ program.command({
82
+ command: 'sig',
83
+ desc: t('lzc_cli.lib.sig.index.sig_cmd_desc', 'LPK signature tools'),
84
+ builder: (args) => {
85
+ args.command(subCommands);
86
+ },
87
+ });
88
+ }
package/lib/utils.js CHANGED
@@ -426,12 +426,14 @@ export function isUserApp(manifest) {
426
426
  return !!manifest['application']['user_app'];
427
427
  }
428
428
 
429
- export async function tarContentDir(from, to, cwd = './') {
429
+ export async function tarContentDir(from, to, cwd = './', options = {}) {
430
+ const gzipEnabled = !!options.gzip;
430
431
  return new Promise((resolve, reject) => {
431
432
  const dest = fs.createWriteStream(to);
432
433
  tar.c(
433
434
  {
434
435
  cwd: cwd,
436
+ gzip: gzipEnabled,
435
437
  filter: (filePath) => {
436
438
  logger.debug(`tar gz ${filePath}`);
437
439
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazycatcloud/lzc-cli",
3
- "version": "1.3.14",
3
+ "version": "2.0.0-pre.0",
4
4
  "description": "lazycat cloud developer kit",
5
5
  "scripts": {
6
6
  "release": "release-it patch",
@@ -49,6 +49,7 @@
49
49
  "inquirer": "^10.1.8",
50
50
  "isbinaryfile": "^5.0.2",
51
51
  "js-yaml": "^4.1.0",
52
+ "lodash": "^4.17.23",
52
53
  "lodash.debounce": "^4.0.8",
53
54
  "lodash.merge": "^4.6.2",
54
55
  "lodash.mergewith": "^4.6.2",
package/scripts/cli.js CHANGED
@@ -14,6 +14,7 @@ import { appstoreCommand } from '../lib/appstore/index.js';
14
14
  import { lpkAppCommand, lpkProjectCommand } from '../lib/app/index.js';
15
15
  import { configCommand } from '../lib/config/index.js';
16
16
  import { lzcDockerCommand } from '../lib/docker/index.js';
17
+ import { lpkCommand } from '../lib/lpk/index.js';
17
18
 
18
19
  function setLoggerLevel({ log }) {
19
20
  logger.setLevel(log, false);
@@ -26,6 +27,7 @@ function checkLatestVersion(controller) {
26
27
  case 'project':
27
28
  case 'app':
28
29
  case 'appstore':
30
+ case 'lpk':
29
31
  getLatestVersion(controller);
30
32
  }
31
33
  }
@@ -63,6 +65,7 @@ lpkAppCommand(program);
63
65
  lpkProjectCommand(program);
64
66
  appstoreCommand(program);
65
67
  lzcDockerCommand(program);
68
+ lpkCommand(program);
66
69
 
67
70
  // 当没有参数的时候,默认显示帮助。
68
71
  (async () => {
@@ -84,6 +87,7 @@ lzcDockerCommand(program);
84
87
  case 'project':
85
88
  case 'appstore':
86
89
  case 'config':
90
+ case 'lpk':
87
91
  program.showHelp();
88
92
  return;
89
93
  }
@@ -1,6 +1,13 @@
1
1
 
2
2
  # 懒猫云应用
3
3
 
4
+ ## 第一次部署(先看到结果)
5
+ ```bash
6
+ npm install
7
+ lzc-cli project deploy
8
+ lzc-cli project info
9
+ ```
10
+
4
11
  ## 构建
5
12
  ```
6
13
  lzc-cli project build -o you-awesome.lpk
@@ -9,12 +16,13 @@ lzc-cli project build -o you-awesome.lpk
9
16
 
10
17
  ## 安装
11
18
  ```
12
- lzc-cli app install you-awesome.lpk
19
+ lzc-cli lpk install you-awesome.lpk
13
20
  ```
14
21
 
15
- ## 开发
22
+ ## 修改源码后再次部署
16
23
  ```
17
- lzc-cli project devshell -b
24
+ lzc-cli project deploy
25
+ lzc-cli project log -f
18
26
  ```
19
27
 
20
28
  ## 成为懒猫云应用开发者
@@ -0,0 +1,27 @@
1
+ name: ${name} # app名称
2
+ package: ${package} # app的唯一标识符
3
+ version: 0.0.1 # app的版本
4
+ description: # app描述
5
+
6
+ license: https://choosealicense.com/licenses/mit/
7
+ homepage: # 项目主页或文档地址
8
+ author: # app author
9
+
10
+ # application 是默认前台容器,对应固定 service 名 app
11
+ application:
12
+ subdomain: ${subdomain} # 默认访问子域名前缀
13
+ routes:
14
+ # 将入口流量转发到 desktop service 的 6901 端口(VNC Web)
15
+ - /=http://desktop:6901/
16
+ # app 在启动时依赖 desktop 就绪
17
+ depends_on:
18
+ - desktop
19
+ # 多实例:每个用户部署自己的实例
20
+ multi_instance: true
21
+
22
+ services:
23
+ desktop:
24
+ # 使用容器内桌面用户启动
25
+ user: lazycat:kasm-user
26
+ # 引用 lzc-build.yml 里的 images.app-runtime
27
+ image: embed:app-runtime
@@ -7,8 +7,10 @@ license: https://choosealicense.com/licenses/mit/
7
7
  homepage: # 出现bug时候提交反馈的地方
8
8
  author: # app author
9
9
 
10
- #application作为一个特殊的container运行,对应的service名称为固定的`app`,其他service可以通过此名称与app进行通讯
10
+ # application 作为默认前台容器运行,对应固定 serviceapp
11
11
  application:
12
- subdomain: ${subdomain} #期望的app域名
12
+ subdomain: ${subdomain} # 默认访问子域名前缀
13
13
  routes:
14
+ # file:// 路由:直接返回静态文件(前端构建产物)
15
+ # 该示例默认读取 /lzcapp/pkg/content/dist 下的内容
14
16
  - /=file:///lzcapp/pkg/content/dist