@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.
- package/README.md +30 -5
- package/changelog.md +4 -0
- package/lib/app/index.js +174 -58
- package/lib/app/lpk_build.js +192 -17
- package/lib/app/lpk_build_images.js +728 -0
- package/lib/app/lpk_create.js +93 -21
- package/lib/app/lpk_create_generator.js +144 -9
- package/lib/app/lpk_devshell.js +33 -19
- package/lib/app/lpk_embed_images.js +257 -0
- package/lib/app/lpk_installer.js +14 -7
- package/lib/app/project_cp.js +64 -0
- package/lib/app/project_deploy.js +33 -0
- package/lib/app/project_exec.js +45 -0
- package/lib/app/project_info.js +106 -0
- package/lib/app/project_log.js +67 -0
- package/lib/app/project_runtime.js +261 -0
- package/lib/app/project_start.js +100 -0
- package/lib/box/index.js +101 -4
- package/lib/box/ssh_remote.js +259 -0
- package/lib/build_remote.js +22 -0
- package/lib/config/index.js +1 -1
- package/lib/debug_bridge.js +837 -46
- package/lib/docker/index.js +30 -10
- package/lib/i18n/index.js +1 -0
- package/lib/i18n/locales/en/translation.json +17 -5
- package/lib/i18n/locales/zh/translation.json +16 -4
- package/lib/lpk/core.js +487 -0
- package/lib/lpk/index.js +210 -0
- package/lib/sig/core.js +254 -0
- package/lib/sig/index.js +88 -0
- package/lib/utils.js +3 -1
- package/package.json +2 -1
- package/scripts/cli.js +4 -0
- package/template/_lpk/README.md +11 -3
- package/template/_lpk/gui-vnc.manifest.yml.in +27 -0
- package/template/_lpk/manifest.yml.in +4 -2
- package/template/_lpk/todolist-golang.manifest.yml.in +16 -0
- package/template/_lpk/todolist-java.manifest.yml.in +15 -0
- package/template/_lpk/todolist-python.manifest.yml.in +15 -0
- package/template/_lpk/vue.lzc-build.yml.in +0 -44
- package/template/blank/_gitignore +1 -0
- package/template/blank/lzc-build.yml +25 -40
- package/template/blank/lzc-manifest.yml +14 -7
- package/template/golang/Dockerfile +19 -0
- package/template/golang/README.md +33 -0
- package/template/golang/_gitignore +3 -0
- package/template/golang/go.mod +3 -0
- package/template/golang/lzc-build.yml +21 -0
- package/template/golang/lzc-icon.png +0 -0
- package/template/golang/main.go +252 -0
- package/template/golang/run.sh +3 -0
- package/template/golang/web/index.html +238 -0
- package/template/gui-vnc/README.md +19 -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.yml +23 -0
- package/template/gui-vnc/lzc-icon.png +0 -0
- package/template/python/Dockerfile +15 -0
- package/template/python/README.md +33 -0
- package/template/python/_gitignore +3 -0
- package/template/python/app.py +110 -0
- package/template/python/lzc-build.yml +21 -0
- package/template/python/lzc-icon.png +0 -0
- package/template/python/requirements.txt +1 -0
- package/template/python/run.sh +3 -0
- package/template/python/web/index.html +238 -0
- package/template/springboot/Dockerfile +20 -0
- package/template/springboot/README.md +33 -0
- package/template/springboot/_gitignore +3 -0
- package/template/springboot/lzc-build.yml +21 -0
- package/template/springboot/lzc-icon.png +0 -0
- package/template/springboot/pom.xml +38 -0
- package/template/springboot/run.sh +3 -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 +17 -7
- package/template/vue/_gitignore +1 -0
- package/template/vue/lzc-build.yml +31 -42
- package/template/vue/src/App.vue +36 -25
- package/template/vue/src/style.css +106 -49
- package/template/vue-minidb/README.md +34 -0
- package/template/vue-minidb/_gitignore +26 -0
- package/template/vue-minidb/index.html +13 -0
- package/template/vue-minidb/lzc-build.yml +48 -0
- package/template/vue-minidb/lzc-icon.png +0 -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
|
@@ -6,6 +6,7 @@ import { DebugBridge } from '../debug_bridge.js';
|
|
|
6
6
|
import shellapi from '../shellapi.js';
|
|
7
7
|
import { t } from '../i18n/index.js';
|
|
8
8
|
import { triggerApk } from './apkshell.js';
|
|
9
|
+
import { resolveBuildRemoteFromFile } from '../build_remote.js';
|
|
9
10
|
|
|
10
11
|
export const installConfig = { apk: true };
|
|
11
12
|
// 从一个目录中找出修改时间最新的包
|
|
@@ -20,15 +21,15 @@ function findOnceLpkByDir(dir = process.cwd()) {
|
|
|
20
21
|
return b.mtime - a.mtime;
|
|
21
22
|
})
|
|
22
23
|
.find((entry) => {
|
|
23
|
-
return entry.filename.endsWith('.lpk');
|
|
24
|
+
return entry.filename.endsWith('.lpk') || entry.filename.endsWith('.lpk.tar');
|
|
24
25
|
});
|
|
25
26
|
return pkg?.filename ? path.resolve(dir, pkg.filename) : '';
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export class LpkInstaller {
|
|
29
|
-
constructor() {
|
|
30
|
+
constructor() {}
|
|
30
31
|
|
|
31
|
-
async init() {
|
|
32
|
+
async init() {}
|
|
32
33
|
|
|
33
34
|
// deploy 构建和安装
|
|
34
35
|
async deploy(builder) {
|
|
@@ -115,24 +116,30 @@ export class LpkInstaller {
|
|
|
115
116
|
}
|
|
116
117
|
|
|
117
118
|
try {
|
|
118
|
-
const
|
|
119
|
+
const buildRemote = resolveBuildRemoteFromFile(process.cwd());
|
|
120
|
+
if (!buildRemote && (!shellapi.uid || !shellapi.boxname)) {
|
|
121
|
+
await shellapi.init();
|
|
122
|
+
}
|
|
123
|
+
const bridge = new DebugBridge(process.cwd(), buildRemote);
|
|
119
124
|
await bridge.init();
|
|
120
125
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_start_tips', '开始安装应用'));
|
|
121
126
|
await bridge.install(pkgPath, manifest ? manifest['package'] : '');
|
|
122
127
|
logger.info('\n');
|
|
123
128
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_success_tips', `安装成功!`));
|
|
124
129
|
if (manifest) {
|
|
130
|
+
const boxname = buildRemote ? buildRemote.boxname : shellapi.boxname;
|
|
125
131
|
logger.info(
|
|
126
132
|
t('lzc_cli.lib.app.lpk_installer.install_from_file_done_tips', `👉 请在浏览器中访问 https://{{ subdomain }}.{{ boxname }}.heiyu.space`, {
|
|
127
133
|
subdomain: manifest['application']['subdomain'],
|
|
128
|
-
boxname
|
|
129
|
-
interpolation: { escapeValue: false }
|
|
134
|
+
boxname,
|
|
135
|
+
interpolation: { escapeValue: false },
|
|
130
136
|
}),
|
|
131
137
|
);
|
|
132
138
|
logger.info(t('lzc_cli.lib.app.lpk_installer.install_from_file_login_tips', `👉 并使用微服的用户名和密码登录`));
|
|
133
139
|
}
|
|
134
140
|
} catch (error) {
|
|
135
|
-
|
|
141
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
142
|
+
logger.error(t('lzc_cli.lib.app.lpk_installer.install_from_file_fail_tips', `安装失败: {{ error }}`, { error: errorMsg, interpolation: { escapeValue: false } }));
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
6
|
+
import { resolveProjectRuntime, ensureProjectServiceRunning } from './project_runtime.js';
|
|
7
|
+
|
|
8
|
+
function normalizeSourcePath(source) {
|
|
9
|
+
const sourcePath = path.resolve(process.cwd(), String(source));
|
|
10
|
+
if (!fs.existsSync(sourcePath)) {
|
|
11
|
+
throw new Error(`Source path not found: ${sourcePath}`);
|
|
12
|
+
}
|
|
13
|
+
return sourcePath;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createTarStreamForSource(sourcePath) {
|
|
17
|
+
const sourceName = path.basename(sourcePath);
|
|
18
|
+
const sourceParent = path.dirname(sourcePath);
|
|
19
|
+
return tar.c(
|
|
20
|
+
{
|
|
21
|
+
cwd: sourceParent,
|
|
22
|
+
portable: true,
|
|
23
|
+
},
|
|
24
|
+
[sourceName],
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function projectCpCommand() {
|
|
29
|
+
return {
|
|
30
|
+
command: 'cp <src> <dest>',
|
|
31
|
+
desc: 'Copy local files into a project service container',
|
|
32
|
+
builder: (args) => {
|
|
33
|
+
args.option('s', {
|
|
34
|
+
alias: 'service',
|
|
35
|
+
describe: 'Service name in docker compose project',
|
|
36
|
+
type: 'string',
|
|
37
|
+
default: 'app',
|
|
38
|
+
});
|
|
39
|
+
args.option('c', {
|
|
40
|
+
alias: 'config',
|
|
41
|
+
describe: 'Build config file name',
|
|
42
|
+
type: 'string',
|
|
43
|
+
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
handler: async ({ src, dest, service, config }) => {
|
|
47
|
+
if (String(dest).includes(':')) {
|
|
48
|
+
throw new Error('Destination path must not include service prefix');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
52
|
+
const targetService = String(service || 'app');
|
|
53
|
+
const containerId = await ensureProjectServiceRunning(runtime, targetService);
|
|
54
|
+
|
|
55
|
+
const sourcePath = normalizeSourcePath(src);
|
|
56
|
+
const tarStream = createTarStreamForSource(sourcePath);
|
|
57
|
+
const dockerCpArgs = ['cp', '-', `${containerId}:${String(dest)}`];
|
|
58
|
+
|
|
59
|
+
logger.debug('project cp service:', targetService);
|
|
60
|
+
logger.debug('project cp docker cp:', dockerCpArgs.join(' '));
|
|
61
|
+
await runtime.bridge.lzcDockerPipe(dockerCpArgs, tarStream);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { LpkBuild } from './lpk_build.js';
|
|
3
|
+
import { resolveProjectRuntime, resolveProjectDeployConfigPath } from './project_runtime.js';
|
|
4
|
+
|
|
5
|
+
export function projectDeployCommand() {
|
|
6
|
+
return {
|
|
7
|
+
command: 'deploy',
|
|
8
|
+
desc: 'Build and install project to target box',
|
|
9
|
+
builder: (args) => {
|
|
10
|
+
args.option('c', {
|
|
11
|
+
alias: 'config',
|
|
12
|
+
describe: 'Build config file name',
|
|
13
|
+
type: 'string',
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
handler: async ({ config }) => {
|
|
17
|
+
const configPath = resolveProjectDeployConfigPath(process.cwd(), config);
|
|
18
|
+
const runtime = await resolveProjectRuntime(process.cwd(), configPath);
|
|
19
|
+
logger.info(`Deploy config: ${runtime.configPath}`);
|
|
20
|
+
|
|
21
|
+
const lpkBuild = await new LpkBuild(runtime.projectCwd, runtime.configName).init();
|
|
22
|
+
lpkBuild.onBeforeBuildPackage(async (options) => {
|
|
23
|
+
delete options['devshell'];
|
|
24
|
+
return options;
|
|
25
|
+
});
|
|
26
|
+
const pkgPath = await lpkBuild.exec();
|
|
27
|
+
|
|
28
|
+
logger.info(`Install package: ${pkgPath}`);
|
|
29
|
+
await runtime.bridge.install(pkgPath, runtime.pkgId);
|
|
30
|
+
logger.info('Project deployed successfully.');
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
3
|
+
import { resolveProjectRuntime, ensureProjectServiceRunning } from './project_runtime.js';
|
|
4
|
+
|
|
5
|
+
export function projectExecCommand() {
|
|
6
|
+
return {
|
|
7
|
+
command: 'exec [cmd..]',
|
|
8
|
+
desc: 'Execute command in a project service container',
|
|
9
|
+
builder: (args) => {
|
|
10
|
+
args.option('s', {
|
|
11
|
+
alias: 'service',
|
|
12
|
+
describe: 'Service name in docker compose project',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: 'app',
|
|
15
|
+
});
|
|
16
|
+
args.option('c', {
|
|
17
|
+
alias: 'config',
|
|
18
|
+
describe: 'Build config file name',
|
|
19
|
+
type: 'string',
|
|
20
|
+
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
21
|
+
});
|
|
22
|
+
args.option('t', {
|
|
23
|
+
alias: 'tty',
|
|
24
|
+
describe: 'Allocate tty for exec',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: true,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
handler: async ({ cmd, service, config, tty }) => {
|
|
30
|
+
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
31
|
+
const targetService = String(service || 'app');
|
|
32
|
+
await ensureProjectServiceRunning(runtime, targetService);
|
|
33
|
+
const execCommand = Array.isArray(cmd) && cmd.length > 0 ? cmd.map((item) => String(item)) : ['/bin/sh'];
|
|
34
|
+
|
|
35
|
+
const composeArgs = ['-p', runtime.composeProjectName, 'exec'];
|
|
36
|
+
if (!tty) {
|
|
37
|
+
composeArgs.push('-T');
|
|
38
|
+
}
|
|
39
|
+
composeArgs.push(targetService, ...execCommand);
|
|
40
|
+
|
|
41
|
+
logger.debug('project exec:', composeArgs.join(' '));
|
|
42
|
+
await runtime.bridge.lzcDockerCompose(composeArgs);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
3
|
+
import { resolveProjectRuntime, getProjectDeployInfo, getProjectComposePs, getProjectErrmsgByDeployId } from './project_runtime.js';
|
|
4
|
+
|
|
5
|
+
function takeFirstLines(text, maxLines = 50) {
|
|
6
|
+
const lines = String(text ?? '').split(/\r?\n/);
|
|
7
|
+
const limit = Number.isInteger(maxLines) && maxLines > 0 ? maxLines : 50;
|
|
8
|
+
if (lines.length <= limit) {
|
|
9
|
+
return {
|
|
10
|
+
text: lines.join('\n'),
|
|
11
|
+
truncated: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
text: lines.slice(0, limit).join('\n'),
|
|
16
|
+
truncated: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function trimErrmsgByDockerConfigs(text) {
|
|
21
|
+
const raw = String(text ?? '');
|
|
22
|
+
const marker = '---------------docker-configs:-----------';
|
|
23
|
+
const idx = raw.indexOf(marker);
|
|
24
|
+
if (idx >= 0) {
|
|
25
|
+
return raw.slice(0, idx).trim();
|
|
26
|
+
}
|
|
27
|
+
return raw.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveTargetUrl(domain) {
|
|
31
|
+
const rawDomain = String(domain ?? '').trim();
|
|
32
|
+
if (!rawDomain) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
if (rawDomain.startsWith('http://') || rawDomain.startsWith('https://')) {
|
|
36
|
+
return rawDomain;
|
|
37
|
+
}
|
|
38
|
+
return `https://${rawDomain}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function projectInfoCommand() {
|
|
42
|
+
return {
|
|
43
|
+
command: 'info',
|
|
44
|
+
desc: 'Show project deployment and runtime info',
|
|
45
|
+
builder: (args) => {
|
|
46
|
+
args.option('c', {
|
|
47
|
+
alias: 'config',
|
|
48
|
+
describe: 'Build config file name',
|
|
49
|
+
type: 'string',
|
|
50
|
+
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
handler: async ({ config }) => {
|
|
54
|
+
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
55
|
+
const deploy = await getProjectDeployInfo(runtime);
|
|
56
|
+
const instanceStatus = String(deploy.instanceStatus ?? '').trim();
|
|
57
|
+
const instanceStatusLower = instanceStatus.toLowerCase();
|
|
58
|
+
const targetMode = runtime.bridge.isBuildRemoteMode() ? 'build-remote' : 'box-shell';
|
|
59
|
+
const targetUrl = resolveTargetUrl(deploy.domain);
|
|
60
|
+
|
|
61
|
+
logger.info(`Target mode: ${targetMode}`);
|
|
62
|
+
logger.info(`Target box: ${runtime.bridge.boxname}`);
|
|
63
|
+
if (targetUrl) {
|
|
64
|
+
logger.info(`Target URL: ${targetUrl}`);
|
|
65
|
+
}
|
|
66
|
+
logger.info(`Project package: ${runtime.pkgId}`);
|
|
67
|
+
logger.info(`Local version: ${deploy.localVersion || '(empty)'}`);
|
|
68
|
+
logger.info(`Deployed status: ${deploy.appStatus || '(unknown)'}`);
|
|
69
|
+
logger.info(`Instance status: ${deploy.instanceStatus || '(unknown)'}`);
|
|
70
|
+
logger.info(`Deployed version: ${deploy.deployedVersion || '(empty)'}`);
|
|
71
|
+
logger.info(`Current version deployed: ${deploy.currentVersionDeployed ? 'yes' : 'no'}`);
|
|
72
|
+
if (deploy.deployId) {
|
|
73
|
+
logger.info(`Deploy DIR: /lzcsys/data/system/pkgm/run/${deploy.deployId}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (deploy.isRunning) {
|
|
77
|
+
logger.info('Project app is running.');
|
|
78
|
+
const psOutput = await getProjectComposePs(runtime);
|
|
79
|
+
if (psOutput) {
|
|
80
|
+
console.log(psOutput);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (instanceStatusLower.includes('starting')) {
|
|
85
|
+
logger.info('Project app is starting. Please wait and run "lzc-cli project info" again.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (instanceStatusLower.includes('error')) {
|
|
89
|
+
logger.info('Project app is in error state.');
|
|
90
|
+
const errmsg = await getProjectErrmsgByDeployId(runtime, deploy.deployId);
|
|
91
|
+
const cleanedErrmsg = trimErrmsgByDockerConfigs(errmsg);
|
|
92
|
+
if (cleanedErrmsg) {
|
|
93
|
+
const { text: partialErrmsg, truncated } = takeFirstLines(cleanedErrmsg, 50);
|
|
94
|
+
logger.info(`Error message (first 50 lines):\n${partialErrmsg}`);
|
|
95
|
+
if (truncated) {
|
|
96
|
+
logger.info('Need more logs? Run "lzc-cli project log" for full output.');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
logger.info('Error message: (empty)');
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logger.info('Project app is not running. Run "lzc-cli project start" first.');
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import logger from 'loglevel';
|
|
2
|
+
import { DEFAULT_BUILD_CONFIG_FILE } from '../build_remote.js';
|
|
3
|
+
import { resolveProjectRuntime, ensureProjectRunning, ensureProjectServiceRunning, getComposeProject } from './project_runtime.js';
|
|
4
|
+
|
|
5
|
+
export function projectLogCommand() {
|
|
6
|
+
return {
|
|
7
|
+
command: 'log',
|
|
8
|
+
desc: 'Show logs of project containers',
|
|
9
|
+
builder: (args) => {
|
|
10
|
+
args.option('s', {
|
|
11
|
+
alias: 'service',
|
|
12
|
+
describe: 'Service name in docker compose project',
|
|
13
|
+
type: 'string',
|
|
14
|
+
});
|
|
15
|
+
args.option('c', {
|
|
16
|
+
alias: 'config',
|
|
17
|
+
describe: 'Build config file name',
|
|
18
|
+
type: 'string',
|
|
19
|
+
default: DEFAULT_BUILD_CONFIG_FILE,
|
|
20
|
+
});
|
|
21
|
+
args.option('f', {
|
|
22
|
+
alias: 'follow',
|
|
23
|
+
describe: 'Follow log output',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
default: true,
|
|
26
|
+
});
|
|
27
|
+
args.option('tail', {
|
|
28
|
+
describe: 'Number of lines to show from the end of logs',
|
|
29
|
+
type: 'number',
|
|
30
|
+
default: 200,
|
|
31
|
+
});
|
|
32
|
+
args.option('since', {
|
|
33
|
+
describe: 'Show logs since timestamp or relative duration',
|
|
34
|
+
type: 'string',
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
handler: async ({ service, config, follow, tail, since }) => {
|
|
38
|
+
const runtime = await resolveProjectRuntime(process.cwd(), config);
|
|
39
|
+
const targetService = String(service ?? '').trim();
|
|
40
|
+
if (targetService) {
|
|
41
|
+
await ensureProjectServiceRunning(runtime, targetService);
|
|
42
|
+
} else {
|
|
43
|
+
await ensureProjectRunning(runtime);
|
|
44
|
+
const composeProject = await getComposeProject(runtime);
|
|
45
|
+
if (!composeProject || !composeProject.status.startsWith('running(')) {
|
|
46
|
+
throw new Error('Project app is not running. Run "lzc-cli project start" first.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const composeArgs = ['-p', runtime.composeProjectName, 'logs'];
|
|
50
|
+
if (follow) {
|
|
51
|
+
composeArgs.push('-f');
|
|
52
|
+
}
|
|
53
|
+
if (tail !== undefined && tail !== null) {
|
|
54
|
+
composeArgs.push('--tail', String(tail));
|
|
55
|
+
}
|
|
56
|
+
if (since) {
|
|
57
|
+
composeArgs.push('--since', String(since));
|
|
58
|
+
}
|
|
59
|
+
if (targetService) {
|
|
60
|
+
composeArgs.push(targetService);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.debug('project log:', composeArgs.join(' '));
|
|
64
|
+
await runtime.bridge.lzcDockerCompose(composeArgs);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|