@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.
- package/README.md +47 -7
- package/changelog.md +14 -0
- package/lib/app/apkshell.js +7 -44
- package/lib/app/index.js +178 -64
- package/lib/app/lpk_build.js +446 -61
- package/lib/app/lpk_build_images.js +749 -0
- package/lib/app/lpk_create.js +192 -45
- package/lib/app/lpk_create_generator.js +141 -13
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +17 -9
- package/lib/app/manifest_build.js +259 -0
- package/lib/app/project_cp.js +59 -0
- package/lib/app/project_deploy.js +58 -0
- package/lib/app/project_exec.js +82 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +62 -0
- package/lib/app/project_runtime.js +356 -0
- package/lib/app/project_start.js +95 -0
- package/lib/app/project_sync.js +499 -0
- package/lib/appstore/apkshell.js +50 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +21 -0
- package/lib/debug_bridge.js +891 -83
- package/lib/docker/index.js +30 -10
- package/lib/i18n/locales/en/translation.json +262 -255
- package/lib/i18n/locales/zh/translation.json +262 -255
- package/lib/lpk/core.js +488 -0
- package/lib/lpk/index.js +210 -0
- package/lib/migrate/index.js +52 -0
- package/lib/package_info.js +135 -0
- package/lib/shellapi.js +35 -1
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +94 -15
- package/package.json +3 -3
- package/scripts/cli.js +6 -0
- package/scripts/smoke/frontend-dev-entry.mjs +104 -0
- package/scripts/smoke/template-project.mjs +311 -0
- package/template/_lpk/README.md +15 -4
- package/template/_lpk/gui-vnc.manifest.yml.in +18 -0
- package/template/_lpk/hello-vue.manifest.yml.in +38 -0
- package/template/_lpk/manifest.yml.in +4 -11
- package/template/_lpk/package.yml.in +7 -0
- package/template/_lpk/todolist-golang.manifest.yml.in +30 -0
- package/template/_lpk/todolist-java.manifest.yml.in +29 -0
- package/template/_lpk/todolist-python.manifest.yml.in +37 -0
- package/template/_lpk/todolist-serverless.manifest.yml.in +38 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/lzc-build.dev.yml +4 -0
- package/template/blank/lzc-build.yml +24 -41
- package/template/blank/lzc-manifest.yml +7 -9
- package/template/blank/package.yml +7 -0
- package/template/golang/Dockerfile +19 -0
- package/template/golang/Dockerfile.dev +20 -0
- package/template/golang/README.md +44 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/_lzcdevignore +21 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.dev.yml +12 -0
- package/template/golang/lzc-build.yml +16 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/manifest.dev.page.js +24 -0
- package/template/golang/run.sh +10 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +23 -0
- package/template/gui-vnc/_gitignore +2 -0
- package/template/gui-vnc/images/Dockerfile +30 -0
- package/template/gui-vnc/images/kasmvnc.yaml +33 -0
- package/template/gui-vnc/images/startup-script.desktop +9 -0
- package/template/gui-vnc/images/startup-script.sh +6 -0
- package/template/gui-vnc/lzc-build.dev.yml +4 -0
- package/template/gui-vnc/lzc-build.yml +18 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/Dockerfile.dev +18 -0
- package/template/python/README.md +50 -0
- package/template/python/_gitignore +3 -0
- package/template/python/_lzcdevignore +21 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.dev.yml +12 -0
- package/template/python/lzc-build.yml +16 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/manifest.dev.page.js +25 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +14 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/Dockerfile.dev +20 -0
- package/template/springboot/README.md +44 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/_lzcdevignore +21 -0
- package/template/springboot/lzc-build.dev.yml +12 -0
- package/template/springboot/lzc-build.yml +16 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/manifest.dev.page.js +24 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +10 -0
- package/template/springboot/src/main/java/cloud/lazycat/app/Application.java +132 -0
- package/template/springboot/src/main/resources/application.properties +1 -0
- package/template/springboot/src/main/resources/static/index.html +238 -0
- package/template/vue/README.md +18 -21
- package/template/vue/lzc-build.dev.yml +7 -0
- package/template/vue/lzc-build.yml +30 -43
- package/template/vue/manifest.dev.page.js +50 -0
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +26 -0
- package/template/vue-minidb/_gitignore +25 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.dev.yml +7 -0
- package/template/vue-minidb/lzc-build.yml +46 -0
- package/template/vue-minidb/lzc-icon.png +0 -0
- package/template/vue-minidb/manifest.dev.page.js +50 -0
- package/template/vue-minidb/package.json +21 -0
- package/template/vue-minidb/public/vite.svg +1 -0
- package/template/vue-minidb/src/App.vue +206 -0
- package/template/vue-minidb/src/assets/vue.svg +1 -0
- package/template/vue-minidb/src/main.ts +5 -0
- package/template/vue-minidb/src/style.css +136 -0
- package/template/vue-minidb/src/vite-env.d.ts +1 -0
- package/template/vue-minidb/tsconfig.app.json +24 -0
- package/template/vue-minidb/tsconfig.json +7 -0
- package/template/vue-minidb/tsconfig.node.json +22 -0
- package/template/vue-minidb/vite.config.ts +10 -0
- /package/template/{vue → vue-minidb}/src/components/HelloWorld.vue +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import logger from 'loglevel';
|
|
5
|
+
import * as tar from 'tar';
|
|
6
|
+
import { isFileExist } from '../utils.js';
|
|
7
|
+
import { DebugBridge } from '../debug_bridge.js';
|
|
8
|
+
import shellApi from '../shellapi.js';
|
|
9
|
+
import { t } from '../i18n/index.js';
|
|
10
|
+
import { resolveBuildRemoteFromFile } from '../build_remote.js';
|
|
11
|
+
|
|
12
|
+
async function sha256File(filePath) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const hash = crypto.createHash('sha256');
|
|
15
|
+
let size = 0;
|
|
16
|
+
const stream = fs.createReadStream(filePath);
|
|
17
|
+
stream.on('data', (chunk) => {
|
|
18
|
+
size += chunk.length;
|
|
19
|
+
hash.update(chunk);
|
|
20
|
+
});
|
|
21
|
+
stream.on('error', reject);
|
|
22
|
+
stream.on('end', () => {
|
|
23
|
+
resolve({
|
|
24
|
+
digest: hash.digest('hex'),
|
|
25
|
+
size,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sha256Buffer(buffer) {
|
|
32
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function copyFileToOciBlob(sourcePath, blobsDir) {
|
|
36
|
+
const { digest, size } = await sha256File(sourcePath);
|
|
37
|
+
const blobPath = path.join(blobsDir, digest);
|
|
38
|
+
if (!isFileExist(blobPath)) {
|
|
39
|
+
fs.copyFileSync(sourcePath, blobPath);
|
|
40
|
+
}
|
|
41
|
+
return { digest, size };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function copyBufferToOciBlob(content, blobsDir) {
|
|
45
|
+
const digest = sha256Buffer(content);
|
|
46
|
+
const blobPath = path.join(blobsDir, digest);
|
|
47
|
+
if (!isFileExist(blobPath)) {
|
|
48
|
+
fs.writeFileSync(blobPath, content);
|
|
49
|
+
}
|
|
50
|
+
return { digest, size: content.length };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectImagesFromManifest(manifest) {
|
|
54
|
+
const images = [];
|
|
55
|
+
|
|
56
|
+
const appImage = manifest?.application?.image;
|
|
57
|
+
if (typeof appImage === 'string' && appImage.trim()) {
|
|
58
|
+
images.push(appImage.trim());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const services = manifest?.services;
|
|
62
|
+
if (services && typeof services === 'object') {
|
|
63
|
+
for (const service of Object.values(services)) {
|
|
64
|
+
if (typeof service?.image === 'string' && service.image.trim()) {
|
|
65
|
+
images.push(service.image.trim());
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...new Set(images)];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function replaceManifestImageToDigest(manifest, imageDigestMap) {
|
|
74
|
+
let replaced = 0;
|
|
75
|
+
const unresolved = [];
|
|
76
|
+
if (!imageDigestMap || imageDigestMap.size === 0) {
|
|
77
|
+
return { replaced, unresolved };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const replaceOne = (sourceImage) => {
|
|
81
|
+
if (typeof sourceImage !== 'string') {
|
|
82
|
+
return sourceImage;
|
|
83
|
+
}
|
|
84
|
+
const image = sourceImage.trim();
|
|
85
|
+
if (!image || image.startsWith('sha256:')) {
|
|
86
|
+
return sourceImage;
|
|
87
|
+
}
|
|
88
|
+
const digest = imageDigestMap.get(image);
|
|
89
|
+
if (!digest) {
|
|
90
|
+
unresolved.push(image);
|
|
91
|
+
return sourceImage;
|
|
92
|
+
}
|
|
93
|
+
if (digest !== sourceImage) {
|
|
94
|
+
replaced += 1;
|
|
95
|
+
}
|
|
96
|
+
return digest;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (manifest?.application?.image) {
|
|
100
|
+
manifest.application.image = replaceOne(manifest.application.image);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const services = manifest?.services;
|
|
104
|
+
if (services && typeof services === 'object') {
|
|
105
|
+
for (const service of Object.values(services)) {
|
|
106
|
+
if (service && typeof service === 'object' && typeof service.image === 'string') {
|
|
107
|
+
service.image = replaceOne(service.image);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
replaced,
|
|
114
|
+
unresolved: [...new Set(unresolved)],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function convertDockerArchiveToOciLayout(dockerArchivePath, imageRootDir) {
|
|
119
|
+
const extractDir = fs.mkdtempSync('.lzc-cli-docker-archive');
|
|
120
|
+
try {
|
|
121
|
+
await tar.x({
|
|
122
|
+
file: dockerArchivePath,
|
|
123
|
+
cwd: extractDir,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const dockerManifestFile = path.join(extractDir, 'manifest.json');
|
|
127
|
+
if (!isFileExist(dockerManifestFile)) {
|
|
128
|
+
throw new Error(t('lzc_cli.lib.app.lpk_build.convert_archive_manifest_missing', 'manifest.json not found in docker archive'));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dockerManifests = JSON.parse(fs.readFileSync(dockerManifestFile, 'utf-8'));
|
|
132
|
+
if (!Array.isArray(dockerManifests) || dockerManifests.length === 0) {
|
|
133
|
+
throw new Error(t('lzc_cli.lib.app.lpk_build.convert_archive_manifest_invalid', 'invalid docker archive manifest'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const blobsDir = path.join(imageRootDir, 'blobs', 'sha256');
|
|
137
|
+
fs.mkdirSync(blobsDir, { recursive: true });
|
|
138
|
+
|
|
139
|
+
const indexDescriptors = [];
|
|
140
|
+
const imageDigestMap = new Map();
|
|
141
|
+
for (const item of dockerManifests) {
|
|
142
|
+
const configFile = item?.Config ? path.join(extractDir, item.Config) : '';
|
|
143
|
+
if (!configFile || !isFileExist(configFile)) {
|
|
144
|
+
throw new Error(t('lzc_cli.lib.app.lpk_build.convert_archive_config_missing', `config file missing in docker archive: ${item?.Config || ''}`));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const config = await copyFileToOciBlob(configFile, blobsDir);
|
|
148
|
+
const imageDigest = `sha256:${config.digest}`;
|
|
149
|
+
const layers = [];
|
|
150
|
+
for (const layer of item?.Layers ?? []) {
|
|
151
|
+
const layerFile = path.join(extractDir, layer);
|
|
152
|
+
if (!isFileExist(layerFile)) {
|
|
153
|
+
throw new Error(t('lzc_cli.lib.app.lpk_build.convert_archive_layer_missing', `layer file missing in docker archive: ${layer}`));
|
|
154
|
+
}
|
|
155
|
+
const layerBlob = await copyFileToOciBlob(layerFile, blobsDir);
|
|
156
|
+
layers.push({
|
|
157
|
+
mediaType: 'application/vnd.oci.image.layer.v1.tar',
|
|
158
|
+
digest: `sha256:${layerBlob.digest}`,
|
|
159
|
+
size: layerBlob.size,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const manifestContent = Buffer.from(
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
schemaVersion: 2,
|
|
166
|
+
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
167
|
+
config: {
|
|
168
|
+
mediaType: 'application/vnd.oci.image.config.v1+json',
|
|
169
|
+
digest: `sha256:${config.digest}`,
|
|
170
|
+
size: config.size,
|
|
171
|
+
},
|
|
172
|
+
layers,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
const imageManifest = copyBufferToOciBlob(manifestContent, blobsDir);
|
|
176
|
+
const descriptor = {
|
|
177
|
+
mediaType: 'application/vnd.oci.image.manifest.v1+json',
|
|
178
|
+
digest: `sha256:${imageManifest.digest}`,
|
|
179
|
+
size: imageManifest.size,
|
|
180
|
+
};
|
|
181
|
+
const refName = item?.RepoTags?.[0];
|
|
182
|
+
if (typeof refName === 'string' && refName) {
|
|
183
|
+
descriptor.annotations = {
|
|
184
|
+
'org.opencontainers.image.ref.name': refName,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
for (const repoTag of item?.RepoTags ?? []) {
|
|
188
|
+
if (typeof repoTag !== 'string' || !repoTag) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
imageDigestMap.set(repoTag, imageDigest);
|
|
192
|
+
}
|
|
193
|
+
indexDescriptors.push(descriptor);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.mkdirSync(imageRootDir, { recursive: true });
|
|
197
|
+
fs.writeFileSync(path.join(imageRootDir, 'oci-layout'), JSON.stringify({ imageLayoutVersion: '1.0.0' }));
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
path.join(imageRootDir, 'index.json'),
|
|
200
|
+
JSON.stringify(
|
|
201
|
+
{
|
|
202
|
+
schemaVersion: 2,
|
|
203
|
+
manifests: indexDescriptors,
|
|
204
|
+
},
|
|
205
|
+
null,
|
|
206
|
+
2,
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
return imageDigestMap;
|
|
210
|
+
} finally {
|
|
211
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function embedImagesToTempDir(manifest, tempDir) {
|
|
216
|
+
const images = collectImagesFromManifest(manifest);
|
|
217
|
+
if (images.length === 0) {
|
|
218
|
+
logger.warn(t('lzc_cli.lib.app.lpk_build.embed_images_skip_no_image', 'No image field found in manifest, skip embedding images'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const buildRemote = resolveBuildRemoteFromFile(process.cwd());
|
|
223
|
+
if (!buildRemote) {
|
|
224
|
+
await shellApi.init();
|
|
225
|
+
}
|
|
226
|
+
const bridge = new DebugBridge(process.cwd(), buildRemote);
|
|
227
|
+
await bridge.init();
|
|
228
|
+
|
|
229
|
+
for (const image of images) {
|
|
230
|
+
if (image.includes('$')) {
|
|
231
|
+
throw new Error(t('lzc_cli.lib.app.lpk_build.embed_images_placeholder_fail', `Found unresolved image placeholder: ${image}`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (image.startsWith('sha256:')) {
|
|
235
|
+
logger.debug(`skip pull image by digest id: ${image}`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
logger.info(t('lzc_cli.lib.app.lpk_build.embed_images_pull_tips', `Pull image on box: ${image}`));
|
|
239
|
+
await bridge.lzcDockerPull(image);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const dockerArchivePath = path.join(tempDir, 'images.docker.tar');
|
|
243
|
+
logger.info(t('lzc_cli.lib.app.lpk_build.embed_images_export_tips', 'Export images from box to OCI layout'));
|
|
244
|
+
await bridge.lzcDockerSave(images, dockerArchivePath);
|
|
245
|
+
|
|
246
|
+
const imageDigestMap = await convertDockerArchiveToOciLayout(dockerArchivePath, path.join(tempDir, 'images'));
|
|
247
|
+
const { replaced, unresolved } = replaceManifestImageToDigest(manifest, imageDigestMap);
|
|
248
|
+
if (replaced > 0) {
|
|
249
|
+
logger.info(t('lzc_cli.lib.app.lpk_build.embed_images_rewrite_manifest_tips', `Rewrite ${replaced} image references in manifest.yml`));
|
|
250
|
+
}
|
|
251
|
+
if (unresolved.length > 0) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
t('lzc_cli.lib.app.lpk_build.embed_images_rewrite_manifest_unresolved_fail', `Failed to rewrite image references to sha256 digest: ${unresolved.join(', ')}`),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
fs.rmSync(dockerArchivePath, { force: true });
|
|
257
|
+
}
|
package/lib/app/lpk_installer.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import logger from 'loglevel';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { Downloader, extractLpkSync } from '../utils.js';
|
|
5
|
+
import { loadEffectiveManifestFromFiles } from '../package_info.js';
|
|
5
6
|
import { DebugBridge } from '../debug_bridge.js';
|
|
6
7
|
import shellapi from '../shellapi.js';
|
|
7
8
|
import { t } from '../i18n/index.js';
|
|
8
9
|
import { triggerApk } from './apkshell.js';
|
|
10
|
+
import { resolveBuildRemoteFromFile } from '../build_remote.js';
|
|
9
11
|
|
|
10
|
-
export const installConfig = { apk: true
|
|
12
|
+
export const installConfig = { apk: true };
|
|
11
13
|
// 从一个目录中找出修改时间最新的包
|
|
12
14
|
function findOnceLpkByDir(dir = process.cwd()) {
|
|
13
15
|
const pkg = fs
|
|
@@ -20,7 +22,7 @@ function findOnceLpkByDir(dir = process.cwd()) {
|
|
|
20
22
|
return b.mtime - a.mtime;
|
|
21
23
|
})
|
|
22
24
|
.find((entry) => {
|
|
23
|
-
return entry.filename.endsWith('.lpk');
|
|
25
|
+
return entry.filename.endsWith('.lpk') || entry.filename.endsWith('.lpk.tar');
|
|
24
26
|
});
|
|
25
27
|
return pkg?.filename ? path.resolve(dir, pkg.filename) : '';
|
|
26
28
|
}
|
|
@@ -94,8 +96,8 @@ export class LpkInstaller {
|
|
|
94
96
|
const tempDir = fs.mkdtempSync('.lzc-cli-install');
|
|
95
97
|
let manifest;
|
|
96
98
|
try {
|
|
97
|
-
|
|
98
|
-
manifest =
|
|
99
|
+
extractLpkSync(pkgPath, tempDir, ['manifest.yml', 'package.yml', 'icon.png']);
|
|
100
|
+
manifest = loadEffectiveManifestFromFiles(path.join(tempDir, 'manifest.yml')).manifest;
|
|
99
101
|
if (!manifest['application']['subdomain']) {
|
|
100
102
|
throw t('lzc_cli.lib.app.lpk_installer.install_from_file_manifest_not_exists_app_subdomain_fail', 'manifest.yml 中的 `application.subdomain` 字段不能为空');
|
|
101
103
|
}
|
|
@@ -115,24 +117,30 @@ export class LpkInstaller {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
+
const buildRemote = resolveBuildRemoteFromFile(process.cwd());
|
|
121
|
+
if (!buildRemote && (!shellapi.uid || !shellapi.boxname)) {
|
|
122
|
+
await shellapi.init();
|
|
123
|
+
}
|
|
124
|
+
const bridge = new DebugBridge(process.cwd(), buildRemote);
|
|
125
|
+
await bridge.init();
|
|
120
126
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_start_tips', '开始安装应用'));
|
|
121
127
|
await bridge.install(pkgPath, manifest ? manifest['package'] : '');
|
|
122
128
|
logger.info('\n');
|
|
123
129
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_success_tips', `安装成功!`));
|
|
124
130
|
if (manifest) {
|
|
131
|
+
const boxname = buildRemote ? buildRemote.boxname : shellapi.boxname;
|
|
125
132
|
logger.info(
|
|
126
133
|
t('lzc_cli.lib.app.lpk_installer.install_from_file_done_tips', `👉 请在浏览器中访问 https://{{ subdomain }}.{{ boxname }}.heiyu.space`, {
|
|
127
134
|
subdomain: manifest['application']['subdomain'],
|
|
128
|
-
boxname
|
|
135
|
+
boxname,
|
|
129
136
|
interpolation: { escapeValue: false },
|
|
130
137
|
}),
|
|
131
138
|
);
|
|
132
139
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_login_tips', `👉 并使用微服的用户名和密码登录`));
|
|
133
140
|
}
|
|
134
141
|
} catch (error) {
|
|
135
|
-
|
|
142
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
143
|
+
logger.error(t('lzc_cli.lib.app.lpk_installer.install_from_file_fail_tips', `安装失败: {{ error }}`, { error: errorMsg, interpolation: { escapeValue: false } }));
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const ENV_KEY_REGEXP = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
|
+
const BUILD_DIRECTIVE_PREFIX = '#@build';
|
|
6
|
+
|
|
7
|
+
function normalizeDirectiveValue(rawValue) {
|
|
8
|
+
const trimmed = String(rawValue ?? '').trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
13
|
+
return trimmed.slice(1, -1);
|
|
14
|
+
}
|
|
15
|
+
return trimmed;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeBuildEnvEntries(rawEnvs) {
|
|
19
|
+
if (rawEnvs === undefined || rawEnvs === null) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(rawEnvs)) {
|
|
23
|
+
throw new Error('envs must be a string array');
|
|
24
|
+
}
|
|
25
|
+
const normalized = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (let index = 0; index < rawEnvs.length; index += 1) {
|
|
28
|
+
const rawEntry = rawEnvs[index];
|
|
29
|
+
if (typeof rawEntry !== 'string') {
|
|
30
|
+
throw new Error(`envs[${index}] must be a string`);
|
|
31
|
+
}
|
|
32
|
+
const entry = rawEntry.trim();
|
|
33
|
+
if (!entry) {
|
|
34
|
+
throw new Error(`envs[${index}] must not be empty`);
|
|
35
|
+
}
|
|
36
|
+
const equalIndex = entry.indexOf('=');
|
|
37
|
+
if (equalIndex <= 0) {
|
|
38
|
+
throw new Error(`envs[${index}] must use KEY=VALUE format`);
|
|
39
|
+
}
|
|
40
|
+
const key = entry.slice(0, equalIndex).trim();
|
|
41
|
+
const value = entry.slice(equalIndex + 1);
|
|
42
|
+
if (!ENV_KEY_REGEXP.test(key)) {
|
|
43
|
+
throw new Error(`envs[${index}] has invalid key "${key}"`);
|
|
44
|
+
}
|
|
45
|
+
if (seen.has(key)) {
|
|
46
|
+
throw new Error(`envs contains duplicated key "${key}"`);
|
|
47
|
+
}
|
|
48
|
+
seen.add(key);
|
|
49
|
+
normalized.push(`${key}=${value}`);
|
|
50
|
+
}
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildVarsFromEnvEntries(rawEnvs) {
|
|
55
|
+
const envs = normalizeBuildEnvEntries(rawEnvs);
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const entry of envs) {
|
|
58
|
+
const equalIndex = entry.indexOf('=');
|
|
59
|
+
const key = entry.slice(0, equalIndex);
|
|
60
|
+
const value = entry.slice(equalIndex + 1);
|
|
61
|
+
result[key] = value;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseBuildDirective(line) {
|
|
67
|
+
const trimmed = String(line ?? '').trim();
|
|
68
|
+
if (!trimmed.startsWith(BUILD_DIRECTIVE_PREFIX)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const body = trimmed.slice(BUILD_DIRECTIVE_PREFIX.length).trim();
|
|
72
|
+
if (!body) {
|
|
73
|
+
throw new Error('build directive is empty');
|
|
74
|
+
}
|
|
75
|
+
const spaceIndex = body.indexOf(' ');
|
|
76
|
+
const command = (spaceIndex >= 0 ? body.slice(0, spaceIndex) : body).trim();
|
|
77
|
+
const args = (spaceIndex >= 0 ? body.slice(spaceIndex + 1) : '').trim();
|
|
78
|
+
if (!command) {
|
|
79
|
+
throw new Error('build directive command is empty');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
command,
|
|
83
|
+
args,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function evaluateBuildCondition(expr, context) {
|
|
88
|
+
const normalized = String(expr ?? '').trim();
|
|
89
|
+
if (!normalized) {
|
|
90
|
+
throw new Error('build if condition is empty');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let match = normalized.match(/^profile\s*(==|=|!=)\s*(.+)$/);
|
|
94
|
+
if (match) {
|
|
95
|
+
const expected = normalizeDirectiveValue(match[2]);
|
|
96
|
+
const actual = String(context.profile ?? '');
|
|
97
|
+
return match[1] === '!=' ? actual !== expected : actual === expected;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
match = normalized.match(/^env\.([A-Za-z_][A-Za-z0-9_]*)\s*(==|=|!=)\s*(.+)$/);
|
|
101
|
+
if (match) {
|
|
102
|
+
const key = match[1];
|
|
103
|
+
const expected = normalizeDirectiveValue(match[3]);
|
|
104
|
+
const actual = String(context.envs?.[key] ?? '');
|
|
105
|
+
return match[2] === '!=' ? actual !== expected : actual === expected;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
match = normalized.match(/^env\.([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
109
|
+
if (match) {
|
|
110
|
+
const key = match[1];
|
|
111
|
+
return String(context.envs?.[key] ?? '').trim() !== '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error(`unsupported build condition "${normalized}"`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveIncludePath(rawPath, rootDir) {
|
|
118
|
+
const normalized = normalizeDirectiveValue(rawPath);
|
|
119
|
+
if (!normalized) {
|
|
120
|
+
throw new Error('build include path is empty');
|
|
121
|
+
}
|
|
122
|
+
if (path.isAbsolute(normalized)) {
|
|
123
|
+
return normalized;
|
|
124
|
+
}
|
|
125
|
+
return path.resolve(rootDir, normalized);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatSourceRef(filePath, lineNumber) {
|
|
129
|
+
return `${filePath}:${lineNumber}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function indentIncludedText(text, indent) {
|
|
133
|
+
if (!text) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
const prefix = String(indent ?? '');
|
|
137
|
+
return String(text)
|
|
138
|
+
.split('\n')
|
|
139
|
+
.map((line) => {
|
|
140
|
+
if (!line) {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
return `${prefix}${line}`;
|
|
144
|
+
})
|
|
145
|
+
.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function processManifestText(rawText, context, options) {
|
|
149
|
+
const filePath = options.filePath;
|
|
150
|
+
const rootDir = options.rootDir;
|
|
151
|
+
const allowDirectives = options.allowDirectives;
|
|
152
|
+
const lines = String(rawText ?? '').split('\n');
|
|
153
|
+
const output = [];
|
|
154
|
+
const stack = [{
|
|
155
|
+
type: 'root',
|
|
156
|
+
active: true,
|
|
157
|
+
}];
|
|
158
|
+
|
|
159
|
+
const currentActive = () => stack[stack.length - 1].active;
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
162
|
+
const line = lines[index];
|
|
163
|
+
const directive = parseBuildDirective(line);
|
|
164
|
+
if (!directive) {
|
|
165
|
+
if (currentActive()) {
|
|
166
|
+
output.push(line);
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!allowDirectives) {
|
|
172
|
+
throw new Error(`nested build directive is not allowed in included file at ${formatSourceRef(filePath, index + 1)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
switch (directive.command) {
|
|
176
|
+
case 'if': {
|
|
177
|
+
const parentActive = currentActive();
|
|
178
|
+
const condition = evaluateBuildCondition(directive.args, context);
|
|
179
|
+
stack.push({
|
|
180
|
+
type: 'if',
|
|
181
|
+
parentActive,
|
|
182
|
+
condition,
|
|
183
|
+
elseSeen: false,
|
|
184
|
+
active: parentActive && condition,
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case 'else': {
|
|
189
|
+
if (stack.length === 1 || stack[stack.length - 1].type !== 'if') {
|
|
190
|
+
throw new Error(`build else without matching if at ${formatSourceRef(filePath, index + 1)}`);
|
|
191
|
+
}
|
|
192
|
+
const top = stack[stack.length - 1];
|
|
193
|
+
if (top.elseSeen) {
|
|
194
|
+
throw new Error(`duplicate build else at ${formatSourceRef(filePath, index + 1)}`);
|
|
195
|
+
}
|
|
196
|
+
top.elseSeen = true;
|
|
197
|
+
top.active = top.parentActive && !top.condition;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'end': {
|
|
201
|
+
if (stack.length === 1 || stack[stack.length - 1].type !== 'if') {
|
|
202
|
+
throw new Error(`build end without matching if at ${formatSourceRef(filePath, index + 1)}`);
|
|
203
|
+
}
|
|
204
|
+
stack.pop();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'include': {
|
|
208
|
+
if (!currentActive()) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
const lineIndent = line.match(/^\s*/)?.[0] ?? '';
|
|
212
|
+
const includePath = resolveIncludePath(directive.args, rootDir);
|
|
213
|
+
if (!fs.existsSync(includePath)) {
|
|
214
|
+
throw new Error(`build include file not found at ${formatSourceRef(filePath, index + 1)}: ${includePath}`);
|
|
215
|
+
}
|
|
216
|
+
const includedText = fs.readFileSync(includePath, 'utf8');
|
|
217
|
+
const rendered = processManifestText(includedText, context, {
|
|
218
|
+
filePath: includePath,
|
|
219
|
+
rootDir,
|
|
220
|
+
allowDirectives: false,
|
|
221
|
+
});
|
|
222
|
+
if (rendered.length > 0) {
|
|
223
|
+
output.push(indentIncludedText(rendered, lineIndent));
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`unsupported build directive "${directive.command}" at ${formatSourceRef(filePath, index + 1)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (stack.length !== 1) {
|
|
233
|
+
throw new Error(`unclosed build if block in ${filePath}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return output.join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function preprocessManifestText(rawText, context, options = {}) {
|
|
240
|
+
const filePath = options.filePath || '<manifest>';
|
|
241
|
+
const rootDir = options.rootDir || process.cwd();
|
|
242
|
+
return processManifestText(rawText, {
|
|
243
|
+
profile: String(context?.profile ?? '').trim(),
|
|
244
|
+
envs: { ...(context?.envs ?? {}) },
|
|
245
|
+
}, {
|
|
246
|
+
filePath,
|
|
247
|
+
rootDir,
|
|
248
|
+
allowDirectives: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function preprocessManifestFile(manifestPath, context) {
|
|
253
|
+
const filePath = path.resolve(manifestPath);
|
|
254
|
+
const rawText = fs.readFileSync(filePath, 'utf8');
|
|
255
|
+
return preprocessManifestText(rawText, context, {
|
|
256
|
+
filePath,
|
|
257
|
+
rootDir: path.dirname(filePath),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as tar from 'tar';
|
|
4
|
+
import logger from 'loglevel';
|
|
5
|
+
import { addProjectTargetOptions, resolveProjectRuntime, ensureProjectServiceRunning } from './project_runtime.js';
|
|
6
|
+
|
|
7
|
+
function normalizeSourcePath(source) {
|
|
8
|
+
const sourcePath = path.resolve(process.cwd(), String(source));
|
|
9
|
+
if (!fs.existsSync(sourcePath)) {
|
|
10
|
+
throw new Error(`Source path not found: ${sourcePath}`);
|
|
11
|
+
}
|
|
12
|
+
return sourcePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createTarStreamForSource(sourcePath) {
|
|
16
|
+
const sourceName = path.basename(sourcePath);
|
|
17
|
+
const sourceParent = path.dirname(sourcePath);
|
|
18
|
+
return tar.c(
|
|
19
|
+
{
|
|
20
|
+
cwd: sourceParent,
|
|
21
|
+
portable: true,
|
|
22
|
+
},
|
|
23
|
+
[sourceName],
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function projectCpCommand() {
|
|
28
|
+
return {
|
|
29
|
+
command: 'cp <src> <dest>',
|
|
30
|
+
desc: 'Copy local files into a project service container',
|
|
31
|
+
builder: (args) => {
|
|
32
|
+
args.option('s', {
|
|
33
|
+
alias: 'service',
|
|
34
|
+
describe: 'Service name in docker compose project',
|
|
35
|
+
type: 'string',
|
|
36
|
+
default: 'app',
|
|
37
|
+
});
|
|
38
|
+
addProjectTargetOptions(args);
|
|
39
|
+
},
|
|
40
|
+
handler: async ({ src, dest, service, config, dev, release }) => {
|
|
41
|
+
if (String(dest).includes(':')) {
|
|
42
|
+
throw new Error('Destination path must not include service prefix');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const runtime = await resolveProjectRuntime(process.cwd(), { config, dev, release, command: 'lzc-cli project cp' });
|
|
46
|
+
logger.info(`Build config: ${runtime.configPath}`);
|
|
47
|
+
const targetService = String(service || 'app');
|
|
48
|
+
const containerId = await ensureProjectServiceRunning(runtime, targetService);
|
|
49
|
+
|
|
50
|
+
const sourcePath = normalizeSourcePath(src);
|
|
51
|
+
const tarStream = createTarStreamForSource(sourcePath);
|
|
52
|
+
const dockerCpArgs = ['cp', '-', `${containerId}:${String(dest)}`];
|
|
53
|
+
|
|
54
|
+
logger.debug('project cp service:', targetService);
|
|
55
|
+
logger.debug('project cp docker cp:', dockerCpArgs.join(' '));
|
|
56
|
+
await runtime.bridge.lzcDockerPipe(dockerCpArgs, tarStream);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { LpkBuild } from './lpk_build.js';
|
|
3
|
+
import { printProjectInfo } from './project_info.js';
|
|
4
|
+
import { addProjectTargetOptions, resolveProjectRuntime, resolveProjectDeployConfigPath } from './project_runtime.js';
|
|
5
|
+
|
|
6
|
+
export function isAppContainerNotReadyError(error) {
|
|
7
|
+
const message = String(error?.message ?? error ?? '');
|
|
8
|
+
return message.includes('No such container') || message.includes('is not running');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function syncProjectDevID(runtime, { waitForContainer = false } = {}) {
|
|
12
|
+
const devId = await runtime.bridge.resolveDevId();
|
|
13
|
+
logger.info(`Sync dev.id: ${devId || '<empty>'}`);
|
|
14
|
+
const maxAttempts = waitForContainer ? 30 : 1;
|
|
15
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
16
|
+
try {
|
|
17
|
+
await runtime.bridge.syncDevID(runtime.pkgId, devId, runtime.userApp);
|
|
18
|
+
logger.info('dev id synced successfully.');
|
|
19
|
+
return;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!waitForContainer || !isAppContainerNotReadyError(error) || attempt === maxAttempts) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
if (attempt === 1) {
|
|
25
|
+
logger.info('Waiting for app container before syncing dev.id...');
|
|
26
|
+
}
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function projectDeployCommand() {
|
|
33
|
+
return {
|
|
34
|
+
command: 'deploy',
|
|
35
|
+
desc: 'Build and install project to target box',
|
|
36
|
+
builder: (args) => {
|
|
37
|
+
addProjectTargetOptions(args);
|
|
38
|
+
},
|
|
39
|
+
handler: async ({ config, dev, release }) => {
|
|
40
|
+
const configPath = resolveProjectDeployConfigPath(process.cwd(), { config, dev, release, command: 'lzc-cli project deploy' });
|
|
41
|
+
const runtime = await resolveProjectRuntime(process.cwd(), configPath);
|
|
42
|
+
logger.info(`Build config: ${runtime.configPath}`);
|
|
43
|
+
|
|
44
|
+
const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName, { forceV2: true }).init();
|
|
45
|
+
lpkBuild.onBeforeBuildPackage(async (options) => {
|
|
46
|
+
delete options.devshell;
|
|
47
|
+
return options;
|
|
48
|
+
});
|
|
49
|
+
const pkgPath = await lpkBuild.exec();
|
|
50
|
+
|
|
51
|
+
logger.info(`Install package: ${pkgPath}`);
|
|
52
|
+
await runtime.bridge.install(pkgPath, runtime.pkgId);
|
|
53
|
+
await syncProjectDevID(runtime, { waitForContainer: true });
|
|
54
|
+
logger.info('Project deployed successfully.');
|
|
55
|
+
await printProjectInfo(runtime);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|