@lazycatcloud/lzc-cli 1.3.17 → 2.0.0-pre.1

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 (128) hide show
  1. package/README.md +47 -7
  2. package/changelog.md +14 -0
  3. package/lib/app/apkshell.js +7 -44
  4. package/lib/app/index.js +178 -64
  5. package/lib/app/lpk_build.js +446 -61
  6. package/lib/app/lpk_build_images.js +749 -0
  7. package/lib/app/lpk_create.js +192 -45
  8. package/lib/app/lpk_create_generator.js +141 -13
  9. package/lib/app/lpk_devshell.js +33 -19
  10. package/lib/app/lpk_embed_images.js +257 -0
  11. package/lib/app/lpk_installer.js +17 -9
  12. package/lib/app/manifest_build.js +259 -0
  13. package/lib/app/project_cp.js +59 -0
  14. package/lib/app/project_deploy.js +58 -0
  15. package/lib/app/project_exec.js +82 -0
  16. package/lib/app/project_info.js +106 -0
  17. package/lib/app/project_log.js +62 -0
  18. package/lib/app/project_runtime.js +356 -0
  19. package/lib/app/project_start.js +95 -0
  20. package/lib/app/project_sync.js +499 -0
  21. package/lib/appstore/apkshell.js +50 -0
  22. package/lib/box/index.js +101 -4
  23. package/lib/box/ssh_remote.js +259 -0
  24. package/lib/build_remote.js +21 -0
  25. package/lib/debug_bridge.js +891 -83
  26. package/lib/docker/index.js +30 -10
  27. package/lib/i18n/locales/en/translation.json +262 -255
  28. package/lib/i18n/locales/zh/translation.json +262 -255
  29. package/lib/lpk/core.js +488 -0
  30. package/lib/lpk/index.js +210 -0
  31. package/lib/migrate/index.js +52 -0
  32. package/lib/package_info.js +135 -0
  33. package/lib/shellapi.js +35 -1
  34. package/lib/sig/core.js +254 -0
  35. package/lib/sig/index.js +88 -0
  36. package/lib/utils.js +94 -15
  37. package/package.json +3 -3
  38. package/scripts/cli.js +6 -0
  39. package/scripts/smoke/frontend-dev-entry.mjs +104 -0
  40. package/scripts/smoke/template-project.mjs +311 -0
  41. package/template/_lpk/README.md +15 -4
  42. package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
  43. package/template/_lpk/hello-vue.manifest.yml.in +38 -0
  44. package/template/_lpk/manifest.yml.in +4 -11
  45. package/template/_lpk/package.yml.in +7 -0
  46. package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
  47. package/template/_lpk/todolist-java.manifest.yml.in +29 -0
  48. package/template/_lpk/todolist-python.manifest.yml.in +37 -0
  49. package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
  50. package/template/_lpk/vue.lzc-build.yml.in +0 -44
  51. package/template/blank/lzc-build.dev.yml +4 -0
  52. package/template/blank/lzc-build.yml +24 -41
  53. package/template/blank/lzc-manifest.yml +7 -9
  54. package/template/blank/package.yml +7 -0
  55. package/template/golang/Dockerfile +19 -0
  56. package/template/golang/Dockerfile.dev +20 -0
  57. package/template/golang/README.md +44 -0
  58. package/template/golang/_gitignore +3 -0
  59. package/template/golang/_lzcdevignore +21 -0
  60. package/template/golang/go.mod +3 -0
  61. package/template/golang/lzc-build.dev.yml +12 -0
  62. package/template/golang/lzc-build.yml +16 -0
  63. package/template/golang/lzc-icon.png +0 -0
  64. package/template/golang/main.go +252 -0
  65. package/template/golang/manifest.dev.page.js +24 -0
  66. package/template/golang/run.sh +10 -0
  67. package/template/golang/web/index.html +238 -0
  68. package/template/gui-vnc/README.md +23 -0
  69. package/template/gui-vnc/_gitignore +2 -0
  70. package/template/gui-vnc/images/Dockerfile +30 -0
  71. package/template/gui-vnc/images/kasmvnc.yaml +33 -0
  72. package/template/gui-vnc/images/startup-script.desktop +9 -0
  73. package/template/gui-vnc/images/startup-script.sh +6 -0
  74. package/template/gui-vnc/lzc-build.dev.yml +4 -0
  75. package/template/gui-vnc/lzc-build.yml +18 -0
  76. package/template/gui-vnc/lzc-icon.png +0 -0
  77. package/template/python/Dockerfile +15 -0
  78. package/template/python/Dockerfile.dev +18 -0
  79. package/template/python/README.md +50 -0
  80. package/template/python/_gitignore +3 -0
  81. package/template/python/_lzcdevignore +21 -0
  82. package/template/python/app.py +110 -0
  83. package/template/python/lzc-build.dev.yml +12 -0
  84. package/template/python/lzc-build.yml +16 -0
  85. package/template/python/lzc-icon.png +0 -0
  86. package/template/python/manifest.dev.page.js +25 -0
  87. package/template/python/requirements.txt +1 -0
  88. package/template/python/run.sh +14 -0
  89. package/template/python/web/index.html +238 -0
  90. package/template/springboot/Dockerfile +20 -0
  91. package/template/springboot/Dockerfile.dev +20 -0
  92. package/template/springboot/README.md +44 -0
  93. package/template/springboot/_gitignore +3 -0
  94. package/template/springboot/_lzcdevignore +21 -0
  95. package/template/springboot/lzc-build.dev.yml +12 -0
  96. package/template/springboot/lzc-build.yml +16 -0
  97. package/template/springboot/lzc-icon.png +0 -0
  98. package/template/springboot/manifest.dev.page.js +24 -0
  99. package/template/springboot/pom.xml +38 -0
  100. package/template/springboot/run.sh +10 -0
  101. package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
  102. package/template/springboot/src/main/resources/application.properties +1 -0
  103. package/template/springboot/src/main/resources/static/index.html +238 -0
  104. package/template/vue/README.md +18 -21
  105. package/template/vue/lzc-build.dev.yml +7 -0
  106. package/template/vue/lzc-build.yml +30 -43
  107. package/template/vue/manifest.dev.page.js +50 -0
  108. package/template/vue/src/App.vue +36 -25
  109. package/template/vue/src/style.css +106 -49
  110. package/template/vue-minidb/README.md +26 -0
  111. package/template/vue-minidb/_gitignore +25 -0
  112. package/template/vue-minidb/index.html +13 -0
  113. package/template/vue-minidb/lzc-build.dev.yml +7 -0
  114. package/template/vue-minidb/lzc-build.yml +46 -0
  115. package/template/vue-minidb/lzc-icon.png +0 -0
  116. package/template/vue-minidb/manifest.dev.page.js +50 -0
  117. package/template/vue-minidb/package.json +21 -0
  118. package/template/vue-minidb/public/vite.svg +1 -0
  119. package/template/vue-minidb/src/App.vue +206 -0
  120. package/template/vue-minidb/src/assets/vue.svg +1 -0
  121. package/template/vue-minidb/src/main.ts +5 -0
  122. package/template/vue-minidb/src/style.css +136 -0
  123. package/template/vue-minidb/src/vite-env.d.ts +1 -0
  124. package/template/vue-minidb/tsconfig.app.json +24 -0
  125. package/template/vue-minidb/tsconfig.json +7 -0
  126. package/template/vue-minidb/tsconfig.node.json +22 -0
  127. package/template/vue-minidb/vite.config.ts +10 -0
  128. /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import logger from 'loglevel';
4
+ import { LpkBuild } from '../app/lpk_build.js';
5
+ import { dumpToYaml, loadFromYaml } from '../utils.js';
6
+ import { PACKAGE_FILE_NAME, splitManifestAndPackageInfo } from '../package_info.js';
7
+
8
+ async function migrateHandler({ context, file }) {
9
+ const cwd = context ? path.resolve(context) : process.cwd();
10
+ const build = new LpkBuild(cwd, file || 'lzc-build.yml');
11
+ const manifestPath = build.manifestFilePath;
12
+ const packageFilePath = build.packageFilePath;
13
+
14
+ if (fs.existsSync(packageFilePath)) {
15
+ logger.info(`package metadata already exists at ${packageFilePath}`);
16
+ return;
17
+ }
18
+
19
+ let manifest;
20
+ try {
21
+ manifest = loadFromYaml(manifestPath) ?? {};
22
+ } catch {
23
+ throw new Error(`Cannot auto-migrate templated manifest: ${manifestPath}`);
24
+ }
25
+
26
+ const split = splitManifestAndPackageInfo(manifest);
27
+ if (Object.keys(split.packageInfo).length === 0) {
28
+ throw new Error(`No static package fields found in ${manifestPath}`);
29
+ }
30
+
31
+ dumpToYaml(split.packageInfo, packageFilePath);
32
+ dumpToYaml(split.manifest, manifestPath);
33
+
34
+ logger.info(`wrote ${PACKAGE_FILE_NAME}: ${packageFilePath}`);
35
+ logger.info(`updated manifest: ${manifestPath}`);
36
+ }
37
+
38
+ export function migrateCommand(program) {
39
+ program.command({
40
+ command: 'migrate [context]',
41
+ desc: 'Migrate manifest package metadata to package.yml',
42
+ builder: (args) => {
43
+ args.option('f', {
44
+ alias: 'file',
45
+ describe: 'Build config file',
46
+ type: 'string',
47
+ default: 'lzc-build.yml',
48
+ });
49
+ },
50
+ handler: migrateHandler,
51
+ });
52
+ }
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ import { fakeLoadManifestText, fakeLoadManifestYml, loadFromYaml } from './utils.js';
5
+
6
+ export const PACKAGE_FILE_NAME = 'package.yml';
7
+ export const STATIC_PACKAGE_FIELDS = [
8
+ 'package',
9
+ 'version',
10
+ 'name',
11
+ 'description',
12
+ 'author',
13
+ 'license',
14
+ 'homepage',
15
+ 'min_os_version',
16
+ 'unsupported_platforms',
17
+ 'locales',
18
+ ];
19
+
20
+ function hasOwn(obj, key) {
21
+ return Object.prototype.hasOwnProperty.call(obj ?? {}, key);
22
+ }
23
+
24
+ function readYamlIfExists(filePath) {
25
+ if (!filePath || !fs.existsSync(filePath)) {
26
+ return null;
27
+ }
28
+ return yaml.load(fs.readFileSync(filePath, 'utf-8')) ?? {};
29
+ }
30
+
31
+ function tryLoadManifestText(manifestText) {
32
+ const text = String(manifestText ?? '');
33
+ const hasGoTemplate = text.includes('{{') && text.includes('}}');
34
+ try {
35
+ return {
36
+ data: yaml.load(text) ?? {},
37
+ excp: hasGoTemplate,
38
+ };
39
+ } catch {
40
+ return {
41
+ data: fakeLoadManifestText(text) ?? {},
42
+ excp: true,
43
+ };
44
+ }
45
+ }
46
+
47
+ export function loadManifestFile(manifestPath) {
48
+ try {
49
+ return loadFromYaml(manifestPath) ?? {};
50
+ } catch {
51
+ return fakeLoadManifestYml(manifestPath) ?? {};
52
+ }
53
+ }
54
+
55
+ export function loadManifestText(manifestText) {
56
+ return tryLoadManifestText(manifestText).data;
57
+ }
58
+
59
+ export function loadPackageFile(packageFilePath) {
60
+ return readYamlIfExists(packageFilePath);
61
+ }
62
+
63
+ export function defaultPackageFilePath(manifestPath) {
64
+ return path.join(path.dirname(manifestPath), PACKAGE_FILE_NAME);
65
+ }
66
+
67
+ export function findManifestStaticFields(manifest) {
68
+ return STATIC_PACKAGE_FIELDS.filter((field) => hasOwn(manifest, field));
69
+ }
70
+
71
+ export function ensureManifestStaticFieldsAbsent(manifest, manifestPath) {
72
+ const invalidFields = findManifestStaticFields(manifest);
73
+ if (invalidFields.length === 0) {
74
+ return;
75
+ }
76
+ throw new Error(`Static package fields must be moved from ${manifestPath} to ${PACKAGE_FILE_NAME}: ${invalidFields.join(', ')}`);
77
+ }
78
+
79
+ export function applyPackageFileToManifest(manifest, packageInfo) {
80
+ const nextManifest = { ...(manifest ?? {}) };
81
+ for (const field of STATIC_PACKAGE_FIELDS) {
82
+ delete nextManifest[field];
83
+ }
84
+ if (!packageInfo) {
85
+ return nextManifest;
86
+ }
87
+ for (const field of STATIC_PACKAGE_FIELDS) {
88
+ if (hasOwn(packageInfo, field)) {
89
+ nextManifest[field] = packageInfo[field];
90
+ }
91
+ }
92
+ return nextManifest;
93
+ }
94
+
95
+ export function splitManifestAndPackageInfo(manifest, basePackageInfo = null) {
96
+ const manifestOnly = { ...(manifest ?? {}) };
97
+ const packageInfo = { ...(basePackageInfo ?? {}) };
98
+ for (const field of STATIC_PACKAGE_FIELDS) {
99
+ if (hasOwn(manifestOnly, field)) {
100
+ packageInfo[field] = manifestOnly[field];
101
+ delete manifestOnly[field];
102
+ }
103
+ }
104
+ return {
105
+ manifest: manifestOnly,
106
+ packageInfo,
107
+ };
108
+ }
109
+
110
+ export function loadEffectiveManifest(manifestPath, options = {}) {
111
+ const hasInlineManifestText = Object.prototype.hasOwnProperty.call(options, 'manifestText');
112
+ const loadedManifest = hasInlineManifestText
113
+ ? tryLoadManifestText(options.manifestText)
114
+ : { data: loadManifestFile(manifestPath), excp: false };
115
+ const sourceManifest = loadedManifest.data;
116
+ const packageFilePath = options.packageFilePath || defaultPackageFilePath(manifestPath);
117
+ const hasPackageFile = fs.existsSync(packageFilePath);
118
+ const packageInfo = hasPackageFile ? loadPackageFile(packageFilePath) ?? {} : null;
119
+ if (hasPackageFile && options.strictStaticFields) {
120
+ ensureManifestStaticFieldsAbsent(sourceManifest, manifestPath);
121
+ }
122
+ const manifest = hasPackageFile ? applyPackageFileToManifest(sourceManifest, packageInfo) : sourceManifest;
123
+ return {
124
+ manifest,
125
+ sourceManifest,
126
+ packageInfo,
127
+ hasPackageFile,
128
+ packageFilePath,
129
+ excpManifest: loadedManifest.excp,
130
+ };
131
+ }
132
+
133
+ export function loadEffectiveManifestFromFiles(manifestPath, options = {}) {
134
+ return loadEffectiveManifest(manifestPath, options);
135
+ }
package/lib/shellapi.js CHANGED
@@ -23,7 +23,9 @@ function getShellAPIConfigDir() {
23
23
  }
24
24
 
25
25
  class ShellApi {
26
- constructor() { }
26
+ constructor() {
27
+ this.clientId = '';
28
+ }
27
29
 
28
30
  async init() {
29
31
  // 检查当前 shell 环境上下文是否配置 HTTP_PROXY
@@ -109,6 +111,38 @@ NOTE:在指定环境变量的模式下,有些接口依旧不能访问,为
109
111
  });
110
112
  }
111
113
 
114
+ async queryShellCoreInfo() {
115
+ if (!this.client) {
116
+ return {};
117
+ }
118
+ return new Promise((resolve, reject) => {
119
+ this.client.queryShellCoreInfo({}, this.metadata, function (err, response) {
120
+ if (err) {
121
+ reject(err);
122
+ return;
123
+ }
124
+ resolve(response);
125
+ });
126
+ });
127
+ }
128
+
129
+ async resolveClientId() {
130
+ const cached = String(this.clientId ?? '').trim();
131
+ if (cached) {
132
+ return cached;
133
+ }
134
+ if (!this.client) {
135
+ return '';
136
+ }
137
+ const info = await this.queryShellCoreInfo();
138
+ const clientId = String(info?.id ?? '').trim();
139
+ if (!clientId) {
140
+ throw new Error('resolve client id failed: empty shell core id');
141
+ }
142
+ this.clientId = clientId;
143
+ return clientId;
144
+ }
145
+
112
146
  async initBoxInfo() {
113
147
  try {
114
148
  const boxes = await this.boxList();
@@ -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 { loadEffectiveManifestFromFiles } from '../package_info.js';
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 = loadEffectiveManifestFromFiles(manifestPath).manifest;
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
+ }